WIP reorganization
This commit is contained in:
1 parent
6706dfe634
commit
054e37dfb7
266 files changed
+98
-620
No files matched your search
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"parser": "babel-eslint",
|
||||
"parserOptions": {
|
||||
"allowImportExportEverywhere": true
|
||||
},
|
||||
"plugins": [
|
||||
"meteor"
|
||||
],
|
||||
"env": {
|
||||
/* Allows global vars from the Meteor environment to pass and enables certain rules */
|
||||
"meteor": true,
|
||||
"node": true,
|
||||
"browser": true
|
||||
},
|
||||
"extends": [
|
||||
"airbnb",
|
||||
"plugin:meteor/recommended"
|
||||
],
|
||||
"settings": {
|
||||
"import/resolver": "meteor"
|
||||
},
|
||||
"rules": {
|
||||
"meteor/no-session": 0, // We are actually using Session for now...
|
||||
"meteor/eventmap-params": [2, {"eventParamName": "event"}],
|
||||
"meteor/eventmap-params": [2, {"templateInstanceParamName": "instance"}],
|
||||
"import/no-extraneous-dependencies": 0,
|
||||
"import/no-unresolved": 0, // There are a bunch of ESLint problems with Meteor's resolver
|
||||
"import/no-duplicates": 0, // So we are disabling these for now
|
||||
"import/extensions": 0,
|
||||
"import/no-absolute-path": 0,
|
||||
"no-console": 0, // For development
|
||||
"no-plusplus": ["error", { "allowForLoopAfterthoughts": true }],
|
||||
"indent": ["error", 4],
|
||||
"new-cap": 0, // Until Match has an exception
|
||||
"func-names": 0, // This is a bit of an annoying rule
|
||||
"no-underscore-dangle": 0, // Doesn't seem to mesh with _id for MongoDB Ids (or SimpleSchema)
|
||||
"max-len": 0, // TODO: re-enable the rules below and fix all of the errors
|
||||
"consistent-return": 0,
|
||||
"no-param-reassign": 0,
|
||||
"no-mixed-operators": 0,
|
||||
"arrow-body-style": 0,
|
||||
"valid-typeof": 0,
|
||||
"import/prefer-default-export": 0
|
||||
"no-undef": 0
|
||||
},
|
||||
"globals": {}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
# This file contains information which helps Meteor properly upgrade your
|
||||
# app when you run 'meteor update'. You should check it into version control
|
||||
# with your project.
|
||||
|
||||
notices-for-0.9.0
|
||||
notices-for-0.9.1
|
||||
0.9.4-platform-file
|
||||
notices-for-facebook-graph-api-2
|
||||
1.2.0-standard-minifiers-package
|
||||
1.2.0-meteor-platform-split
|
||||
1.2.0-cordova-changes
|
||||
1.2.0-breaking-changes
|
||||
1.3.0-split-minifiers-package
|
||||
1.3.5-remove-old-dev-bundle-link
|
||||
1.4.0-remove-old-dev-bundle-link
|
||||
1.4.1-add-shell-server-package
|
||||
1.4.3-split-account-service-packages
|
||||
1.5-add-dynamic-import-package
|
||||
1.7-split-underscore-from-meteor-base
|
||||
@@ -0,0 +1,2 @@
|
||||
dev_bundle
|
||||
local
|
||||
@@ -0,0 +1,7 @@
|
||||
# This file contains a token that is unique to your project.
|
||||
# Check it into your repository along with the rest of this directory.
|
||||
# It can be used for purposes such as:
|
||||
# - ensuring you don't accidentally deploy one app on top of another
|
||||
# - providing package authors with aggregated statistics
|
||||
|
||||
1q3vtk41m2yk4k1rqj08l
|
||||
@@ -0,0 +1,136 @@
|
||||
{
|
||||
"starrynight": {
|
||||
"version": "3.7.0"
|
||||
},
|
||||
"nightwatch": {
|
||||
"version": "0.8.6"
|
||||
},
|
||||
"src_folders": [
|
||||
"./tests/nightwatch/walkthroughs"
|
||||
],
|
||||
"output_folder": "./tests/nightwatch/reports",
|
||||
"custom_commands_path": [
|
||||
"./tests/nightwatch/commands",
|
||||
"./tests/nightwatch/commands/api/meteor",
|
||||
"./tests/nightwatch/commands/actions",
|
||||
"./tests/nightwatch/commands/components",
|
||||
"./tests/nightwatch/commands/methods"
|
||||
],
|
||||
"custom_assertions_path": [
|
||||
"./tests/nightwatch/assertions"
|
||||
],
|
||||
"globals_path": "./tests/nightwatch/globals.json",
|
||||
"selenium": {
|
||||
"start_process": true,
|
||||
"server_path": "${npm_config_prefix}/lib/node_modules/starrynight/node_modules/selenium-server-standalone-jar/jar/selenium-server-standalone-2.45.0.jar",
|
||||
"log_path": "tests/nightwatch/logs",
|
||||
"host": "127.0.0.1",
|
||||
"port": 4444,
|
||||
"cli_args": {
|
||||
"webdriver.chrome.driver": "${npm_config_prefix}/lib/node_modules/starrynight/node_modules/chromedriver/bin/chromedriver"
|
||||
}
|
||||
},
|
||||
"test_settings": {
|
||||
"default": {
|
||||
"launch_url": "http://localhost:5000",
|
||||
"selenium_host": "127.0.0.1",
|
||||
"selenium_port": 4444,
|
||||
"pathname": "/wd/hub",
|
||||
"silent": true,
|
||||
"disable_colors": false,
|
||||
"firefox_profile": false,
|
||||
"ie_driver": "",
|
||||
"screenshots": {
|
||||
"enabled": false,
|
||||
"path": "./tests/nightwatch/screenshots"
|
||||
},
|
||||
"desiredCapabilities": {
|
||||
"browserName": "chrome",
|
||||
"javascriptEnabled": true,
|
||||
"acceptSslCerts": true,
|
||||
"loggingPrefs": {
|
||||
"browser": "ALL"
|
||||
}
|
||||
},
|
||||
"exclude": "./tests/nightwatch/unittests/*"
|
||||
},
|
||||
"phantomjs": {
|
||||
"desiredCapabilities": {
|
||||
"browserName": "phantomjs",
|
||||
"javascriptEnabled": true,
|
||||
"databaseEnabled": false,
|
||||
"locationContextEnabled": false,
|
||||
"applicationCacheEnabled": false,
|
||||
"browserConnectionEnabled": false,
|
||||
"webStorageEnabled": false,
|
||||
"acceptSslCerts": true,
|
||||
"rotatable": false,
|
||||
"nativeEvents": false,
|
||||
"phantomjs.binary.path": "${npm_config_prefix}/lib/node_modules/starrynight/node_modules/phantomjs/bin/phantomjs"
|
||||
}
|
||||
},
|
||||
"travis": {
|
||||
"launch_url": "http://localhost:3000",
|
||||
"selenium_host": "127.0.0.1",
|
||||
"selenium_port": 4444,
|
||||
"pathname": "/wd/hub",
|
||||
"silent": true,
|
||||
"disable_colors": false,
|
||||
"firefox_profile": false,
|
||||
"screenshots": {
|
||||
"enabled": false,
|
||||
"path": "./tests/nightwatch/screenshots"
|
||||
},
|
||||
"desiredCapabilities": {
|
||||
"browserName": "firefox",
|
||||
"javascriptEnabled": true,
|
||||
"databaseEnabled": true,
|
||||
"locationContextEnabled": true,
|
||||
"applicationCacheEnabled": true,
|
||||
"browserConnectionEnabled": true,
|
||||
"webStorageEnabled": true,
|
||||
"acceptSslCerts": true,
|
||||
"rotatable": true,
|
||||
"nativeEvents": true
|
||||
}
|
||||
},
|
||||
"travischrome": {
|
||||
"launch_url": "http://localhost:3000",
|
||||
"selenium_host": "127.0.0.1",
|
||||
"selenium_port": 4444,
|
||||
"pathname": "/wd/hub",
|
||||
"silent": true,
|
||||
"disable_colors": false,
|
||||
"firefox_profile": false,
|
||||
"screenshots": {
|
||||
"enabled": false,
|
||||
"path": "./tests/nightwatch/screenshots"
|
||||
},
|
||||
"desiredCapabilities": {
|
||||
"browserName": "chrome",
|
||||
"javascriptEnabled": true,
|
||||
"databaseEnabled": true,
|
||||
"locationContextEnabled": true,
|
||||
"applicationCacheEnabled": true,
|
||||
"browserConnectionEnabled": true,
|
||||
"webStorageEnabled": true,
|
||||
"acceptSslCerts": true,
|
||||
"rotatable": true,
|
||||
"nativeEvents": true,
|
||||
"chromeOptions": {
|
||||
"args": [
|
||||
"--no-sandbox"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"unittests": {
|
||||
"selenium": {
|
||||
"start_process": false,
|
||||
"start_session": false
|
||||
},
|
||||
"filter": "./tests/nightwatch/unittests/*",
|
||||
"exclude": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
# Meteor packages used by this project, one per line.
|
||||
# Check this file (and the other files in this directory) into your repository.
|
||||
#
|
||||
# 'meteor add' and 'meteor remove' will edit this file for you,
|
||||
# but you can also edit it by hand.
|
||||
|
||||
npm-bcrypt@0.9.3
|
||||
|
||||
meteor-base@1.4.0 # Packages every Meteor app needs to have
|
||||
mobile-experience@1.0.5 # Packages for a great mobile UX
|
||||
mongo@1.5.0 # The database Meteor supports right now
|
||||
blaze-html-templates@1.0.4 # Compile .html files into Meteor Blaze views
|
||||
session@1.1.7 # Client-side reactive dictionary for your app
|
||||
jquery@1.11.10 # Helpful client-side library
|
||||
tracker@1.2.0 # Meteor's client-side reactive programming library
|
||||
standard-minifier-css@1.4.1
|
||||
standard-minifier-js@2.3.4
|
||||
http@1.4.1
|
||||
promise@0.11.1
|
||||
stylus@2.513.13
|
||||
random@1.1.0
|
||||
reactive-var@1.0.11
|
||||
reactive-dict@1.2.0
|
||||
check@1.3.1
|
||||
email@1.2.3
|
||||
ecmascript@0.11.1 # Enable ECMAScript2015+ syntax in app code
|
||||
|
||||
clinical:active-entry
|
||||
clinical:theming
|
||||
clinical:fonts
|
||||
clinical:hipaa-audit-log
|
||||
clinical:hipaa-logger
|
||||
|
||||
# OHIF Packages
|
||||
ohif:polyfill
|
||||
ohif:design
|
||||
ohif:core
|
||||
ohif:header
|
||||
ohif:cornerstone
|
||||
ohif:cornerstone-settings
|
||||
ohif:viewerbase
|
||||
ohif:studies
|
||||
ohif:study-list
|
||||
ohif:hanging-protocols
|
||||
ohif:metadata
|
||||
|
||||
# Necessary for Lesion Trakcer
|
||||
ohif:lesiontracker
|
||||
ohif:user-management
|
||||
ohif:user-meteor-accounts
|
||||
ohif:select-tree
|
||||
|
||||
accounts-base@1.4.2
|
||||
accounts-password@1.5.1
|
||||
|
||||
fortawesome:fontawesome
|
||||
momentjs:moment@2.15.1
|
||||
aldeed:simple-schema # Third party package to deal with schemas
|
||||
aldeed:template-extension@4.0.0
|
||||
aldeed:collection2
|
||||
zuuk:stale-session
|
||||
shell-server@0.3.1
|
||||
underscore
|
||||
@@ -0,0 +1,2 @@
|
||||
server
|
||||
browser
|
||||
@@ -0,0 +1 @@
|
||||
METEOR@1.7.0.3
|
||||
@@ -0,0 +1,145 @@
|
||||
accounts-base@1.4.2
|
||||
accounts-password@1.5.1
|
||||
aldeed:collection2@2.10.0
|
||||
aldeed:collection2-core@1.2.0
|
||||
aldeed:schema-deny@1.1.0
|
||||
aldeed:schema-index@1.1.1
|
||||
aldeed:simple-schema@1.5.4
|
||||
aldeed:template-extension@4.1.0
|
||||
allow-deny@1.1.0
|
||||
amplify@1.0.0
|
||||
autoupdate@1.4.1
|
||||
babel-compiler@7.1.1
|
||||
babel-runtime@1.2.2
|
||||
base64@1.0.11
|
||||
binary-heap@1.0.10
|
||||
blaze@2.3.2
|
||||
blaze-html-templates@1.1.2
|
||||
blaze-tools@1.0.10
|
||||
boilerplate-generator@1.5.0
|
||||
caching-compiler@1.1.12
|
||||
caching-html-compiler@1.1.3
|
||||
callback-hook@1.1.0
|
||||
check@1.3.1
|
||||
clinical:active-entry@1.5.16
|
||||
clinical:auto-resizing@0.2.0
|
||||
clinical:fonts@1.1.6
|
||||
clinical:hipaa-audit-log@2.4.2
|
||||
clinical:hipaa-logger@1.3.0
|
||||
clinical:router@2.0.19
|
||||
clinical:router-location@2.1.0
|
||||
clinical:router-middleware-stack@2.1.2
|
||||
clinical:router-url@2.1.0
|
||||
clinical:theming@0.4.10
|
||||
ddp@1.4.0
|
||||
ddp-client@2.3.3
|
||||
ddp-common@1.4.0
|
||||
ddp-rate-limiter@1.0.7
|
||||
ddp-server@2.2.0
|
||||
deps@1.0.12
|
||||
diff-sequence@1.1.0
|
||||
dynamic-import@0.4.1
|
||||
ecmascript@0.11.1
|
||||
ecmascript-runtime@0.7.0
|
||||
ecmascript-runtime-client@0.7.1
|
||||
ecmascript-runtime-server@0.7.0
|
||||
ejson@1.1.0
|
||||
email@1.2.3
|
||||
es5-shim@4.8.0
|
||||
fastclick@1.0.13
|
||||
fortawesome:fontawesome@4.7.0
|
||||
geojson-utils@1.0.10
|
||||
grove:less@0.2.0
|
||||
hot-code-push@1.0.4
|
||||
html-tools@1.0.11
|
||||
htmljs@1.0.11
|
||||
http@1.4.1
|
||||
id-map@1.1.0
|
||||
iron:controller@1.0.12
|
||||
iron:core@1.0.11
|
||||
iron:dynamic-template@1.0.12
|
||||
iron:layout@1.0.12
|
||||
johdirr:meteor-git-rev@0.0.4
|
||||
jquery@1.11.11
|
||||
launch-screen@1.1.1
|
||||
livedata@1.0.18
|
||||
localstorage@1.2.0
|
||||
logging@1.1.20
|
||||
mdg:validation-error@0.5.1
|
||||
meteor@1.9.2
|
||||
meteor-base@1.4.0
|
||||
meteor-platform@1.2.6
|
||||
minifier-css@1.3.1
|
||||
minifier-js@2.3.5
|
||||
minimongo@1.4.4
|
||||
mobile-experience@1.0.5
|
||||
mobile-status-bar@1.0.14
|
||||
modern-browsers@0.1.2
|
||||
modules@0.12.2
|
||||
modules-runtime@0.10.2
|
||||
momentjs:moment@2.22.2
|
||||
mongo@1.5.1
|
||||
mongo-dev-server@1.1.0
|
||||
mongo-id@1.0.7
|
||||
mrt:moment@2.8.1
|
||||
natestrauser:select2@4.0.3
|
||||
npm-bcrypt@0.9.3
|
||||
npm-mongo@3.0.7
|
||||
observe-sequence@1.0.16
|
||||
ohif:commands@0.0.1
|
||||
ohif:core@0.0.1
|
||||
ohif:cornerstone@0.0.1
|
||||
ohif:cornerstone-settings@0.0.1
|
||||
ohif:design@0.0.1
|
||||
ohif:hanging-protocols@0.0.1
|
||||
ohif:header@0.0.1
|
||||
ohif:hotkeys@0.0.1
|
||||
ohif:lesiontracker@0.0.1
|
||||
ohif:log@0.0.1
|
||||
ohif:measurements@0.0.1
|
||||
ohif:metadata@0.0.1
|
||||
ohif:polyfill@0.0.1
|
||||
ohif:select-tree@0.0.1
|
||||
ohif:servers@0.0.1
|
||||
ohif:studies@0.0.1
|
||||
ohif:study-list@0.0.1
|
||||
ohif:themes@0.0.1
|
||||
ohif:themes-common@0.0.1
|
||||
ohif:user-management@0.0.1
|
||||
ohif:user-meteor-accounts@0.0.1
|
||||
ohif:viewerbase@0.0.1
|
||||
ohif:wadoproxy@0.0.1
|
||||
ordered-dict@1.1.0
|
||||
promise@0.11.1
|
||||
raix:eventemitter@0.1.3
|
||||
random@1.1.0
|
||||
rate-limit@1.0.9
|
||||
reactive-dict@1.2.0
|
||||
reactive-var@1.0.11
|
||||
reload@1.2.0
|
||||
retry@1.1.0
|
||||
routepolicy@1.0.13
|
||||
service-configuration@1.0.11
|
||||
session@1.1.7
|
||||
sha@1.0.9
|
||||
shell-server@0.3.1
|
||||
silentcicero:jszip@0.0.4
|
||||
socket-stream-client@0.2.2
|
||||
spacebars@1.0.15
|
||||
spacebars-compiler@1.1.3
|
||||
srp@1.0.10
|
||||
standard-app-packages@1.0.9
|
||||
standard-minifier-css@1.4.1
|
||||
standard-minifier-js@2.3.4
|
||||
stylus@2.513.14
|
||||
templating@1.3.2
|
||||
templating-compiler@1.3.3
|
||||
templating-runtime@1.3.2
|
||||
templating-tools@1.1.2
|
||||
tracker@1.2.0
|
||||
ui@1.0.13
|
||||
underscore@1.0.10
|
||||
url@1.2.0
|
||||
webapp@1.6.2
|
||||
webapp-hashing@1.0.9
|
||||
zuuk:stale-session@1.0.8
|
||||
@@ -0,0 +1,8 @@
|
||||
# LesionTracker
|
||||
LesionTracker is an open source, zero-footprint image viewer focused on oncology clinical trial workflows. It is built using Meteor platform which enables modularity with a set of packages of the OHIF framework.
|
||||
|
||||
|
||||
### Why LesionTracker?
|
||||
LesionTracker brings an user experience for the basic tumor metrics through web browser. It provides a DICOM server for storing and streaming images to the viewer, tools for measuring and following lesions over time, and a database for measurements of overlay data. A web-based system for image assessments rather than a workstation-based installed application will improve workflow efficiency, enhance accessibility, and promote collaborative image review for the radiologists at cooperating cancer centers.
|
||||
|
||||
To get started, checkout [LesionTracker Documentation](https://github.com/OHIF/Viewers/wiki/LesionTracker)
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Session } from 'meteor/session';
|
||||
|
||||
if (Meteor.isClient){
|
||||
ActiveEntry.configure({
|
||||
logo: {
|
||||
url: '/mini-circles.png',
|
||||
displayed: true
|
||||
},
|
||||
signIn: {
|
||||
displayFullName: true,
|
||||
destination: '/studylist'
|
||||
},
|
||||
signUp: {
|
||||
destination: '/studylist'
|
||||
},
|
||||
themeColors: {
|
||||
primary: ""
|
||||
},
|
||||
passwordOptions: {
|
||||
showPasswordStrengthIndicator: false,
|
||||
requireRegexValidation: true,
|
||||
//requireStrongPasswords: false
|
||||
passwordHistoryCount: 6,
|
||||
failedAttemptsLimit: 5
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Session.set('ThemeConfig', {
|
||||
palette: {
|
||||
colorA: "",
|
||||
colorB: "",
|
||||
colorC: "",
|
||||
colorD: "",
|
||||
colorE: ""
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
if (Meteor.isServer){
|
||||
Accounts.emailTemplates.siteName = 'AwesomeSite';
|
||||
Accounts.emailTemplates.from = 'AwesomeSite Admin <accounts@example.com>';
|
||||
Accounts.emailTemplates.enrollAccount.subject = function(user) {
|
||||
return 'Welcome to Awesome Town, ' + user.profile.name;
|
||||
};
|
||||
|
||||
Accounts.emailTemplates.enrollAccount.text = function(user, url) {
|
||||
return 'You have been selected to participate in building a better future!'
|
||||
+ ' To activate your account, simply click the link below:\n\n'
|
||||
+ url;
|
||||
};
|
||||
|
||||
Meteor.startup(function() {
|
||||
//process.env.MAIL_URL = 'smtp://sandboxid.mailgun.org:mypassword@smtp.mailgun.org:587';
|
||||
});
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1,2 @@
|
||||
set METEOR_PACKAGE_DIRS=..\Packages
|
||||
meteor --settings ../config/medkenOrthanc.json
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
METEOR_PACKAGE_DIRS="../Packages" meteor --settings ../config/medkenOrthanc.json
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
declare config='../config/orthancDICOMWeb.json'
|
||||
|
||||
if [ $# -gt 0 ]; then
|
||||
if [ "$1" = '--dimse' ]; then
|
||||
config="${config%/*}/orthancDIMSE.json"
|
||||
[ -f "$config" ] && echo "DIMSE config file selected: $config"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo 'Starting Meteor server...'
|
||||
METEOR_PACKAGE_DIRS="../Packages" meteor --settings "$config"
|
||||
@@ -0,0 +1,2 @@
|
||||
set METEOR_PACKAGE_DIRS=..\Packages
|
||||
meteor --settings ../config/orthancDICOMWeb.json
|
||||
Executable
+2
@@ -0,0 +1,2 @@
|
||||
echo "Starting Meteor server..."
|
||||
METEOR_PACKAGE_DIRS="../Packages" meteor --settings ../config/orthancDICOMWeb.json
|
||||
@@ -0,0 +1,3 @@
|
||||
<body>
|
||||
<div id="notificationArea" class="notification-area"></div>
|
||||
</body>
|
||||
@@ -0,0 +1,2 @@
|
||||
body
|
||||
background-color: black
|
||||
@@ -0,0 +1,29 @@
|
||||
<template name="app">
|
||||
{{#header brandHref='http://ohif.org' headerClasses=instance.headerClasses}}
|
||||
{{#section 'brand'}}
|
||||
<svg class="logo-image">
|
||||
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-ohif-logo"}}></use>
|
||||
</svg>
|
||||
<div class="logo-text">Open Health Imaging Foundation</div>
|
||||
{{/section}}
|
||||
{{#section 'headerAfterBrand'}}
|
||||
{{#if studyListToggleText}}
|
||||
<a href="#" class="btn studyListLinkSection pull-left js-toggle-studyList" id="{{dasherize studyListToggleText}}-btn">
|
||||
{{studyListToggleText}}
|
||||
</a>
|
||||
{{/if}}
|
||||
{{/section}}
|
||||
{{#section 'headerMenu'}}
|
||||
<span class="research-use pull-left m-r-1">
|
||||
INVESTIGATIONAL USE ONLY
|
||||
</span>
|
||||
<span class="user-name pull-left m-r-1">{{userName}}</span>
|
||||
<div class="menu-toggle pull-right">
|
||||
<i class="fa fa-cog"></i>
|
||||
<span class="caret-down"></span>
|
||||
</div>
|
||||
{{/section}}
|
||||
{{/header}}
|
||||
|
||||
{{>Template.dynamic template=(choose this.template 'studylist') data=this}}
|
||||
</template>
|
||||
@@ -0,0 +1,125 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { Session } from 'meteor/session';
|
||||
import { Router } from 'meteor/clinical:router';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
Template.app.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
instance.headerClasses = new ReactiveVar('');
|
||||
|
||||
OHIF.header.dropdown.setItems([{
|
||||
action: OHIF.user.audit,
|
||||
text: 'View Audit Log',
|
||||
iconClasses: 'log',
|
||||
iconSvgUse: 'packages/ohif_viewerbase/assets/icons.svg#log',
|
||||
separatorAfter: true
|
||||
}, {
|
||||
action: () => OHIF.ui.showDialog('themeSelectorModal'),
|
||||
text: 'Themes',
|
||||
iconClasses: 'theme',
|
||||
iconSvgUse: 'packages/ohif_viewerbase/assets/icons.svg#theme',
|
||||
separatorAfter: true
|
||||
}, {
|
||||
action: () => OHIF.ui.showDialog('serverInformationModal'),
|
||||
text: 'Server Information',
|
||||
iconClasses: 'server',
|
||||
iconSvgUse: 'packages/ohif_viewerbase/assets/icons.svg#server',
|
||||
separatorAfter: true
|
||||
}, {
|
||||
action: () => OHIF.ui.showDialog('userPreferencesDialog'),
|
||||
text: 'Preferences',
|
||||
icon: 'fa fa-user',
|
||||
separatorAfter: true
|
||||
}, {
|
||||
action: OHIF.user.changePassword,
|
||||
text: 'Change Password',
|
||||
iconClasses: 'password',
|
||||
iconSvgUse: 'packages/ohif_viewerbase/assets/icons.svg#password'
|
||||
}, {
|
||||
action: OHIF.user.logout,
|
||||
text: 'Logout',
|
||||
iconClasses: 'logout',
|
||||
iconSvgUse: 'packages/ohif_viewerbase/assets/icons.svg#logout'
|
||||
}]);
|
||||
|
||||
instance.autorun(() => {
|
||||
const currentRoute = Router.current();
|
||||
if (!currentRoute) return;
|
||||
const routeName = currentRoute.route.getName();
|
||||
const isViewer = routeName.indexOf('viewer') === 0;
|
||||
|
||||
// Add or remove the strech class from body
|
||||
$(document.body)[isViewer ? 'addClass' : 'removeClass']('stretch');
|
||||
|
||||
// Set the header on its bigger version if the viewer is not opened
|
||||
instance.headerClasses.set(isViewer ? '' : 'header-big');
|
||||
|
||||
// Set the viewer open state on session
|
||||
Session.set('ViewerOpened', isViewer);
|
||||
});
|
||||
});
|
||||
|
||||
Template.app.events({
|
||||
'click .js-toggle-studyList'(event, instance) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const isViewer = Session.get('ViewerOpened');
|
||||
|
||||
if (!isViewer) {
|
||||
const timepointId = OHIF.viewer.data.currentTimepointId;
|
||||
if (timepointId) {
|
||||
Router.go('viewerTimepoint', { timepointId });
|
||||
} else {
|
||||
const { studyInstanceUids } = OHIF.viewer.data;
|
||||
Router.go('viewerStudies', { studyInstanceUids });
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
OHIF.ui.unsavedChanges.presentProactiveDialog('viewer.*', (hasChanges, userChoice) => {
|
||||
if (!hasChanges) {
|
||||
return Router.go('studylist');
|
||||
}
|
||||
|
||||
switch (userChoice) {
|
||||
case 'abort-action':
|
||||
return;
|
||||
case 'save-changes':
|
||||
OHIF.ui.unsavedChanges.trigger('viewer', 'save', false);
|
||||
OHIF.ui.unsavedChanges.clear('viewer.*');
|
||||
break;
|
||||
case 'abandon-changes':
|
||||
OHIF.ui.unsavedChanges.clear('viewer.*');
|
||||
break;
|
||||
}
|
||||
|
||||
Router.go('studylist');
|
||||
}, {
|
||||
position: {
|
||||
x: event.clientX + 15,
|
||||
y: event.clientY + 15
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Template.app.helpers({
|
||||
userName: OHIF.user.getName,
|
||||
|
||||
studyListToggleText() {
|
||||
const isViewer = Session.get('ViewerOpened');
|
||||
|
||||
// Return empty if viewer was not opened yet
|
||||
if (!OHIF.utils.ObjectPath.get(OHIF, 'viewer.data.studyInstanceUids')) return;
|
||||
|
||||
return isViewer ? 'Study list' : 'Back to viewer';
|
||||
},
|
||||
|
||||
dasherize(text) {
|
||||
return text.replace(/ /g, '-').toLowerCase();
|
||||
}
|
||||
});
|
||||
|
||||
Session.set('defaultSignInMessage', 'Tumor tracking in your browser.');
|
||||
@@ -0,0 +1,88 @@
|
||||
@require "{ohif:design}/app"
|
||||
|
||||
body>.header
|
||||
|
||||
.brand
|
||||
height: 30px
|
||||
display: inline-block
|
||||
text-decoration: none
|
||||
|
||||
.logo-image
|
||||
display: inline-block
|
||||
fill: transparent
|
||||
float: left
|
||||
height: 100%
|
||||
margin: 0 8px 0 0
|
||||
width: 30px
|
||||
|
||||
.logo-text
|
||||
display: inline-block
|
||||
font-family: $logoFontFamily
|
||||
font-size: 14px
|
||||
font-weight: $logoFontWeight
|
||||
theme('color', '$textPrimaryColor')
|
||||
line-height: 30px
|
||||
|
||||
.header-menu
|
||||
.user-name
|
||||
font-size: 13px
|
||||
|
||||
.menu-toggle
|
||||
theme('border-left', 'solid 1px $defaultColor')
|
||||
display: inline-block
|
||||
height: 18px
|
||||
padding: 0 8px
|
||||
|
||||
.fa
|
||||
theme('color', '$defaultColor')
|
||||
font-size: 17px
|
||||
line-height: 18px
|
||||
|
||||
.caret-down
|
||||
theme('color', '$uiGrayLight')
|
||||
margin: 0 4px 4px 2px
|
||||
|
||||
.btn
|
||||
theme('color', '$textSecondaryColor')
|
||||
cursor: pointer
|
||||
font-size: 13px
|
||||
font-weight: 500
|
||||
line-height: 26px
|
||||
|
||||
&:hover
|
||||
theme('color', '$hoverColor')
|
||||
|
||||
&:active
|
||||
theme('color', '$activeColor')
|
||||
|
||||
.studyListLinkSection
|
||||
theme('border-left', '%s solid $uiBorderColor' % $uiBorderThickness)
|
||||
margin: 3px 0 0 10px
|
||||
padding: 0 0 0 10px
|
||||
|
||||
&#back-to-viewer-btn
|
||||
border-left: 0
|
||||
|
||||
&.header-big
|
||||
padding-left: $studyListPadding
|
||||
padding-right: $studyListPadding
|
||||
|
||||
.brand
|
||||
height: 100%
|
||||
width: 80%
|
||||
line-height: $topBarExpandedHeight - 10px
|
||||
|
||||
.logo-image
|
||||
margin: 0 20px 0 0
|
||||
width: 50px
|
||||
|
||||
.logo-text
|
||||
font-size: 30px
|
||||
|
||||
.studyListLinkSection
|
||||
border: none
|
||||
left: 0
|
||||
margin: 0
|
||||
padding: 0
|
||||
position: absolute
|
||||
top: 0
|
||||
@@ -0,0 +1,37 @@
|
||||
<template name="toolbarSection">
|
||||
<div class="toolbarSection noselect p-x-2">
|
||||
<div class="clearfix">
|
||||
<div class="pull-left m-t-1">
|
||||
{{>roundedButtonGroup leftSidebarToggleButtonData}}
|
||||
</div>
|
||||
|
||||
{{>toolbarSectionTools toolbarButtons=toolbarButtons}}
|
||||
|
||||
<div class="pull-right m-t-1 rm-x-1">
|
||||
{{#form (extend api=instance.api)}}
|
||||
{{#button class='btn p-x-2' action='save' disabled=(isFinishDisabled)}}Save{{/button}}
|
||||
{{/form}}
|
||||
</div>
|
||||
|
||||
<div id="toggleMeasurements" class="pull-right m-t-1 rm-x-1">
|
||||
{{>roundedButtonGroup rightSidebarToggleButtonData}}
|
||||
</div>
|
||||
<div id="toggleHUD" class="toolbarSectionButton pull-right rp-x-1 m-t-1 rm-r-3 {{#if session 'measurementTableHudOpen'}}active{{/if}}">
|
||||
<div class="svgContainer">
|
||||
<svg>
|
||||
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-hud"}}></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="buttonLabel">HUD</div>
|
||||
</div>
|
||||
<div id="toggleTrial" class="toolbarSectionButton pull-right rp-x-1 m-t-1 rm-r-1">
|
||||
<div class="svgContainer">
|
||||
<svg>
|
||||
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-trial-info"}}></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="buttonLabel">Trial</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,320 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { Session } from 'meteor/session';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { Viewerbase } from 'meteor/ohif:viewerbase';
|
||||
|
||||
Template.toolbarSection.helpers({
|
||||
isFinishDisabled() {
|
||||
const instance = Template.instance();
|
||||
|
||||
// Run this computation on save or every time any measurement / timepoint suffer changes
|
||||
OHIF.ui.unsavedChanges.depend();
|
||||
instance.saveObserver.depend();
|
||||
Session.get('LayoutManagerUpdated');
|
||||
|
||||
return OHIF.ui.unsavedChanges.probe('viewer.*') === 0;
|
||||
},
|
||||
|
||||
leftSidebarToggleButtonData() {
|
||||
const instance = Template.instance();
|
||||
return {
|
||||
toggleable: true,
|
||||
key: 'leftSidebar',
|
||||
value: instance.data.state,
|
||||
options: [{
|
||||
value: 'studies',
|
||||
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-studies',
|
||||
svgWidth: 15,
|
||||
svgHeight: 13,
|
||||
bottomLabel: 'Studies'
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
rightSidebarToggleButtonData() {
|
||||
const instance = Template.instance();
|
||||
return {
|
||||
toggleable: true,
|
||||
key: 'rightSidebar',
|
||||
value: instance.data.state,
|
||||
options: [{
|
||||
value: 'measurements',
|
||||
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-measurements-lesions',
|
||||
svgWidth: 18,
|
||||
svgHeight: 10,
|
||||
bottomLabel: 'Measurements'
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
toolbarButtons() {
|
||||
// Check if the measure tools shall be disabled
|
||||
const isToolDisabled = false; //!Template.instance().data.timepointApi;
|
||||
|
||||
const targetSubTools = [];
|
||||
|
||||
targetSubTools.push({
|
||||
id: 'bidirectional',
|
||||
title: 'Bidirectional',
|
||||
classes: 'imageViewerTool rm-l-3',
|
||||
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-tools-measure-target',
|
||||
disabled: isToolDisabled
|
||||
});
|
||||
|
||||
targetSubTools.push({
|
||||
id: 'targetCR',
|
||||
title: 'CR Target',
|
||||
classes: 'imageViewerTool',
|
||||
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-tools-measure-target-cr',
|
||||
disabled: isToolDisabled
|
||||
});
|
||||
|
||||
targetSubTools.push({
|
||||
id: 'targetUN',
|
||||
title: 'UN Target',
|
||||
classes: 'imageViewerTool',
|
||||
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-tools-measure-target-un',
|
||||
disabled: isToolDisabled
|
||||
});
|
||||
|
||||
const extraTools = [];
|
||||
|
||||
extraTools.push({
|
||||
id: 'stackScroll',
|
||||
title: 'Stack Scroll',
|
||||
classes: 'imageViewerTool',
|
||||
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-tools-stack-scroll'
|
||||
});
|
||||
|
||||
extraTools.push({
|
||||
id: 'resetViewport',
|
||||
title: 'Reset',
|
||||
classes: 'imageViewerCommand',
|
||||
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-tools-reset'
|
||||
});
|
||||
|
||||
extraTools.push({
|
||||
id: 'rotateR',
|
||||
title: 'Rotate Right',
|
||||
classes: 'imageViewerCommand',
|
||||
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-tools-rotate-right'
|
||||
});
|
||||
|
||||
extraTools.push({
|
||||
id: 'flipH',
|
||||
title: 'Flip H',
|
||||
classes: 'imageViewerCommand',
|
||||
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-tools-flip-horizontal'
|
||||
});
|
||||
|
||||
extraTools.push({
|
||||
id: 'flipV',
|
||||
title: 'Flip V',
|
||||
classes: 'imageViewerCommand',
|
||||
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-tools-flip-vertical'
|
||||
});
|
||||
|
||||
extraTools.push({
|
||||
id: 'invert',
|
||||
title: 'Invert',
|
||||
classes: 'imageViewerCommand',
|
||||
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-tools-invert'
|
||||
});
|
||||
|
||||
extraTools.push({
|
||||
id: 'magnify',
|
||||
title: 'Magnify',
|
||||
classes: 'imageViewerTool',
|
||||
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-tools-magnify'
|
||||
});
|
||||
|
||||
extraTools.push({
|
||||
id: 'ellipticalRoi',
|
||||
title: 'Ellipse',
|
||||
classes: 'imageViewerTool',
|
||||
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-tools-elliptical-roi'
|
||||
});
|
||||
|
||||
extraTools.push({
|
||||
id: 'toggleDownloadDialog',
|
||||
title: 'Download',
|
||||
classes: 'imageViewerCommand',
|
||||
iconClasses: 'fa fa-camera',
|
||||
active: () => $('#imageDownloadDialog').is(':visible')
|
||||
});
|
||||
|
||||
extraTools.push({
|
||||
id: 'toggleCineDialog',
|
||||
title: 'CINE',
|
||||
classes: 'imageViewerCommand',
|
||||
iconClasses: 'fa fa-youtube-play',
|
||||
active: () => $('#cineDialog').is(':visible')
|
||||
});
|
||||
|
||||
const buttonData = [];
|
||||
|
||||
buttonData.push({
|
||||
id: 'zoom',
|
||||
title: 'Zoom',
|
||||
classes: 'imageViewerTool',
|
||||
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-tools-zoom'
|
||||
});
|
||||
|
||||
buttonData.push({
|
||||
id: 'wwwc',
|
||||
title: 'Levels',
|
||||
classes: 'imageViewerTool',
|
||||
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-tools-levels'
|
||||
});
|
||||
|
||||
buttonData.push({
|
||||
id: 'pan',
|
||||
title: 'Pan',
|
||||
classes: 'imageViewerTool',
|
||||
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-tools-pan'
|
||||
});
|
||||
|
||||
buttonData.push({
|
||||
id: 'linkStackScroll',
|
||||
title: 'Link',
|
||||
classes: 'imageViewerCommand toolbarSectionButton nonAutoDisableState',
|
||||
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-tools-link',
|
||||
disableFunction: Viewerbase.viewportUtils.isStackScrollLinkingDisabled
|
||||
});
|
||||
|
||||
buttonData.push({
|
||||
id: 'toggleTarget',
|
||||
title: 'Target',
|
||||
classes: 'rm-l-3',
|
||||
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-tools-measure-target',
|
||||
disabled: isToolDisabled,
|
||||
subTools: targetSubTools
|
||||
});
|
||||
|
||||
buttonData.push({
|
||||
id: 'nonTarget',
|
||||
title: 'Non-Target',
|
||||
classes: 'imageViewerTool',
|
||||
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-tools-measure-non-target',
|
||||
disabled: isToolDisabled
|
||||
});
|
||||
|
||||
buttonData.push({
|
||||
id: 'length',
|
||||
title: 'Temp',
|
||||
classes: 'imageViewerTool',
|
||||
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-tools-measure-temp'
|
||||
});
|
||||
|
||||
buttonData.push({
|
||||
id: 'toggleMore',
|
||||
title: 'More',
|
||||
classes: 'rp-x-1 rm-l-3',
|
||||
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-tools-more',
|
||||
disabled: isToolDisabled,
|
||||
subTools: extraTools
|
||||
});
|
||||
|
||||
return buttonData;
|
||||
}
|
||||
});
|
||||
|
||||
Template.toolbarSection.events({
|
||||
'click #toggleTarget'(event, instance) {
|
||||
const $target = $(event.currentTarget);
|
||||
if (!$target.hasClass('active') && $target.hasClass('expanded')) {
|
||||
Viewerbase.toolManager.setActiveTool('bidirectional');
|
||||
}
|
||||
},
|
||||
|
||||
'click #toggleHUD'(event) {
|
||||
const $this = $(event.currentTarget);
|
||||
|
||||
// Stop here if the tool is disabled
|
||||
if ($this.hasClass('disabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const state = Session.get('measurementTableHudOpen');
|
||||
Session.set('measurementTableHudOpen', !state);
|
||||
},
|
||||
|
||||
'click #toggleTrial'(event) {
|
||||
if (!$(event.currentTarget).hasClass('disabled')) {
|
||||
OHIF.ui.showDialog('trialOptionsModal');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Template.toolbarSection.onCreated( function() {
|
||||
const instance = Template.instance();
|
||||
|
||||
instance.path = 'viewer.studyViewer.measurements';
|
||||
instance.saveObserver = new Tracker.Dependency();
|
||||
instance.api = {
|
||||
save() {
|
||||
// Clear signaled unsaved changes...
|
||||
const successHandler = () => {
|
||||
OHIF.ui.unsavedChanges.clear(`${instance.path}.*`);
|
||||
instance.saveObserver.changed();
|
||||
};
|
||||
|
||||
// Display the error messages
|
||||
const errorHandler = data => {
|
||||
OHIF.ui.showDialog('dialogInfo', Object.assign({ class: 'themed' }, data));
|
||||
};
|
||||
|
||||
const promise = instance.data.measurementApi.storeMeasurements();
|
||||
promise.then(successHandler).catch(errorHandler);
|
||||
OHIF.ui.showDialog('dialogLoading', {
|
||||
promise,
|
||||
text: 'Saving measurement data'
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
};
|
||||
|
||||
instance.unsavedChangesHandler = () => {
|
||||
const isNotDisabled = !instance.$('.js-finish-case').hasClass('disabled');
|
||||
if (isNotDisabled && instance.progressPercent.get() === 100) {
|
||||
instance.api.save();
|
||||
}
|
||||
};
|
||||
|
||||
// Attach handler for unsaved changes dialog...
|
||||
OHIF.ui.unsavedChanges.attachHandler(instance.path, 'save', instance.unsavedChangesHandler);
|
||||
});
|
||||
|
||||
Template.toolbarSection.onRendered(function() {
|
||||
// TODO: Figure out a way to disable/enable the buttons with each status from New API
|
||||
// // Set disabled/enabled tool buttons that are set in toolManager
|
||||
// const states = Viewerbase.toolManager.getToolDefaultStates();
|
||||
// const disabledToolButtons = states.disabledToolButtons;
|
||||
// const allToolbarButtons = $('.toolbarSection').find('.toolbarSectionButton:not(.nonAutoDisableState)');
|
||||
//
|
||||
// // Additional toolbar buttons whose classes are not toolbarSectionButton
|
||||
// allToolbarButtons.push($('#toolbarSectionEntry')[0]);
|
||||
// allToolbarButtons.push($('#toggleMeasurements')[0]);
|
||||
//
|
||||
// if (disabledToolButtons && disabledToolButtons.length > 0) {
|
||||
// for (let i = 0; i < allToolbarButtons.length; i++) {
|
||||
// const toolbarButton = allToolbarButtons[i];
|
||||
// const index = disabledToolButtons.indexOf($(toolbarButton).attr('id'));
|
||||
// if (index !== -1) {
|
||||
// $(toolbarButton).addClass('disabled');
|
||||
// $(toolbarButton).find('*').addClass('disabled');
|
||||
// } else {
|
||||
// $(toolbarButton).removeClass('disabled');
|
||||
// $(toolbarButton).find('*').removeClass('disabled');
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
});
|
||||
|
||||
Template.caseProgress.onDestroyed(() => {
|
||||
const instance = Template.instance();
|
||||
// Remove unsaved changes handler after this view has been destroyed...
|
||||
OHIF.ui.unsavedChanges.removeHandler(instance.path, 'save', instance.unsavedChangesHandler);
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
@import "{ohif:design}/app"
|
||||
|
||||
.toolbarSection
|
||||
theme('border-bottom', '%s solid $uiBorderColor' % $uiBorderThickness)
|
||||
flex: 0 0 auto
|
||||
height: $toolbarHeight
|
||||
padding-top: 6px
|
||||
position: relative
|
||||
transition(height 300ms ease)
|
||||
width: 100%
|
||||
|
||||
&.expanded
|
||||
height: $toolbarHeight + $toolbarDrawerHeight
|
||||
|
||||
.toolbarSectionEntry
|
||||
theme('color', '$defaultColor')
|
||||
cursor: pointer
|
||||
display: inline-block
|
||||
theme('fill', '$defaultColor')
|
||||
margin-top: 3px
|
||||
min-width: 30px
|
||||
theme('stroke', '$defaultColor')
|
||||
text-align: center
|
||||
|
||||
.saveMeasurements
|
||||
display: inline-block
|
||||
margin-left: 22px
|
||||
@@ -0,0 +1,17 @@
|
||||
<template name="viewer">
|
||||
<div class="viewerDialogs">
|
||||
{{#if and Template.subscriptionsReady dataSourcesReady}}
|
||||
{{>confirmDeleteDialog}}
|
||||
{{>measurementTableHUD (clone this)}}
|
||||
{{>cineDialog}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div id="viewer">
|
||||
{{#if and Template.subscriptionsReady dataSourcesReady}}
|
||||
{{>toolbarSection (clone this state=state)}}
|
||||
{{>viewerSection (clone this state=state)}}
|
||||
{{else}}
|
||||
{{>loadingText}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,385 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Template } from 'meteor/templating';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
import { Session } from 'meteor/session';
|
||||
import { ReactiveDict } from 'meteor/reactive-dict';
|
||||
import { _ } from 'meteor/underscore';
|
||||
import { $ } from 'meteor/jquery';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
import 'meteor/ohif:cornerstone';
|
||||
import 'meteor/ohif:viewerbase';
|
||||
import 'meteor/ohif:metadata';
|
||||
|
||||
Meteor.startup(() => {
|
||||
Session.set('TimepointsReady', false);
|
||||
Session.set('MeasurementsReady', false);
|
||||
|
||||
OHIF.viewer.displaySeriesQuickSwitch = true;
|
||||
OHIF.viewer.stackImagePositionOffsetSynchronizer = new OHIF.viewerbase.StackImagePositionOffsetSynchronizer();
|
||||
|
||||
// Create the synchronizer used to update reference lines
|
||||
OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer('cornerstonenewimage', cornerstoneTools.updateImageSynchronizer);
|
||||
|
||||
OHIF.viewer.metadataProvider = new OHIF.cornerstone.MetadataProvider();
|
||||
|
||||
// Metadata configuration
|
||||
const metadataProvider = OHIF.viewer.metadataProvider;
|
||||
cornerstone.metaData.addProvider(metadataProvider.getProvider());
|
||||
|
||||
// Target tools configuration
|
||||
OHIF.lesiontracker.configureTargetToolsHandles();
|
||||
});
|
||||
|
||||
Template.viewer.onCreated(() => {
|
||||
Session.set('ViewerReady', false);
|
||||
|
||||
const instance = Template.instance();
|
||||
|
||||
// Define the OHIF.viewer.data global object
|
||||
OHIF.viewer.data = OHIF.viewer.data || Session.get('ViewerData') || {};
|
||||
|
||||
const { TimepointApi, MeasurementApi, ConformanceCriteria } = OHIF.measurements;
|
||||
|
||||
const currentTimepointId = OHIF.viewer.data.currentTimepointId;
|
||||
const timepointApi = new TimepointApi(currentTimepointId);
|
||||
const measurementApi = new MeasurementApi(timepointApi);
|
||||
const conformanceCriteria = new ConformanceCriteria(measurementApi, timepointApi);
|
||||
const apis = {
|
||||
timepointApi,
|
||||
measurementApi,
|
||||
conformanceCriteria
|
||||
};
|
||||
|
||||
Object.assign(OHIF.viewer, apis);
|
||||
Object.assign(instance.data, apis);
|
||||
|
||||
instance.state = new ReactiveDict();
|
||||
instance.state.set('leftSidebar', Session.get('leftSidebar'));
|
||||
instance.state.set('rightSidebar', Session.get('rightSidebar'));
|
||||
|
||||
const viewportUtils = OHIF.viewerbase.viewportUtils;
|
||||
|
||||
OHIF.viewer.functionList = $.extend(OHIF.viewer.functionList, {
|
||||
toggleLesionTrackerTools: OHIF.lesiontracker.toggleLesionTrackerTools,
|
||||
bidirectional: () => {
|
||||
// Used for hotkeys
|
||||
OHIF.viewerbase.toolManager.setActiveTool('bidirectional');
|
||||
},
|
||||
nonTarget: () => {
|
||||
// Used for hotkeys
|
||||
OHIF.viewerbase.toolManager.setActiveTool('nonTarget');
|
||||
},
|
||||
// Viewport functions
|
||||
toggleCineDialog: viewportUtils.toggleCineDialog,
|
||||
clearTools: viewportUtils.clearTools,
|
||||
resetViewport: viewportUtils.resetViewport,
|
||||
invert: viewportUtils.invert,
|
||||
flipV: viewportUtils.flipV,
|
||||
flipH: viewportUtils.flipH,
|
||||
rotateL: viewportUtils.rotateL,
|
||||
rotateR: viewportUtils.rotateR,
|
||||
linkStackScroll: viewportUtils.linkStackScroll
|
||||
});
|
||||
|
||||
if (OHIF.viewer.data.loadedSeriesData) {
|
||||
OHIF.log.info('Reloading previous loadedSeriesData');
|
||||
OHIF.viewer.loadedSeriesData = OHIF.viewer.data.loadedSeriesData;
|
||||
} else {
|
||||
OHIF.log.info('Setting default viewer data');
|
||||
OHIF.viewer.loadedSeriesData = {};
|
||||
OHIF.viewer.data.loadedSeriesData = {};
|
||||
}
|
||||
|
||||
Session.set('activeViewport', OHIF.viewer.data.activeViewport || false);
|
||||
|
||||
// Set lesion tool buttons as disabled if pixel spacing is not available for active element
|
||||
instance.autorun(OHIF.lesiontracker.pixelSpacingAutorunCheck);
|
||||
|
||||
// @TypeSafeStudies
|
||||
// Clears OHIF.viewer.Studies collection
|
||||
OHIF.viewer.Studies.removeAll();
|
||||
|
||||
// @TypeSafeStudies
|
||||
// Clears OHIF.viewer.StudyMetadataList collection
|
||||
OHIF.viewer.StudyMetadataList.removeAll();
|
||||
|
||||
instance.data.studies.forEach(study => {
|
||||
const studyMetadata = new OHIF.metadata.StudyMetadata(study, study.studyInstanceUid);
|
||||
let displaySets = study.displaySets;
|
||||
|
||||
if (!study.displaySets) {
|
||||
displaySets = OHIF.viewerbase.sortingManager.getDisplaySets(studyMetadata);
|
||||
study.displaySets = displaySets;
|
||||
}
|
||||
|
||||
studyMetadata.setDisplaySets(displaySets);
|
||||
|
||||
study.selected = true;
|
||||
OHIF.viewer.Studies.insert(study);
|
||||
OHIF.viewer.StudyMetadataList.insert(studyMetadata);
|
||||
});
|
||||
|
||||
const patientId = instance.data.studies[0].patientId;
|
||||
|
||||
// LT-382: Preventing HP to keep identifying studies in timepoints that might be removed
|
||||
instance.data.studies.forEach(study => (delete study.timepointType));
|
||||
|
||||
// TODO: Consider combining the retrieval calls into one?
|
||||
const timepointsPromise = timepointApi.retrieveTimepoints({ patientId });
|
||||
timepointsPromise.then(() => {
|
||||
const timepoints = timepointApi.all();
|
||||
|
||||
// Set timepointType in studies to be used in hanging protocol engine
|
||||
timepoints.forEach(timepoint => {
|
||||
timepoint.studyInstanceUids.forEach(studyInstanceUid => {
|
||||
const study = _.find(instance.data.studies, element => {
|
||||
return element.studyInstanceUid === studyInstanceUid;
|
||||
});
|
||||
if (!study) {
|
||||
return;
|
||||
}
|
||||
|
||||
// @TODO: Maybe this should be a setCustomAttribute?
|
||||
study.timepointType = timepoint.timepointType;
|
||||
});
|
||||
});
|
||||
|
||||
Session.set('TimepointsReady', true);
|
||||
|
||||
const timepointIds = timepoints.map(t => t.timepointId);
|
||||
|
||||
const measurementsPromise = measurementApi.retrieveMeasurements(patientId, timepointIds);
|
||||
measurementsPromise.then(() => {
|
||||
Session.set('MeasurementsReady', true);
|
||||
|
||||
measurementApi.syncMeasurementsAndToolData();
|
||||
});
|
||||
});
|
||||
|
||||
// Provide the necessary data to the Measurement API and Timepoint API
|
||||
const prior = timepointApi.prior();
|
||||
if (prior) {
|
||||
measurementApi.priorTimepointId = prior.timepointId;
|
||||
}
|
||||
|
||||
// Enable/Disable Lesion Tracker Tools if the opened study is associated or not
|
||||
OHIF.lesiontracker.toggleLesionTrackerToolsButtons(!!currentTimepointId);
|
||||
|
||||
let firstMeasurementActivated = false;
|
||||
instance.autorun(() => {
|
||||
if (!Session.get('TimepointsReady') ||
|
||||
!Session.get('MeasurementsReady') ||
|
||||
!Session.get('ViewerReady') ||
|
||||
firstMeasurementActivated) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find and activate the first measurement by Lesion Number
|
||||
// NOTE: This is inefficient, we should be using a hanging protocol
|
||||
// to hang the first measurement's imageId immediately, rather
|
||||
// than changing images after initial loading...
|
||||
const config = OHIF.measurements.MeasurementApi.getConfiguration();
|
||||
const tools = config.measurementTools[0].childTools;
|
||||
const firstTool = tools[Object.keys(tools)[0]];
|
||||
const measurementTypeId = firstTool.id;
|
||||
|
||||
const collection = measurementApi.tools[measurementTypeId];
|
||||
const sorting = {
|
||||
sort: {
|
||||
measurementNumber: -1
|
||||
}
|
||||
};
|
||||
|
||||
const data = collection.find({}, sorting).fetch();
|
||||
|
||||
const current = timepointApi.current();
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
|
||||
let timepoints = [current];
|
||||
const prior = timepointApi.prior();
|
||||
if (prior) {
|
||||
timepoints.push(prior);
|
||||
}
|
||||
|
||||
// TODO: Clean this up, it's probably an inefficient way to get what we need
|
||||
const groupObject = _.groupBy(data, m => m.measurementNumber);
|
||||
|
||||
// Reformat the data
|
||||
const rows = Object.keys(groupObject).map(key => ({
|
||||
measurementTypeId: measurementTypeId,
|
||||
measurementNumber: key,
|
||||
entries: groupObject[key]
|
||||
}));
|
||||
|
||||
const rowItem = rows[0];
|
||||
|
||||
// Activate the first lesion
|
||||
if (rowItem) {
|
||||
OHIF.measurements.jumpToRowItem(rowItem, timepoints);
|
||||
}
|
||||
|
||||
firstMeasurementActivated = true;
|
||||
});
|
||||
|
||||
instance.measurementModifiedHandler = _.throttle((event, instance) => {
|
||||
OHIF.measurements.MeasurementHandlers.onModified(event, instance);
|
||||
}, 300);
|
||||
});
|
||||
|
||||
/**
|
||||
* Sets sidebar configuration and active tool based on viewer template instance
|
||||
* @param {Object} instance Template instance for viewer template
|
||||
*/
|
||||
const setActiveToolAndSidebar = () => {
|
||||
const instance = Template.instance();
|
||||
const { studies, currentTimepointId, measurementApi, timepointIds } = instance.data;
|
||||
|
||||
// Default actions for Associated Studies
|
||||
if (currentTimepointId) {
|
||||
// Follow-up studies: same as the first measurement in the table
|
||||
// Baseline studies: target-tool
|
||||
if (studies[0]) {
|
||||
let activeTool;
|
||||
// In follow-ups, get the baseline timepointId
|
||||
const timepointId = timepointIds.find(id => id !== currentTimepointId);
|
||||
|
||||
// Follow-up studies
|
||||
if (studies[0].timepointType === 'followup' && timepointId) {
|
||||
const measurementTools = OHIF.measurements.MeasurementApi.getConfiguration().measurementTools;
|
||||
|
||||
// Create list of measurement tools
|
||||
const measurementTypes = measurementTools.map(
|
||||
tool => {
|
||||
const { id, cornerstoneToolType } = tool;
|
||||
return {
|
||||
id,
|
||||
cornerstoneToolType
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Iterate over each measurement tool to find the first baseline
|
||||
// measurement. If so, stops the loop and prevent fetching from all
|
||||
// collections
|
||||
measurementTypes.every(({ id, cornerstoneToolType }) => {
|
||||
// Get measurement
|
||||
if (measurementApi[id]) {
|
||||
const measurement = measurementApi[id].findOne({ timepointId });
|
||||
|
||||
// Found a measurement, save tool and stop loop
|
||||
if (measurement) {
|
||||
const isArray = Array.isArray(cornerstoneToolType);
|
||||
activeTool = isArray ? cornerstoneToolType[0] : cornerstoneToolType;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// If not set, for associated studies default is target-tool
|
||||
OHIF.viewerbase.toolManager.setActiveTool(activeTool || 'bidirectional');
|
||||
}
|
||||
|
||||
// Toggle Measurement Table
|
||||
if (instance.state) {
|
||||
instance.state.set('rightSidebar', 'measurements');
|
||||
}
|
||||
}
|
||||
// Hide as default for single study
|
||||
else {
|
||||
if (instance.state) {
|
||||
instance.state.set('rightSidebar', null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Inits OHIF Hanging Protocol's onReady.
|
||||
* It waits for OHIF Hanging Protocol to be ready to instantiate the ProtocolEngine
|
||||
* Hanging Protocol will use OHIF LayoutManager to render viewports properly
|
||||
*/
|
||||
|
||||
const initHangingProtocol = () => {
|
||||
// When Hanging Protocol is ready
|
||||
HP.ProtocolStore.onReady(() => {
|
||||
|
||||
setActiveToolAndSidebar();
|
||||
|
||||
// Gets all StudyMetadata objects: necessary for Hanging Protocol to access study metadata
|
||||
const studyMetadataList = OHIF.viewer.StudyMetadataList.all();
|
||||
|
||||
// Caches Layout Manager: Hanging Protocol uses it for layout management according to current protocol
|
||||
const layoutManager = OHIF.viewerbase.layoutManager;
|
||||
|
||||
// Instantiate StudyMetadataSource: necessary for Hanging Protocol to get study metadata
|
||||
const studyMetadataSource = new OHIF.studies.classes.OHIFStudyMetadataSource();
|
||||
|
||||
// Creates Protocol Engine object with required arguments
|
||||
const ProtocolEngine = new HP.ProtocolEngine(layoutManager, studyMetadataList, [], studyMetadataSource);
|
||||
|
||||
// Sets up Hanging Protocol engine
|
||||
HP.setEngine(ProtocolEngine);
|
||||
|
||||
Session.set('ViewerReady', true);
|
||||
|
||||
Session.set('activeViewport', 0);
|
||||
});
|
||||
};
|
||||
|
||||
Template.viewer.onRendered(function() {
|
||||
this.autorun(() => {
|
||||
// To make sure ohif viewerMain is rendered before initializing Hanging Protocols
|
||||
const isOHIFViewerMainRendered = Session.get('OHIFViewerMainRendered');
|
||||
|
||||
// To avoid first run
|
||||
if (isOHIFViewerMainRendered) {
|
||||
// To run only when ViewerMainRendered dependency has changed.
|
||||
// because initHangingProtocol can have other reactive components
|
||||
Tracker.nonreactive(initHangingProtocol);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.viewer.helpers({
|
||||
dataSourcesReady() {
|
||||
// TODO: Find a better way to do this
|
||||
const ready = Session.get('TimepointsReady') && Session.get('MeasurementsReady');
|
||||
OHIF.log.info('dataSourcesReady? : ' + ready);
|
||||
return ready;
|
||||
},
|
||||
|
||||
state() {
|
||||
return Template.instance().state;
|
||||
}
|
||||
});
|
||||
|
||||
Template.viewer.events({
|
||||
'cornerstonetoolsmeasurementadded .imageViewerViewport'(event, instance) {
|
||||
const originalEvent = event.originalEvent;
|
||||
OHIF.measurements.MeasurementHandlers.onAdded(originalEvent, instance);
|
||||
},
|
||||
|
||||
'cornerstonetoolsmeasurementmodified .imageViewerViewport'(event, instance) {
|
||||
const originalEvent = event.originalEvent;
|
||||
instance.measurementModifiedHandler(originalEvent, instance);
|
||||
},
|
||||
|
||||
'cornerstonemeasurementremoved .imageViewerViewport'(event, instance) {
|
||||
const originalEvent = event.originalEvent;
|
||||
OHIF.measurements.MeasurementHandlers.onRemoved(originalEvent, instance);
|
||||
}
|
||||
});
|
||||
|
||||
Template.viewer.onDestroyed(() => {
|
||||
Session.set('ViewerMainReady', false);
|
||||
Session.set('TimepointsReady', false);
|
||||
Session.set('MeasurementsReady', false);
|
||||
|
||||
OHIF.viewer.stackImagePositionOffsetSynchronizer.deactivate();
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
@import "{ohif:design}/app"
|
||||
|
||||
#viewer
|
||||
background-color: black
|
||||
height: 100%
|
||||
width: 100%
|
||||
|
||||
#imageViewerViewports
|
||||
.viewportContainer
|
||||
border: none !important
|
||||
outline: 0 !important // Prevent blue outline in Chrome
|
||||
position: relative
|
||||
|
||||
&:hover
|
||||
&.active
|
||||
&:hover.active
|
||||
border: none !important
|
||||
outline: 0 !important // Prevent blue outline in Chrome
|
||||
|
||||
.imageViewerViewport
|
||||
theme('border-bottom', '%s solid $uiBorderColor' % $uiBorderThickness)
|
||||
|
||||
canvas
|
||||
border-color: transparent
|
||||
border-style: solid
|
||||
border-width: 1px
|
||||
|
||||
&:not(:last-child)
|
||||
.imageViewerViewport
|
||||
theme('border-right', '%s solid $uiBorderColor' % $uiBorderThickness)
|
||||
|
||||
&:first-child
|
||||
.imageViewerViewport
|
||||
theme('border-left', '%s solid $uiBorderColor' % $uiBorderThickness)
|
||||
|
||||
&.active
|
||||
.imageViewerViewport
|
||||
border-right: none !important
|
||||
|
||||
canvas
|
||||
theme('border-color', '$uiBorderColorActive')
|
||||
|
||||
|
||||
.sidebar-left-open
|
||||
#imageViewerViewports
|
||||
.viewportContainer
|
||||
&:first-child
|
||||
.removable
|
||||
.imageViewerViewport
|
||||
border-left: none
|
||||
@@ -0,0 +1,21 @@
|
||||
<template name="viewerSection">
|
||||
<div class="viewerSection">
|
||||
<div class="sidebarMenu sidebar-left {{#if leftSidebarOpen}}sidebar-open{{/if}}">
|
||||
{{#if isTimepointBrowser}}
|
||||
{{>timepointBrowserSidebar (clone this)}}
|
||||
{{else}}
|
||||
{{#if instance.loading.get}}
|
||||
{{>loadingText}}
|
||||
{{else}}
|
||||
{{>studyBrowserSidebar (clone this studiesInformation=instance.studiesInformation.get)}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="mainContent {{#if leftSidebarOpen}}sidebar-left-open{{/if}} {{#if rightSidebarOpen}}sidebar-right-open{{/if}}">
|
||||
{{>viewerMain (clone this)}}
|
||||
</div>
|
||||
<div class="sidebarMenu sidebar-right {{#if rightSidebarOpen}}sidebar-open{{/if}}">
|
||||
{{>measurementTable (clone this)}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { $ } from 'meteor/jquery';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
Template.viewerSection.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
instance.isTimepointBrowser = () => !!OHIF.viewer.data.currentTimepointId;
|
||||
|
||||
OHIF.viewer.quickSwitchStudyBrowserTemplate = 'timepointBrowserQuickSwitch';
|
||||
if (!instance.isTimepointBrowser()) {
|
||||
OHIF.viewer.quickSwitchStudyBrowserTemplate = 'studyBrowserQuickSwitch';
|
||||
instance.loading = new ReactiveVar(true);
|
||||
instance.studiesInformation = new ReactiveVar([]);
|
||||
const filter = { studyInstanceUid: OHIF.viewer.data.studyInstanceUids };
|
||||
OHIF.studies.searchStudies(filter).then(studiesData => {
|
||||
instance.loading.set(false);
|
||||
instance.studiesInformation.set(studiesData);
|
||||
}).catch(error => {
|
||||
instance.loading.set(false);
|
||||
const text = 'An error has occurred while retrieving studies information';
|
||||
OHIF.ui.notifications.danger({ text });
|
||||
OHIF.log.error(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Template.viewerSection.events({
|
||||
'transitionend .sidebarMenu'(event) {
|
||||
if (!event.target.classList.contains('sidebarMenu')) return;
|
||||
window.ResizeViewportManager.handleResize();
|
||||
},
|
||||
|
||||
'ohif.measurements.timepoint.changeViewType .timepoint-browser-list'(event, instance, viewType) {
|
||||
const $browserList = $(event.currentTarget);
|
||||
const $allBrowserItems = $browserList.find('.timepoint-browser-item');
|
||||
|
||||
// Removes all active classes to collapse the timepoints and studies
|
||||
$allBrowserItems.removeClass('active');
|
||||
|
||||
if (viewType === 'key') {
|
||||
const { timepointIds, currentTimepointId } = OHIF.viewer.data;
|
||||
timepointIds.forEach(timepointId => {
|
||||
const $browserItem = $allBrowserItems.filter(`[data-id=${timepointId}]`);
|
||||
$browserItem.find('.timepoint-item').trigger('ohif.measurements.timepoint.load');
|
||||
});
|
||||
|
||||
// Show only current timepoint expanded on key timepoints tab
|
||||
const $browserItem = $allBrowserItems.filter(`[data-id=${currentTimepointId}]`);
|
||||
$browserItem.find('.timepoint-item').trigger('click');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Template.viewerSection.helpers({
|
||||
leftSidebarOpen() {
|
||||
return Template.instance().data.state.get('leftSidebar');
|
||||
},
|
||||
|
||||
rightSidebarOpen() {
|
||||
return Template.instance().data.state.get('rightSidebar');
|
||||
},
|
||||
|
||||
isTimepointBrowser() {
|
||||
return Template.instance().isTimepointBrowser();
|
||||
},
|
||||
|
||||
studiesInformation() {
|
||||
return Template.instance().studiesInformation.get();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
@require '{ohif:design}/app'
|
||||
|
||||
.viewerSection
|
||||
display: flex
|
||||
flex: 1
|
||||
flex-flow: row nowrap
|
||||
align-items: stretch
|
||||
height: 'calc(100% - %s)' % ($toolbarHeight + $topBarHeight)
|
||||
width: 100%
|
||||
|
||||
.sidebarMenu
|
||||
height: 100%
|
||||
// required transformation to make inner fixed elements relative to this one
|
||||
transform(scale(1))
|
||||
transition($sidebarTransition)
|
||||
|
||||
.sidebar-option
|
||||
height: 100%
|
||||
max-width: inherit
|
||||
position: absolute
|
||||
transform(translateX(100%))
|
||||
transition($sidebarTransition)
|
||||
width: 100%
|
||||
|
||||
&.active
|
||||
transform(translateX(0%))
|
||||
|
||||
.sidebar-left
|
||||
theme('border-right', '%s solid $uiBorderColor' % $uiBorderThickness)
|
||||
flex: 1
|
||||
margin-left: - $studiesSidebarMenuWidth
|
||||
max-width: $studiesSidebarMenuWidth
|
||||
order: 1
|
||||
visibility: hidden
|
||||
|
||||
&.sidebar-open
|
||||
margin-left: 0
|
||||
visibility: visible
|
||||
|
||||
.loadingTextDiv
|
||||
theme('color', '$textSecondaryColor')
|
||||
font-size: 32px
|
||||
|
||||
.mainContent
|
||||
flex: 1
|
||||
height: 100%
|
||||
order: 2
|
||||
overflow: visible
|
||||
position: relative
|
||||
transition($sidebarTransition)
|
||||
width: 100%
|
||||
|
||||
.viewerMain
|
||||
left: 0
|
||||
position: absolute
|
||||
top: 0
|
||||
|
||||
.sidebar-right
|
||||
flex: 1
|
||||
margin-right: - $rightSidebarMenuWidth
|
||||
max-width: $rightSidebarMenuWidth
|
||||
order: 3
|
||||
position: relative
|
||||
visibility: hidden
|
||||
|
||||
&[data-timepoints="3"]
|
||||
margin-right: - ($rightSidebarMenuWidth + 135.5px)
|
||||
max-width: $rightSidebarMenuWidth + 135.5px
|
||||
|
||||
&[data-timepoints="4"]
|
||||
margin-right: - ($rightSidebarMenuWidth + 270px)
|
||||
max-width: $rightSidebarMenuWidth + 270px
|
||||
|
||||
&.sidebar-open
|
||||
margin-right: 0
|
||||
visibility: visible
|
||||
|
||||
.studiesListedChanger
|
||||
theme('border-bottom', '%s solid $uiBorderColor' % $uiBorderThickness)
|
||||
padding: 20px 10px
|
||||
text-align: center
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { cornerstoneWADOImageLoader } from 'meteor/ohif:cornerstone';
|
||||
|
||||
Meteor.startup(function() {
|
||||
const maxWebWorkers = Math.max(navigator.hardwareConcurrency - 1, 1);
|
||||
const config = {
|
||||
maxWebWorkers: maxWebWorkers,
|
||||
startWebWorkersOnDemand: true,
|
||||
webWorkerPath : OHIF.utils.absoluteUrl('packages/ohif_cornerstone/public/js/cornerstoneWADOImageLoaderWebWorker.es5.js'),
|
||||
taskConfiguration: {
|
||||
'decodeTask' : {
|
||||
loadCodecsOnStartup : true,
|
||||
initializeCodecsOnStartup: false,
|
||||
codecsPath: OHIF.utils.absoluteUrl('packages/ohif_cornerstone/public/js/cornerstoneWADOImageLoaderCodecs.es5.js'),
|
||||
usePDFJS: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
cornerstoneWADOImageLoader.webWorkerManager.initialize(config);
|
||||
|
||||
cornerstoneWADOImageLoader.configure({
|
||||
beforeSend: function(xhr) {
|
||||
const userId = Meteor.userId();
|
||||
const loginToken = Accounts._storedLoginToken();
|
||||
if (userId && loginToken) {
|
||||
xhr.setRequestHeader("x-user-id", userId);
|
||||
xhr.setRequestHeader("x-auth-token", loginToken);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Lesion Tracker</title>
|
||||
<meta name="description" content="Prototype viewer application for oncology">
|
||||
<!-- Prevents page zooming -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
|
||||
<link rel="shortcut icon" type="image/png" href="/images/logo.png" sizes="16x16 32x32 64x64">
|
||||
<!--Best display for mobile devices-->
|
||||
<meta http-equiv="cleartype" content="on">
|
||||
<meta name="MobileOptimized" content="320">
|
||||
<meta name="HandheldFriendly" content="True">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
|
||||
<!-- Make SVG work on IE -->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
</head>
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
Meteor.startup(() => {
|
||||
console.log('Adding Lesion Tracker Hanging Protocols');
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Define Baseline protocol
|
||||
const proto = new HP.Protocol('LT_Baseline');
|
||||
proto.locked = true;
|
||||
|
||||
const isBaseline = new HP.ProtocolMatchingRule();
|
||||
isBaseline.required = true;
|
||||
isBaseline.weight = 1;
|
||||
isBaseline.attribute = 'timepointType';
|
||||
isBaseline.constraint = {
|
||||
equals: {
|
||||
value: 'baseline'
|
||||
}
|
||||
};
|
||||
|
||||
proto.addProtocolMatchingRule(isBaseline);
|
||||
|
||||
const oneByOne = new HP.ViewportStructure('grid', {
|
||||
rows: 1,
|
||||
columns: 1
|
||||
});
|
||||
|
||||
// Stage 1
|
||||
const single = new HP.Viewport();
|
||||
|
||||
const baseline = new HP.StudyMatchingRule(true);
|
||||
baseline.required = true;
|
||||
baseline.attribute = 'timepointType';
|
||||
baseline.constraint = {
|
||||
equals: {
|
||||
value: 'baseline'
|
||||
}
|
||||
};
|
||||
|
||||
single.studyMatchingRules.push(baseline);
|
||||
|
||||
const first = new HP.Stage(oneByOne, 'oneByOne');
|
||||
first.viewports.push(single);
|
||||
|
||||
proto.addStage(first);
|
||||
|
||||
HP.lesionTrackerBaselineProtocol = proto;
|
||||
HP.lesionTrackerBaselineProtocol.id = 'lesionTrackerBaselineProtocol';
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Define Followup Protocol
|
||||
const protoFollowup = new HP.Protocol('LT_BaselineFollowup');
|
||||
protoFollowup.locked = true;
|
||||
|
||||
const isFollowup = new HP.ProtocolMatchingRule();
|
||||
isFollowup.required = true;
|
||||
isFollowup.weight = 2;
|
||||
isFollowup.attribute = 'timepointType';
|
||||
isFollowup.constraint = {
|
||||
equals: {
|
||||
value: 'followup'
|
||||
}
|
||||
};
|
||||
|
||||
protoFollowup.addProtocolMatchingRule(isFollowup);
|
||||
|
||||
const oneByTwo = new HP.ViewportStructure('grid', {
|
||||
rows: 1,
|
||||
columns: 2
|
||||
});
|
||||
|
||||
// Stage 1
|
||||
const left = new HP.Viewport();
|
||||
const right = new HP.Viewport();
|
||||
|
||||
const baseline2 = new HP.StudyMatchingRule(true);
|
||||
baseline2.required = true;
|
||||
baseline2.attribute = 'timepointType';
|
||||
baseline2.constraint = {
|
||||
equals: {
|
||||
value: 'baseline'
|
||||
}
|
||||
};
|
||||
|
||||
const followup = new HP.StudyMatchingRule();
|
||||
followup.required = true;
|
||||
followup.attribute = 'timepointType';
|
||||
followup.constraint = {
|
||||
equals: {
|
||||
value: 'followup'
|
||||
}
|
||||
};
|
||||
|
||||
left.studyMatchingRules.push(followup);
|
||||
right.studyMatchingRules.push(baseline2);
|
||||
|
||||
const first2 = new HP.Stage(oneByTwo, 'oneByTwo');
|
||||
first2.viewports.push(left);
|
||||
first2.viewports.push(right);
|
||||
|
||||
protoFollowup.addStage(first2);
|
||||
|
||||
HP.lesionTrackerFollowupProtocol = protoFollowup;
|
||||
HP.lesionTrackerFollowupProtocol.id = 'lesionTrackerFollowupProtocol';
|
||||
|
||||
HP.ProtocolStore.onReady(() => {
|
||||
console.log('Inserting lesion tracker protocols');
|
||||
HP.ProtocolStore.addProtocol(HP.lesionTrackerBaselineProtocol);
|
||||
HP.ProtocolStore.addProtocol(HP.lesionTrackerFollowupProtocol);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { _ } from 'meteor/underscore';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
Meteor.startup(() => {
|
||||
const { toolManager } = OHIF.viewerbase;
|
||||
const contextName = 'viewer';
|
||||
|
||||
// Enable the custom tools
|
||||
const customTools = [{
|
||||
id: 'bidirectional',
|
||||
name: 'Target'
|
||||
}, {
|
||||
id: 'nonTarget',
|
||||
name: 'Non-Target'
|
||||
}, {
|
||||
id: 'targetCR',
|
||||
name: 'CR Target'
|
||||
}, {
|
||||
id: 'targetUN',
|
||||
name: 'UN Target'
|
||||
}];
|
||||
customTools.forEach(tool => {
|
||||
_.defaults(OHIF.hotkeys.defaults[contextName], { [tool.id]: '' });
|
||||
OHIF.commands.register(contextName, tool.id, {
|
||||
name: tool.name,
|
||||
action: tool.action || (() => toolManager.setActiveTool(tool.id))
|
||||
});
|
||||
});
|
||||
|
||||
// Enable the custom commands
|
||||
const customCommands = [{
|
||||
id: 'linkStackScroll',
|
||||
name: 'Link',
|
||||
action: OHIF.viewerbase.viewportUtils.linkStackScroll
|
||||
}, {
|
||||
id: 'saveMeasurements',
|
||||
name: 'Save measurements',
|
||||
hotkey: 'CTRL+S',
|
||||
action() {
|
||||
const activeTimepoint = OHIF.measurements.getActiveTimepoint();
|
||||
if (!activeTimepoint) return;
|
||||
OHIF.measurements.saveMeasurements(OHIF.viewer.measurementApi, activeTimepoint.timepointId);
|
||||
}
|
||||
}];
|
||||
customCommands.forEach(command => {
|
||||
_.defaults(OHIF.hotkeys.defaults[contextName], { [command.id]: command.hotkey || '' });
|
||||
OHIF.commands.register(contextName, command.id, {
|
||||
name: command.name,
|
||||
action: command.action || (() => toolManager.setActiveTool(command.id))
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import loglevel from 'loglevel';
|
||||
log = loglevel.getLogger('OHIFViewer');
|
||||
log.setLevel('info');
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Router } from 'meteor/clinical:router';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
Router.configure({
|
||||
loadingTemplate: 'loading'
|
||||
});
|
||||
|
||||
// If we are running a disconnected client similar to the StandaloneViewer
|
||||
// (see https://docs.ohif.org/standalone-viewer/usage.html) we don't want
|
||||
// our routes to get stuck while waiting for Pub / Sub.
|
||||
//
|
||||
// In this case, the developer is required to add Servers and specify
|
||||
// a CurrentServer with some other approach (e.g. a separate script).
|
||||
if (Meteor.settings &&
|
||||
Meteor.settings.public &&
|
||||
Meteor.settings.public.clientOnly !== true) {
|
||||
Router.waitOn(function() {
|
||||
return [
|
||||
Meteor.subscribe('servers'),
|
||||
Meteor.subscribe('currentServer')
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
Router.onBeforeAction('loading');
|
||||
|
||||
Router.onBeforeAction(function() {
|
||||
// verifyEmail controls whether emailVerification template will be rendered or not
|
||||
const publicSettings = Meteor.settings && Meteor.settings.public;
|
||||
const verifyEmail = publicSettings && publicSettings.verifyEmail || false;
|
||||
|
||||
// Check if user is signed in or needs an email verification
|
||||
if (!Meteor.userId() && !Meteor.loggingIn()) {
|
||||
this.render('entrySignIn');
|
||||
} else if (verifyEmail && Meteor.user().emails && !Meteor.user().emails[0].verified) {
|
||||
this.render('emailVerification');
|
||||
} else {
|
||||
this.next();
|
||||
}
|
||||
}, {
|
||||
except: ['entrySignIn', 'entrySignUp', 'forgotPassword', 'resetPassword', 'emailVerification']
|
||||
});
|
||||
|
||||
Router.route('/', function() {
|
||||
Router.go('studylist', {}, { replaceState: true });
|
||||
}, { name: 'home' });
|
||||
|
||||
Router.route('/studylist', {
|
||||
name: 'studylist',
|
||||
onBeforeAction: function() {
|
||||
const next = this.next;
|
||||
|
||||
// Retrieve the timepoints data to display in studylist
|
||||
const promise = OHIF.studylist.timepointApi.retrieveTimepoints({});
|
||||
promise.then(() => next());
|
||||
},
|
||||
action: function() {
|
||||
this.render('app', { data: { template: 'studylist' } });
|
||||
}
|
||||
});
|
||||
|
||||
Router.route('/viewer/timepoints/:timepointId', function() {
|
||||
const timepointId = this.params.timepointId;
|
||||
OHIF.viewerbase.renderViewer(this, { timepointId });
|
||||
}, { name: 'viewerTimepoint' });
|
||||
|
||||
Router.route('/viewer/studies/:studyInstanceUids', function() {
|
||||
const studyInstanceUids = this.params.studyInstanceUids.split(';');
|
||||
OHIF.viewerbase.renderViewer(this, { studyInstanceUids });
|
||||
}, { name: 'viewerStudies' });
|
||||
|
||||
// OHIF #98 Show specific series of study
|
||||
Router.route('/study/:studyInstanceUid/series/:seriesInstanceUids', function () {
|
||||
const studyInstanceUid = this.params.studyInstanceUid;
|
||||
const seriesInstanceUids = this.params.seriesInstanceUids.split(';');
|
||||
OHIF.viewerbase.renderViewer(this, { studyInstanceUids: [studyInstanceUid], seriesInstanceUids });
|
||||
}, { name: 'viewerSeries' });
|
||||
Generated
+2284
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "LesionTracker",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"fix": "eslint --fix .",
|
||||
"pretest": "npm run lint --silent"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.0.0-beta.51",
|
||||
"bcrypt": "^3.0.1",
|
||||
"loglevel": "^1.6.1",
|
||||
"url": "^0.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^10.0.1",
|
||||
"eslint": "^5.6.0",
|
||||
"eslint-config-airbnb": "^17.1.0",
|
||||
"eslint-import-resolver-meteor": "^0.4.0",
|
||||
"eslint-plugin-import": "^2.14.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.1.1",
|
||||
"eslint-plugin-meteor": "^5.1.0",
|
||||
"eslint-plugin-react": "^7.11.1"
|
||||
},
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/OHIF/Viewers.git"
|
||||
},
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/OHIF/Viewers/issues"
|
||||
},
|
||||
"homepage": "https://github.com/OHIF/Viewers"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,52 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
Meteor.startup(function () {
|
||||
|
||||
// Mail server settings
|
||||
var username = Meteor.settings && Meteor.settings.mailServerSettings && Meteor.settings.mailServerSettings.username || null;
|
||||
var password = Meteor.settings && Meteor.settings.mailServerSettings && Meteor.settings.mailServerSettings.password || null;
|
||||
var server = Meteor.settings && Meteor.settings.mailServerSettings && Meteor.settings.mailServerSettings.server || null;
|
||||
var port = Meteor.settings && Meteor.settings.mailServerSettings && Meteor.settings.mailServerSettings.port || null;
|
||||
var verifyEmail = Meteor.settings && Meteor.settings.public && Meteor.settings.public.verifyEmail || false;
|
||||
var siteName = Meteor.settings && Meteor.settings.public && Meteor.settings.public.siteName || "Lesion Tracker";
|
||||
|
||||
if (username && password && server && port) {
|
||||
Accounts.emailTemplates.siteName = siteName;
|
||||
Accounts.emailTemplates.from = siteName+' Admin <'+username+'>';
|
||||
|
||||
process.env.MAIL_URL = 'smtp://' +
|
||||
encodeURIComponent(username) + ':' +
|
||||
encodeURIComponent(password) + '@' +
|
||||
encodeURIComponent(server) + ':' + port;
|
||||
|
||||
// Subject line of the email.
|
||||
Accounts.emailTemplates.verifyEmail.subject = function(user) {
|
||||
return 'Confirm Your Email Address for '+siteName;
|
||||
};
|
||||
|
||||
// Email text
|
||||
Accounts.emailTemplates.verifyEmail.text = function(user, url) {
|
||||
return 'Thank you for registering. Please click on the following link to verify your email address: \r\n' + url;
|
||||
};
|
||||
|
||||
// Reset password mail
|
||||
Accounts.emailTemplates.resetPassword.subject = function() {
|
||||
return 'Reset your '+siteName+' password'
|
||||
};
|
||||
|
||||
Accounts.urls.resetPassword = function(token) {
|
||||
return OHIF.utils.absoluteUrl('resetPassword/' + token);
|
||||
};
|
||||
|
||||
Accounts.emailTemplates.resetPassword.text = function(user, url) {
|
||||
return "Hello " + user.profile.fullName + ",\n\n" +
|
||||
"Click the following link to set your new password:\n" +
|
||||
url + "\n\n";
|
||||
};
|
||||
|
||||
// Send email when account is created
|
||||
Accounts.config({
|
||||
sendVerificationEmail: verifyEmail
|
||||
});
|
||||
}
|
||||
});
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.10"
|
||||
#sudo: required
|
||||
|
||||
sudo: required
|
||||
|
||||
env:
|
||||
global:
|
||||
- METEOR_ENV=development
|
||||
|
||||
before_install:
|
||||
- "curl -L http://git.io/ejPSng | /bin/sh"
|
||||
@@ -0,0 +1,34 @@
|
||||
## Contributing
|
||||
|
||||
**Submit your Pull Request on a Feature Branch**
|
||||
|
||||
- To ensure your pull-request has the greatest chance of getting merged in, please submit it on a feature branch rather than directly to master.
|
||||
- Please see [A successful Git branching model](http://nvie.com/posts/a-successful-git-branching-model/) for more details.
|
||||
|
||||
**Quality Assurance**
|
||||
- Pull Requests will be generally accepted as long as the QA tests pass on [Circle CI](https://circleci.com/gh/clinical-meteor/clinical-active-entry).
|
||||
- Begin a Pull Request by logging an Issue for discussion.
|
||||
- Next, clone the package into a project for local development.
|
||||
- Use ``git checkout -b newfeature`` to create a new branch.
|
||||
- Run the package verification tests to ensure that the test runner works.
|
||||
- Add an it() clause at the bottom of the [activeEntryTests.js](https://github.com/clinical-meteor/clinical-active-entry/blob/master/tests/gagarin/activeEntryTests.js) file for each new feature you wish to implement.
|
||||
- Sketch out a test script for a feature.
|
||||
- Run the verification tests, and confirm that the new test fails.
|
||||
- Update the package with the new feature until the new test passes.
|
||||
- Push the package to the repo on GitHub.
|
||||
- Submit a pull-request.
|
||||
- If the PR passes tests on Circle CI, we'll merge it in!
|
||||
|
||||
**Reference Implementation**
|
||||
- Please see the [ChecklistManifest](http://checklist-manifesto.meteor.com/) and it's [source code](https://github.com/clinical-meteor/checklist-manifesto) for a reference implementation of the ActiveEntry package. It's being built as a workqueue, and is being designed to be FDA, HIPAA, and HL7 FHIR compliant.
|
||||
|
||||
**Feature Toggling**
|
||||
- In general, it's a best practice to implement features that can be enabled/disabled. Some people will want a feature; others will not. The best way to toggle features is adding them to a config file, which is what the [ActiveEntry](https://github.com/clinical-meteor/clinical-active-entry/blob/master/lib/ActiveEntry.js) object is for.
|
||||
- It's recommended that fields be added to the ActiveEntryConfig object that can be used to enabled/disable the feature you are implementing.
|
||||
- The ActiveEntryConfig object can be roughly considered as analogous to the ``props`` field that is passed into React templates.
|
||||
|
||||
**Theming and Styling**
|
||||
- Different apps require different presentation/style layers, so Clinical Meteor has implemented a [Theme](https://github.com/clinical-meteor/clinical-theming/blob/master/objects/Theme.js) object which manages theming. Generally speaking, we're trying to avoid the inclusion of third-party component libraries, UI widgets, and CSS frameworks, and keeping the Clinical Meteor packages as close to the default HTML that Blaze produces as possible (while using line styles for animation effects).
|
||||
- Generally speaking, Bootstrap HTML structure in acceptable in designing pages, but avoid including global CSS files, and keep any CSS locally scoped using LESS nested classes. This approach allows people to add the Bootstrap library if they want it, prevents cascading leaks, and allows more fine-grained control over CSS to implement animation effects.
|
||||
- Pull requests that include visual integration with other projects are acceptable, but please submit an issue for discussion before submitting PRs with wholesale changes that include Jade, Material UI, React, etc.
|
||||
- The ActiveEntry pages are designed to be used in an IronRouter Layout template. As such, no height/width constraints are provided. It's assumed that the layout template will take care of such things. But basic padding/margin is provided.
|
||||
Executable
+130
@@ -0,0 +1,130 @@
|
||||
## clinical:active-entry
|
||||
|
||||
This package provides the SignIn, SignUp, and ForgotPassword pages.
|
||||
|
||||
[](https://circleci.com/gh/clinical-meteor/active-entry/tree/master)
|
||||
|
||||
===============================
|
||||
#### Installation
|
||||
|
||||
````
|
||||
meteor add clinical:active-entry
|
||||
````
|
||||
|
||||
===============================
|
||||
#### Entry Flowchart
|
||||
|
||||
The following diagram represents the entry workflow that is being implemented in this package. This package is under active development, and is about half completed. Pull requests which help implement the following workflow will be fast-tracked and accepted into the package.
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
===============================
|
||||
#### Routing API
|
||||
|
||||
````
|
||||
/entrySignIn
|
||||
/entrySignUp
|
||||
/forgotPassword
|
||||
````
|
||||
|
||||
===============================
|
||||
#### Component API
|
||||
|
||||
````
|
||||
{{> entrySignIn }}
|
||||
{{> entrySignUp }}
|
||||
{{> forgotPassword }}
|
||||
````
|
||||
|
||||
|
||||
===============================
|
||||
#### ActiveEntry Configuration
|
||||
|
||||
````js
|
||||
|
||||
if(Meteor.isClient){
|
||||
ActiveEntry.configure({
|
||||
logo: {
|
||||
url: "/mini-circles.png",
|
||||
displayed: true
|
||||
},
|
||||
signIn: {
|
||||
displayFullName: true,
|
||||
destination: "/table/users"
|
||||
},
|
||||
signUp: {
|
||||
destination: "/table/users"
|
||||
},
|
||||
themeColors: {
|
||||
primary: ""
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if(Meteor.isServer){
|
||||
Accounts.emailTemplates.siteName = "AwesomeSite";
|
||||
Accounts.emailTemplates.from = "AwesomeSite Admin <accounts@example.com>";
|
||||
Accounts.emailTemplates.enrollAccount.subject = function (user) {
|
||||
return "Welcome to Awesome Town, " + user.profile.name;
|
||||
};
|
||||
Accounts.emailTemplates.enrollAccount.text = function (user, url) {
|
||||
return "You have been selected to participate in building a better future!"
|
||||
+ " To activate your account, simply click the link below:\n\n"
|
||||
+ url;
|
||||
};
|
||||
|
||||
Meteor.startup(function(){
|
||||
process.env.MAIL_URL = 'smtp://sandboxid.mailgun.org:mypassword@smtp.mailgun.org:587';
|
||||
})
|
||||
}
|
||||
````
|
||||
Alternatively, you may want to set the ``MAIL_URL`` via an external environment variable, particularly if you're using a SaaS hosting provider.
|
||||
|
||||
````sh
|
||||
MAIL_URL = 'smtp://sandboxid.mailgun.org:mypassword@smtp.mailgun.org:587' meteor
|
||||
````
|
||||
|
||||
===============================
|
||||
#### Local Development
|
||||
|
||||
Simply clone the repository into your ``/packages`` directory. You can also specify the packages you want to develop locally in your ``.git-packages.json`` file, and use starrynight to fetch them.
|
||||
|
||||
````bash
|
||||
# clone a single package into your application
|
||||
git clone http://github.com/clinical-meteor/clinical-active-entry packages/active-entry
|
||||
|
||||
# fetch all the packages listed in git-packages.json
|
||||
starrynight fetch
|
||||
````
|
||||
|
||||
===============================
|
||||
#### Quality Assurance Testing
|
||||
|
||||
There are two types of quality assurance tests you can run: verification and validation tests. Verification tests are similar to unit or integration tests; and can run either at the application or package level. Validation tests are application-wide, but often require commands exposed in packages. So you'll need to run the ``autoconfig`` command to scan the filesystem for validation commands. See [http://starrynight.meteor.com/](http://starrynight.meteor.com/) for more details.
|
||||
|
||||
````bash
|
||||
# install the testing utility
|
||||
npm install -g starrynight
|
||||
|
||||
# verification testing (a.k.a. package-level unit/integration testing)
|
||||
starrynight run-tests --type package-verification
|
||||
|
||||
#to run validation tests, you'll need an ``.initializeUsers()`` function
|
||||
meteor add clinical:accounts-housemd
|
||||
|
||||
#validation testing (a.k.a. application acceptance/end-to-end testing)
|
||||
starrynight autoscan
|
||||
starrynight run-tests --type validation
|
||||
````
|
||||
|
||||
===============================
|
||||
#### Contributing
|
||||
|
||||
See our [notes on contributing](https://github.com/clinical-meteor/clinical-active-entry/blob/master/Contributing.md).
|
||||
|
||||
===============================
|
||||
#### Licensing
|
||||
|
||||

|
||||
@@ -0,0 +1,84 @@
|
||||
## Customize the test machine
|
||||
machine:
|
||||
node:
|
||||
version: 0.10.33
|
||||
|
||||
# Timezone
|
||||
timezone:
|
||||
America/Los_Angeles # Set the timezone
|
||||
|
||||
# Add some environment variables
|
||||
environment:
|
||||
CIRCLE_ENV: test
|
||||
CXX: g++-4.8
|
||||
DISPLAY: :99.0
|
||||
NPM_PREFIX: /home/ubuntu/nvm/v0.10.33
|
||||
|
||||
|
||||
## Customize checkout
|
||||
checkout:
|
||||
post:
|
||||
#- git submodule sync
|
||||
#- git submodule update --init --recursive # use submodules
|
||||
|
||||
#general:
|
||||
# build_dir: helloworld
|
||||
|
||||
## Customize dependencies
|
||||
dependencies:
|
||||
cache_directories:
|
||||
- ~/.meteor # relative to the user's home directory
|
||||
- ~/nvm/v0.10.33/lib/node_modules/starrynight
|
||||
- ~/nvm/v0.10.33/bin/starrynight
|
||||
|
||||
pre:
|
||||
# Install Starrynight unless it is cached
|
||||
- if [ ! -e ~/nvm/v0.10.33/bin/starrynight ]; then npm install -g starrynight; else echo "Starrynight seems to be cached"; fi;
|
||||
# Install Meteor
|
||||
- mkdir -p ${HOME}/.meteor
|
||||
# If Meteor is already cached, do not need to build it again.
|
||||
- if [ ! -e ${HOME}/.meteor/meteor ]; then curl https://install.meteor.com | /bin/sh; else echo "Meteor seems to be cached"; fi;
|
||||
# Link the meteor executable into /usr/bin
|
||||
- sudo ln -s $HOME/.meteor/meteor /usr/bin/meteor
|
||||
# Check if the helloworld directory already exists, if it doesn't, create the helloworld app
|
||||
# The following doesn't work, because it should be checking ${HOME}/active-entry/helloworld
|
||||
# - if [ ! -e ${HOME}/helloworld ]; then meteor create --release METEOR@1.1.0.3 helloworld; else echo "helloworld app seems to be cached"; fi;
|
||||
|
||||
override:
|
||||
- cd ${HOME} && meteor create --release METEOR@1.1.0.3 helloworld
|
||||
- cd ${HOME}/helloworld
|
||||
- cd ${HOME}/helloworld && ls -la
|
||||
- cd ${HOME}/helloworld && rm helloworld.*
|
||||
- cd ${HOME}/helloworld && mkdir packages && mkdir packages/active-entry
|
||||
- cp -R * ${HOME}/helloworld/packages/active-entry
|
||||
- cd ${HOME}/helloworld && meteor add anti:gagarin@0.4.11 accounts-base accounts-password session meteor-platform clinical:user-model clinical:active-entry
|
||||
- cd ${HOME}/helloworld && starrynight autoconfig
|
||||
- cd ${HOME}/helloworld && meteor list
|
||||
- cat tests/gagarin/activeEntryTests.js
|
||||
- ls -la
|
||||
- cd ~ && ls -la
|
||||
- cd ${HOME} && pwd
|
||||
- cd ${HOME} && ls -la
|
||||
- cd ${HOME}/helloworld && ls -la
|
||||
|
||||
## Customize test commands
|
||||
test:
|
||||
pre:
|
||||
- cd helloworld && meteor:
|
||||
background: true
|
||||
- sleep 30
|
||||
override:
|
||||
- cd ${HOME}/helloworld && starrynight run-tests --type package-verification
|
||||
|
||||
## Customize deployment commands
|
||||
#deployment:
|
||||
# production:
|
||||
# branch: master
|
||||
# commands:
|
||||
# - printf "<Meteor username>\n<Meteor password>\n" | meteor deploy circlecivelocity.meteor.com
|
||||
|
||||
## Custom notifications
|
||||
#notify:
|
||||
#webhooks:
|
||||
# A list of hashes representing hooks. Only the url field is supported.
|
||||
#- url: https://someurl.com/hooks/circle
|
||||
@@ -0,0 +1,48 @@
|
||||
<template name="changePassword">
|
||||
<div id="changePassword" class="page entryPage" style="{{getOpacityWithCorner}}">
|
||||
<div class="content-scrollable">
|
||||
<div class="wrapper-auth">
|
||||
<div class="entryLogo" style="background-image: url('{{getLogoUrl}}')"></div>
|
||||
|
||||
<h1 id="changePasswordPageTitle" class="title-auth">Change Password</h1>
|
||||
<div id="changePasswordPageMessage" class="subtitle-auth" style="{{getChangePasswordMessageColor}}">{{getChangePasswordMessage}}</div>
|
||||
|
||||
<form>
|
||||
{{#if changePasswordErrorMessages}}
|
||||
<div id="errorMessages" class="list-errors">
|
||||
{{#each changePasswordErrorMessages}}
|
||||
<div class="alert alert-danger list-item">{{this}}</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="input-symbol">
|
||||
<input id="changePasswordPageOldPasswordInput" type="password" name="oldPassword" placeholder="Old Password" style="{{getPasswordStyling}}" />
|
||||
<span class="fa fa-lock" title="Password"></span>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<div class="input-symbol">
|
||||
<input id="changePasswordPagePasswordInput" type="password" name="password" placeholder="Password" style="{{getPasswordStyling}}" />
|
||||
<span class="fa fa-lock" title="Password"></span>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<div class="input-symbol">
|
||||
<input id="changePasswordPagePasswordConfirmInput" type="password" name="confirm" placeholder="Confirm Password" style="{{getConfirmPasswordStyling}}" />
|
||||
<span class="fa fa-lock" title="Confirm Password"></span>
|
||||
</div>
|
||||
|
||||
<br><br>
|
||||
|
||||
<button id="changePasswordButton" type="submit" class="btn-gray btn-main btn-large" style="{{getButtonColor}}">Change Password</button>
|
||||
<br><br>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,100 @@
|
||||
//==========================================
|
||||
|
||||
Router.route('/changePassword', {
|
||||
name: "changePassword",
|
||||
template: "changePassword"
|
||||
});
|
||||
|
||||
|
||||
Template.changePassword.helpers({
|
||||
getChangePasswordMessageColor: function (){
|
||||
if (ActiveEntry.errorMessages.get('changePasswordError')) {
|
||||
return "color: #a94442; background-color: #f2dede; border-color: #ebccd1;"
|
||||
} else {
|
||||
return "color: black;"
|
||||
}
|
||||
},
|
||||
getChangePasswordMessage: function (){
|
||||
if (ActiveEntry.errorMessages.get('changePasswordError')) {
|
||||
return ActiveEntry.errorMessages.get('changePasswordError');
|
||||
} else {
|
||||
return Session.get('defaultSignInMessage');
|
||||
}
|
||||
},
|
||||
getPasswordStyling: function () {
|
||||
if (ActiveEntry.errorMessages.equals('password', "Password is required")) {
|
||||
return "border: 1px solid #a94442";
|
||||
} else if (ActiveEntry.errorMessages.equals('password', "Password is weak")) {
|
||||
return "border: 1px solid #f2dede";
|
||||
} else if (ActiveEntry.errorMessages.equals('password', "Password present")) {
|
||||
return "border: 1px solid green";
|
||||
} else {
|
||||
return "border: 1px solid gray";
|
||||
}
|
||||
},
|
||||
getConfirmPasswordStyling: function () {
|
||||
if (ActiveEntry.errorMessages.equals('confirm', "Password is required")) {
|
||||
return "border: 1px solid #a94442";
|
||||
} else if (ActiveEntry.errorMessages.equals('confirm', "Passwords do not match")) {
|
||||
return "border: 1px solid #a94442";
|
||||
} else if (ActiveEntry.errorMessages.equals('confirm', "Password is weak")) {
|
||||
return "border: 1px solid #f2dede";
|
||||
} else if (ActiveEntry.errorMessages.equals('confirm', "Passwords match")) {
|
||||
return "border: 1px solid green";
|
||||
} else {
|
||||
return "border: 1px solid gray";
|
||||
}
|
||||
},
|
||||
|
||||
changePasswordErrorMessages: function() {
|
||||
var allErrorMessages = Object.keys(ActiveEntry.errorMessages.all()).filter(function(key) {
|
||||
return (key === "password" || key === "confirm") && ActiveEntry.errorMessages.get(key);
|
||||
});
|
||||
|
||||
if (allErrorMessages.length > 0) {
|
||||
var errorMessage = ActiveEntry.errorMessages.get(allErrorMessages[0]);
|
||||
if (errorMessage) {
|
||||
return [errorMessage];
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Template.changePassword.events({
|
||||
'change, keyup #changePasswordPagePasswordInput': function (event, template) {
|
||||
var password = $('[name="password"]').val();
|
||||
var confirmPassword = $('[name="confirm"]').val();
|
||||
|
||||
ActiveEntry.verifyPassword(password);
|
||||
ActiveEntry.errorMessages.set('changePasswordError', null);
|
||||
},
|
||||
'change, keyup #changePasswordPagePasswordConfirmInput': function (event, template) {
|
||||
var password = $('[name="password"]').val();
|
||||
var confirmPassword = $('[name="confirm"]').val();
|
||||
|
||||
ActiveEntry.verifyConfirmPassword(password, confirmPassword);
|
||||
ActiveEntry.errorMessages.set('changePasswordError', null);
|
||||
},
|
||||
"submit": function (event, template) {
|
||||
event.preventDefault();
|
||||
|
||||
var oldPassword = $('[name="oldPassword"]').val();
|
||||
|
||||
var password = $('[name="password"]').val();
|
||||
var confirmPassword = $('[name="confirm"]').val();
|
||||
|
||||
ActiveEntry.verifyConfirmPassword(password, confirmPassword);
|
||||
ActiveEntry.errorMessages.set('changePasswordError', null);
|
||||
|
||||
|
||||
if (ActiveEntry.errorMessages.get('password') || ActiveEntry.errorMessages.get('confirm') || !oldPassword) {
|
||||
return;
|
||||
}
|
||||
|
||||
ActiveEntry.changePassword(oldPassword, password);
|
||||
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
#changePassword{
|
||||
input{
|
||||
padding-left: 40px;
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
|
||||
/**
|
||||
* @summary Determines if a block of code should be displayed based on whether the logo is set to be displayed.
|
||||
* @locus Client
|
||||
* @memberOf Entry
|
||||
* @name {{logoIsDisplayed}}
|
||||
* @version 1.2.3
|
||||
* @returns {Boolean}
|
||||
* @example
|
||||
* ```html
|
||||
* {{#if logoIsDisplayed}}
|
||||
* <div class="entryLogo" style="background-image: url('{{logoUrl}}')"></div>
|
||||
* {{/if}}
|
||||
* ```
|
||||
*/
|
||||
Template.registerHelper("logoIsDisplayed", function (argument){
|
||||
var config = Session.get('Photonic.ActiveEntry');
|
||||
if(config && config.logo && config.logo.displayed){
|
||||
return config.logo.displayed;
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* @summary Retruns the Url of the logo asset.
|
||||
* @locus Client
|
||||
* @memberOf Entry
|
||||
* @name {{logoUrl}}
|
||||
* @version 1.2.3
|
||||
* @returns {String}
|
||||
* @example
|
||||
* ```html
|
||||
* {{#if logoIsDisplayed}}
|
||||
* <div class="entryLogo" style="background-image: url('{{logoUrl}}')"></div>
|
||||
* {{/if}}
|
||||
* ```
|
||||
*/
|
||||
Template.registerHelper("logoUrl", function (argument){
|
||||
var config = Session.get('Photonic.ActiveEntry');
|
||||
if(config && config.logo && config.logo.url){
|
||||
return config.logo.url;
|
||||
}else{
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Template.registerHelper("getButtonColor", function (argument){
|
||||
return "background-color: " + Theme.getPaletteColor("colorB") + "; ";
|
||||
});
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
// @import 'client/app/stylesheets/util/reset.lessimport';
|
||||
// @import 'client/app/stylesheets/util/lesshat.lessimport';
|
||||
// @import 'client/app/stylesheets/util/typography.lessimport';
|
||||
|
||||
|
||||
// this probably wants to be moved into clinical:glass-ui
|
||||
.entryPage {
|
||||
input, button, select{
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-family: 'Open Sans', "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-style: 400;
|
||||
padding: .75rem 0;
|
||||
line-height: 1.5rem !important;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-sizing: border-box;
|
||||
//color: #333333;
|
||||
outline: none;
|
||||
padding-left: 3em;
|
||||
}
|
||||
}
|
||||
|
||||
.entryLogo{
|
||||
//background-color: lightgray;
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.btn-primary{
|
||||
background-color: forestgreen;
|
||||
color: #ffffff;
|
||||
}
|
||||
.btn-gray{
|
||||
background-color: #aaaaaa;
|
||||
color: white;
|
||||
}
|
||||
.btn-main{
|
||||
font-weight: 300;
|
||||
letter-spacing: 3px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.input-symbol {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.entryPage {
|
||||
text-align: center;
|
||||
|
||||
input, button{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fa{
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
padding-left: 15px;
|
||||
padding-top: 14px;
|
||||
}
|
||||
|
||||
.checkmarkIcon{
|
||||
position: absolute;
|
||||
color: #831d2c;
|
||||
}
|
||||
.btn-gray{
|
||||
width: 100%;
|
||||
}
|
||||
.checkmarkTitle{
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
.fa{
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
padding-left: 15px;
|
||||
padding-top: 16px;
|
||||
// color: #555555;
|
||||
}
|
||||
|
||||
.wrapper-auth {
|
||||
//padding-top: 4em;
|
||||
|
||||
@media screen and (min-width: 40em) {
|
||||
margin: 0 auto;
|
||||
max-width: 480px;
|
||||
width: 80%;
|
||||
}
|
||||
@media screen and (max-width: 480px) {
|
||||
margin: 0 auto;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
// .input-symbol{
|
||||
// border: 1px solid gray;
|
||||
// }
|
||||
|
||||
.title-auth {
|
||||
color: black;
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
|
||||
.subtitle-auth {
|
||||
color: black;
|
||||
//margin: 0 15% 3rem;
|
||||
margin-bottom: 2rem;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
form {
|
||||
input{
|
||||
color: #333333;
|
||||
height: 48px;
|
||||
border-left: solid 1px !important;
|
||||
}
|
||||
.input-symbol {
|
||||
margin-bottom: 1px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
//margin: 1em 5% 0;
|
||||
//width: 90%;
|
||||
margin-top: 1em;
|
||||
|
||||
@media screen and (min-width: 40em) {
|
||||
// margin-left: 0;
|
||||
// margin-right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.list-errors {
|
||||
margin-top: -2rem;
|
||||
.taskItems {
|
||||
//.title-caps;
|
||||
background: white;
|
||||
color: red;
|
||||
font-size: .625em; // 10px
|
||||
margin-bottom: 1px;
|
||||
padding: .7rem 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-auth-alt {
|
||||
//.font-s1;
|
||||
//.position(absolute, auto, 0, 1em, 0);
|
||||
color: black;
|
||||
display: inline-block;
|
||||
|
||||
@media screen and (min-width: 40em) {
|
||||
bottom: 0;
|
||||
margin-top: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
// @media only screen and (max-width: 704px) {
|
||||
// .fa{
|
||||
// left: 5%;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
|
||||
|
||||
exports.command = function () {
|
||||
this
|
||||
.execute(function () {
|
||||
return Meteor.logout();
|
||||
}).pause(1000);
|
||||
|
||||
return this;
|
||||
};
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
exports.command = function (email, password) {
|
||||
|
||||
this.verify.elementPresent("#entrySignIn");
|
||||
|
||||
if (email) {
|
||||
this
|
||||
.verify.elementPresent("#signInPageEmailInput")
|
||||
.clearValue("#signInPageEmailInput")
|
||||
.setValue("#signInPageEmailInput", email);
|
||||
}
|
||||
|
||||
|
||||
if (password) {
|
||||
this
|
||||
.verify.elementPresent("#signInPagePasswordInput")
|
||||
.clearValue("#signInPagePasswordInput")
|
||||
.setValue("#signInPagePasswordInput", password);
|
||||
}
|
||||
|
||||
this.click("#signInToAppButton").pause(1000);
|
||||
|
||||
return this;
|
||||
};
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
exports.command = function (fullname) {
|
||||
this
|
||||
.sectionBreak(".signOut()");
|
||||
|
||||
if (fullname) {
|
||||
this
|
||||
.verify.elementPresent("#usernameLink")
|
||||
.verify.containsText("#usernameLink", fullname);
|
||||
}
|
||||
|
||||
this
|
||||
.verify.elementPresent("#logoutLink")
|
||||
.click("#logoutLink").pause(1000);
|
||||
|
||||
if (fullname) {
|
||||
this
|
||||
.verify.elementPresent("#usernameLink")
|
||||
.verify.containsText("#usernameLink", "Sign In");
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
exports.command = function (email, password, title, message) {
|
||||
this
|
||||
// .verify.element("#entrySignIn").to.be.visible
|
||||
// .verify.element("#signInPageTitle").to.be.visible
|
||||
// .verify.element("#signInPageMessage").to.be.visible
|
||||
// .verify.element("#signInPageEmailInput").to.be.visible
|
||||
// .verify.element("#signInPagePasswordInput").to.be.visible
|
||||
// .verify.element("#signInToAppButton").to.be.visible
|
||||
// .verify.element("#needAnAccountButton").to.be.visible
|
||||
//
|
||||
|
||||
.verify.elementPresent("#entrySignIn")
|
||||
.verify.elementPresent("#signInPageTitle")
|
||||
.verify.elementPresent("#signInPageMessage")
|
||||
.verify.elementPresent("#signInPageEmailInput")
|
||||
.verify.elementPresent("#signInPagePasswordInput")
|
||||
.verify.elementPresent("#signInToAppButton")
|
||||
.verify.elementPresent("#needAnAccountButton");
|
||||
|
||||
if (email) {
|
||||
this.verify.containsText("#signInPageEmailInput", email);
|
||||
}
|
||||
if (password) {
|
||||
this.verify.containsText("#signInPageEmailInput", password);
|
||||
}
|
||||
|
||||
if (title) {
|
||||
this.verify.containsText("#signInPageTitle", title);
|
||||
}
|
||||
if (message) {
|
||||
this.verify.containsText("#signInPageMessage", message);
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
<template name="entrySignIn">
|
||||
<div id="entrySignIn" class="page entryPage" style="{{getOpacityWithCorner}}">
|
||||
|
||||
<div class="content-scrollable">
|
||||
<div class="wrapper-auth">
|
||||
{{#if logoIsDisplayed}}
|
||||
<div id="entryAppLogo" class="entryLogo" style="background-image: url('{{logoUrl}}')"></div>
|
||||
{{/if}}
|
||||
|
||||
{{#if isLoggedIn}}
|
||||
<h1 id="signInPageTitle" class="title-auth">{{fullName}}</h1>
|
||||
<div id="signInPageMessage" class="subtitle-auth">You are currently signed in.</div>
|
||||
|
||||
<button id="logoutButton" class="btn-primary btn-main btn-large" style="{{getButtonColor}}">Logout</button>
|
||||
{{else}}
|
||||
<h1 id="signInPageTitle" class="title-auth">Sign In.</h1>
|
||||
<p id="signInPageMessage" class="subtitle-auth" style="{{getSignInMessageColor}}">{{{getSignInMessage}}}</p>
|
||||
|
||||
{{#if isLDAPSet}}
|
||||
<form>
|
||||
{{#if ldapErrorMessages}}
|
||||
<div id="errorMessages" class="list-errors">
|
||||
{{#each ldapErrorMessages}}
|
||||
<div class="alert alert-danger list-item">{{this}}</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="input-symbol">
|
||||
<input id="signInLDAPUsernameInput" type="text" name="username" placeholder="Username" style="{{getLDAPUsernameValidationStyling}}" />
|
||||
<i class="fa fa-envelope-o" title="Your Email"></i>
|
||||
</div>
|
||||
<br><br>
|
||||
|
||||
<div class="input-symbol">
|
||||
<input id="signInLDAPPasswordInput" type="password" name="password" placeholder="Password" style={{getLDAPPasswordValidationStyling}} />
|
||||
<span class="fa fa-lock" title="Password"></span>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<br><br>
|
||||
<button id="signInLDAPToAppButton" class="btn-primary btn-main btn-large" style="{{getButtonColor}}">{{getButtonText}}</button>
|
||||
{{else}}
|
||||
<form>
|
||||
|
||||
<div class="input-symbol">
|
||||
<input id="signInPageEmailInput" type="text" name="email" placeholder="Your Email" style="{{getEmailValidationStyling}}" />
|
||||
<i class="fa fa-envelope-o" title="Your Email"></i>
|
||||
</div>
|
||||
<br><br>
|
||||
|
||||
<div class="input-symbol">
|
||||
<input id="signInPagePasswordInput" type="password" name="password" placeholder="Password" style={{getPasswordValidationStyling}} />
|
||||
<span class="fa fa-lock" title="Password"></span>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<br><br>
|
||||
<button id="signInToAppButton" class="btn-primary btn-main btn-large disabledButton" disabled style="{{getButtonColor}}">{{getButtonText}}</button>
|
||||
|
||||
<br><br>
|
||||
<button id="needAnAccountButton" class="btn-gray btn-main btn-large">Need an account?</button>
|
||||
|
||||
<br><br>
|
||||
<button id="forgotPasswordButton" class="btn-gray btn-main btn-large">Forgot password?</button>
|
||||
|
||||
{{/if}}
|
||||
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,205 @@
|
||||
|
||||
// REFACTOR: Move to ActiveRecord object
|
||||
//Session.set("defaultSignInMessage", "Improve your clinical practice with checklists.");
|
||||
|
||||
//==================================================================================================
|
||||
// ROUTER
|
||||
|
||||
Router.route('/entrySignIn', {
|
||||
template: 'entrySignIn',
|
||||
name: 'entrySignIn'
|
||||
});
|
||||
Router.route('/sign-in', {
|
||||
template: 'entrySignIn',
|
||||
name: 'signInRoute'
|
||||
});
|
||||
|
||||
//==================================================================================================
|
||||
// COMPONENT OUTPUTS
|
||||
|
||||
|
||||
|
||||
|
||||
Template.entrySignIn.helpers({
|
||||
getSignInMessageColor: function (){
|
||||
if (ActiveEntry.errorMessages.get('signInError')) {
|
||||
return "color: #a94442; background-color: #f2dede; border-color: #ebccd1;"
|
||||
} else {
|
||||
return "color: black;"
|
||||
}
|
||||
},
|
||||
getSignInMessage: function (){
|
||||
if (ActiveEntry.errorMessages.get('signInError')) {
|
||||
return ActiveEntry.errorMessages.get('signInError');
|
||||
} else {
|
||||
return Session.get('defaultSignInMessage');
|
||||
}
|
||||
},
|
||||
getButtonText: function () {
|
||||
return "Sign In";
|
||||
// if (ActiveEntry.errorMessages.get('signInError')){
|
||||
// return ActiveEntry.errorMessages.get('signInError');
|
||||
// } else {
|
||||
// return "Sign In";
|
||||
// }
|
||||
},
|
||||
getEmailValidationStyling: function () {
|
||||
if (ActiveEntry.errorMessages.equals('email', "Email is required")) {
|
||||
return "border: 1px solid #a94442";
|
||||
} else if (ActiveEntry.errorMessages.equals('email', "Email is poorly formatted")) {
|
||||
return "border: 1px solid #f2dede";
|
||||
} else if (ActiveEntry.successMessages.equals('email', "Email present")) {
|
||||
return "border: 1px solid green";
|
||||
} else {
|
||||
return "border: 1px solid gray";
|
||||
}
|
||||
},
|
||||
getPasswordValidationStyling: function () {
|
||||
if (ActiveEntry.errorMessages.equals('password', "Password is required")) {
|
||||
return "border: 1px solid #a94442";
|
||||
} else if (ActiveEntry.errorMessages.equals('password', Session.get('passwordWarning'))) {
|
||||
return "border: 1px solid #f2dede";
|
||||
} else if (ActiveEntry.successMessages.equals('password', "Password present")) {
|
||||
return "border: 1px solid green";
|
||||
} else {
|
||||
return "border: 1px solid gray";
|
||||
}
|
||||
},
|
||||
|
||||
getLDAPUsernameValidationStyling: function() {
|
||||
if (ActiveEntry.errorMessages.equals('ldapUsername', "Username is required")) {
|
||||
return "border: 1px solid #a94442";
|
||||
} else if (ActiveEntry.successMessages.equals('ldapUsername', "Username present")) {
|
||||
return "border: 1px solid green";
|
||||
} else {
|
||||
return "border: 1px solid gray";
|
||||
}
|
||||
},
|
||||
getLDAPPasswordValidationStyling: function() {
|
||||
if (ActiveEntry.errorMessages.equals('ldapPassword', "Password is required")) {
|
||||
return "border: 1px solid #a94442";
|
||||
} else if (ActiveEntry.successMessages.equals('ldapPassword', "Password present")) {
|
||||
return "border: 1px solid green";
|
||||
} else {
|
||||
return "border: 1px solid gray";
|
||||
}
|
||||
},
|
||||
isLDAPSet: function() {
|
||||
return Session.get('isLDAPSet');
|
||||
},
|
||||
ldapErrorMessages: function () {
|
||||
if (ActiveEntry.errorMessages.get("ldapUsername")) {
|
||||
return [ActiveEntry.errorMessages.get("ldapUsername")];
|
||||
} else if(ActiveEntry.errorMessages.get("ldapPassword")) {
|
||||
return [ActiveEntry.errorMessages.get("ldapPassword")];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
//==================================================================================================
|
||||
// COMPONENT OUTPUTS
|
||||
|
||||
Template.entrySignIn.events({
|
||||
'click #logoutButton': function () {
|
||||
Meteor.logout();
|
||||
},
|
||||
'click #forgotPasswordButton': function (event) {
|
||||
event.preventDefault();
|
||||
ActiveEntry.reset();
|
||||
Router.go('/forgotPassword');
|
||||
},
|
||||
"click #needAnAccountButton": function (event) {
|
||||
event.preventDefault();
|
||||
ActiveEntry.reset();
|
||||
Router.go('/entrySignUp');
|
||||
},
|
||||
'keyup input[name="email"]': function (event, template) {
|
||||
var email = $('input[name="email"]').val();
|
||||
|
||||
ActiveEntry.verifyEmail(email);
|
||||
ActiveEntry.errorMessages.set('signInError', null);
|
||||
setSignInButtonStyling();
|
||||
},
|
||||
'change input[name="email"]': function (event, template) {
|
||||
var email = $('input[name="email"]').val();
|
||||
|
||||
ActiveEntry.verifyEmail(email);
|
||||
ActiveEntry.errorMessages.set('signInError', null);
|
||||
setSignInButtonStyling();
|
||||
},
|
||||
'keyup #signInPagePasswordInput': function (event, template) {
|
||||
var password = $('input[name="password"]').val();
|
||||
|
||||
ActiveEntry.verifyPassword(password);
|
||||
ActiveEntry.errorMessages.set('signInError', null);
|
||||
setSignInButtonStyling();
|
||||
},
|
||||
'change #signInPagePasswordInput': function (event, template) {
|
||||
var password = $('input[name="password"]').val();
|
||||
|
||||
ActiveEntry.verifyPassword(password);
|
||||
ActiveEntry.errorMessages.set('signInError', null);
|
||||
setSignInButtonStyling();
|
||||
},
|
||||
'keyup, change #signInLDAPUsernameInput': function (event, template) {
|
||||
var username = $('#signInLDAPUsernameInput').val();
|
||||
ActiveEntry.verifyLDAPUsername(username);
|
||||
ActiveEntry.errorMessages.set('signInError', null);
|
||||
},
|
||||
'keyup, change #signInLDAPPasswordInput': function (event, template) {
|
||||
var password = $('#signInLDAPPasswordInput').val();
|
||||
ActiveEntry.verifyLDAPPassword(password);
|
||||
ActiveEntry.errorMessages.set('signInError', null);
|
||||
},
|
||||
// 'submit': function (event, template) {
|
||||
// event.preventDefault();
|
||||
// var emailValue = template.$('[name=email]').val();
|
||||
// var passwordValue = template.$('[name=password]').val();
|
||||
//
|
||||
// ActiveEntry.signIn(emailValue, passwordValue);
|
||||
// },
|
||||
'click #signInToAppButton': function (event, template){
|
||||
ActiveEntry.reset();
|
||||
// var emailValue = template.$('[name=email]').val();
|
||||
// var passwordValue = template.$('[name=password]').val();
|
||||
var emailValue = template.$('#signInPageEmailInput').val();
|
||||
var passwordValue = template.$('#signInPagePasswordInput').val();
|
||||
|
||||
ActiveEntry.signIn(emailValue, passwordValue);
|
||||
event.preventDefault();
|
||||
},
|
||||
'keyup #entrySignIn': function(event, template) {
|
||||
if(event.keyCode == 13) {
|
||||
$("#signInToAppButton").click();
|
||||
$("#signInLDAPToAppButton").click();
|
||||
|
||||
}
|
||||
},
|
||||
'click #signInLDAPToAppButton': function(e, template) {
|
||||
var username = template.$("#signInLDAPUsernameInput").val();
|
||||
var password = template.$("#signInLDAPPasswordInput").val();
|
||||
ActiveEntry.loginWithLDAP(username, password);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
//==================================================================================================
|
||||
|
||||
// Sets SignInButton Styling according to email and password fields
|
||||
function setSignInButtonStyling() {
|
||||
var signInToAppButton = $("#signInToAppButton");
|
||||
if ($("#signInPagePasswordInput").val() && ActiveEntry.successMessages.get('email')) {
|
||||
// Set button as enable
|
||||
signInToAppButton.removeClass("disabledButton");
|
||||
signInToAppButton.attr("disabled", false);
|
||||
} else {
|
||||
signInToAppButton.addClass("disabledButton");
|
||||
signInToAppButton.attr("disabled", true);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#entrySignIn{
|
||||
padding: 40px;
|
||||
input{
|
||||
padding-left: 40px;
|
||||
color: #333333 !important;
|
||||
}
|
||||
#errorMessages{
|
||||
padding-top: .5rem;
|
||||
padding-bottom: .5rem;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
width: 100.5%;
|
||||
margin-bottom: 15px;
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.disabledButton {
|
||||
border: none;
|
||||
background-image: none;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
||||
filter: alpha(opacity=40);
|
||||
-khtml-opacity: 0.40;
|
||||
-moz-opacity: 0.40;
|
||||
opacity: 0.40;
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// async version calls method on the server
|
||||
exports.command = function () {
|
||||
var client = this;
|
||||
|
||||
this
|
||||
.timeoutsAsyncScript(5000)
|
||||
.executeAsync(function (data, meteorCallback) {
|
||||
//return HipaaLogger.logEventObject(data);
|
||||
Meteor.call("dropEntryUsers", data, function (meteorError, meteorResult) {
|
||||
var response = (meteorError ? {
|
||||
error: meteorError
|
||||
} : {
|
||||
result: meteorResult
|
||||
});
|
||||
meteorCallback(response);
|
||||
});
|
||||
}, [], function (result) {
|
||||
//console.log("result.value", result.value);
|
||||
//client.assert.ok(result.value);
|
||||
}).pause(1000);
|
||||
return this;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
// resetEntry.js
|
||||
exports.command = function () {
|
||||
|
||||
this
|
||||
.timeoutsAsyncScript(5000)
|
||||
.executeAsync(function (data, meteorCallback) {
|
||||
ActiveEntry.reset();
|
||||
}, []).pause(1000);
|
||||
return this;
|
||||
};
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
exports.command = function (email, password, fullName) {
|
||||
|
||||
this.verify.elementPresent("#entrySignUp");
|
||||
|
||||
if (email) {
|
||||
this
|
||||
.verify.elementPresent("#signUpPageEmailInput")
|
||||
.clearValue("#signUpPageEmailInput")
|
||||
.setValue("#signUpPageEmailInput", email);
|
||||
}
|
||||
|
||||
|
||||
if (password) {
|
||||
this
|
||||
.verify.elementPresent("#signUpPagePasswordInput")
|
||||
.clearValue("#signUpPagePasswordInput")
|
||||
.setValue("#signUpPagePasswordInput", password)
|
||||
|
||||
.verify.elementPresent("#signUpPagePasswordConfirmInput")
|
||||
.clearValue("#signUpPagePasswordConfirmInput")
|
||||
.setValue("#signUpPagePasswordConfirmInput", password);
|
||||
}
|
||||
|
||||
|
||||
if (fullName) {
|
||||
this
|
||||
.verify.elementPresent("#signUpPageFullNameInput")
|
||||
.clearValue("#signUpPageFullNameInput")
|
||||
.setValue("#signUpPageFullNameInput", fullName);
|
||||
}
|
||||
|
||||
|
||||
this.click("#signUpPageJoinNowButton").pause(300);
|
||||
|
||||
|
||||
return this;
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
exports.command = function (email, password, fullName, message) {
|
||||
this
|
||||
.verify.elementPresent("#entrySignUp")
|
||||
.verify.elementPresent("#signUpPageTitle")
|
||||
.verify.elementPresent("#signUpPageMessage")
|
||||
.verify.elementPresent("#signUpPageFullNameInput")
|
||||
.verify.elementPresent("#signUpPageEmailInput")
|
||||
.verify.elementPresent("#signUpPagePasswordInput")
|
||||
.verify.elementPresent("#signUpPagePasswordConfirmInput")
|
||||
|
||||
.verify.elementPresent("#signUpPageJoinNowButton")
|
||||
.verify.elementPresent("#signUpPageSignInButton");
|
||||
|
||||
if (email) {
|
||||
this.verify.containsText("#signUpPageEmailInput", email);
|
||||
}
|
||||
if (password) {
|
||||
this.verify.containsText("#signUpPagePasswordInput", password)
|
||||
.verify.containsText("#signUpPagePasswordConfirmInput", password);
|
||||
}
|
||||
if (fullName) {
|
||||
this.verify.containsText("#signUpPageFullNameInput", fullName);
|
||||
}
|
||||
if (message) {
|
||||
this.verify.elementPresent("#errorMessages")
|
||||
.verify.containsText("#errorMessages", message);
|
||||
}
|
||||
|
||||
|
||||
return this;
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
<template name="entrySignUp">
|
||||
<div id="entrySignUp" class="page entryPage" style="{{getOpacityWithCorner}}">
|
||||
|
||||
<div class="content-scrollable">
|
||||
<div class="wrapper-auth">
|
||||
{{#if logoIsDisplayed}}
|
||||
<div id="entryAppLogo" class="entryLogo" style="background-image: url('{{logoUrl}}')"></div>
|
||||
{{/if}}
|
||||
|
||||
<h1 id="signUpPageTitle" class="title-auth">Join.</h1>
|
||||
<div id="signUpPageMessage" class="subtitle-auth" style="{{getSignUpMessageColor}}">{{getSignUpMessage}}</div>
|
||||
|
||||
<form id="entrySignUpForm">
|
||||
|
||||
{{#if entryErrorMessages}}
|
||||
<div id="errorMessages" class="list-errors">
|
||||
{{#each entryErrorMessages}}
|
||||
<div class="alert alert-danger list-item">{{this}}</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="input-symbol ">
|
||||
<input id="signUpPageFullNameInput" type="text" name="fullName" placeholder="Full Name" style="{{getFullNameStyling}}" />
|
||||
<i class="fa fa-user" title="Full Name"></i>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<div class="input-symbol">
|
||||
<input id="signUpPageEmailInput" type="email" name="email" placeholder="Email Address" style="{{getEmailStyling}}" />
|
||||
<i class="fa fa-envelope-o" title="Your Email"></i>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<div class="input-symbol ">
|
||||
<input id="signUpPagePasswordInput" type="password" name="password" placeholder="Password" style="{{getPasswordStyling}}" />
|
||||
<span class="fa fa-lock" title="Password"></span>
|
||||
<!-- <span class="icon-lock" title="Password"></span> -->
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<div class="input-symbol">
|
||||
<input id="signUpPagePasswordConfirmInput" type="password" name="confirm" placeholder="Confirm Password" style="{{getConfirmPasswordStyling}}" />
|
||||
<span class="fa fa-lock" title="Confirm Password"></span>
|
||||
<!-- <span class="icon-lock" title="Confirm Password"></span> -->
|
||||
</div>
|
||||
</form>
|
||||
<br>
|
||||
<br>
|
||||
<button id="signUpPageJoinNowButton" type="submit" class="btn-primary btn-main btn-large" style="{{getButtonColor}}">Join Now</button>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
<button id="signUpPageSignInButton" class="btn-gray btn-main btn-large">Have an account? Sign in</button>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- <a id="signUpPageSignInButton" href="{{pathFor 'entrySignIn'}}" class="link-auth-alt">Have an account? Sign in</a> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,171 @@
|
||||
//==================================================================================================
|
||||
// ROUTER
|
||||
|
||||
Router.route('/entrySignUp', {
|
||||
template: 'entrySignUp',
|
||||
name: 'entrySignUp'
|
||||
});
|
||||
Router.route('/sign-up', {
|
||||
template: 'entrySignUp',
|
||||
name: 'signUpRoute'
|
||||
});
|
||||
|
||||
//==================================================================================================
|
||||
|
||||
|
||||
|
||||
Template.entrySignUp.helpers({
|
||||
getSignUpMessageColor: function (){
|
||||
if (ActiveEntry.errorMessages.get('signInError')) {
|
||||
return "color: #a94442; background-color: #f2dede; border-color: #ebccd1;";
|
||||
} else {
|
||||
return "color: black;";
|
||||
}
|
||||
},
|
||||
getSignUpMessage: function (){
|
||||
if (ActiveEntry.errorMessages.get('signInError')) {
|
||||
return ActiveEntry.errorMessages.get('signInError');
|
||||
} else {
|
||||
return Session.get('defaultSignInMessage');
|
||||
}
|
||||
},
|
||||
entryErrorMessages: function () {
|
||||
var allErrorMessages = Object.keys(ActiveEntry.errorMessages.all()).filter(function(key) {
|
||||
return key !== "signInError" && ActiveEntry.errorMessages.get(key);
|
||||
});
|
||||
|
||||
if (allErrorMessages.length > 0) {
|
||||
var errorMessage = ActiveEntry.errorMessages.get(allErrorMessages[0]);
|
||||
if (errorMessage) {
|
||||
return [errorMessage];
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
},
|
||||
getButtonText: function () {
|
||||
if (ActiveEntry.errorMessages.get('signInError')) {
|
||||
return ActiveEntry.errorMessages.get('signInError').message;
|
||||
} else {
|
||||
return "Sign In";
|
||||
}
|
||||
},
|
||||
getEmailStyling: function () {
|
||||
if (ActiveEntry.errorMessages.equals('email', "Email is required")) {
|
||||
return "border: 1px solid #a94442";
|
||||
} else if (ActiveEntry.errorMessages.equals('email', "Email is poorly formatted")) {
|
||||
return "border: 1px solid #f2dede";
|
||||
} else if (ActiveEntry.successMessages.equals('email', "Email present")) {
|
||||
return "border: 1px solid green";
|
||||
} else {
|
||||
return "border: 1px solid gray";
|
||||
}
|
||||
},
|
||||
getPasswordStyling: function () {
|
||||
if (ActiveEntry.errorMessages.equals('password', "Password is required")) {
|
||||
return "border: 1px solid #a94442";
|
||||
} else if (ActiveEntry.errorMessages.equals('password', Session.get('passwordWarning'))) {
|
||||
return "border: 1px solid #f2dede";
|
||||
} else if (ActiveEntry.successMessages.equals('password', "Password present")) {
|
||||
return "border: 1px solid green";
|
||||
} else {
|
||||
return "border: 1px solid gray";
|
||||
}
|
||||
},
|
||||
getConfirmPasswordStyling: function () {
|
||||
if (ActiveEntry.errorMessages.equals('confirm', "Password is required")) {
|
||||
return "border: 1px solid #a94442";
|
||||
} else if (ActiveEntry.errorMessages.equals('confirm', "Passwords do not match")) {
|
||||
return "border: 1px solid #a94442";
|
||||
} else if (ActiveEntry.errorMessages.equals('confirm', "Password is weak")) {
|
||||
return "border: 1px solid #f2dede";
|
||||
} else if (ActiveEntry.successMessages.equals('confirm', "Passwords match")) {
|
||||
return "border: 1px solid green";
|
||||
} else {
|
||||
return "border: 1px solid gray";
|
||||
}
|
||||
},
|
||||
getFullNameStyling: function () {
|
||||
if (ActiveEntry.errorMessages.equals('fullName', "Name is required")) {
|
||||
return "border: 1px solid #a94442";
|
||||
} else if (ActiveEntry.errorMessages.equals('fullName', "Name is probably not complete")) {
|
||||
return "border: 1px solid #f2dede";
|
||||
} else if (ActiveEntry.successMessages.equals('fullName', "Name present")) {
|
||||
return "border: 1px solid green";
|
||||
} else {
|
||||
return "border: 1px solid gray";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Template.entrySignUp.events({
|
||||
"click #signUpPageSignInButton": function (event) {
|
||||
event.preventDefault();
|
||||
ActiveEntry.reset();
|
||||
Router.go('/entrySignIn');
|
||||
},
|
||||
'change, keyup #signUpPageEmailInput': function (event, template) {
|
||||
var email = $('[name="email"]').val();
|
||||
|
||||
ActiveEntry.verifyEmail(email);
|
||||
ActiveEntry.errorMessages.set('signInError', null);
|
||||
},
|
||||
'change, keyup #signUpPagePasswordInput': function (event, template) {
|
||||
var password = $('[name="password"]').val();
|
||||
|
||||
ActiveEntry.verifyPassword(password);
|
||||
ActiveEntry.errorMessages.set('signInError', null);
|
||||
},
|
||||
'change, keyup #signUpPagePasswordConfirmInput': function (event, template) {
|
||||
|
||||
var password = $('[name="password"]').val();
|
||||
var confirmPassword = $('[name="confirm"]').val();
|
||||
// var password = $('#signUpPagePasswordInput').val();
|
||||
// var confirmPassword = $('#signUpPagePasswordConfirmInput').val();
|
||||
|
||||
ActiveEntry.verifyConfirmPassword(password, confirmPassword);
|
||||
ActiveEntry.errorMessages.set('signInError', null);
|
||||
},
|
||||
'change, keyup #signUpPageFullNameInput': function (event, template) {
|
||||
var fullName = template.$('[name="fullName"]').val();
|
||||
|
||||
ActiveEntry.verifyFullName(fullName);
|
||||
ActiveEntry.errorMessages.set('signInError', null);
|
||||
},
|
||||
'click #signUpPageJoinNowButton': function (event, template) {
|
||||
ActiveEntry.signUp(
|
||||
$('#signUpPageEmailInput').val(),
|
||||
$('#signUpPagePasswordInput').val(),
|
||||
$('#signUpPagePasswordConfirmInput').val(),
|
||||
$('#signUpPageFullNameInput').val()
|
||||
);
|
||||
},
|
||||
'keyup #entrySignUp': function(event, template) {
|
||||
if(event.keyCode == 13) {
|
||||
ActiveEntry.verifyFullName($("#signUpPageFullNameInput").val());
|
||||
ActiveEntry.verifyEmail($("#signUpPageEmailInput").val());
|
||||
ActiveEntry.verifyPassword($("#signUpPagePasswordInput").val());
|
||||
ActiveEntry.verifyConfirmPassword($("#signUpPagePasswordInput").val(), $("#signUpPagePasswordConfirmInput").val());
|
||||
|
||||
if (!ActiveEntry.errorMessages.get('signInError') &&
|
||||
ActiveEntry.successMessages.get('fullName') &&
|
||||
ActiveEntry.successMessages.get('email') &&
|
||||
ActiveEntry.successMessages.get('password') &&
|
||||
ActiveEntry.successMessages.get('confirm')) {
|
||||
$("#signUpPageJoinNowButton").click();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Template.entrySignUp.onRendered(function() {
|
||||
// Password strength meter for password inputs
|
||||
if (passwordValidationSettings.showPasswordStrengthIndicator) {
|
||||
this.$('#signUpPagePasswordInput').pwstrength(passwordValidationSettings.pwstrengthOptions);
|
||||
}
|
||||
|
||||
// Update password warning message if zxcvbn is active and zxcvbn function is defined
|
||||
if(passwordValidationSettings.requireStrongPasswords) {
|
||||
Session.set('passwordWarning', 'Password is weak');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
#entrySignUp{
|
||||
padding: 40px;
|
||||
input{
|
||||
padding-left: 40px !important;
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
#entrySignUp .progress{
|
||||
height: 5px;
|
||||
margin-bottom: 1px
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
exports.command = function () {
|
||||
this
|
||||
.verify.elementPresent("#forgotPassword"); return this;
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<template name="forgotPassword">
|
||||
<div id="forgotPassword" class="page entryPage" style="{{getOpacityWithCorner}}">
|
||||
<div class="content-scrollable">
|
||||
<div class="wrapper-auth">
|
||||
<div class="entryLogo" style="background-image: url('{{getLogoUrl}}')"></div>
|
||||
|
||||
<h1 id="signInPageTitle" class="title-auth">Forgot Password</h1>
|
||||
<div id="signInPageMessage" class="subtitle-auth" >Improve your clinical practice with checklists.</div>
|
||||
<p id="signInPageMessage" class="subtitle-auth" style="{{getForgotPasswordMessageColor}}">{{{getForgotPasswordMessage}}}</p>
|
||||
|
||||
<form>
|
||||
<div class="input-symbol">
|
||||
<input id="signInPageEmailInput" type="email" name="email" placeholder="Your Email" style="{{getForgotPasswordStyle}}" />
|
||||
<i class="fa fa-envelope-o" title="Your Email"></i>
|
||||
</div>
|
||||
<br><br>
|
||||
|
||||
<button id="sendReminderButton" type="submit" class="btn-gray btn-main btn-large" style="{{getButtonColor}}">Send Reminder</button><br><br>
|
||||
{{#if forgotPasswordNotification}}
|
||||
<div class="alert alert-success">
|
||||
{{{forgotPasswordNotification}}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,36 @@
|
||||
//==========================================
|
||||
|
||||
Router.route('/forgotPassword', {
|
||||
name: "forgotPassword",
|
||||
template: "forgotPassword"
|
||||
});
|
||||
|
||||
Template.forgotPassword.helpers({
|
||||
getForgotPasswordMessageColor: function (){
|
||||
if (ActiveEntry.errorMessages.get('forgotPassword')) {
|
||||
return "color: #a94442; background-color: #f2dede; border-color: #ebccd1;"
|
||||
} else {
|
||||
return "color: black;"
|
||||
}
|
||||
},
|
||||
getForgotPasswordMessage: function (){
|
||||
return ActiveEntry.errorMessages.get('forgotPassword');
|
||||
},
|
||||
getForgotPasswordStyle: function (){
|
||||
return "border: 1px solid gray";
|
||||
},
|
||||
forgotPasswordNotification: function() {
|
||||
return ActiveEntry.successMessages.get("forgotPassword");
|
||||
}
|
||||
});
|
||||
|
||||
Template.forgotPassword.events({
|
||||
"submit": function (event, template) {
|
||||
event.preventDefault();
|
||||
console.log('send reminder!');
|
||||
ActiveEntry.successMessages.set("forgotPassword", "Your password reset email is sending...");
|
||||
var emailAddress = $('#signInPageEmailInput').val();
|
||||
ActiveEntry.forgotPassword(emailAddress);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
#forgotPassword{
|
||||
input{
|
||||
padding-left: 40px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
<template name="resetPassword">
|
||||
{{#if resetPassword}}
|
||||
<div id="resetPassword" class="page entryPage" style="{{getOpacityWithCorner}}">
|
||||
<div class="content-scrollable">
|
||||
<div class="wrapper-auth">
|
||||
<div class="entryLogo" style="background-image: url('{{getLogoUrl}}')"></div>
|
||||
|
||||
<h1 id="signInPageTitle" class="title-auth">Reset Password</h1>
|
||||
<p id="signInPageMessage" class="subtitle-auth" style="{{getResetPasswordInMessageColor}}">{{{getResetPasswordMessage}}}</p>
|
||||
|
||||
<form>
|
||||
{{#if resetPasswordErrorMessages}}
|
||||
<div id="errorMessages" class="list-errors">
|
||||
{{#each resetPasswordErrorMessages}}
|
||||
<div class="alert alert-danger list-item">{{this}}</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="input-symbol ">
|
||||
<input id="resetPasswordInput" type="password" name="password" placeholder="Password" style="{{getPasswordStyling}}" />
|
||||
<span class="fa fa-lock" title="Password"></span>
|
||||
<!-- <span class="icon-lock" title="Password"></span> -->
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<div class="input-symbol">
|
||||
<input id="resetPasswordConfirmInput" type="password" name="confirm" placeholder="Confirm Password" style="{{getConfirmPasswordStyling}}" />
|
||||
<span class="fa fa-lock" title="Confirm Password"></span>
|
||||
<!-- <span class="icon-lock" title="Confirm Password"></span> -->
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
</form>
|
||||
<button id="resetPasswordButton" type="submit" class="btn-gray btn-main btn-large" style="{{getButtonColor}}">Reset Password</button><br><br>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
@@ -0,0 +1,67 @@
|
||||
|
||||
Router.route('/resetPassword/:token', {
|
||||
template: 'resetPassword',
|
||||
name: 'resetPassword',
|
||||
onBeforeAction: function() {
|
||||
var token = this.params.token;
|
||||
Session.set('_resetPasswordToken', token);
|
||||
this.next();
|
||||
}
|
||||
});
|
||||
|
||||
// Reset password template
|
||||
Template.resetPassword.helpers({
|
||||
getResetPasswordInMessageColor: function (){
|
||||
if (ActiveEntry.errorMessages.get('resetPasswordError')) {
|
||||
return "color: #a94442; background-color: #f2dede; border-color: #ebccd1;"
|
||||
} else {
|
||||
return "color: black;"
|
||||
}
|
||||
},
|
||||
getResetPasswordMessage: function (){
|
||||
if (ActiveEntry.errorMessages.get('resetPasswordError')) {
|
||||
return ActiveEntry.errorMessages.get('resetPasswordError');
|
||||
} else {
|
||||
return Session.get('defaultSignInMessage');
|
||||
}
|
||||
},
|
||||
resetPassword: function(){
|
||||
return Session.get('_resetPasswordToken');
|
||||
},
|
||||
resetPasswordErrorMessages: function() {
|
||||
if (ActiveEntry.errorMessages.get("password")) {
|
||||
return [ActiveEntry.errorMessages.get("password")];
|
||||
}
|
||||
if (ActiveEntry.errorMessages.get("confirm")) {
|
||||
return [ActiveEntry.errorMessages.get("confirm")];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
Template.resetPassword.events({
|
||||
'keyup #resetPasswordInput': function (event, template) {
|
||||
var password = $('#resetPasswordInput').val();
|
||||
ActiveEntry.verifyPassword(password);
|
||||
ActiveEntry.errorMessages.set('resetPasswordError', null);
|
||||
},
|
||||
'keyup #resetPasswordConfirmInput': function (event, template) {
|
||||
|
||||
var password = $('#resetPasswordInput').val();
|
||||
var confirmPassword = $('#resetPasswordConfirmInput').val();
|
||||
|
||||
ActiveEntry.verifyConfirmPassword(password, confirmPassword);
|
||||
ActiveEntry.errorMessages.set('resetPasswordError', null);
|
||||
},
|
||||
'click #resetPasswordButton': function(e, template) {
|
||||
e.preventDefault();
|
||||
var password = $('#resetPasswordInput').val();
|
||||
var passwordConfirm = $('#resetPasswordConfirmInput').val();
|
||||
|
||||
if (ActiveEntry.errorMessages.get('password') || ActiveEntry.errorMessages.get('confirm')) {
|
||||
return;
|
||||
}
|
||||
ActiveEntry.resetPassword(password, passwordConfirm);
|
||||
}
|
||||
});
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
|
||||
|
||||
if (Meteor.isClient) {
|
||||
|
||||
|
||||
Accounts.onResetPasswordLink(function (token, done){
|
||||
console.log('Accounts.onResearchPasswordLink');
|
||||
console.log('Sending reset password email...');
|
||||
console.log('NOT IMPLEMENTED YET. PLEASE LOG AN ISSUE');
|
||||
console.log('token: ' + token);
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
Accounts.onEnrollmentLink(function (token, done){
|
||||
console.log('Accounts.onResearchPasswordLink');
|
||||
console.log('Sending enrollment email...');
|
||||
console.log('NOT IMPLEMENTED YET. PLEASE LOG AN ISSUE');
|
||||
console.log('token: ' + token);
|
||||
done();
|
||||
});
|
||||
|
||||
Accounts.onEmailVerificationLink(function (token, done){
|
||||
console.log('Accounts.onEmailVerificationLink');
|
||||
console.log('Sending verification email...');
|
||||
console.log('NOT IMPLEMENTED YET. PLEASE LOG AN ISSUE');
|
||||
console.log('token: ' + token);
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (Meteor.isServer){
|
||||
// Support for playing D&D: Roll 3d6 for dexterity
|
||||
Accounts.onCreateUser(function(options, user) {
|
||||
|
||||
var d6 = function () { return Math.floor(Random.fraction() * 6) + 1; };
|
||||
user.dexterity = d6() + d6() + d6();
|
||||
user.role = "user";
|
||||
|
||||
// We still want the default hook's 'profile' behavior.
|
||||
if (options.profile){
|
||||
user.profile = options.profile;
|
||||
}
|
||||
|
||||
return user;
|
||||
});
|
||||
}
|
||||
+545
@@ -0,0 +1,545 @@
|
||||
|
||||
|
||||
ActiveEntry = {};
|
||||
ActiveEntry.isAbc = function () {
|
||||
return "abc";
|
||||
};
|
||||
|
||||
|
||||
|
||||
if (Meteor.isClient) {
|
||||
Session.setDefault('Photonic.ActiveEntry', {
|
||||
logo: {
|
||||
url: "https://upload.wikimedia.org/wikipedia/commons/1/1a/Photon-photon_scattering.png",
|
||||
displayed: true
|
||||
},
|
||||
signIn: {
|
||||
displayFullName: true,
|
||||
destination: "/"
|
||||
},
|
||||
signUp: {
|
||||
destination: "/"
|
||||
},
|
||||
themeColors: {
|
||||
primary: ""
|
||||
},
|
||||
passwordOptions: {
|
||||
showPasswordStrengthIndicator: true,
|
||||
requireRegexValidation: true,
|
||||
//requireStrongPasswords: false
|
||||
passwordHistoryCount: 6,
|
||||
failedAttemptsLimit: 5,
|
||||
passwordExpirationDays: 90,
|
||||
inactivityPeriodDays: 180,
|
||||
expireTimeInMinute: 30
|
||||
}
|
||||
});
|
||||
|
||||
ActiveEntry.errorMessages = new ReactiveDict('errorMessages');
|
||||
ActiveEntry.errorMessages.set('signInError', false);
|
||||
|
||||
// Success messages
|
||||
ActiveEntry.successMessages = new ReactiveDict('successMessages');
|
||||
|
||||
// Change password warning message according to whether zxcvbn is turned on
|
||||
Session.set('passwordWarning', 'Password must have at least 8 characters. It must contain at least 1 uppercase, 1 lowercase, 1 number and 1 special character.');
|
||||
|
||||
// Activate LDAP if ldap url and port is set in settings.json
|
||||
Meteor.call('isLDAPSet', function(error, isSet) {
|
||||
Session.set('isLDAPSet', isSet);
|
||||
});
|
||||
}
|
||||
|
||||
if (Meteor.isServer) {
|
||||
LDAP_DEFAULTS = {};
|
||||
LDAP_DEFAULTS.url = Meteor.settings.ldap && Meteor.settings.ldap.url;
|
||||
LDAP_DEFAULTS.port = Meteor.settings.ldap && Meteor.settings.ldap.port;
|
||||
}
|
||||
|
||||
ActiveEntry.configure = function (configObject) {
|
||||
if (Meteor.isClient) {
|
||||
|
||||
// Set passwordOptions if they are not defined
|
||||
if (!configObject.passwordOptions) {
|
||||
configObject.passwordOptions = {
|
||||
showPasswordStrengthIndicator: true,
|
||||
requireRegexValidation: false,
|
||||
//requireStrongPasswords: false
|
||||
passwordHistoryCount: 6,
|
||||
failedAttemptsLimit: 5,
|
||||
passwordExpirationDays: 90,
|
||||
inactivityPeriodDays: 180,
|
||||
expireTimeInMinute: 30
|
||||
}
|
||||
}
|
||||
Session.set('Photonic.ActiveEntry', configObject);
|
||||
}
|
||||
};
|
||||
|
||||
ActiveEntry.verifyPassword = function (password) {
|
||||
if (password.length === 0) {
|
||||
ActiveEntry.errorMessages.set('password', 'Password is required');
|
||||
ActiveEntry.successMessages.set('password', null);
|
||||
} else if (!checkPasswordStrength(password)) {
|
||||
ActiveEntry.errorMessages.set('password', Session.get('passwordWarning'));
|
||||
ActiveEntry.successMessages.set('password', null);
|
||||
} else {
|
||||
ActiveEntry.errorMessages.set('password', null);
|
||||
ActiveEntry.successMessages.set('password', 'Password present');
|
||||
}
|
||||
};
|
||||
|
||||
ActiveEntry.verifyConfirmPassword = function (password, confirmPassword) {
|
||||
// we have two different logic checks happening in this function
|
||||
// would be reasonable to separate them out into separate functions
|
||||
if (confirmPassword === "") {
|
||||
ActiveEntry.errorMessages.set('confirm', 'Password is required');
|
||||
ActiveEntry.successMessages.set('confirm', null);
|
||||
} else if (confirmPassword === password) {
|
||||
ActiveEntry.errorMessages.set('confirm', null);
|
||||
ActiveEntry.successMessages.set('confirm', 'Passwords match');
|
||||
} else {
|
||||
ActiveEntry.errorMessages.set('confirm', 'Passwords do not match');
|
||||
ActiveEntry.successMessages.set('confirm', null);
|
||||
}
|
||||
};
|
||||
|
||||
ActiveEntry.verifyEmail = function (email) {
|
||||
if (email.length === 0) {
|
||||
ActiveEntry.errorMessages.set('email', 'Email is required');
|
||||
ActiveEntry.successMessages.set('email', null);
|
||||
} else if (email.indexOf("@") === -1){
|
||||
ActiveEntry.errorMessages.set('email', 'Email is poorly formatted');
|
||||
ActiveEntry.successMessages.set('email', null);
|
||||
} else if (email.indexOf("@") >= 0){
|
||||
ActiveEntry.errorMessages.set('email', null);
|
||||
ActiveEntry.successMessages.set('email', 'Email present');
|
||||
}
|
||||
};
|
||||
|
||||
ActiveEntry.verifyFullName = function (fullName) {
|
||||
if (fullName.length === 0) {
|
||||
ActiveEntry.errorMessages.set('fullName', 'Name is required');
|
||||
ActiveEntry.successMessages.set('fullName', null);
|
||||
} else if (fullName.indexOf(" ") === -1){
|
||||
ActiveEntry.errorMessages.set('fullName', 'Name is probably not complete');
|
||||
ActiveEntry.successMessages.set('fullName', null);
|
||||
} else if (fullName.indexOf(" ") >= 0){
|
||||
//ActiveEntry.errorMessages.set('fullName', 'Name present');
|
||||
ActiveEntry.errorMessages.set('fullName', null);
|
||||
ActiveEntry.successMessages.set('fullName', 'Name present');
|
||||
}
|
||||
};
|
||||
|
||||
ActiveEntry.verifyLDAPUsername = function(username) {
|
||||
if (username === "") {
|
||||
ActiveEntry.errorMessages.set("ldapUsername", "Username is required");
|
||||
ActiveEntry.successMessages.set("ldapUsername", null);
|
||||
} else {
|
||||
ActiveEntry.errorMessages.set("ldapUsername", null);
|
||||
ActiveEntry.successMessages.set("ldapUsername", "Username present");
|
||||
}
|
||||
};
|
||||
|
||||
ActiveEntry.verifyLDAPPassword = function(password) {
|
||||
if (password === "") {
|
||||
ActiveEntry.errorMessages.set("ldapPassword", "Password is required");
|
||||
ActiveEntry.successMessages.set("ldapPassword", null);
|
||||
} else {
|
||||
ActiveEntry.errorMessages.set("ldapPassword", null);
|
||||
ActiveEntry.successMessages.set("ldapPassword", "Password present");
|
||||
}
|
||||
};
|
||||
|
||||
ActiveEntry.signIn = function (emailValue, passwordValue){
|
||||
|
||||
ActiveEntry.verifyPassword(passwordValue);
|
||||
ActiveEntry.verifyEmail(emailValue);
|
||||
|
||||
var signInArgs = {email: emailValue, password: passwordValue};
|
||||
|
||||
var ActiveEntryConfig = Session.get('Photonic.ActiveEntry');
|
||||
var passwordOptions = ActiveEntryConfig && ActiveEntryConfig.passwordOptions;
|
||||
|
||||
Meteor.call("isAccountInactive",emailValue, passwordOptions.inactivityPeriodDays, function(error, isAccountInactive) {
|
||||
if (error) {
|
||||
console.warn(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAccountInactive) {
|
||||
// Lock account
|
||||
ActiveEntry.lockAccount(signInArgs);
|
||||
} else {
|
||||
// Check account is locked
|
||||
ActiveEntry.isAccountLocked(signInArgs, passwordOptions);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
ActiveEntry.lockAccount = function(signInArgs) {
|
||||
var emailValue = signInArgs && signInArgs.email;
|
||||
if (!emailValue) {
|
||||
return;
|
||||
}
|
||||
Meteor.call("lockAccount", emailValue);
|
||||
ActiveEntry.errorMessages.set('signInError', "Your account has been locked due to inactivity.");
|
||||
};
|
||||
|
||||
ActiveEntry.isAccountLocked = function(signInArgs, passwordOptions) {
|
||||
var emailValue = signInArgs && signInArgs.email;
|
||||
if (!emailValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
Meteor.call("isAccountLocked", emailValue, function (error, isAccountLocked) {
|
||||
if (error) {
|
||||
console.warn(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAccountLocked) {
|
||||
ActiveEntry.errorMessages.set('signInError', "Your account has been locked.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get failed attempts count
|
||||
ActiveEntry.getFailedAttemptsCount(signInArgs, passwordOptions);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
ActiveEntry.getFailedAttemptsCount = function(signInArgs, passwordOptions) {
|
||||
var emailValue = signInArgs && signInArgs.email;
|
||||
if (!emailValue) {
|
||||
return;
|
||||
}
|
||||
Meteor.call("getFailedAttemptsCount", emailValue, function(error, failedAttemptsCount) {
|
||||
if (error) {
|
||||
console.warn(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (failedAttemptsCount != passwordOptions.failedAttemptsLimit) {
|
||||
// Login with password
|
||||
ActiveEntry.loginWithPassword(signInArgs, passwordOptions);
|
||||
} else {
|
||||
ActiveEntry.errorMessages.set('signInError', "Your account has been locked.");
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
ActiveEntry.loginWithPassword = function(signInArgs, passwordOptions) {
|
||||
var emailValue = signInArgs && signInArgs.email;
|
||||
var password = signInArgs && signInArgs.password;
|
||||
|
||||
if (!emailValue || !password) {
|
||||
return;
|
||||
}
|
||||
|
||||
Meteor.loginWithPassword(emailValue, password, function (loginError, result) {
|
||||
if (loginError) {
|
||||
// Login failed
|
||||
if (loginError.error == 403) {
|
||||
ActiveEntry.updateFailedAttempts(signInArgs, passwordOptions, loginError);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset failed attempts
|
||||
Meteor.call("resetFailedAttempts", emailValue);
|
||||
|
||||
// Check password expiration
|
||||
ActiveEntry.isPasswordExpired(passwordOptions);
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
ActiveEntry.isPasswordExpired = function(passwordOptions) {
|
||||
// if password expired, route to changePassword page
|
||||
Meteor.call("isPasswordExpired", passwordOptions.passwordExpirationDays, function(error, isPasswordExpired) {
|
||||
if (error) {
|
||||
console.warn(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update last login time
|
||||
Meteor.call("updateLastLoginDate");
|
||||
|
||||
if (isPasswordExpired) {
|
||||
ActiveEntry.errorMessages.set('changePasswordError', 'Your password expired. Please change your password.');
|
||||
Router.go('/changePassword');
|
||||
} else {
|
||||
var ActiveEntryConfig = Session.get('Photonic.ActiveEntry');
|
||||
Router.go(ActiveEntryConfig.signIn.destination);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ActiveEntry.updateFailedAttempts = function(signInArgs, passwordOptions, loginError) {
|
||||
var emailValue = signInArgs && signInArgs.email;
|
||||
|
||||
if (!emailValue) {
|
||||
return;
|
||||
}
|
||||
Meteor.call("updateFailedAttempts", emailValue, passwordOptions.failedAttemptsLimit, function(error, failedAttemptCount) {
|
||||
if (error) {
|
||||
console.warn(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (failedAttemptCount == passwordOptions.failedAttemptsLimit) {
|
||||
ActiveEntry.errorMessages.set('signInError', "Too many failed login attempts. Your account has been locked.");
|
||||
} else if (failedAttemptCount < passwordOptions.failedAttemptsLimit) {
|
||||
ActiveEntry.errorMessages.set('signInError', loginError.message + "<br />" +(passwordOptions.failedAttemptsLimit - failedAttemptCount) + " attempts remaining.");
|
||||
} else {
|
||||
ActiveEntry.errorMessages.set('signInError', loginError.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
ActiveEntry.loginWithLDAP = function(username, password) {
|
||||
ActiveEntry.verifyLDAPUsername(username);
|
||||
ActiveEntry.verifyLDAPPassword(password);
|
||||
ActiveEntry.errorMessages.set('signInError', null);
|
||||
|
||||
if (ActiveEntry.errorMessages.get("ldapUsername") || ActiveEntry.errorMessages.get("ldapPassword")) {
|
||||
return;
|
||||
}
|
||||
|
||||
Meteor.loginWithLDAP(username, password, {
|
||||
// The dn value depends on what you want to search/auth against
|
||||
// The structure will depend on how your ldap server
|
||||
// is configured or structured.
|
||||
dn: "uid=" + username + ",ou=users,ou=system",
|
||||
// The search value is optional. Set it if your search does not
|
||||
// work with the bind dn.
|
||||
searchResultsProfileMap: [
|
||||
{
|
||||
resultKey: 'cn',
|
||||
profileProperty: 'fullName'
|
||||
},
|
||||
{
|
||||
resultKey: 'mail',
|
||||
profileProperty: 'email'
|
||||
}
|
||||
]
|
||||
}, function(error) {
|
||||
if (error) {
|
||||
ActiveEntry.errorMessages.set('signInError', error.errorType+" ["+error.error+"]");
|
||||
return;
|
||||
}
|
||||
ActiveEntry.errorMessages.set('signInError', null);
|
||||
|
||||
// Update last login time
|
||||
Meteor.call("updateLastLoginDate");
|
||||
|
||||
var ActiveEntryConfig = Session.get('Photonic.ActiveEntry');
|
||||
Router.go(ActiveEntryConfig.signIn.destination);
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
ActiveEntry.signUp = function (emailValue, passwordValue, confirmPassword, fullName){
|
||||
ActiveEntry.verifyEmail(emailValue);
|
||||
ActiveEntry.verifyPassword(passwordValue);
|
||||
ActiveEntry.verifyConfirmPassword(passwordValue, confirmPassword);
|
||||
ActiveEntry.verifyFullName(fullName);
|
||||
ActiveEntry.errorMessages.set('signInError', null);
|
||||
|
||||
var errorIsFound = false;
|
||||
Object.keys(ActiveEntry.errorMessages.keys).forEach(function(key) {
|
||||
if (ActiveEntry.errorMessages.get(key) !== "null" && ActiveEntry.errorMessages.get(key) !== null) {
|
||||
errorIsFound = true;
|
||||
}
|
||||
});
|
||||
|
||||
if(errorIsFound) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Capitalize first letter of every word in fullName
|
||||
var capitalizedFullName = fullName.replace(/[^\s]+/g, function(str){
|
||||
return str.substr(0,1).toUpperCase()+str.substr(1).toLowerCase();
|
||||
});
|
||||
|
||||
Accounts.createUser({
|
||||
email: emailValue,
|
||||
password: passwordValue,
|
||||
profile: {
|
||||
fullName: capitalizedFullName
|
||||
}
|
||||
}, function (error, result) {
|
||||
if (error) {
|
||||
ActiveEntry.errorMessages.set('signInError', error.message);
|
||||
return;
|
||||
}
|
||||
// Add password in previousPasswords field
|
||||
ActiveEntry.insertHashedPassword(passwordValue);
|
||||
|
||||
// Update password set date
|
||||
ActiveEntry.updatePasswordSetDate();
|
||||
|
||||
// Update last login time
|
||||
Meteor.call("updateLastLoginDate");
|
||||
|
||||
var ActiveEntryConfig = Session.get('Photonic.ActiveEntry');
|
||||
Router.go(ActiveEntryConfig.signUp.destination);
|
||||
|
||||
});
|
||||
};
|
||||
ActiveEntry.changePassword = function(oldPassword, password) {
|
||||
Meteor.call("checkPasswordExistence", new String(password).hashCode(), function(error, isPasswordExisted) {
|
||||
if (error) {
|
||||
console.warn(error.message);
|
||||
ActiveEntry.errorMessages.set('changePasswordError', error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPasswordExisted) {
|
||||
ActiveEntry.errorMessages.set('changePasswordError', 'Password is used before. Please change your new password.');
|
||||
} else {
|
||||
ActiveEntry.errorMessages.set('changePasswordError', null);
|
||||
|
||||
// If password is not found in password history, change the password
|
||||
Accounts.changePassword(oldPassword, password, function(error) {
|
||||
if (error) {
|
||||
console.warn(error);
|
||||
ActiveEntry.errorMessages.set('changePasswordError', error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the new password
|
||||
ActiveEntry.insertHashedPassword(password);
|
||||
|
||||
// Update password expiration date
|
||||
ActiveEntry.updatePasswordSetDate();
|
||||
|
||||
// Logout
|
||||
ActiveEntry.signOut();
|
||||
// Go to signIn page for new entry
|
||||
Router.go('/entrySignIn');
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
ActiveEntry.forgotPassword = function(emailAddress) {
|
||||
ActiveEntry.verifyEmail(emailAddress);
|
||||
ActiveEntry.errorMessages.set("forgotPassword", null);
|
||||
ActiveEntry.successMessages.set("forgotPassword", null);
|
||||
|
||||
if (ActiveEntry.errorMessages.get("email")) {
|
||||
return;
|
||||
}
|
||||
|
||||
Accounts.forgotPassword({email:emailAddress }, function(error){
|
||||
if (error) {
|
||||
console.warn(error.message);
|
||||
ActiveEntry.errorMessages.set("forgotPassword", error.message);
|
||||
ActiveEntry.successMessages.set("forgotPassword", null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show email sent notification
|
||||
ActiveEntry.successMessages.set("forgotPassword", "Your password reset email is sent to <strong>"+emailAddress+"</strong>");
|
||||
});
|
||||
};
|
||||
|
||||
ActiveEntry.resetPassword = function(passwordValue, confirmPassword) {
|
||||
ActiveEntry.verifyPassword(passwordValue);
|
||||
ActiveEntry.verifyConfirmPassword(passwordValue, confirmPassword);
|
||||
ActiveEntry.errorMessages.set("resetPassword", null);
|
||||
|
||||
// Check error messages
|
||||
if (ActiveEntry.errorMessages.get("password") || ActiveEntry.errorMessages.get("confirm")) {
|
||||
return;
|
||||
}
|
||||
|
||||
var ActiveEntryConfig = Session.get('Photonic.ActiveEntry');
|
||||
var passwordOptions = ActiveEntryConfig && ActiveEntryConfig.passwordOptions;
|
||||
|
||||
// Check token is expired
|
||||
Meteor.call('checkResetTokenIsExpired', Session.get('_resetPasswordToken'), passwordOptions.expireTimeInMinute, function(error, isTokenExpired) {
|
||||
if (error) {
|
||||
console.log(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTokenExpired) {
|
||||
console.log("Your link is expired");
|
||||
// Go to forgotPassword to create a new reset link
|
||||
ActiveEntry.errorMessages.set("forgotPassword", 'Your link is expired. Please create a new reset link.');
|
||||
Router.go('/forgotPassword');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check password history
|
||||
Meteor.call("checkResetPasswordExistence", new String(passwordValue).hashCode(), Session.get('_resetPasswordToken'), function(error, isPasswordExisted) {
|
||||
if (error) {
|
||||
console.warn(error.message);
|
||||
ActiveEntry.errorMessages.set('resetPasswordError', error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPasswordExisted) {
|
||||
|
||||
ActiveEntry.errorMessages.set('resetPasswordError', 'Password is used before. Please change your new password.');
|
||||
} else {
|
||||
|
||||
ActiveEntry.errorMessages.set('resetPasswordError', null);
|
||||
Accounts.resetPassword(Session.get('_resetPasswordToken'), passwordValue, function(error) {
|
||||
if (error) {
|
||||
ActiveEntry.errorMessages.set("resetPassword", error.message);
|
||||
return;
|
||||
}
|
||||
Session.set('_resetPasswordToken', null);
|
||||
// Save the new password
|
||||
ActiveEntry.insertHashedPassword(passwordValue);
|
||||
|
||||
// Update password expiration date
|
||||
ActiveEntry.updatePasswordSetDate();
|
||||
|
||||
// Update last login time
|
||||
Meteor.call("updateLastLoginDate");
|
||||
|
||||
var ActiveEntryConfig = Session.get('Photonic.ActiveEntry');
|
||||
Router.go(ActiveEntryConfig.signIn.destination);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
// Insert hashed password in previousPasswords fields
|
||||
ActiveEntry.insertHashedPassword = function(passwordValue) {
|
||||
var ActiveEntryConfig = Session.get('Photonic.ActiveEntry');
|
||||
var passwordHistoryCount = ActiveEntryConfig && ActiveEntryConfig.passwordOptions && ActiveEntryConfig.passwordOptions.passwordHistoryCount || 6;
|
||||
Meteor.call("insertHashedPassword", [new String(passwordValue).hashCode(),passwordHistoryCount]);
|
||||
};
|
||||
|
||||
ActiveEntry.updatePasswordSetDate = function() {
|
||||
Meteor.call("updatePasswordSetDate");
|
||||
};
|
||||
|
||||
ActiveEntry.signOut = function (){
|
||||
Meteor.logout();
|
||||
};
|
||||
|
||||
ActiveEntry.reset = function (){
|
||||
ActiveEntry.errorMessages.set('signInError', false);
|
||||
ActiveEntry.errorMessages.set('fullName', false);
|
||||
ActiveEntry.errorMessages.set('email', false);
|
||||
ActiveEntry.errorMessages.set('confirm', false);
|
||||
ActiveEntry.errorMessages.set('password', false);
|
||||
};
|
||||
|
||||
ActiveEntry.logoIsDisplayed = function (){
|
||||
var ActiveEntryConfig = Session.get('Photonic.ActiveEntry');
|
||||
return ActiveEntryConfig.logo.displayed;
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
passwordValidationSettings = {};
|
||||
|
||||
function getPasswordValidationSettings () {
|
||||
var ActiveEntryConfiguration = Session.get('Photonic.ActiveEntry');
|
||||
var validationSettings = {};
|
||||
|
||||
validationSettings.showPasswordStrengthIndicator = ActiveEntryConfiguration && ActiveEntryConfiguration.passwordOptions && ActiveEntryConfiguration.passwordOptions.showPasswordStrengthIndicator || false;
|
||||
validationSettings.requireRegexValidation = ActiveEntryConfiguration && ActiveEntryConfiguration.passwordOptions && ActiveEntryConfiguration.passwordOptions.requireRegexValidation || false;
|
||||
|
||||
if (validationSettings.showPasswordStrengthIndicator) {
|
||||
// Set password strength meter options
|
||||
validationSettings.pwstrengthOptions = {
|
||||
common: {
|
||||
minChar: 8
|
||||
},
|
||||
ui: {
|
||||
showVerdictsInsideProgressBar: true,
|
||||
showStatus: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Check if codetheweb:zxcvbn is defined
|
||||
if (typeof(zxcvbn) === typeof(Function)) {
|
||||
validationSettings.requireStrongPasswords = ActiveEntryConfiguration && ActiveEntryConfiguration.passwordOptions && ActiveEntryConfiguration.passwordOptions.requireStrongPasswords || false;
|
||||
// Set zxcvbn in pw strength meter
|
||||
if (validationSettings.showPasswordStrengthIndicator) {
|
||||
validationSettings.pwstrengthOptions.common.zxcvbn = passwordValidationSettings.requireStrongPasswords;
|
||||
}
|
||||
}
|
||||
|
||||
return validationSettings;
|
||||
}
|
||||
|
||||
Meteor.startup(function() {
|
||||
passwordValidationSettings = getPasswordValidationSettings();
|
||||
});
|
||||
|
||||
checkPasswordStrength = function(password) {
|
||||
if (passwordValidationSettings.requireStrongPasswords) {
|
||||
// Check zxcvbn
|
||||
var zxcvbnResult = zxcvbn(password);
|
||||
if (zxcvbnResult && zxcvbnResult.score > 2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} else if (passwordValidationSettings.requireRegexValidation) {
|
||||
// Apply validation rule
|
||||
var result = password.search(/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*])[0-9a-zA-Z!@#$%^&*]{8,}$/i);
|
||||
if (result > -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
String.prototype.hashCode = function() {
|
||||
var hash = 0, i, chr, len;
|
||||
if (this.length === 0) return hash;
|
||||
for (i = 0, len = this.length; i < len; i++) {
|
||||
chr = this.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + chr;
|
||||
hash |= 0; // Convert to 32bit integer
|
||||
}
|
||||
return hash;
|
||||
};
|
||||
@@ -0,0 +1,737 @@
|
||||
/*!
|
||||
* jQuery Password Strength plugin for Twitter Bootstrap
|
||||
*
|
||||
* Copyright (c) 2008-2013 Tane Piper
|
||||
* Copyright (c) 2013 Alejandro Blanco
|
||||
* Dual licensed under the MIT and GPL licenses.
|
||||
*/
|
||||
|
||||
(function (jQuery) {
|
||||
// Source: src/rules.js
|
||||
|
||||
|
||||
|
||||
var rulesEngine = {};
|
||||
|
||||
try {
|
||||
if (!jQuery && module && module.exports) {
|
||||
var jQuery = require("jquery"),
|
||||
jsdom = require("jsdom").jsdom;
|
||||
jQuery = jQuery(jsdom().parentWindow);
|
||||
}
|
||||
} catch (ignore) {}
|
||||
|
||||
(function ($, rulesEngine) {
|
||||
"use strict";
|
||||
var validation = {};
|
||||
|
||||
rulesEngine.forbiddenSequences = [
|
||||
"0123456789", "abcdefghijklmnopqrstuvwxyz", "qwertyuiop", "asdfghjkl",
|
||||
"zxcvbnm", "!@#$%^&*()_+"
|
||||
];
|
||||
|
||||
validation.wordNotEmail = function (options, word, score) {
|
||||
if (word.match(
|
||||
/^([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@((((([a-z0-9]{1}[a-z0-9\-]{0,62}[a-z0-9]{1})|[a-z])\.)+[a-z]{2,6})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)$/i
|
||||
)) {
|
||||
return score;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
validation.wordLength = function (options, word, score) {
|
||||
var wordlen = word.length,
|
||||
lenScore = Math.pow(wordlen, options.rules.raisePower);
|
||||
if (wordlen < options.common.minChar) {
|
||||
lenScore = (lenScore + score);
|
||||
}
|
||||
return lenScore;
|
||||
};
|
||||
|
||||
validation.wordSimilarToUsername = function (options, word, score) {
|
||||
var username = $(options.common.usernameField).val();
|
||||
if (username && word.toLowerCase().match(username.replace(
|
||||
/[\-\[\]\/\{\}\(\)\*\+\=\?\:\.\\\^\$\|\!\,]/g, "\\$&").toLowerCase())) {
|
||||
return score;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
validation.wordTwoCharacterClasses = function (options, word, score) {
|
||||
if (word.match(/([a-z].*[A-Z])|([A-Z].*[a-z])/) ||
|
||||
(word.match(/([a-zA-Z])/) && word.match(/([0-9])/)) ||
|
||||
(word.match(/(.[!,@,#,$,%,\^,&,*,?,_,~])/) && word.match(/[a-zA-Z0-9_]/))) {
|
||||
return score;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
validation.wordRepetitions = function (options, word, score) {
|
||||
if (word.match(/(.)\1\1/)) {
|
||||
return score;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
validation.wordSequences = function (options, word, score) {
|
||||
var found = false,
|
||||
j;
|
||||
if (word.length > 2) {
|
||||
$.each(rulesEngine.forbiddenSequences, function (idx, seq) {
|
||||
if (found) {
|
||||
return;
|
||||
}
|
||||
var sequences = [seq, seq.split('').reverse().join('')];
|
||||
$.each(sequences, function (idx, sequence) {
|
||||
for (j = 0; j < (word.length - 2); j += 1) { // iterate the word trough a sliding window of size 3:
|
||||
if (sequence.indexOf(word.toLowerCase().substring(j, j + 3)) > -1) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
if (found) {
|
||||
return score;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
validation.wordLowercase = function (options, word, score) {
|
||||
return word.match(/[a-z]/) && score;
|
||||
};
|
||||
|
||||
validation.wordUppercase = function (options, word, score) {
|
||||
return word.match(/[A-Z]/) && score;
|
||||
};
|
||||
|
||||
validation.wordOneNumber = function (options, word, score) {
|
||||
return word.match(/\d+/) && score;
|
||||
};
|
||||
|
||||
validation.wordThreeNumbers = function (options, word, score) {
|
||||
return word.match(/(.*[0-9].*[0-9].*[0-9])/) && score;
|
||||
};
|
||||
|
||||
validation.wordOneSpecialChar = function (options, word, score) {
|
||||
return word.match(/[!,@,#,$,%,\^,&,*,?,_,~]/) && score;
|
||||
};
|
||||
|
||||
validation.wordTwoSpecialChar = function (options, word, score) {
|
||||
return word.match(/(.*[!,@,#,$,%,\^,&,*,?,_,~].*[!,@,#,$,%,\^,&,*,?,_,~])/) && score;
|
||||
};
|
||||
|
||||
validation.wordUpperLowerCombo = function (options, word, score) {
|
||||
return word.match(/([a-z].*[A-Z])|([A-Z].*[a-z])/) && score;
|
||||
};
|
||||
|
||||
validation.wordLetterNumberCombo = function (options, word, score) {
|
||||
return word.match(/([a-zA-Z])/) && word.match(/([0-9])/) && score;
|
||||
};
|
||||
|
||||
validation.wordLetterNumberCharCombo = function (options, word, score) {
|
||||
return word.match(
|
||||
/([a-zA-Z0-9].*[!,@,#,$,%,\^,&,*,?,_,~])|([!,@,#,$,%,\^,&,*,?,_,~].*[a-zA-Z0-9])/) &&
|
||||
score;
|
||||
};
|
||||
|
||||
rulesEngine.validation = validation;
|
||||
|
||||
rulesEngine.executeRules = function (options, word) {
|
||||
var totalScore = 0;
|
||||
|
||||
$.each(options.rules.activated, function (rule, active) {
|
||||
if (active) {
|
||||
var score = options.rules.scores[rule],
|
||||
funct = rulesEngine.validation[rule],
|
||||
result,
|
||||
errorMessage;
|
||||
|
||||
if (!$.isFunction(funct)) {
|
||||
funct = options.rules.extra[rule];
|
||||
}
|
||||
|
||||
if ($.isFunction(funct)) {
|
||||
result = funct(options, word, score);
|
||||
if (result) {
|
||||
totalScore += result;
|
||||
}
|
||||
if (result < 0 || (!$.isNumeric(result) && !result)) {
|
||||
errorMessage = options.ui.spanError(options, rule);
|
||||
if (errorMessage.length > 0) {
|
||||
options.instances.errors.push(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return totalScore;
|
||||
};
|
||||
}(jQuery, rulesEngine));
|
||||
|
||||
try {
|
||||
if (module && module.exports) {
|
||||
module.exports = rulesEngine;
|
||||
}
|
||||
} catch (ignore) {}
|
||||
|
||||
// Source: src/options.js
|
||||
|
||||
|
||||
|
||||
var defaultOptions = {};
|
||||
|
||||
defaultOptions.common = {};
|
||||
defaultOptions.common.minChar = 6;
|
||||
defaultOptions.common.usernameField = "#username";
|
||||
defaultOptions.common.userInputs = [
|
||||
// Selectors for input fields with user input
|
||||
];
|
||||
defaultOptions.common.onLoad = undefined;
|
||||
defaultOptions.common.onKeyUp = undefined;
|
||||
defaultOptions.common.zxcvbn = false;
|
||||
defaultOptions.common.zxcvbnTerms = [
|
||||
// List of disrecommended words
|
||||
];
|
||||
defaultOptions.common.debug = false;
|
||||
|
||||
defaultOptions.rules = {};
|
||||
defaultOptions.rules.extra = {};
|
||||
defaultOptions.rules.scores = {
|
||||
wordNotEmail: -100,
|
||||
wordLength: -50,
|
||||
wordSimilarToUsername: -100,
|
||||
wordSequences: -20,
|
||||
wordTwoCharacterClasses: 2,
|
||||
wordRepetitions: -25,
|
||||
wordLowercase: 1,
|
||||
wordUppercase: 3,
|
||||
wordOneNumber: 3,
|
||||
wordThreeNumbers: 5,
|
||||
wordOneSpecialChar: 3,
|
||||
wordTwoSpecialChar: 5,
|
||||
wordUpperLowerCombo: 2,
|
||||
wordLetterNumberCombo: 2,
|
||||
wordLetterNumberCharCombo: 2
|
||||
};
|
||||
defaultOptions.rules.activated = {
|
||||
wordNotEmail: true,
|
||||
wordLength: true,
|
||||
wordSimilarToUsername: true,
|
||||
wordSequences: true,
|
||||
wordTwoCharacterClasses: false,
|
||||
wordRepetitions: false,
|
||||
wordLowercase: true,
|
||||
wordUppercase: true,
|
||||
wordOneNumber: true,
|
||||
wordThreeNumbers: true,
|
||||
wordOneSpecialChar: true,
|
||||
wordTwoSpecialChar: true,
|
||||
wordUpperLowerCombo: true,
|
||||
wordLetterNumberCombo: true,
|
||||
wordLetterNumberCharCombo: true
|
||||
};
|
||||
defaultOptions.rules.raisePower = 1.4;
|
||||
|
||||
defaultOptions.ui = {};
|
||||
defaultOptions.ui.bootstrap2 = false;
|
||||
defaultOptions.ui.bootstrap4 = false;
|
||||
defaultOptions.ui.colorClasses = ["danger", "warning", "success"];
|
||||
defaultOptions.ui.showProgressBar = true;
|
||||
defaultOptions.ui.showPopover = false;
|
||||
defaultOptions.ui.popoverPlacement = "bottom";
|
||||
defaultOptions.ui.showStatus = false;
|
||||
defaultOptions.ui.spanError = function (options, key) {
|
||||
"use strict";
|
||||
var text = options.ui.errorMessages[key];
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
return '<span style="color: #d52929">' + text + '</span>';
|
||||
};
|
||||
defaultOptions.ui.popoverError = function (errors) {
|
||||
"use strict";
|
||||
var message = "<div>Errors:<ul class='error-list' style='margin-bottom: 0;'>";
|
||||
|
||||
jQuery.each(errors, function (idx, err) {
|
||||
message += "<li>" + err + "</li>";
|
||||
});
|
||||
message += "</ul></div>";
|
||||
return message;
|
||||
};
|
||||
defaultOptions.ui.errorMessages = {
|
||||
wordLength: "Your password is too short",
|
||||
wordNotEmail: "Do not use your email as your password",
|
||||
wordSimilarToUsername: "Your password cannot contain your username",
|
||||
wordTwoCharacterClasses: "Use different character classes",
|
||||
wordRepetitions: "Too many repetitions",
|
||||
wordSequences: "Your password contains sequences"
|
||||
};
|
||||
defaultOptions.ui.verdicts = ["Weak", "Normal", "Medium", "Strong", "Very Strong"];
|
||||
defaultOptions.ui.showVerdicts = true;
|
||||
defaultOptions.ui.showVerdictsInsideProgressBar = false;
|
||||
defaultOptions.ui.useVerdictCssClass = false;
|
||||
defaultOptions.ui.showErrors = false;
|
||||
defaultOptions.ui.container = undefined;
|
||||
defaultOptions.ui.viewports = {
|
||||
progress: undefined,
|
||||
verdict: undefined,
|
||||
errors: undefined
|
||||
};
|
||||
defaultOptions.ui.scores = [14, 26, 38, 50];
|
||||
|
||||
// Source: src/ui.js
|
||||
|
||||
|
||||
|
||||
var ui = {};
|
||||
|
||||
(function ($, ui) {
|
||||
"use strict";
|
||||
|
||||
var statusClasses = ["error", "warning", "success"];
|
||||
|
||||
ui.getContainer = function (options, $el) {
|
||||
var $container;
|
||||
|
||||
$container = $(options.ui.container);
|
||||
if (!($container && $container.length === 1)) {
|
||||
$container = $el.parent();
|
||||
}
|
||||
return $container;
|
||||
};
|
||||
|
||||
ui.findElement = function ($container, viewport, cssSelector) {
|
||||
if (viewport) {
|
||||
return $container.find(viewport).find(cssSelector);
|
||||
}
|
||||
return $container.find(cssSelector);
|
||||
};
|
||||
|
||||
ui.getUIElements = function (options, $el) {
|
||||
var $container, selector, result;
|
||||
|
||||
if (options.instances.viewports) {
|
||||
return options.instances.viewports;
|
||||
}
|
||||
|
||||
$container = ui.getContainer(options, $el);
|
||||
|
||||
result = {};
|
||||
if (options.ui.bootstrap4) {
|
||||
selector = "progress.progress";
|
||||
} else {
|
||||
selector = "div.progress";
|
||||
}
|
||||
result.$progressbar = ui.findElement($container, options.ui.viewports.progress,
|
||||
selector);
|
||||
if (options.ui.showVerdictsInsideProgressBar) {
|
||||
result.$verdict = result.$progressbar.find("span.password-verdict");
|
||||
}
|
||||
|
||||
if (!options.ui.showPopover) {
|
||||
if (!options.ui.showVerdictsInsideProgressBar) {
|
||||
result.$verdict = ui.findElement($container, options.ui.viewports.verdict,
|
||||
"span.password-verdict");
|
||||
}
|
||||
result.$errors = ui.findElement($container, options.ui.viewports.errors,
|
||||
"ul.error-list");
|
||||
}
|
||||
|
||||
options.instances.viewports = result;
|
||||
return result;
|
||||
};
|
||||
|
||||
ui.initProgressBar = function (options, $el) {
|
||||
var $container = ui.getContainer(options, $el),
|
||||
progressbar = "<div class='progress'><div class='"; // Boostrap 2
|
||||
|
||||
if (!options.ui.bootstrap2 && !options.ui.bootstrap4) {
|
||||
// Bootstrap 3
|
||||
progressbar += "progress-";
|
||||
}
|
||||
progressbar += "bar'>";
|
||||
if (options.ui.bootstrap4) {
|
||||
// Boostrap 4
|
||||
progressbar = "<progress class='progress' value='0' max='100'>";
|
||||
}
|
||||
if (options.ui.showVerdictsInsideProgressBar) {
|
||||
progressbar += "<span class='password-verdict'></span>";
|
||||
}
|
||||
if (options.ui.bootstrap4) {
|
||||
progressbar += "</progress>";
|
||||
} else {
|
||||
progressbar += "</div></div>";
|
||||
}
|
||||
|
||||
if (options.ui.viewports.progress) {
|
||||
$container.find(options.ui.viewports.progress).append(progressbar);
|
||||
} else {
|
||||
$(progressbar).insertAfter($el);
|
||||
}
|
||||
};
|
||||
|
||||
ui.initHelper = function (options, $el, html, viewport) {
|
||||
var $container = ui.getContainer(options, $el);
|
||||
if (viewport) {
|
||||
$container.find(viewport).append(html);
|
||||
} else {
|
||||
$(html).insertAfter($el);
|
||||
}
|
||||
};
|
||||
|
||||
ui.initVerdict = function (options, $el) {
|
||||
ui.initHelper(options, $el, "<span class='password-verdict'></span>",
|
||||
options.ui.viewports.verdict);
|
||||
};
|
||||
|
||||
ui.initErrorList = function (options, $el) {
|
||||
ui.initHelper(options, $el, "<ul class='error-list'></ul>",
|
||||
options.ui.viewports.errors);
|
||||
};
|
||||
|
||||
ui.initPopover = function (options, $el) {
|
||||
$el.popover("destroy");
|
||||
$el.popover({
|
||||
html: true,
|
||||
placement: options.ui.popoverPlacement,
|
||||
trigger: "manual",
|
||||
content: " "
|
||||
});
|
||||
};
|
||||
|
||||
ui.initUI = function (options, $el) {
|
||||
if (options.ui.showPopover) {
|
||||
ui.initPopover(options, $el);
|
||||
} else {
|
||||
if (options.ui.showErrors) {
|
||||
ui.initErrorList(options, $el);
|
||||
}
|
||||
if (options.ui.showVerdicts && !options.ui.showVerdictsInsideProgressBar) {
|
||||
ui.initVerdict(options, $el);
|
||||
}
|
||||
}
|
||||
if (options.ui.showProgressBar) {
|
||||
ui.initProgressBar(options, $el);
|
||||
}
|
||||
};
|
||||
|
||||
ui.updateProgressBar = function (options, $el, cssClass, percentage) {
|
||||
var $progressbar = ui.getUIElements(options, $el).$progressbar,
|
||||
$bar = $progressbar.find(".progress-bar"),
|
||||
cssPrefix = "progress-";
|
||||
|
||||
if (options.ui.bootstrap2) {
|
||||
$bar = $progressbar.find(".bar");
|
||||
cssPrefix = "";
|
||||
}
|
||||
|
||||
$.each(options.ui.colorClasses, function (idx, value) {
|
||||
if (options.ui.bootstrap4) {
|
||||
$progressbar.removeClass(cssPrefix + value);
|
||||
} else {
|
||||
$bar.removeClass(cssPrefix + "bar-" + value);
|
||||
}
|
||||
});
|
||||
if (options.ui.bootstrap4) {
|
||||
$progressbar.addClass(cssPrefix + options.ui.colorClasses[cssClass]);
|
||||
$progressbar.val(percentage);
|
||||
} else {
|
||||
$bar.addClass(cssPrefix + "bar-" + options.ui.colorClasses[cssClass]);
|
||||
$bar.css("width", percentage + '%');
|
||||
}
|
||||
};
|
||||
|
||||
ui.updateVerdict = function (options, $el, cssClass, text) {
|
||||
var $verdict = ui.getUIElements(options, $el).$verdict;
|
||||
$verdict.removeClass(options.ui.colorClasses.join(' '));
|
||||
if (cssClass > -1) {
|
||||
$verdict.addClass(options.ui.colorClasses[cssClass]);
|
||||
}
|
||||
$verdict.html(text);
|
||||
};
|
||||
|
||||
ui.updateErrors = function (options, $el) {
|
||||
var $errors = ui.getUIElements(options, $el).$errors,
|
||||
html = "";
|
||||
$.each(options.instances.errors, function (idx, err) {
|
||||
html += "<li>" + err + "</li>";
|
||||
});
|
||||
$errors.html(html);
|
||||
};
|
||||
|
||||
ui.updatePopover = function (options, $el, verdictText) {
|
||||
var popover = $el.data("bs.popover"),
|
||||
html = "",
|
||||
hide = true;
|
||||
|
||||
if (options.ui.showVerdicts &&
|
||||
!options.ui.showVerdictsInsideProgressBar &&
|
||||
verdictText.length > 0) {
|
||||
html = "<h5><span class='password-verdict'>" + verdictText +
|
||||
"</span></h5>";
|
||||
hide = false;
|
||||
}
|
||||
if (options.ui.showErrors) {
|
||||
if (options.instances.errors.length > 0) {
|
||||
hide = false;
|
||||
}
|
||||
html += options.ui.popoverError(options.instances.errors);
|
||||
}
|
||||
|
||||
if (hide) {
|
||||
$el.popover("hide");
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.ui.bootstrap2) {
|
||||
popover = $el.data("popover");
|
||||
}
|
||||
|
||||
if (popover.$arrow && popover.$arrow.parents("body").length > 0) {
|
||||
$el.find("+ .popover .popover-content").html(html);
|
||||
} else {
|
||||
// It's hidden
|
||||
popover.options.content = html;
|
||||
$el.popover("show");
|
||||
}
|
||||
};
|
||||
|
||||
ui.updateFieldStatus = function (options, $el, cssClass) {
|
||||
var targetClass = options.ui.bootstrap2 ? ".control-group" : ".form-group",
|
||||
$container = $el.parents(targetClass).first();
|
||||
|
||||
$.each(statusClasses, function (idx, css) {
|
||||
if (!options.ui.bootstrap2) {
|
||||
css = "has-" + css;
|
||||
}
|
||||
$container.removeClass(css);
|
||||
});
|
||||
|
||||
cssClass = statusClasses[cssClass];
|
||||
if (!options.ui.bootstrap2) {
|
||||
cssClass = "has-" + cssClass;
|
||||
}
|
||||
$container.addClass(cssClass);
|
||||
};
|
||||
|
||||
ui.percentage = function (score, maximun) {
|
||||
var result = Math.floor(100 * score / maximun);
|
||||
result = result <= 0 ? 1 : result; // Don't show the progress bar empty
|
||||
result = result > 100 ? 100 : result;
|
||||
return result;
|
||||
};
|
||||
|
||||
ui.getVerdictAndCssClass = function (options, score) {
|
||||
var cssClass, verdictText, level;
|
||||
|
||||
if (score <= 0) {
|
||||
cssClass = 0;
|
||||
level = -1;
|
||||
verdictText = options.ui.verdicts[0];
|
||||
} else if (score < options.ui.scores[0]) {
|
||||
cssClass = 0;
|
||||
level = 0;
|
||||
verdictText = options.ui.verdicts[0];
|
||||
} else if (score < options.ui.scores[1]) {
|
||||
cssClass = 0;
|
||||
level = 1;
|
||||
verdictText = options.ui.verdicts[1];
|
||||
} else if (score < options.ui.scores[2]) {
|
||||
cssClass = 1;
|
||||
level = 2;
|
||||
verdictText = options.ui.verdicts[2];
|
||||
} else if (score < options.ui.scores[3]) {
|
||||
cssClass = 1;
|
||||
level = 3;
|
||||
verdictText = options.ui.verdicts[3];
|
||||
} else {
|
||||
cssClass = 2;
|
||||
level = 4;
|
||||
verdictText = options.ui.verdicts[4];
|
||||
}
|
||||
|
||||
return [verdictText, cssClass, level];
|
||||
};
|
||||
|
||||
ui.updateUI = function (options, $el, score) {
|
||||
var cssClass, barPercentage, verdictText, verdictCssClass;
|
||||
|
||||
cssClass = ui.getVerdictAndCssClass(options, score);
|
||||
verdictText = score === 0 ? '' : cssClass[0];
|
||||
cssClass = cssClass[1];
|
||||
verdictCssClass = options.ui.useVerdictCssClass ? cssClass : -1;
|
||||
|
||||
if (options.ui.showProgressBar) {
|
||||
barPercentage = ui.percentage(score, options.ui.scores[3]);
|
||||
ui.updateProgressBar(options, $el, cssClass, barPercentage);
|
||||
if (options.ui.showVerdictsInsideProgressBar) {
|
||||
ui.updateVerdict(options, $el, verdictCssClass, verdictText);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.ui.showStatus) {
|
||||
ui.updateFieldStatus(options, $el, cssClass);
|
||||
}
|
||||
|
||||
if (options.ui.showPopover) {
|
||||
ui.updatePopover(options, $el, verdictText);
|
||||
} else {
|
||||
if (options.ui.showVerdicts && !options.ui.showVerdictsInsideProgressBar) {
|
||||
ui.updateVerdict(options, $el, verdictCssClass, verdictText);
|
||||
}
|
||||
if (options.ui.showErrors) {
|
||||
ui.updateErrors(options, $el);
|
||||
}
|
||||
}
|
||||
};
|
||||
}(jQuery, ui));
|
||||
|
||||
// Source: src/methods.js
|
||||
|
||||
|
||||
|
||||
var methods = {};
|
||||
|
||||
(function ($, methods) {
|
||||
"use strict";
|
||||
var onKeyUp, applyToAll;
|
||||
|
||||
onKeyUp = function (event) {
|
||||
var $el = $(event.target),
|
||||
options = $el.data("pwstrength-bootstrap"),
|
||||
word = $el.val(),
|
||||
userInputs,
|
||||
verdictText,
|
||||
verdictLevel,
|
||||
score;
|
||||
|
||||
if (options === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
options.instances.errors = [];
|
||||
if (word.length === 0) {
|
||||
score = 0;
|
||||
} else {
|
||||
if (options.common.zxcvbn) {
|
||||
userInputs = [];
|
||||
$.each(options.common.userInputs.concat([options.common.usernameField]), function (
|
||||
idx, selector) {
|
||||
var value = $(selector).val();
|
||||
if (value) {
|
||||
userInputs.push(value);
|
||||
}
|
||||
});
|
||||
userInputs = userInputs.concat(options.common.zxcvbnTerms);
|
||||
score = Math.log2(zxcvbn(word, userInputs).guesses);
|
||||
} else {
|
||||
score = rulesEngine.executeRules(options, word);
|
||||
}
|
||||
}
|
||||
ui.updateUI(options, $el, score);
|
||||
verdictText = ui.getVerdictAndCssClass(options, score);
|
||||
verdictLevel = verdictText[2];
|
||||
verdictText = verdictText[0];
|
||||
|
||||
if (options.common.debug) {
|
||||
console.log(score + ' - ' + verdictText);
|
||||
}
|
||||
|
||||
if ($.isFunction(options.common.onKeyUp)) {
|
||||
options.common.onKeyUp(event, {
|
||||
score: score,
|
||||
verdictText: verdictText,
|
||||
verdictLevel: verdictLevel
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
methods.init = function (settings) {
|
||||
this.each(function (idx, el) {
|
||||
// Make it deep extend (first param) so it extends too the
|
||||
// rules and other inside objects
|
||||
var clonedDefaults = $.extend(true, {}, defaultOptions),
|
||||
localOptions = $.extend(true, clonedDefaults, settings),
|
||||
$el = $(el);
|
||||
|
||||
localOptions.instances = {};
|
||||
$el.data("pwstrength-bootstrap", localOptions);
|
||||
$el.on("keyup", onKeyUp);
|
||||
$el.on("change", onKeyUp);
|
||||
$el.on("paste", onKeyUp);
|
||||
|
||||
ui.initUI(localOptions, $el);
|
||||
if ($.trim($el.val())) { // Not empty, calculate the strength
|
||||
$el.trigger("keyup");
|
||||
}
|
||||
|
||||
if ($.isFunction(localOptions.common.onLoad)) {
|
||||
localOptions.common.onLoad();
|
||||
}
|
||||
});
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
methods.destroy = function () {
|
||||
this.each(function (idx, el) {
|
||||
var $el = $(el),
|
||||
options = $el.data("pwstrength-bootstrap"),
|
||||
elements = ui.getUIElements(options, $el);
|
||||
elements.$progressbar.remove();
|
||||
elements.$verdict.remove();
|
||||
elements.$errors.remove();
|
||||
$el.removeData("pwstrength-bootstrap");
|
||||
});
|
||||
};
|
||||
|
||||
methods.forceUpdate = function () {
|
||||
this.each(function (idx, el) {
|
||||
var event = {
|
||||
target: el
|
||||
};
|
||||
onKeyUp(event);
|
||||
});
|
||||
};
|
||||
|
||||
methods.addRule = function (name, method, score, active) {
|
||||
this.each(function (idx, el) {
|
||||
var options = $(el).data("pwstrength-bootstrap");
|
||||
|
||||
options.rules.activated[name] = active;
|
||||
options.rules.scores[name] = score;
|
||||
options.rules.extra[name] = method;
|
||||
});
|
||||
};
|
||||
|
||||
applyToAll = function (rule, prop, value) {
|
||||
this.each(function (idx, el) {
|
||||
$(el).data("pwstrength-bootstrap").rules[prop][rule] = value;
|
||||
});
|
||||
};
|
||||
|
||||
methods.changeScore = function (rule, score) {
|
||||
applyToAll.call(this, rule, "scores", score);
|
||||
};
|
||||
|
||||
methods.ruleActive = function (rule, active) {
|
||||
applyToAll.call(this, rule, "activated", active);
|
||||
};
|
||||
|
||||
$.fn.pwstrength = function (method) {
|
||||
var result;
|
||||
|
||||
if (methods[method]) {
|
||||
result = methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
|
||||
} else if (typeof method === "object" || !method) {
|
||||
result = methods.init.apply(this, arguments);
|
||||
} else {
|
||||
$.error("Method " + method + " does not exist on jQuery.pwstrength-bootstrap");
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}(jQuery, methods));
|
||||
}(jQuery));
|
||||
Executable
+89
@@ -0,0 +1,89 @@
|
||||
Package.describe({
|
||||
name: 'clinical:active-entry',
|
||||
version: '1.5.16',
|
||||
summary: 'SignIn, SignUp, and ForgotPassword pages for Clinical Framework.',
|
||||
git: 'https://github.com/clinical-meteor/clinical-active-entry',
|
||||
documentation: 'README.md'
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
api.versionsFrom('1.1.0.3');
|
||||
|
||||
api.use([
|
||||
'meteor-platform',
|
||||
'templating',
|
||||
'clinical:router@2.0.19',
|
||||
'grove:less@0.1.1',
|
||||
'session',
|
||||
'reactive-dict'
|
||||
//'codetheweb:zxcvbn'
|
||||
], ['client']);
|
||||
|
||||
api.use([
|
||||
'accounts-base',
|
||||
'accounts-password'
|
||||
]);
|
||||
|
||||
api.use([
|
||||
'zuuk:stale-session@1.0.8',
|
||||
'random'
|
||||
], ['client', 'server']);
|
||||
|
||||
api.addFiles([
|
||||
'lib/ActiveEntry.js',
|
||||
'lib/Accounts.js'
|
||||
]);
|
||||
|
||||
api.addFiles([
|
||||
'lib/jquery.pwstrength.bootstrap.js',
|
||||
'lib/checkPasswordStrength.js',
|
||||
'lib/hashCodeGenerator.js'
|
||||
], ['client']);
|
||||
|
||||
api.imply('accounts-base');
|
||||
api.imply('accounts-password');
|
||||
|
||||
api.addFiles([
|
||||
'components/entryPages.js',
|
||||
'components/entryPages.less',
|
||||
|
||||
'components/entrySignIn/entrySignIn.html',
|
||||
'components/entrySignIn/entrySignIn.js',
|
||||
'components/entrySignIn/entrySignIn.less',
|
||||
|
||||
'components/entrySignUp/entrySignUp.html',
|
||||
'components/entrySignUp/entrySignUp.js',
|
||||
'components/entrySignUp/entrySignUp.less',
|
||||
|
||||
'components/forgotPassword/forgotPassword.html',
|
||||
'components/forgotPassword/forgotPassword.js',
|
||||
'components/forgotPassword/forgotPassword.less',
|
||||
|
||||
'components/changePassword/changePassword.html',
|
||||
'components/changePassword/changePassword.js',
|
||||
'components/changePassword/changePassword.less',
|
||||
|
||||
'components/resetPassword/resetPassword.html',
|
||||
'components/resetPassword/resetPassword.js'
|
||||
], ['client']);
|
||||
|
||||
|
||||
api.addFiles('server/methods.js', "server", {testOnly: true});
|
||||
|
||||
api.export("ActiveEntry");
|
||||
});
|
||||
|
||||
|
||||
Package.onTest(function (api) {
|
||||
api.use([
|
||||
'templating',
|
||||
'clinical:router@2.0.19',
|
||||
'grove:less@0.1.1',
|
||||
'standard-app-packages'
|
||||
], ['client']);
|
||||
|
||||
api.use('tinytest');
|
||||
api.use('clinical:active-entry');
|
||||
api.use('clinical:verification');
|
||||
api.addFiles('tests/gagarin/activeEntryTests.js');
|
||||
});
|
||||
@@ -0,0 +1,202 @@
|
||||
Meteor.methods({
|
||||
initializeEntryUsers: function (){
|
||||
console.log('Initializing Users', Meteor.users.find().fetch());
|
||||
|
||||
},
|
||||
|
||||
dropEntryUsers: function (){
|
||||
console.log('Drop Users', Meteor.users.find().fetch());
|
||||
Meteor.users.find().forEach(function(user){
|
||||
Meteor.users.remove({_id: user._id});
|
||||
});
|
||||
},
|
||||
|
||||
insertHashedPassword: function(passwordParameters) {
|
||||
var hashedPassword = passwordParameters[0];
|
||||
var passwordHistoryCount = passwordParameters[1];
|
||||
|
||||
var userId = Meteor.userId();
|
||||
var previousPasswords = Meteor.users.findOne({_id: userId}).previousPasswords;
|
||||
if (previousPasswords) {
|
||||
if (previousPasswords.length == passwordHistoryCount) {
|
||||
// Remove oldest password
|
||||
var ascSortedPasswords = _.sortBy(previousPasswords, function(previousPassword){ return previousPassword.createdAt; });
|
||||
ascSortedPasswords.splice(0, 1);
|
||||
previousPasswords = ascSortedPasswords;
|
||||
}
|
||||
|
||||
previousPasswords.push({hashedPassword: hashedPassword, createdAt: new Date()});
|
||||
Meteor.users.update({_id: userId}, {$set: {previousPasswords: previousPasswords}});
|
||||
} else {
|
||||
Meteor.users.update({_id: userId}, {$set: {previousPasswords: [{hashedPassword: hashedPassword, createdAt: new Date(), select: false}]}});
|
||||
}
|
||||
},
|
||||
|
||||
checkPasswordExistence: function(hashedPassword) {
|
||||
var previousPasswords = Meteor.users.find({_id: Meteor.userId()}).fetch()[0].previousPasswords;
|
||||
for(var i=0; i< previousPasswords.length; i++) {
|
||||
var recordedHashedPassword = previousPasswords[i].hashedPassword;
|
||||
if (recordedHashedPassword == hashedPassword) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
getFailedAttemptsCount: function(emailAddress) {
|
||||
// Check if the user actually exists, and if not, stop here
|
||||
var currentUser = Meteor.users.findOne({"emails.address": emailAddress});
|
||||
if (!currentUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
return currentUser.failedPasswordAttempts || 0;
|
||||
},
|
||||
|
||||
updateFailedAttempts: function(emailAddress, failedAttemptsLimit) {
|
||||
|
||||
// Check if the user actually exists, and if not, stop here
|
||||
var currentUser = Meteor.users.findOne({"emails.address": emailAddress});
|
||||
if (!currentUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
var failedAttemptCount = currentUser.failedPasswordAttempts || 0;
|
||||
if (failedAttemptCount == failedAttemptsLimit) {
|
||||
return failedAttemptCount;
|
||||
} else {
|
||||
if (failedAttemptCount == (failedAttemptsLimit - 1)) {
|
||||
// Locked user account
|
||||
Meteor.users.update({"emails.address": emailAddress}, {$set: {"profile.isLocked": true, failedPasswordAttempts: failedAttemptCount + 1}});
|
||||
} else if (failedAttemptCount < (failedAttemptsLimit - 1)) {
|
||||
Meteor.users.update({"emails.address": emailAddress}, {$set: {failedPasswordAttempts: failedAttemptCount + 1}});
|
||||
}
|
||||
}
|
||||
|
||||
return failedAttemptCount + 1;
|
||||
},
|
||||
|
||||
lockAccount: function(emailAddress) {
|
||||
// Check if the user actually exists, and if not, stop here
|
||||
var currentUser = Meteor.users.findOne({"emails.address": emailAddress});
|
||||
if (!currentUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
Meteor.users.update({"emails.address": emailAddress}, {$set: {"profile.isLocked": true}});
|
||||
},
|
||||
|
||||
resetFailedAttempts: function(emailAddress) {
|
||||
Meteor.users.update({"emails.address": emailAddress}, {$set: {failedPasswordAttempts: 0}});
|
||||
},
|
||||
|
||||
updatePasswordSetDate: function() {
|
||||
Meteor.users.update({_id: Meteor.userId()}, {$set: {"services.password.setDate": new Date()}});
|
||||
},
|
||||
|
||||
isPasswordExpired: function(passwordExpirationDays) {
|
||||
var passwordSetDate = Meteor.users.find({_id: Meteor.userId()}).fetch()[0].services.password.setDate;
|
||||
if (!passwordSetDate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
passwordSetDate.setDate(passwordSetDate.getDate() + passwordExpirationDays);
|
||||
|
||||
if (passwordSetDate <= new Date()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
isAccountLocked: function(emailAddress) {
|
||||
// Check if the user actually exists, and if not, stop here
|
||||
var currentUser = Meteor.users.findOne({"emails.address": emailAddress});
|
||||
if (!currentUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
return currentUser.profile.isLocked || false;
|
||||
},
|
||||
|
||||
updateLastLoginDate: function () {
|
||||
var user = Meteor.users.findOne(Meteor.userId());
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
var priorLoginDate = user.lastLoginDate;
|
||||
if (!priorLoginDate) {
|
||||
priorLoginDate = new Date();
|
||||
}
|
||||
// Update priorLoginDate and lastLoginDate
|
||||
Meteor.users.update({_id: Meteor.userId()}, {$set: {priorLoginDate: priorLoginDate, lastLoginDate: new Date()}});
|
||||
},
|
||||
|
||||
isAccountInactive: function (emailAddress, inactivityPeriodDays) {
|
||||
// Check if the user actually exists, and if not, stop here
|
||||
var currentUser = Meteor.users.findOne({"emails.address": emailAddress});
|
||||
if (!currentUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
var lastLoginDate = currentUser.lastLoginDate;
|
||||
if (!lastLoginDate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
lastLoginDate.setDate(lastLoginDate.getDate() + inactivityPeriodDays);
|
||||
|
||||
if (lastLoginDate <= new Date()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
isLDAPSet: function() {
|
||||
if (LDAP_DEFAULTS && LDAP_DEFAULTS.url && LDAP_DEFAULTS.port) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
checkResetTokenIsExpired: function(token, expireTimeInMinute) {
|
||||
var user = Meteor.users.findOne({"services.password.reset.token": token});
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
var tokenCreatedTime = user.services.password.reset.when;
|
||||
if (!tokenCreatedTime) {
|
||||
return;
|
||||
}
|
||||
// Token will be expired if created time is over 30 min as default
|
||||
tokenCreatedTime.setTime(tokenCreatedTime.getTime() + expireTimeInMinute*60000);
|
||||
if (tokenCreatedTime < new Date()) {
|
||||
// Remove reset token
|
||||
Meteor.users.update({_id: user._id}, {$unset: {'services.password.reset': 1}});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
checkResetPasswordExistence: function(hashedPassword, token) {
|
||||
var user = Meteor.users.findOne({"services.password.reset.token": token});
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
var previousPasswords = user.previousPasswords;
|
||||
if (!previousPasswords) {
|
||||
return;
|
||||
}
|
||||
for(var i=0; i< previousPasswords.length; i++) {
|
||||
var recordedHashedPassword = previousPasswords[i].hashedPassword;
|
||||
if (recordedHashedPassword == hashedPassword) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
});
|
||||
@@ -0,0 +1,215 @@
|
||||
// var nightwatch = require('nightwatch');
|
||||
|
||||
describe('clinical:active-entry', function () {
|
||||
var server = meteor();
|
||||
var client = browser(server);
|
||||
|
||||
// before(function () {
|
||||
// return server.promise(function (resolve){
|
||||
// Meteor.users.find().forEach(function(user){
|
||||
// Meteor.users.remove({_id: user._id});
|
||||
// }, function(){
|
||||
// resolve();
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
// afterEach(function (){
|
||||
// return client.promise(function (resolve){
|
||||
// Meteor.logout(function(error, result){
|
||||
// resolve();
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
|
||||
it("ActiveEntry object should be loaded on client and server", function () {
|
||||
return server.execute(function () {
|
||||
expect(ActiveEntry.isAbc()).to.equal('abc');
|
||||
}).then(function (data){
|
||||
return client.execute(function (a) {
|
||||
expect(ActiveEntry.isAbc()).to.equal('abc');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("Error messages should be empty by default", function () {
|
||||
return client.execute(function () {
|
||||
expect(ActiveEntry.errorMessages.get('signInError')).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ActiveEntry.verifyEmail
|
||||
it('Email validation confirms it is a properly formatted email.', function () {
|
||||
return client.execute(function (a) {
|
||||
ActiveEntry.verifyEmail('janedoe@somewhere.com');
|
||||
expect(ActiveEntry.successMessages.get('email')).to.equal("Email present");
|
||||
|
||||
ActiveEntry.verifyEmail('');
|
||||
expect(ActiveEntry.errorMessages.get('email')).to.equal("Email is required");
|
||||
|
||||
ActiveEntry.verifyEmail('janedoe.somewhere.com');
|
||||
expect(ActiveEntry.errorMessages.get('email')).to.equal("Email is poorly formatted");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// ActiveEntry.verifyPassword
|
||||
it('Password validation confirms it is a properly formatted password.', function () {
|
||||
return client.execute(function (a) {
|
||||
ActiveEntry.verifyPassword('');
|
||||
expect(ActiveEntry.errorMessages.get('password')).to.equal("Password is required");
|
||||
|
||||
ActiveEntry.verifyPassword('kittens');
|
||||
expect(ActiveEntry.errorMessages.get('password')).to.equal(Session.get('passwordWarning'));
|
||||
|
||||
ActiveEntry.verifyPassword('K1tt#ns123');
|
||||
expect(ActiveEntry.successMessages.get('password')).to.equal("Password present");
|
||||
});
|
||||
});
|
||||
|
||||
// ActiveEntry.verifyConfirmPassword
|
||||
it('Password match confirms that two passwords are the same.', function () {
|
||||
return client.execute(function (a) {
|
||||
ActiveEntry.verifyConfirmPassword('K1tt#kittens', 'kittens');
|
||||
expect(ActiveEntry.errorMessages.get('confirm')).to.equal("Passwords do not match");
|
||||
|
||||
ActiveEntry.verifyConfirmPassword('kittens123', 'kittens');
|
||||
expect(ActiveEntry.errorMessages.get('confirm')).to.equal("Passwords do not match");
|
||||
|
||||
ActiveEntry.verifyConfirmPassword('kittens123', 'kittens123');
|
||||
expect(ActiveEntry.errorMessages.get('confirm')).to.equal("Passwords match");
|
||||
|
||||
ActiveEntry.verifyConfirmPassword('K1tt#ns123', 'K1tt#ns123');
|
||||
expect(ActiveEntry.successMessages.get('confirm')).to.equal("Passwords match");
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
// ActiveEntry.verifyFullName
|
||||
it('Fullname validation confirms that at least a first and last name are entered.', function () {
|
||||
return client.execute(function (a) {
|
||||
ActiveEntry.verifyFullName('');
|
||||
expect(ActiveEntry.errorMessages.get('fullName')).to.equal("Name is required");
|
||||
|
||||
ActiveEntry.verifyFullName('Jane');
|
||||
expect(ActiveEntry.errorMessages.get('fullName')).to.equal("Name is probably not complete");
|
||||
|
||||
ActiveEntry.verifyFullName('Jane Doe');
|
||||
expect(ActiveEntry.successMessages.get('fullName')).to.equal("Name present");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// // ActiveEntry.signIn
|
||||
// it('Newly created user record should have role, profile, and name set.', function () {
|
||||
// return client.execute(function () {
|
||||
// ActiveEntry.signUp('janedoe@test.org', 'Janed*e123', 'Janed*e123', 'Jane Doe');
|
||||
// expect(ActiveEntry.successMessages.get('fullName')).to.equal("Name present");
|
||||
// }).then(function (){
|
||||
// return server.wait(500, 'until account is created on the server', function () {
|
||||
// return Meteor.users.findOne({'emails.address': 'janedoe@test.org'});
|
||||
// }).then(function (user){
|
||||
// expect(user.role).to.equal('user');
|
||||
// expect(user.profile.fullName).to.equal('Jane Doe');
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// ActiveEntry.signIn
|
||||
it('Newly created user record should have role, profile, and name set.', function () {
|
||||
return client.execute(function () {
|
||||
// ActiveEntry.signUp('janedoe@test.org', 'Janed*e123', 'Janed*e123', 'Jane Doe');
|
||||
ActiveEntry.signUp('janedoe@test.org', 'Janedoe123', 'Janedoe123', 'Jane Doe');
|
||||
expect(ActiveEntry.successMessages.get('fullName')).to.equal("Name present");
|
||||
}).then(function (){
|
||||
return server.wait(500, 'until account is created on the server', function () {
|
||||
return Meteor.users.findOne({'emails.address': 'janedoe@test.org'});
|
||||
}).then(function (user){
|
||||
expect(user.role).to.equal('user');
|
||||
expect(user.profile.fullName).to.equal('Jane Doe');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("Newly created user should have fullName(), preferredName(), and familyName() methods.", function () {
|
||||
return server.execute(function () {
|
||||
var user = Meteor.users.findOne({'emails.address': 'janedoe@test.org'});
|
||||
expect(user).to.be.ok;
|
||||
expect(user.fullName()).to.equal('Jane Doe');
|
||||
expect(user.givenName()).to.equal('Jane');
|
||||
expect(user.familyName()).to.equal('Doe');
|
||||
}).then(function (){
|
||||
// client.wait(500, "until user is logged out", function(){
|
||||
// Meteor.logout();
|
||||
// });
|
||||
return client.promise(function (resolve){
|
||||
Meteor.logout(function (error, result){
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
it("Newly created user can sign in to the application.", function () {
|
||||
return client.execute(function () {
|
||||
expect(Meteor.userId()).to.not.exist;
|
||||
ActiveEntry.signIn('janedoe@test.org', 'Janed*e123');
|
||||
}).then(function (){
|
||||
client.wait(3000, "for user to sign in", function (){
|
||||
expect(Meteor.userId()).to.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
it("Newly created user can sign out of the application.", function () {
|
||||
return client.execute(function () {
|
||||
expect(Meteor.userId()).to.not.exist;
|
||||
ActiveEntry.signIn('janedoe@test.org', 'Janed*e123');
|
||||
}).then(function (){
|
||||
client.wait(3000, "for user to sign in", function (){
|
||||
expect(Meteor.userId()).to.exist;
|
||||
ActiveEntry.signOut('janedoe@test.org');
|
||||
}).then(function (){
|
||||
expect(Meteor.userId()).to.not.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
// it("config should be able to change company logo", function () {
|
||||
//
|
||||
// });
|
||||
// it("config should be able to change entry message text", function () {
|
||||
//
|
||||
// });
|
||||
|
||||
// it("new user should be able to register on desktop", function () {
|
||||
// client.location = "/sign-in";
|
||||
//
|
||||
// client.execute(function () {
|
||||
// expect($('#entrySignIn')).to.exist();
|
||||
// expect($('#signInPageEmailInput')).to.exist();
|
||||
// expect($('#signInPagePasswordInput')).to.exist();
|
||||
// expect($('#signInToAppButton')).to.exist();
|
||||
// }).setValue('#signInPageEmailInput', 'house@test.org')
|
||||
// .setValue('#signInPagePasswordInput', 'house@test.org')
|
||||
// .click('#signInToAppButton').execute(function(){
|
||||
// expect($('#entrySignIn')).to.exist();
|
||||
// expect($('#signInPageEmailInput')).to.exist();
|
||||
// expect($('#signInPagePasswordInput')).to.exist();
|
||||
// expect($('#signInToAppButton')).to.exist();
|
||||
// // expect($('#entrySignIn')).to.not.exist();
|
||||
// // expect($('#signInPageEmailInput')).to.not.exist();
|
||||
// // expect($('#signInPagePasswordInput')).to.not.exist();
|
||||
// // expect($('#signInToAppButton')).to.not.exist();
|
||||
// });
|
||||
// // .then(function(data){
|
||||
// // return server.execute(3000, 'until the account is created', function () {
|
||||
// // expect(Meteor.users.find().count()).to.be.above(20);
|
||||
// // });
|
||||
// // });
|
||||
// });
|
||||
|
||||
|
||||
});
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
// async version calls method on the server
|
||||
exports.command = function () {
|
||||
var client = this;
|
||||
|
||||
this
|
||||
.timeoutsAsyncScript(5000)
|
||||
.executeAsync(function (data, meteorCallback) {
|
||||
//return HipaaLogger.logEventObject(data);
|
||||
Meteor.call("dropCollaborations", data, function (meteorError, meteorResult) {
|
||||
var response = (meteorError ? {
|
||||
error: meteorError
|
||||
} : {
|
||||
result: meteorResult
|
||||
});
|
||||
meteorCallback(response);
|
||||
});
|
||||
}, [], function (result) {
|
||||
console.log("result.value", result.value);
|
||||
client.assert.ok(result.value);
|
||||
}).pause(1000);
|
||||
return this;
|
||||
};
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
// when new user fills out form and registers, new user should get created
|
||||
// when user signs in with username and password, should redirect to home page
|
||||
// newly created user record should have role
|
||||
// newly created user record should have profile
|
||||
// newly created user record should have full, preferred, and family name
|
||||
// user object should return first name
|
||||
// user object should return last name
|
||||
// user should be able to request reset password email
|
||||
// user should be able to request be able to create new account
|
||||
// guest should be notified if username already exists
|
||||
// guest should be notified if passwords do not match
|
||||
// guest should be notified if email is not correctly formatted
|
||||
// new user should be able to register on desktop
|
||||
// new user should be able to register on tablet
|
||||
// existing user should be able to sign in on desktop
|
||||
// existing user should be able to sign in on tablet
|
||||
// existing user should be able to sign in on phone
|
||||
// existing user should be able to change their password
|
||||
// company logo should display on sign//in page
|
||||
|
||||
|
||||
|
||||
module.exports = {
|
||||
tags: ['users', 'entry'],
|
||||
before: function (client) {
|
||||
client
|
||||
.url("http://localhost:3000/entrySignUp")
|
||||
.initializeUsers()
|
||||
.resizeWindow(1600, 1200);
|
||||
},
|
||||
"new user should be able to register on desktop": function (client) {
|
||||
client
|
||||
.verify.elementPresent("#entrySignUp")
|
||||
.verify.elementPresent("#signUpPageTitle")
|
||||
.verify.elementPresent("#signUpPageMessage")
|
||||
.verify.elementPresent("#signUpPageEmailInput")
|
||||
.verify.elementPresent("#signUpPagePasswordInput")
|
||||
.verify.elementPresent("#signUpPageJoinNowButton")
|
||||
.verify.elementPresent("#signUpPageSignInButton");
|
||||
},
|
||||
"company logo should display on sign-in page": function (client) {
|
||||
client
|
||||
.verify.elementPresent("#entrySignUp")
|
||||
.verify.elementPresent("#entryAppLogo");
|
||||
},
|
||||
"user should be able to request be able to create new account": function (client) {
|
||||
client.verify.elementPresent("#signUpPageEmailInput")
|
||||
.verify.elementPresent("#signUpPagePasswordInput")
|
||||
.verify.elementPresent("#signUpPagePasswordInput")
|
||||
.verify.elementPresent("#signUpPageJoinNowButton");
|
||||
},
|
||||
"guest should be notified if password is insecure": function (client) {
|
||||
client
|
||||
.clearValue("input")
|
||||
.verify.elementPresent("#signUpPagePasswordInput")
|
||||
.verify.cssProperty('#signUpPagePasswordInput', 'border', '1px solid gray')
|
||||
.setValue("#signUpPagePasswordInput", "jan")
|
||||
.verify.cssProperty('#signUpPagePasswordInput', 'border', '1px solid rgb(242, 222, 222)')
|
||||
.setValue("#signUpPagePasswordInput", "iceD*e123")
|
||||
.verify.cssProperty('#signUpPagePasswordInput', 'border', '1px solid green')
|
||||
|
||||
.verify.cssProperty('#signUpPagePasswordConfirmInput', 'border', '1px solid gray')
|
||||
.setValue("#signUpPagePasswordConfirmInput", "ja")
|
||||
.verify.cssProperty('#signUpPagePasswordConfirmInput', 'border',
|
||||
'1px solid rgb(242, 222, 222)')
|
||||
.clearValue("#signUpPagePasswordConfirmInput")
|
||||
.setValue("#signUpPagePasswordConfirmInput", "Janiced*e123")
|
||||
.verify.cssProperty('#signUpPagePasswordConfirmInput', 'border', '1px solid green');
|
||||
},
|
||||
"guest should be notified if passwords do not match": function (client) {
|
||||
client
|
||||
.clearValue("#signUpPagePasswordConfirmInput")
|
||||
.clearValue("#signUpPagePasswordInput")
|
||||
.resetEntry()
|
||||
.pause(500)
|
||||
.verify.cssProperty('#signUpPagePasswordInput', 'border', '1px solid gray')
|
||||
.verify.cssProperty('#signUpPagePasswordConfirmInput', 'border', '1px solid gray')
|
||||
.setValue("#signUpPagePasswordInput", "Janiced*e123")
|
||||
.verify.cssProperty('#signUpPagePasswordInput', 'border', '1px solid green')
|
||||
.verify.cssProperty('#signUpPagePasswordConfirmInput', 'border', '1px solid gray')
|
||||
.setValue("#signUpPagePasswordConfirmInput", "Janiced*e123")
|
||||
.verify.cssProperty('#signUpPagePasswordInput', 'border', '1px solid green')
|
||||
.verify.cssProperty('#signUpPagePasswordConfirmInput', 'border', '1px solid green');
|
||||
},
|
||||
"guest should be notified if email is not correctly formatted": function (client) {
|
||||
client
|
||||
.clearValue("#signUpPageEmailInput")
|
||||
.resetEntry()
|
||||
.verify.elementPresent("#signUpPageEmailInput")
|
||||
.verify.cssProperty('#signUpPageEmailInput', 'border', '1px solid gray')
|
||||
.setValue("#signUpPageEmailInput", "janicedoe")
|
||||
.verify.cssProperty('#signUpPageEmailInput', 'border', '1px solid rgb(242, 222, 222)')
|
||||
.setValue("#signUpPageEmailInput", "@symptomatic.io")
|
||||
.verify.cssProperty('#signUpPageEmailInput', 'border', '1px solid green');
|
||||
},
|
||||
"when new user fills out form and registers, new user should get created": function (client) {
|
||||
client
|
||||
.verify.elementPresent("#entrySignUp")
|
||||
|
||||
.clearValue("#signUpPagePasswordConfirmInput")
|
||||
.clearValue("#signUpPagePasswordInput")
|
||||
.clearValue("#signUpPageFullNameInput")
|
||||
.clearValue("#signUpPageEmailInput")
|
||||
.resetEntry()
|
||||
|
||||
.setValue("#signUpPageFullNameInput", "Janice Doe")
|
||||
.setValue("#signUpPageEmailInput", "janicedoe@symptomatic.io")
|
||||
.setValue("#signUpPagePasswordInput", "Janiced*e123")
|
||||
.setValue("#signUpPagePasswordConfirmInput", "Janiced*e123")
|
||||
|
||||
.click("#signUpPageJoinNowButton").pause(1000)
|
||||
|
||||
.verify.containsText("#usernameLink", "janicedoe@symptomatic.io");
|
||||
},
|
||||
"user should be able to signout": function (client) {
|
||||
client
|
||||
.verify.elementPresent("#logoutButton")
|
||||
.click("#logoutButton").pause(300)
|
||||
.verify.containsText("#usernameLink", "Sign In");
|
||||
},
|
||||
"user should be able to request reset password email": function (client) {
|
||||
client
|
||||
.url("http://localhost:3000/entrySignIn")
|
||||
.verify.elementPresent("#forgotPasswordButton")
|
||||
.click("#forgotPasswordButton")
|
||||
.verify.elementPresent("#forgotPassword")
|
||||
.verify.elementPresent("#signInPageEmailInput")
|
||||
.verify.elementPresent("#sendReminderButton");
|
||||
},
|
||||
"existing user should be able to sign in on desktop": function (client) {
|
||||
client
|
||||
.url("http://localhost:3000/entrySignIn")
|
||||
.resizeWindow(1600, 1200)
|
||||
.verify.containsText("#usernameLink", "Sign In")
|
||||
.signIn("janicedoe@symptomatic.io", "Janiced*e123").pause(500)
|
||||
.verify.containsText("#usernameLink", "janicedoe@symptomatic.io")
|
||||
.click("#logoutButton").pause(200)
|
||||
.verify.containsText("#usernameLink", "Sign In");
|
||||
},
|
||||
"existing user should be able to sign in on tablet": function (client) {
|
||||
client
|
||||
.url("http://localhost:3000/entrySignIn")
|
||||
.resizeWindow(1024, 768)
|
||||
.verify.containsText("#usernameLink", "Sign In")
|
||||
.signIn("janicedoe@symptomatic.io", "Janiced*e123").pause(500)
|
||||
.verify.containsText("#usernameLink", "janicedoe@symptomatic.io")
|
||||
.click("#logoutButton").pause(200)
|
||||
.verify.containsText("#usernameLink", "Sign In");
|
||||
},
|
||||
"existing user should be able to sign in on phone": function (client) {
|
||||
client
|
||||
.url("http://localhost:3000/entrySignIn")
|
||||
.resizeWindow(320, 960)
|
||||
// .verify.containsText("#usernameLink", "Sign In")
|
||||
.signIn("janicedoe@symptomatic.io", "Janiced*e123").pause(500)
|
||||
.click("#navbarHeader").pause(300)
|
||||
.verify.containsText("#usernameLink", "janicedoe@symptomatic.io")
|
||||
.click("#logoutButton").pause(200)
|
||||
.verify.containsText("#usernameLink", "Sign In");
|
||||
},
|
||||
"existing user should be able to change their password" : function (client) {
|
||||
client
|
||||
.url("http://localhost:3000/entrySignIn")
|
||||
.resizeWindow(1600, 1200)
|
||||
.verify.containsText("#usernameLink", "Sign In")
|
||||
.signIn("janicedoe@symptomatic.io", "janicedoe123").pause(500)
|
||||
.verify.containsText("#usernameLink", "janicedoe@symptomatic.io")
|
||||
.url("http://localhost:3000/changePassword")
|
||||
.verify.elementPresent("#changePasswordPageOldPasswordInput")
|
||||
.verify.elementPresent("#changePasswordPagePasswordInput")
|
||||
.verify.elementPresent("#changePasswordPagePasswordConfirmInput")
|
||||
.verify.elementPresent("#changePasswordButton")
|
||||
},
|
||||
"existing user should be notified if desired new password is insecure" : function (client) {
|
||||
client
|
||||
.url("http://localhost:3000/entrySignIn")
|
||||
.resizeWindow(1600, 1200)
|
||||
.verify.containsText("#usernameLink", "Sign In")
|
||||
.signIn("janicedoe@symptomatic.io", "janicedoe123").pause(500)
|
||||
.verify.containsText("#usernameLink", "janicedoe@symptomatic.io")
|
||||
.url("http://localhost:3000/changePassword")
|
||||
.verify.elementPresent("#changePasswordPageOldPasswordInput")
|
||||
.verify.elementPresent("#changePasswordPagePasswordInput")
|
||||
.verify.elementPresent("#changePasswordPagePasswordConfirmInput")
|
||||
.verify.elementPresent("#changePasswordButton")
|
||||
.verify.cssProperty('#changePasswordPagePasswordInput', 'border', '1px solid gray')
|
||||
.setValue("#changePasswordPagePasswordInput", "jan")
|
||||
.verify.cssProperty('#changePasswordPagePasswordInput', 'border', '1px solid rgb(242, 222, 222)')
|
||||
.setValue("#changePasswordPagePasswordInput", "icedoe123")
|
||||
.verify.cssProperty('#changePasswordPagePasswordInput', 'border', '1px solid green')
|
||||
.verify.cssProperty('#changePasswordPagePasswordConfirmInput', 'border', '1px solid gray')
|
||||
.setValue("#changePasswordPagePasswordConfirmInput", "ja")
|
||||
.verify.cssProperty('#changePasswordPagePasswordConfirmInput', 'border', '1px solid rgb(242, 222, 222)')
|
||||
.clearValue("#changePasswordPagePasswordConfirmInput")
|
||||
.setValue("#changePasswordPagePasswordConfirmInput", "janicedoe123")
|
||||
.verify.cssProperty('#changePasswordPagePasswordConfirmInput', 'border', '1px solid green')
|
||||
},
|
||||
"if anonymous user tries to log in with non-existing account, a message is shown" : function (client) {
|
||||
client
|
||||
.url("http://localhost:3000/entrySignIn")
|
||||
.resizeWindow(1024, 768)
|
||||
.signIn("alice@symptomatic.io", "alice123").pause(500)
|
||||
.verify.containsText("#signInPageMessage", "User not found [403]")
|
||||
.verify.cssProperty("#signInPageMessage", "color", "rgba(169, 68, 66, 1)")
|
||||
.verify.cssProperty("#signInPageMessage", "background-color", "rgba(242, 222, 222, 1)")
|
||||
.verify.cssProperty("#signInPageMessage", "border-color", "rgb(235, 204, 209)");
|
||||
},
|
||||
"anonymous guest should be notified if email already exists": function (client) {
|
||||
client
|
||||
.url("http://localhost:3000/entrySignUp")
|
||||
.resizeWindow(1024, 768)
|
||||
.signUp("janicedoe@symptomatic.io", "Janiced*e123").pause(500)
|
||||
.click("#signUpPageJoinNowButton").pause(1000)
|
||||
.verify.elementPresent("#signUpPageMessage")
|
||||
.verify.containsText("#signUpPageMessage", "Email already exists. [403]");
|
||||
},
|
||||
after: function (client) {
|
||||
client
|
||||
.dropEntryUsers()
|
||||
.end();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
.build*
|
||||
@@ -0,0 +1,13 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.10"
|
||||
#sudo: required
|
||||
|
||||
sudo: required
|
||||
|
||||
env:
|
||||
global:
|
||||
- METEOR_ENV=development
|
||||
|
||||
before_install:
|
||||
- "curl -L http://git.io/ejPSng | /bin/sh"
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
#### v2.3.7
|
||||
|
||||
* Updated to use clinical:router
|
||||
|
||||
#### v2.4.0
|
||||
|
||||
* clinical:hipaa-loger extracted from package
|
||||
* HipaaLogger and HipaaLog removed
|
||||
@@ -0,0 +1,71 @@
|
||||
clinical:hipaa-audit-log
|
||||
====================================================
|
||||
|
||||
HIPAA logging and audit features for Meteor Apps built with Clinical UI.
|
||||
|
||||

|
||||
|
||||
====================================================
|
||||
#### Installation
|
||||
|
||||
The HIPAA audit log is now split into two packages: one for the logging, and one for the UI. Please see [``clinical:hipaa-logger``](https://github.com/clinical-meteor/hipaa-logger) for the logging portion.
|
||||
|
||||
````
|
||||
meteor add clinical:hipaa-audit-log
|
||||
meteor add clinical:hipaa-logger
|
||||
````
|
||||
|
||||
====================================================
|
||||
#### URL Routes
|
||||
|
||||
Navigate to the audit log via the default route:
|
||||
|
||||
````js
|
||||
Router.go('/audit');
|
||||
````
|
||||
|
||||
====================================================
|
||||
#### Provided Templates
|
||||
|
||||
Three templates are provided by this package:
|
||||
|
||||
````html
|
||||
{{>hipaaAuditLog}}
|
||||
{{>hipaaRibbon}}
|
||||
{{>hipaaLogPage}}
|
||||
````
|
||||
|
||||
====================================================
|
||||
#### Styling and Classes
|
||||
|
||||
You can adjust the styling of the audit log through the configuration object. The following example shows how to style the audit log with Bootstrap controls.
|
||||
|
||||
````js
|
||||
HipaaAuditLog.configure({
|
||||
classes: {
|
||||
input: "form-control squee",
|
||||
select: "form-control",
|
||||
ribbon: ""
|
||||
},
|
||||
highlightColor: "#006289"
|
||||
});
|
||||
````
|
||||
|
||||
|
||||
====================================================
|
||||
#### StarryNight/Nightwatch API - Provides
|
||||
|
||||
````js
|
||||
// component API calls
|
||||
reviewHipaaAuditLogPage()
|
||||
hipaaLogEntryContains(rowIndex, hipaaEvent)
|
||||
|
||||
// actions
|
||||
logHipaaEvent(hipaaEvent, timeout)
|
||||
````
|
||||
|
||||
|
||||
------------------------
|
||||
### License
|
||||
|
||||

|
||||
@@ -0,0 +1,188 @@
|
||||
<template name="hipaaAuditLog">
|
||||
<ul id="hipaaAuditLog" class="list-group">
|
||||
{{#each hipaaAudit}}
|
||||
{{> hipaaEntry}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<template name="hipaaEntry">
|
||||
{{#if logMessageType 'init'}}
|
||||
<li class="hipaaAuditItem list-group-item">
|
||||
<div class="media">
|
||||
<div class="media-icon">
|
||||
<i class="fa fa-gears fa-2x"></i>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<span class="small gray">{{entryTimestamp}}</span><br>
|
||||
<span class="userName" style="{{getHighlightColor}}">{{userName}}</span> initialized HIPAA Audit Log.
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{{/if}}
|
||||
|
||||
|
||||
{{#if logMessageType 'access'}}
|
||||
<li class="hipaaAuditItem list-group-item">
|
||||
<div class="media">
|
||||
<div class="media-icon">
|
||||
<i class="fa fa-database fa-2x"></i>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<span class="small gray">{{entryTimestamp}}</span><br>
|
||||
<span class="userName" style="{{getHighlightColor}}">{{userName}}</span> accessed record(s) <span class="mongoRecordId" style="{{getHighlightColor}}">{{getRecordId}}</span> in the <span class="collectionName" style="{{getHighlightColor}}">{{getCollectionName}}</span> collection.
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{{/if}}
|
||||
|
||||
|
||||
{{#if logMessageType 'create'}}
|
||||
<li class="hipaaAuditItem list-group-item">
|
||||
<div class="media">
|
||||
<div class="media-icon">
|
||||
<i class="fa fa-plus-circle fa-2x"></i>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<span class="small gray">{{entryTimestamp}}</span><br>
|
||||
{{#if hasPatientInfo}}
|
||||
<span class="userName" style="{{getHighlightColor}}">{{userName}}</span> created record <span class="mongoRecordId" style="{{getHighlightColor}}">{{getRecordId}}</span> in the <span class="collectionName" style="{{getHighlightColor}}">{{getCollectionName}}</span> collection, regarding patient <span class="patientName" style="{{getHighlightColor}}">{{getPatientName}}</span>.
|
||||
{{else}}
|
||||
<span class="userName" style="{{getHighlightColor}}">{{userName}}</span> created record <span class="mongoRecordId" style="{{getHighlightColor}}">{{getRecordId}}</span> in the <span class="collectionName" style="{{getHighlightColor}}">{{getCollectionName}}</span> collection.
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{{/if}}
|
||||
|
||||
|
||||
{{#if logMessageType 'modify'}}
|
||||
<li class="hipaaAuditItem list-group-item">
|
||||
<div class="media">
|
||||
<div class="media-icon">
|
||||
<i class="fa fa-pencil fa-2x"></i>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<span class="small gray">{{entryTimestamp}}</span><br>
|
||||
{{#if hasPatientInfo}}
|
||||
<span class="userName" style="{{getHighlightColor}}">{{userName}}</span> modified record <span class="mongoRecordId" style="{{getHighlightColor}}">{{getRecordId}}</span> in the <span class="collectionName" style="{{getHighlightColor}}">{{getCollectionName}}</span> collection, regarding patient <span class="patientName" style="{{getHighlightColor}}">{{getPatientName}}</span>.
|
||||
{{else}}
|
||||
<span class="userName" style="{{getHighlightColor}}">{{userName}}</span> modified record <span class="mongoRecordId" style="{{getHighlightColor}}">{{getRecordId}}</span> in the <span class="collectionName" style="{{getHighlightColor}}">{{getCollectionName}}</span> collection.
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{{/if}}
|
||||
|
||||
|
||||
{{#if logMessageType 'clone'}}
|
||||
<li class="hipaaAuditItem list-group-item">
|
||||
<div class="media">
|
||||
<div class="media-icon">
|
||||
<i class="fa fa-code-fork fa-2x"></i>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<span class="small gray">{{entryTimestamp}}</span><br>
|
||||
<span class="userName" style="{{getHighlightColor}}">{{userName}}</span> cloned record <span class="mongoRecordId" style="{{getHighlightColor}}">{{getRecordId}}</span> in the <span class="collectionName" style="{{getHighlightColor}}">{{getCollectionName}}</span> collection.
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{{/if}}
|
||||
|
||||
|
||||
{{#if logMessageType 'delete'}}
|
||||
<li class="hipaaAuditItem list-group-item">
|
||||
<div class="media">
|
||||
<div class="media-icon">
|
||||
<i class="fa fa-minus-circle fa-2x"></i>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<span class="small gray">{{entryTimestamp}}</span><br>
|
||||
<span class="userName" style="{{getHighlightColor}}">{{userName}}</span> deleted record <span class="mongoRecordId" style="{{getHighlightColor}}">{{getRecordId}}</span> in the <span class="collectionName" style="{{getHighlightColor}}">{{getCollectionName}}</span> collection.
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{{/if}}
|
||||
|
||||
|
||||
{{#if logMessageType 'denied'}}
|
||||
<li class="hipaaAuditItem list-group-item">
|
||||
<div class="media">
|
||||
<div class="media-icon">
|
||||
<i class="fa fa-warning fa-2x"></i>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<span class="small gray">{{entryTimestamp}}</span><br>
|
||||
<span class="userName" style="{{getHighlightColor}}">{{userName}}</span> was denied access to record <span class="mongoRecordId" style="{{getHighlightColor}}">{{getRecordId}}</span> in the <span class="collectionName" style="{{getHighlightColor}}">{{getCollectionName}}</span> collection.
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{{/if}}
|
||||
|
||||
|
||||
{{#if logMessageType 'viewed'}}
|
||||
<li class="hipaaAuditItem list-group-item">
|
||||
<div class="media">
|
||||
<div class="media-icon">
|
||||
<i class="fa fa-eye fa-2x"></i>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<span class="small gray">{{entryTimestamp}}</span><br>
|
||||
{{#if hasPatientInfo}}
|
||||
<span class="userName" style="{{getHighlightColor}}">{{userName}}</span> viewed record <span class="mongoRecordId" style="{{getHighlightColor}}">{{getRecordId}}</span> in the <span class="collectionName" style="{{getHighlightColor}}">{{getCollectionName}}</span> collection, regarding patient <span class="patientName" style="{{getHighlightColor}}">{{getPatientName}}</span>.
|
||||
{{else}}
|
||||
<span class="userName" style="{{getHighlightColor}}">{{userName}}</span> viewed record <span class="mongoRecordId" style="{{getHighlightColor}}">{{getRecordId}}</span> in the <span class="collectionName" style="{{getHighlightColor}}">{{getCollectionName}}</span> collection.
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{{/if}}
|
||||
|
||||
|
||||
{{#if logMessageType 'publish'}}
|
||||
<li class="hipaaAuditItem list-group-item">
|
||||
<div class="media">
|
||||
<div class="media-icon">
|
||||
<i class="fa fa-rss fa-2x"></i>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<span class="small gray">{{entryTimestamp}}</span><br>
|
||||
<span class="userName" style="{{getHighlightColor}}">{{userName}}</span> published record <span class="mongoRecordId" style="{{getHighlightColor}}">{{getRecordId}}</span> in the <span class="collectionName" style="{{getHighlightColor}}">{{getCollectionName}}</span> collection.
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{{/if}}
|
||||
|
||||
|
||||
{{#if logMessageType 'unpublish'}}
|
||||
<li class="hipaaAuditItem list-group-item">
|
||||
<div class="media">
|
||||
<div class="media-icon">
|
||||
<i class="fa fa-circle-o fa-2x"></i>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<span class="small gray">{{entryTimestamp}}</span><br>
|
||||
<span class="userName" style="{{getHighlightColor}}">{{userName}}</span> unpublished record <span class="mongoRecordId" style="{{getHighlightColor}}">{{getRecordId}}</span> in the <span class="collectionName" style="{{getHighlightColor}}">{{getCollectionName}}</span> collection.
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{{/if}}
|
||||
|
||||
|
||||
{{#if logMessageType 'error'}}
|
||||
<li class="hipaaAuditItem list-group-item">
|
||||
<div class="media">
|
||||
<div class="media-icon">
|
||||
<i class="fa fa-bomb fa-2x"></i>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<span class="small gray">{{entryTimestamp}}</span><br>
|
||||
<span class="userName" style="{{getHighlightColor}}">{{userName}}</span> received a data access error while performing an action!<br>
|
||||
<span class="error">{{getErrorMessage}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{{/if}}
|
||||
|
||||
</template>
|
||||
@@ -0,0 +1,160 @@
|
||||
//Hipaa = new Meteor.Collection("hipaa");
|
||||
|
||||
Session.setDefault("hipaaSearchFilter", '');
|
||||
Session.setDefault("hipaaTypeFilter", '');
|
||||
|
||||
// search window defaults to seven days in the past and one day in the future
|
||||
Session.setDefault("beginDateFilter", new Date(moment().subtract(7, "days")).toISOString());
|
||||
Session.setDefault("endDateFilter", new Date(moment().add(1, "days")).toISOString());
|
||||
|
||||
|
||||
|
||||
|
||||
Template.hipaaAuditLog.onRendered(function () {
|
||||
Session.set("ribbonWidth", $('#hipaaRibbon').width());
|
||||
});
|
||||
|
||||
Template.hipaaAuditLog.helpers({
|
||||
getHipaaSearchFilter: function () {
|
||||
return Session.get('hipaaSearchFilter');
|
||||
},
|
||||
hipaaAudit: function () {
|
||||
// return HipaaLog.find();
|
||||
return HipaaLog.find({
|
||||
$or: [
|
||||
{
|
||||
userName: {
|
||||
$regex: Session.get('hipaaSearchFilter'),
|
||||
$options: 'i'
|
||||
}
|
||||
},
|
||||
{
|
||||
patientName: {
|
||||
$regex: Session.get('hipaaSearchFilter'),
|
||||
$options: 'i'
|
||||
}
|
||||
},
|
||||
{
|
||||
recordId: {
|
||||
$regex: Session.get('hipaaSearchFilter'),
|
||||
$options: 'i'
|
||||
}
|
||||
},
|
||||
{
|
||||
collectionName: {
|
||||
$regex: Session.get('hipaaSearchFilter'),
|
||||
$options: 'i'
|
||||
}
|
||||
}
|
||||
],
|
||||
eventType: {
|
||||
$regex: Session.get("hipaaTypeFilter"),
|
||||
$options: 'i'
|
||||
},
|
||||
timestamp: {
|
||||
$lte: new Date(Session.get('endDateFilter')),
|
||||
$gte: new Date(Session.get('beginDateFilter'))
|
||||
}
|
||||
}, {
|
||||
sort: {
|
||||
timestamp: -1
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Template.hipaaAuditLog.events({
|
||||
"keyup #hipaaSearchFilter": function (event, template) {
|
||||
Session.set("hipaaSearchFilter", $('#hipaaSearchFilter').val());
|
||||
},
|
||||
"click .userName": function(event, template) {
|
||||
var userName = $(event.currentTarget).text();
|
||||
Session.set('hipaaSearchFilter', userName);
|
||||
},
|
||||
"click .patientName": function(event, template) {
|
||||
var patientName = $(event.currentTarget).text();
|
||||
var patientNameRegex = patientName.replace(/[!@#$%^&*()+=\-[\]\\';,./{}|":<>?~_]/g, "\\$&");
|
||||
Session.set('hipaaSearchFilter', patientNameRegex);
|
||||
},
|
||||
"click .mongoRecordId": function(event, template) {
|
||||
var mongoRecordId = $(event.currentTarget).text();
|
||||
Session.set('hipaaSearchFilter', mongoRecordId);
|
||||
},
|
||||
"click .collectionName": function(event, template) {
|
||||
var collectionName = $(event.currentTarget).text();
|
||||
Session.set('hipaaSearchFilter', collectionName);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
//==================================================================================================
|
||||
// HIPAA EVENT RECORD
|
||||
|
||||
Template.hipaaEntry.helpers({
|
||||
getHighlightColor: function () {
|
||||
var hipaaAuditLog = Session.get('HipaaAuditLogConfig');
|
||||
if (hipaaAuditLog) {
|
||||
return "color:" + hipaaAuditLog.highlightColor;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
getUserName: function () {
|
||||
if (this.userName) {
|
||||
return this.userName;
|
||||
} else {
|
||||
return "---";
|
||||
}
|
||||
},
|
||||
getPatientName: function () {
|
||||
if (this.patientName) {
|
||||
return this.patientName;
|
||||
} else {
|
||||
return "---";
|
||||
}
|
||||
},
|
||||
hasPatientInfo: function () {
|
||||
if (this.patientName) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
getErrorMessage: function () {
|
||||
if (this.message) {
|
||||
return this.message;
|
||||
} else {
|
||||
return "---";
|
||||
}
|
||||
},
|
||||
getCollectionName: function () {
|
||||
if (this.collectionName) {
|
||||
return this.collectionName;
|
||||
} else {
|
||||
return "---";
|
||||
}
|
||||
},
|
||||
getRecordId: function () {
|
||||
if (this.recordId) {
|
||||
return this.recordId;
|
||||
} else {
|
||||
return "---";
|
||||
}
|
||||
},
|
||||
entryTimestamp: function () {
|
||||
return moment(this.timestamp).format("YYYY, MMM DD, hh:mm A");
|
||||
},
|
||||
entryTime: function () {
|
||||
return moment(this.timestamp).format("HH:MM A");
|
||||
},
|
||||
entryDate: function () {
|
||||
return moment(this.timestamp).format("YYYY, MMM DD");
|
||||
},
|
||||
logMessageType: function (eventType) {
|
||||
if (this.eventType === eventType) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
#hipaaAuditLog{
|
||||
color: #fff;
|
||||
width: 100%;
|
||||
|
||||
.hipaaAuditItem {
|
||||
border: none;
|
||||
background-color: black;
|
||||
list-style: none;
|
||||
padding: 10px;
|
||||
line-height: 25px;
|
||||
|
||||
// There's a CSS rule on ohif-user-management that's changing the .hipaaAuditItem border style (w/ !important)
|
||||
&:first-child {
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
border-top: 1px solid #436270;
|
||||
}
|
||||
|
||||
border-bottom: 1px solid #436270;
|
||||
|
||||
&:nth-child(even) {
|
||||
background-color: #151a1f;
|
||||
}
|
||||
|
||||
&:hover, &:active, &.active {
|
||||
background-color: #2c363f;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
top: 12px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.media-icon {
|
||||
float: left;
|
||||
top: 12px;
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.gray{
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.userName, .patientName, .mongoRecordId, .collectionName{
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<template name="hipaaCloseButton">
|
||||
<div class="close-btn">
|
||||
{{#if header}}
|
||||
<button type="button" class="close">
|
||||
<span>×</span>
|
||||
</button>
|
||||
{{else}}
|
||||
<button type="button" class="btn btn-default">
|
||||
Close
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,12 @@
|
||||
Template.hipaaCloseButton.events({
|
||||
'click button': function() {
|
||||
var history = window.history;
|
||||
|
||||
if(history.length > 0) {
|
||||
history.go(-1);
|
||||
}
|
||||
else {
|
||||
window.location = '/'
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
@tableTextSecondaryColor: #91b9cd;
|
||||
|
||||
.close-btn {
|
||||
text-align: right;
|
||||
padding: 5px 5px 0;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
display: table;
|
||||
content: " ";
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.close {
|
||||
color: @tableTextSecondaryColor;
|
||||
opacity: 1;
|
||||
|
||||
&.close:hover, &.close:focus {
|
||||
color: #fff;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
margin-bottom: 5px;
|
||||
margin-right: 5%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1161px) {
|
||||
.close-btn {
|
||||
.btn-default {
|
||||
margin-right: 3%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 991px) {
|
||||
.close-btn {
|
||||
.btn-default {
|
||||
margin-right: 1%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<template name="hipaaLogPage">
|
||||
<div id="hipaaLogPage" class="page">
|
||||
|
||||
<div class="hipaaHeader clearfix">
|
||||
<div class="header pull-left">
|
||||
Audit Log
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{{> hipaaCloseButton header="true" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{> hipaaRibbon }}
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
{{> hipaaAuditLog}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if hasCloseButtons}}
|
||||
{{> hipaaCloseButton }}
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
Router.route("/audit", {
|
||||
name:"hipaaAuditLogRoute",
|
||||
template:"hipaaLogPage"
|
||||
});
|
||||
|
||||
|
||||
Template.hipaaLogPage.helpers({
|
||||
hasCloseButtons: function() {
|
||||
var hipaaAuditLog = Session.get('HipaaAuditLogConfig');
|
||||
return hipaaAuditLog && hipaaAuditLog.closeButton;
|
||||
}
|
||||
});
|
||||
|
||||
// We probably don't need this, but the subscription in subscriptions.js doesn't seem to be working?
|
||||
Template.hipaaLogPage.onCreated(function() {
|
||||
this.subscribe('HipaaLog');
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
@tableTextSecondaryColor: #91b9cd;
|
||||
|
||||
@hipaaHeaderHeight: 75px;
|
||||
@hipaaHeaderBgColor: #151a1f;
|
||||
|
||||
#hipaaLogPage {
|
||||
background-color: #000;
|
||||
|
||||
.hipaaHeader {
|
||||
background-color: @hipaaHeaderBgColor;
|
||||
height: @hipaaHeaderHeight;
|
||||
margin-bottom: 2px;
|
||||
padding: 0 20px;
|
||||
|
||||
& > div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-size: 22px;
|
||||
font-weight: 300;
|
||||
color: @tableTextSecondaryColor;
|
||||
line-height: @hipaaHeaderHeight;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0;
|
||||
padding: 0 5%;
|
||||
width: 100%;
|
||||
|
||||
& > div.row {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1161px) {
|
||||
#hipaaLogPage {
|
||||
.container {
|
||||
padding: 0 3%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 991px) {
|
||||
#hipaaLogPage {
|
||||
.container {
|
||||
padding: 0 1%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<template name="hipaaRibbon">
|
||||
<div id="hipaaRibbon" class="row {{getRibbonClass}}">
|
||||
<div class="filter col-xs-12 col-sm-4 col-md-6">
|
||||
<span>User</span>
|
||||
<div class="btn-group">
|
||||
<input type="text" name="hipaaSearchFilter" id="hipaaSearchFilter" value="{{getHipaaSearchFilter}}" class="form-control">
|
||||
<span id="searchClear" class="glyphicon glyphicon-remove-circle"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter col-xs-4 col-sm-3 col-md-2 text-center">
|
||||
<span>Begin Date</span>
|
||||
<input type="date" id="beginDateInput" class="{{getInputClass}}" value="{{getBeginDate}}">
|
||||
</div>
|
||||
<div class="filter col-xs-4 col-sm-3 col-md-2 text-center">
|
||||
<span>End Date</span>
|
||||
<input type="date" id="endDateInput" class="{{getInputClass}}" value="{{getEndDate}}">
|
||||
</div>
|
||||
<div class="filter col-xs-4 col-sm-2 text-center">
|
||||
<span>Action</span>
|
||||
<select id="actionFilter" name="actionFilter" class="{{getSelectClass}}">
|
||||
<option id="filterCreatedButton" value="create">Created</option>
|
||||
<option id="filterModifiedButton" value="modify">Modified</option>
|
||||
<option id="filterViewedButton" value="viewed">Viewed</option>
|
||||
<option id="filterAllButton" selected value="">All</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,75 @@
|
||||
Template.hipaaRibbon.events({
|
||||
'keyup #hipaaSearchFilter': function () {
|
||||
Session.set("hipaaSearchFilter", $('#hipaaSearchFilter').val());
|
||||
},
|
||||
"change #beginDateInput": function (event, template) {
|
||||
Session.set("beginDateFilter", $('#beginDateInput').val() + "T00:00:00.000Z");
|
||||
},
|
||||
"change #endDateInput": function (event, template) {
|
||||
Session.set("endDateFilter", $('#endDateInput').val() + "T00:00:00.000Z");
|
||||
},
|
||||
'change #actionFilter': function(e) {
|
||||
var actionType = e.currentTarget.value;
|
||||
Session.set("hipaaTypeFilter", actionType);
|
||||
},
|
||||
'click #filterCreatedButton': function () {
|
||||
Session.set("hipaaTypeFilter", 'create');
|
||||
},
|
||||
'click #filterModifiedButton': function () {
|
||||
Session.set("hipaaTypeFilter", 'modify');
|
||||
},
|
||||
'click #filterViewedButton': function () {
|
||||
Session.set("hipaaTypeFilter", 'viewed');
|
||||
},
|
||||
'click #filterAllButton': function () {
|
||||
Session.set("hipaaTypeFilter", '');
|
||||
},
|
||||
'click #searchClear': function() {
|
||||
Session.set("hipaaSearchFilter", '');
|
||||
}
|
||||
});
|
||||
|
||||
var ribbonBreakPoint = 760;
|
||||
|
||||
|
||||
Meteor.startup(function(){
|
||||
$(window).resize(function(evt) {
|
||||
Session.set("ribbonWidth", $('#hipaaRibbon').width());
|
||||
});
|
||||
});
|
||||
|
||||
Template.hipaaRibbon.helpers({
|
||||
getRibbonClass: function () {
|
||||
var hipaaAuditLog = Session.get('HipaaAuditLogConfig');
|
||||
if (hipaaAuditLog && hipaaAuditLog.classes) {
|
||||
return hipaaAuditLog.classes.ribbon;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
getSelectClass: function () {
|
||||
var hipaaAuditLog = Session.get('HipaaAuditLogConfig');
|
||||
if (hipaaAuditLog && hipaaAuditLog.classes) {
|
||||
return hipaaAuditLog.classes.select;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
getInputClass: function () {
|
||||
var hipaaAuditLog = Session.get('HipaaAuditLogConfig');
|
||||
if (hipaaAuditLog && hipaaAuditLog.classes) {
|
||||
return hipaaAuditLog.classes.input;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
getHipaaSearchFilter: function () {
|
||||
return Session.get('hipaaSearchFilter');
|
||||
},
|
||||
getBeginDate: function () {
|
||||
return moment(Session.get("beginDateFilter")).format("YYYY-MM-DD");
|
||||
},
|
||||
getEndDate: function () {
|
||||
return moment(Session.get("endDateFilter")).format("YYYY-MM-DD");
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
@tableTextPrimaryColor: white;
|
||||
@tableTextSecondaryColor: #91b9cd;
|
||||
|
||||
@inputBackgroundColor: #2c363f;
|
||||
@inputPlaceholderColor: lightgray;
|
||||
|
||||
@hipaaRibbonBgColor: #151a1f;
|
||||
|
||||
#hipaaRibbon {
|
||||
background-color: @hipaaRibbonBgColor;
|
||||
border-bottom: solid 1px #6fbde2;
|
||||
color: @tableTextPrimaryColor;
|
||||
margin: 0 0 2px;
|
||||
padding: 20px 5%;
|
||||
|
||||
.filter {
|
||||
> span {
|
||||
font-size: 15px;
|
||||
display: block;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
min-height: 70px;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="date"] {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
select {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
input, select {
|
||||
height: 40px;
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background-color: @inputBackgroundColor;
|
||||
color: @inputPlaceholderColor;
|
||||
font-size: 10pt;
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
|
||||
transition: all 0.15s ease;
|
||||
-webkit-transition: all 0.15s ease;
|
||||
|
||||
&:active, &:hover {
|
||||
background-color: @inputBackgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
width: 100%;
|
||||
|
||||
#hipaaSearchFilter{
|
||||
padding-right: 35px;
|
||||
}
|
||||
|
||||
#searchClear {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
height: 14px;
|
||||
margin: auto;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media only screen and (max-width: 1161px) {
|
||||
#hipaaRibbon {
|
||||
padding: 20px 3%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 991px) {
|
||||
#hipaaRibbon {
|
||||
padding: 20px 1%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 767px) {
|
||||
#hipaaRibbon {
|
||||
.filter {
|
||||
&:first-child {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Meteor.subscribe('HipaaLog');
|
||||
@@ -0,0 +1,20 @@
|
||||
HipaaAuditLog = {
|
||||
configure: function (configObject) {
|
||||
if (Meteor.isClient) {
|
||||
Session.set('HipaaAuditLogConfig', configObject);
|
||||
}
|
||||
//console.log("HipaaAuditLogConfig.configObject", configObject);
|
||||
}
|
||||
}
|
||||
|
||||
if (Meteor.isClient) {
|
||||
Session.setDefault('HipaaAuditLogConfig', {
|
||||
classes: {
|
||||
input: "",
|
||||
select: "",
|
||||
ribbon: ""
|
||||
},
|
||||
highlightColor: "",
|
||||
closeButton: false
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
Package.describe({
|
||||
summary: "HIPAA audit log for ClinicalFramework.",
|
||||
version: "2.4.2",
|
||||
git: "http://github.com/clinical-meteor/clinical-hipaa-audit-log.git",
|
||||
name: "clinical:hipaa-audit-log"
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.versionsFrom('1.1.0.3');
|
||||
|
||||
api.use('meteor-platform');
|
||||
|
||||
api.use('mrt:moment@2.8.1', 'client');
|
||||
api.use('grove:less@0.1.1', 'client');
|
||||
|
||||
api.use('clinical:router@2.0.19');
|
||||
api.use('clinical:fonts@1.0.0', 'client');
|
||||
api.use('clinical:auto-resizing@0.1.2', 'client');
|
||||
api.use('clinical:hipaa-logger@1.0.0');
|
||||
|
||||
api.imply('clinical:hipaa-logger');
|
||||
|
||||
api.addFiles('lib/HipaaAuditLog.js');
|
||||
|
||||
api.addFiles('server/initialize.js', "server");
|
||||
api.addFiles('server/publication.js', "server");
|
||||
|
||||
api.addFiles('components/hipaaRibbon/hipaaRibbon.html', "client");
|
||||
api.addFiles('components/hipaaRibbon/hipaaRibbon.js', "client");
|
||||
api.addFiles('components/hipaaRibbon/hipaaRibbon.less', "client");
|
||||
|
||||
api.addFiles('components/hipaaAuditLog/hipaaAuditLog.html', "client");
|
||||
api.addFiles('components/hipaaAuditLog/hipaaAuditLog.js', "client");
|
||||
api.addFiles('components/hipaaAuditLog/hipaaAuditLog.less', "client");
|
||||
|
||||
api.addFiles('components/hipaaLogPage/hipaaLogPage.html', "client");
|
||||
api.addFiles('components/hipaaLogPage/hipaaLogPage.js', "client");
|
||||
api.addFiles('components/hipaaLogPage/hipaaLogPage.less', "client");
|
||||
|
||||
api.addFiles('components/hipaaCloseButton/hipaaCloseButton.html', "client");
|
||||
api.addFiles('components/hipaaCloseButton/hipaaCloseButton.js', "client");
|
||||
api.addFiles('components/hipaaCloseButton/hipaaCloseButton.less', "client");
|
||||
|
||||
api.export('HipaaAuditLog');
|
||||
});
|
||||
|
||||
|
||||
|
||||
Package.onTest(function (api) {
|
||||
api.use('tinytest');
|
||||
|
||||
api.use('meteor-platform');
|
||||
api.use('clinical:router@2.0.19', 'client');
|
||||
api.use('mrt:moment@2.8.1', 'client');
|
||||
api.use('grove:less@0.1.1', 'client');
|
||||
|
||||
api.use('clinical:verification');
|
||||
api.use('clinical:fonts@1.0.0');
|
||||
api.use('clinical:hipaa-audit-log');
|
||||
api.use('clinical:hipaa-logger@1.0.0');
|
||||
|
||||
api.addFiles('tests/tinytest/audit-log-tests.js');
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 304 KiB |
@@ -0,0 +1,26 @@
|
||||
Meteor.methods({
|
||||
initializeDemoLog:function (){
|
||||
console.log('initializeDemoLog');
|
||||
|
||||
// hipaaEvent, userId, userName, collectionName, recordId, patientId, patientName, message
|
||||
HipaaLogger.logEvent("init", Meteor.userId(), "Ada Lovelace");
|
||||
|
||||
HipaaLogger.logEvent("create", Meteor.userId(), "Ada Lovelace", "Users", Random.id(), Random.id(), "John Doe");
|
||||
|
||||
HipaaLogger.logEvent("viewed", Meteor.userId(), "Mary Shelley", "Users", Random.id(), Random.id(), "John Doe");
|
||||
HipaaLogger.logEvent("create", Meteor.userId(), "Florence Nightingale", "Vitals", Random.id(), Random.id(), "John Doe");
|
||||
HipaaLogger.logEvent("viewed", Meteor.userId(), "Florence Nightingale", "Medications", Random.id(), Random.id(), "John Doe");
|
||||
HipaaLogger.logEvent("create", Meteor.userId(), "Florence Nightingale", "Medications", Random.id(), Random.id(), "John Doe");
|
||||
HipaaLogger.logEvent("denied", Meteor.userId(), "Kurt Vonnegut", "MedicationPlans", Random.id(), Random.id(), "John Doe");
|
||||
HipaaLogger.logEvent("create", Meteor.userId(), "Florence Nightingale", "Vitals", Random.id(), Random.id(), "John Doe");
|
||||
HipaaLogger.logEvent("viewed", Meteor.userId(), "Florence Nightingale", "MedicationPlans", Random.id(), Random.id(), "John Doe");
|
||||
HipaaLogger.logEvent("modify", Meteor.userId(), "Florence Nightingale", "MedicationPlans", Random.id(), Random.id(), "John Doe");
|
||||
HipaaLogger.logEvent("viewed", Meteor.userId(), "Edward Doisy", "Users", Random.id(), Random.id(), "John Doe");
|
||||
HipaaLogger.logEvent("clone", Meteor.userId(), "Edward Doisy", "MedicationPlans", Random.id(), Random.id(), "John Doe");
|
||||
HipaaLogger.logEvent("publish", Meteor.userId(), "Edward Doisy", "MedicationPlans", Random.id(), Random.id(), "John Doe");
|
||||
HipaaLogger.logEvent("unpublish", Meteor.userId(), "Edward Doisy", "MedicationPlans", Random.id(), Random.id(), "John Doe");
|
||||
HipaaLogger.logEvent("delete", Meteor.userId(), "Ada Lovelace", "MedicationPlans", Random.id(), Random.id(), "John Doe");
|
||||
HipaaLogger.logEvent("viewed", Meteor.userId(), "Florence Nightingale", "Vitals", Random.id(), Random.id(), "John Doe");
|
||||
HipaaLogger.logEvent("create", Meteor.userId(), "Florence Nightingale", "Vitals", Random.id(), Random.id(), "John Doe");
|
||||
}
|
||||
});
|
||||
Loaded 100 of 266 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user