WIP reorganization

This commit is contained in:
Erik Ziegler committed 2018-12-12 09:10:39 +01:00
1 parent 6706dfe634
commit 054e37dfb7
266 files changed
+98 -620

No files matched your search

+47
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
dev_bundle
local
+7
View File
@@ -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": ""
}
}
}
+63
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
server
browser
+1
View File
@@ -0,0 +1 @@
METEOR@1.7.0.3
+145
View File
@@ -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
+8
View File
@@ -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)
+58
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
METEOR_PACKAGE_DIRS="../Packages" meteor --settings ../config/medkenOrthanc.json
+13
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
echo "Starting Meteor server..."
METEOR_PACKAGE_DIRS="../Packages" meteor --settings ../config/orthancDICOMWeb.json
+3
View File
@@ -0,0 +1,3 @@
<body>
<div id="notificationArea" class="notification-area"></div>
</body>
+2
View File
@@ -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
+34
View File
@@ -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);
}
}
});
});
+17
View File
@@ -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))
});
});
});
+3
View File
@@ -0,0 +1,3 @@
import loglevel from 'loglevel';
log = loglevel.getLogger('OHIFViewer');
log.setLevel('info');
+78
View File
@@ -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' });
File diff suppressed because it is too large. Load diff
+38
View File
@@ -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

+52
View File
@@ -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
});
}
});
+13
View File
@@ -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.
+130
View File
@@ -0,0 +1,130 @@
## clinical:active-entry
This package provides the SignIn, SignUp, and ForgotPassword pages.
[![Circle CI](https://circleci.com/gh/clinical-meteor/active-entry/tree/master.svg?style=svg)](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.
![entry-workflow](https://raw.githubusercontent.com/clinical-meteor/active-entry/master/docs/Entry%20Workflow.png)
===============================
#### 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
![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)
+84
View File
@@ -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
View File
@@ -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
View File
@@ -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%;
// }
// }
}
@@ -0,0 +1,10 @@
exports.command = function () {
this
.execute(function () {
return Meteor.logout();
}).pause(1000);
return this;
};
@@ -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;
};
@@ -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;
};
@@ -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
}
@@ -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);
}
});
+48
View File
@@ -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
View File
@@ -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));
+89
View File
@@ -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);
// // });
// // });
// });
});
@@ -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;
};
@@ -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
+71
View File
@@ -0,0 +1,71 @@
clinical:hipaa-audit-log
====================================================
HIPAA logging and audit features for Meteor Apps built with Clinical UI.
![HipaaAuditLogScreenshot](https://raw.githubusercontent.com/awatson1978/clinical-hipaa-audit-log/master/screenshots/auditlog.png)
====================================================
#### 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
![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)
@@ -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>&times;</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