diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..8dd321428 --- /dev/null +++ b/.gitignore @@ -0,0 +1,384 @@ +backend/out/ +# Created by https://www.gitignore.io/api/vim,sass,java,node,code,macos,linux,emacs,gradle,windows,angular,sublimetext,intellij+iml + +### Angular ### +## Angular ## +# compiled output +/dist +/tmp +/app/**/*.js +/app/**/*.js.map + +# dependencies +/node_modules +/bower_components + +# IDEs and editors +/.idea + +# misc +/.sass-cache +/connect.lock +/coverage/* +/libpeerconnection.log +npm-debug.log +testem.log +/typings + +# e2e +/e2e/*.js +/e2e/*.map + +#System Files +.DS_Store + +### Code ### +# Visual Studio Code - https://code.visualstudio.com/ +.settings/ +.vscode/ +tsconfig.json +jsconfig.json + +### Emacs ### +# -*- mode: gitignore; -*- +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* + +# Org-mode +.org-id-locations +*_archive + +# flymake-mode +*_flymake.* + +# eshell files +/eshell/history +/eshell/lastdir + +# elpa packages +/elpa/ + +# reftex files +*.rel + +# AUCTeX auto folder +/auto/ + +# cask packages +.cask/ +dist/ + +# Flycheck +flycheck_*.el + +# server auth directory +/server/ + +# projectiles files +.projectile +projectile-bookmarks.eld + +# directory configuration +.dir-locals.el + +# saveplace +places + +# url cache +url/cache/ + +# cedet +ede-projects.el + +# smex +smex-items + +# company-statistics +company-statistics-cache.el + +# anaconda-mode +anaconda-mode/ + +### Intellij+iml ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# CMake +cmake-build-debug/ + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Ruby plugin and RubyMine +/.rakeTasks + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### Intellij+iml Patch ### +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +### Java ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +### Linux ### + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Node ### +# Logs +logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + + +### Sass ### +.sass-cache/ +*.css.map + +### SublimeText ### +# cache files for sublime text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# workspace files are user-specific +*.sublime-workspace + +# project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using SublimeText +# *.sublime-project + +# sftp configuration file +sftp-config.json + +# Package control specific files +Package Control.last-run +Package Control.ca-list +Package Control.ca-bundle +Package Control.system-ca-bundle +Package Control.cache/ +Package Control.ca-certs/ +Package Control.merged-ca-bundle +Package Control.user-ca-bundle +oscrypto-ca-bundle.crt +bh_unicode_properties.cache + +# Sublime-github package stores a github token in this file +# https://packagecontrol.io/packages/sublime-github +GitHub.sublime-settings + +### Vim ### +# swap +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-v][a-z] +[._]sw[a-p] +# session +Session.vim +# temporary +.netrwhist +# auto-generated tag files +tags + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### Gradle ### +.gradle +**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +# End of https://www.gitignore.io/api/vim,sass,java,node,code,macos,linux,emacs,gradle,windows,angular,sublimetext,intellij+iml + +# Do not ignore typescript config +!tsconfig.json diff --git a/INTERNATIONALIZATION.md b/INTERNATIONALIZATION.md new file mode 100644 index 000000000..0240826a6 --- /dev/null +++ b/INTERNATIONALIZATION.md @@ -0,0 +1,79 @@ +# Internationalization Guide + +## About + +The Shibboleth UI leverages Angular's built in internationalization (i18n) +system feature to allow for localization of the views. + + + +Angular allows any piece of text in an HTML template to be included in the +translation source file. + +## Conventions + +### Choosing an identifier + +To allow for easier identification of relevant strings the identifiers are +provided in the following format + +``` +type--kebab-case-text +``` + +For example + +```html + + + +``` + +Where `type` is one of the provided types listed in [the types section](#types) + +`type` and `text` are separated by two dashes. + +`text` is the text content, dash separated words, all lower case, with +punctuation removed. + +### Types + +The following types are provided: + +* `action` buttons or links that cause a state change within the app +* `label` label for an input or a section +* `warning` messages that warn a user of exceptions in interactions, i.e. + validation messages + +### Localize Text Only + +```html + + + +``` + +### Updating Text + +When updating text, update the identifier to match the new text field. + +```html + + + + + + + +``` diff --git a/README.md b/README.md new file mode 100644 index 000000000..699398da9 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# shibui + +## Requirements + +* Java 8 (note that ONLY Java 8 is supported at this time) + +## Running + +There are currently 2 ways to run the application: + +1. As an executable +1. deployed in a Java Servlet 3.0 container + +### Running as an executable + +`java -jar shibui.jar` + +For complete information on overriding default configuration, see [https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html]. + +### Deploying as a WAR + +The application can be deployed as a WAR file in a Java Servlet 3.0 container. Currently, the application must be run in the root context. + +To override default configuration, see [https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html]. +The easiest way to do this in a servlet container is through the use of system properties + +## Authentication + +Currently, the application is wired with very simple authentication. A password for the user `user` +can be set with the `shibui.default-password` property. If none is set, a default password +will be generated and logged: + +``` +Using default security password: a3d9ab96-9c63-414f-b199-26fcf59e1ffa +``` + +## Default Properties + +This is a reflection of the default `application.properties` file included in the distribution. Note that lines +beginning with `#` are commented out. + + +``` +# Server Configuration +#server.port=8080 + +# Logging Configuration +#logging.config=classpath:log4j2.xml +#logging.level.org.springframework.web=ERROR + +# Database Credentials +spring.datasource.username=shibui +spring.datasource.password=shibui + +# Database Configuration H2 +spring.datasource.url=jdbc:h2:mem:shibui;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE +spring.datasource.platform=h2 +spring.datasource.driverClassName=org.h2.Driver +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.h2.console.enabled=true + + +# Database Configuration PostgreSQL +#spring.datasource.url=jdbc:postgresql://localhost:5432/shibui +#spring.datasource.driverClassName=org.postgresql.Driver +#spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect + +#Maria/MySQL DB +#spring.datasource.url=jdbc:mariadb://localhost:3306/shibui +#spring.datasource.driverClassName=org.mariadb.jdbc.Driver +#spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect + +#Tomcat specific DataSource props. Do we need these? +#spring.datasource.tomcat.maxActive=100 +#spring.datasource.tomcat.minIdle=10 +#spring.datasource.tomcat.maxIdle=10 +#spring.datasource.tomcat.initialSize=50 +#spring.datasource.tomcat.validationQuery=select 1 + +# Liquibase properties +liquibase.enabled=false +#liquibase.change-log=classpath:edu/internet2/tier/shibboleth/admin/ui/database/masterchangelog.xml + +# Hibernate properties +# for production never ever use create, create-drop. It's BEST to use validate +spring.jpa.hibernate.ddl-auto=create +spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl +spring.jpa.show-sql=false +spring.jpa.properties.hibernate.format_sql=false + +spring.jpa.hibernate.use-new-id-generator-mappings=true + +shibui.metadata-dir=/opt/shibboleth-idp/metadata/generated +shibui.logout-url=/dashboard + +spring.profiles.active=default + +#shibui.default-password= +``` diff --git a/backend/build.gradle b/backend/build.gradle new file mode 100644 index 000000000..592c49d82 --- /dev/null +++ b/backend/build.gradle @@ -0,0 +1,161 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath "org.springframework.boot:spring-boot-gradle-plugin:1.5.7.RELEASE" + } +} + +plugins { + id 'java' + id 'groovy' + id 'war' +} + +apply plugin: 'org.springframework.boot' + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + maven { url 'https://build.shibboleth.net/nexus/content/groups/public' } + mavenCentral() +} + +//Integration of the frontend and backend into the build to have all of the UI resources available in the app's jar +jar.dependsOn(':ui:npm_run_buildProd') +jar { + from(tasks.findByPath(':ui:npm_run_buildProd').outputs) { + into 'public' + } +} +jar.baseName = 'shibui' +processResources.dependsOn(':ui:npm_run_buildProd') + +war.dependsOn(':ui:npm_run_buildProd') +war { + from(tasks.findByPath(':ui:npm_run_buildProd').outputs) { + into '/' + } + archiveName = 'shibui.war' +} +war.baseName = 'shibui' + +dependencies { + // opensaml deps + ['opensaml-saml-api', 'opensaml-saml-impl', 'opensaml-xmlsec-api', 'opensaml-xmlsec-impl'].each { + compile "org.opensaml:${it}:${project.'opensaml.version'}" + } + + // shibboleth idp deps + [].each { + compile "net.shibboleth.idp:${it}:${project.'shibboleth.version'}" + } + + // hibernate deps + ['hibernate-core'].each { + compile "org.hibernate:${it}:${project.'hibernate.version'}" + } + + // spring boot plugins + ['starter-web', 'starter-actuator', 'starter-data-jpa', 'starter-security', 'devtools'].each { + compile "org.springframework.boot:spring-boot-${it}" + } + compile "org.liquibase:liquibase-core" + compile group: 'org.jadira.usertype', name: 'usertype.core', version: '6.0.1.GA' + + // TODO: these will likely only be runtimeOnly or test scope, unless we want to ship the libraries with the final product + compile "com.h2database:h2" + runtimeOnly "org.postgresql:postgresql" + runtimeOnly 'org.mariadb.jdbc:mariadb-java-client:2.2.0' + + testCompile "org.springframework.boot:spring-boot-starter-test" + testCompile "org.spockframework:spock-core:1.1-groovy-2.4" + testCompile "org.spockframework:spock-spring:1.1-groovy-2.4" + testCompile "org.xmlunit:xmlunit-core:2.5.1" + testRuntime 'cglib:cglib-nodep:3.2.5' + + //JSON schema generator + testCompile 'com.kjetland:mbknor-jackson-jsonschema_2.12:1.0.28' + testCompile 'javax.validation:validation-api:2.0.1.Final' +} + +def generatedSrcDir = new File(buildDir, 'generated/src/main/java') + +sourceSets { + main { + java { + srcDirs += generatedSrcDir + } + } +} + +task generateSources { + inputs.dir('src/main/templates') + inputs.files fileTree('src/main/resources') { + include '*-config.xml' + } + outputs.dir(generatedSrcDir) + + doLast { + println "generating Builders" + + def processLine = { dest, template -> + def token = dest.split('\\.')[-1].replace('Builder', '') + def infile = file(template) + def outfile = file("${generatedSrcDir}/${dest.replaceAll('\\.', '/')}.java") + def outdir = file(outfile.parent) + if (!outdir.exists()) { + outdir.mkdirs() + } + if (!outfile.exists()) { + println "creating class ${dest}" + outfile.withWriter { writer -> + infile.eachLine { line -> + writer.println(line.replaceAll('\\{\\{TOKEN}}', token)) + } + } + } + } + + def metadataBuilders = new XmlSlurper().parse(file('src/main/resources/jpa-saml2-metadata-config.xml')) + metadataBuilders.ObjectProviders.ObjectProvider.BuilderClass.each { + processLine(it['@className'].toString(), 'src/main/templates/MetadataBuilderTemplate.java') + } + + def assertionBuilders = new XmlSlurper().parse(file('src/main/resources/jpa-saml2-assertion-config.xml')) + assertionBuilders.ObjectProviders.ObjectProvider.BuilderClass.each { + processLine(it['@className'].toString(), 'src/main/templates/AssertionBuilderTemplate.java') + } + + ['jpa-default-config.xml', 'jpa-schema-config.xml'].each { + def builders = new XmlSlurper().parse(file("src/main/resources/${it}")) + builders.ObjectProviders.ObjectProvider.BuilderClass.each { + processLine(it['@className'].toString(), 'src/main/templates/XSBuilderTemplate.java') + } + } + + def metadataUIBuilders = new XmlSlurper().parse(file('src/main/resources/jpa-saml2-metadata-ui-config.xml')) + metadataUIBuilders.ObjectProviders.ObjectProvider.BuilderClass.each { + processLine(it['@className'].toString(), 'src/main/templates/MetadataUIBuilderTemplate.java') + } + + def xmlSecBuilders = new XmlSlurper().parse(file('src/main/resources/jpa-signature-config.xml')) + xmlSecBuilders.ObjectProviders.ObjectProvider.BuilderClass.each { + processLine(it['@className'].toString(), 'src/main/templates/XMLSecBuilderTemplate.java') + } + } +} + +compileJava { + dependsOn generateSources +} + +bootRun { + systemProperties = System.properties +} + +bootRepackage { + mainClass = 'edu.internet2.tier.shibboleth.admin.ui.ShibbolethUiApplication' +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/ShibbolethUiApplication.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/ShibbolethUiApplication.java new file mode 100644 index 000000000..52f2b6250 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/ShibbolethUiApplication.java @@ -0,0 +1,26 @@ +package edu.internet2.tier.shibboleth.admin.ui; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.support.SpringBootServletInitializer; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableScheduling; + +@SpringBootApplication +@EntityScan(basePackages = "edu.internet2.tier.shibboleth.admin.ui.domain") +@EnableJpaAuditing +@EnableScheduling +public class ShibbolethUiApplication extends SpringBootServletInitializer { + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { + return builder.sources(ShibbolethUiApplication.class); + } + + public static void main(String... args) { + SpringApplication.run(ShibbolethUiApplication.class, args); + } + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java new file mode 100644 index 000000000..228a27cf8 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java @@ -0,0 +1,33 @@ +package edu.internet2.tier.shibboleth.admin.ui.configuration; + +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; +import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; +import edu.internet2.tier.shibboleth.admin.ui.scheduled.EntityDescriptorFilesScheduledTasks; +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService; +import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class CoreShibUiConfiguration { + + @Value("${shibui.metadata-dir:/opt/shibboleth-idp/metadata/generated}") + private String metadataDir; + + @Bean + public OpenSamlObjects openSamlObjects() { + return new OpenSamlObjects(); + } + + @Bean + public EntityDescriptorService jpaEntityDescriptorService() { + return new JPAEntityDescriptorServiceImpl(openSamlObjects()); + } + + + @Bean + public EntityDescriptorFilesScheduledTasks entityDescriptorFilesScheduledTasks(EntityDescriptorRepository entityDescriptorRepository) { + return new EntityDescriptorFilesScheduledTasks(this.metadataDir, entityDescriptorRepository, openSamlObjects()); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/H2Configuration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/H2Configuration.java new file mode 100644 index 000000000..e408bd316 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/H2Configuration.java @@ -0,0 +1,27 @@ +package edu.internet2.tier.shibboleth.admin.ui.configuration; + +import org.h2.tools.Server; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +import java.sql.SQLException; + +/** + * Inspired by https://techdev.io/en/developer-blog/querying-the-embedded-h2-database-of-a-spring-boot-application + */ +@Configuration +@Profile("dev") +public class H2Configuration { + + @Value("${h2.tcp.port:9092}") + private String h2TcpPort; + + @Bean + @ConditionalOnExpression("${h2.tcp.enabled:false}") + public Server h2TcpServer() throws SQLException { + return Server.createTcpServer("-tcp", "-tcpAllowOthers", "-tcpPort", h2TcpPort).start(); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/WebSecurityConfig.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/WebSecurityConfig.java new file mode 100644 index 000000000..fe59e92ba --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/WebSecurityConfig.java @@ -0,0 +1,65 @@ +package edu.internet2.tier.shibboleth.admin.ui.configuration; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Profile; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +@EnableWebSecurity +public class WebSecurityConfig { + + @Value("${shibui.logout-url:/dashboard}") + private String logoutUrl; + + @Value("${shibui.default-password:}") + private String defaultPassword; + + @Bean + @Profile("default") + public WebSecurityConfigurerAdapter defaultAuth() { + return new WebSecurityConfigurerAdapter() { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + .and() + .authorizeRequests() + .anyRequest().authenticated() + .and() + .formLogin().and() + .httpBasic().and() + .logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout")).logoutSuccessUrl(logoutUrl); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + // TODO: more configurable authentication + if (defaultPassword != null && !"".equals(defaultPassword)) { + auth + .inMemoryAuthentication() + .withUser("user").password(defaultPassword).roles("USER"); + } else { + super.configure(auth); + } + } + }; + } + + @Bean + @Profile("no-auth") + public WebSecurityConfigurerAdapter noAuthUsedForEaseDevelopment() { + return new WebSecurityConfigurerAdapter() { + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf().disable(); + http.headers().frameOptions().disable(); + } + }; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java new file mode 100644 index 000000000..8f0d83cd9 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java @@ -0,0 +1,176 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; +import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService; +import org.opensaml.core.xml.io.MarshallingException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import javax.annotation.PostConstruct; +import java.net.URI; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api") +public class EntityDescriptorController { + + @Autowired + private EntityDescriptorRepository entityDescriptorRepository; + + @Autowired + private OpenSamlObjects openSamlObjects; + + @Autowired + private EntityDescriptorService entityDescriptorService; + + @Autowired + RestTemplateBuilder restTemplateBuilder; + + private RestTemplate restTemplate; + + private static Logger LOGGER = LoggerFactory.getLogger(EntityDescriptorController.class); + + @PostConstruct + public void initRestTemplate() { + this.restTemplate = restTemplateBuilder.build(); + } + + @PostMapping("/EntityDescriptor") + public ResponseEntity create(@RequestBody EntityDescriptorRepresentation edRepresentation) { + final String entityId = edRepresentation.getEntityId(); + + ResponseEntity existingEntityDescriptorConflictResponse = existingEntityDescriptorCheck(entityId); + if (existingEntityDescriptorConflictResponse != null) { + return existingEntityDescriptorConflictResponse; + } + + EntityDescriptor ed = (EntityDescriptor) entityDescriptorService.createDescriptorFromRepresentation(edRepresentation); + + EntityDescriptor persistedEd = entityDescriptorRepository.save(ed); + edRepresentation.setId(persistedEd.getResourceId()); + edRepresentation.setCreatedDate(persistedEd.getCreatedDate()); + return ResponseEntity.created(getResourceUriFor(persistedEd)).body(entityDescriptorService.createRepresentationFromDescriptor(persistedEd)); + } + + @PostMapping(value = "/EntityDescriptor", consumes = "application/xml") + public ResponseEntity upload(@RequestBody byte[] entityDescriptorXml, @RequestParam String spName) throws Exception { + return handleUploadingEntityDescriptorXml(entityDescriptorXml, spName); + } + + @PostMapping(value = "/EntityDescriptor", consumes = "application/x-www-form-urlencoded") + public ResponseEntity upload(@RequestParam String metadataUrl, @RequestParam String spName) throws Exception { + try { + byte[] xmlContents = this.restTemplate.getForObject(metadataUrl, byte[].class); + return handleUploadingEntityDescriptorXml(xmlContents, spName); + } catch (Throwable e) { + LOGGER.error("Error fetching XML metadata from the provided URL: [{}]. The error is: {}", metadataUrl, e); + e.printStackTrace(); + return ResponseEntity + .badRequest() + .body(String.format("Error fetching XML metadata from the provided URL. Error: %s", e.getMessage())); + } + } + + @PutMapping("/EntityDescriptor/{resourceId}") + public ResponseEntity update(@RequestBody EntityDescriptorRepresentation edRepresentation, @PathVariable String resourceId) { + EntityDescriptor existingEd = entityDescriptorRepository.findByResourceId(resourceId); + if (existingEd == null) { + return ResponseEntity.notFound().build(); + } + EntityDescriptor updatedEd = + EntityDescriptor.class.cast(entityDescriptorService.createDescriptorFromRepresentation(edRepresentation)); + + updatedEd.setAudId(existingEd.getAudId()); + updatedEd.setResourceId(existingEd.getResourceId()); + updatedEd.setCreatedDate(existingEd.getCreatedDate()); + + updatedEd = entityDescriptorRepository.save(updatedEd); + + return ResponseEntity.ok().body(entityDescriptorService.createRepresentationFromDescriptor(updatedEd)); + } + + @GetMapping("/EntityDescriptors") + @Transactional(readOnly = true) + public Iterable getAll() { + return entityDescriptorRepository.findAllByCustomQueryAndStream() + .map(ed -> entityDescriptorService.createRepresentationFromDescriptor(ed)) + .collect(Collectors.toList()); + } + + @GetMapping("/EntityDescriptor/{resourceId}") + public ResponseEntity getOne(@PathVariable String resourceId) { + EntityDescriptor ed = entityDescriptorRepository.findByResourceId(resourceId); + if (ed == null) { + return ResponseEntity.notFound().build(); + } + EntityDescriptorRepresentation edr = entityDescriptorService.createRepresentationFromDescriptor(ed); + + return ResponseEntity.ok(edr); + } + + @GetMapping(value = "/EntityDescriptor/{resourceId}", produces = "application/xml") + public ResponseEntity getOneXml(@PathVariable String resourceId) throws MarshallingException { + EntityDescriptor ed = entityDescriptorRepository.findByResourceId(resourceId); + if (ed == null) { + return ResponseEntity.notFound().build(); + } + final String xml = this.openSamlObjects.marshalToXmlString(ed); + + return ResponseEntity.ok(xml); + } + + private static URI getResourceUriFor(EntityDescriptor ed) { + return ServletUriComponentsBuilder + .fromCurrentServletMapping().path("/api/EntityDescriptor") + .pathSegment(ed.getResourceId()) + .build() + .toUri(); + } + + private ResponseEntity existingEntityDescriptorCheck(String entityId) { + final EntityDescriptor ed = entityDescriptorRepository.findByEntityID(entityId); + if (ed != null) { + HttpHeaders headers = new HttpHeaders(); + headers.setLocation(getResourceUriFor(ed)); + return ResponseEntity + .status(HttpStatus.CONFLICT) + .headers(headers) + .body(String.format("The entity descriptor with entity id [%s] already exists.", entityId)); + } + //No existing entity descriptor, which is an OK condition indicated by returning a null conflict response + return null; + } + + private ResponseEntity handleUploadingEntityDescriptorXml(byte[] rawXmlBytes, String spName) throws Exception { + final EntityDescriptor ed = EntityDescriptor.class.cast(openSamlObjects.unmarshalFromXml(rawXmlBytes)); + + ResponseEntity existingEntityDescriptorConflictResponse = existingEntityDescriptorCheck(ed.getEntityID()); + if (existingEntityDescriptorConflictResponse != null) { + return existingEntityDescriptorConflictResponse; + } + + ed.setServiceProviderName(spName); + final EntityDescriptor persistedEd = entityDescriptorRepository.save(ed); + return ResponseEntity.created(getResourceUriFor(persistedEd)) + .body(entityDescriptorService.createRepresentationFromDescriptor(persistedEd)); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractAttributeExtensibleXMLObject.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractAttributeExtensibleXMLObject.java new file mode 100644 index 000000000..bbe1dc2fd --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractAttributeExtensibleXMLObject.java @@ -0,0 +1,25 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import org.opensaml.core.xml.AttributeExtensibleXMLObject; +import org.opensaml.core.xml.util.AttributeMap; + +import javax.annotation.Nonnull; +import javax.persistence.MappedSuperclass; +import javax.persistence.Transient; + +@MappedSuperclass +public abstract class AbstractAttributeExtensibleXMLObject extends AbstractXMLObject implements AttributeExtensibleXMLObject { + + private transient final AttributeMap unknownAttributes; + + AbstractAttributeExtensibleXMLObject() { + unknownAttributes = new AttributeMap(this); + } + + @Nonnull + @Override + @Transient + public AttributeMap getUnknownAttributes() { + return this.unknownAttributes; + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractAuditable.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractAuditable.java new file mode 100644 index 000000000..fe3f06ba8 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractAuditable.java @@ -0,0 +1,94 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +import javax.persistence.Column; +import javax.persistence.EntityListeners; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.MappedSuperclass; +import javax.validation.constraints.NotNull; + + +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class AbstractAuditable implements Auditable { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + protected Long id; + + @CreationTimestamp + @CreatedDate + @Column(nullable = false, updatable = false) + private LocalDateTime createdDate; + + @UpdateTimestamp + @LastModifiedDate + @Column(nullable = false) + private LocalDateTime modifiedDate; + + @Column(name = "created_by") + @CreatedBy + private String createdBy; + + @Column(name = "modified_by") + @LastModifiedBy + private String modifiedBy; + + + @Override + public Long getAudId() { + return id; + } + + @Override + public void setAudId(@NotNull Long id) { + this.id = id; + } + + @Override + public LocalDateTime getCreatedDate() { + return createdDate; + } + + @Override + public void setCreatedDate(LocalDateTime createdDate) { + this.createdDate = createdDate; + } + + @Override + public LocalDateTime getModifiedDate() { + return modifiedDate; + } + + @Override + public void setModifiedDate(LocalDateTime modifiedDate) { + this.modifiedDate = modifiedDate; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public String getModifiedBy() { + return modifiedBy; + } + + public void setModifiedBy(String modifiedBy) { + this.modifiedBy = modifiedBy; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractDescriptor.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractDescriptor.java new file mode 100644 index 000000000..e68f3ae23 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractDescriptor.java @@ -0,0 +1,105 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import org.hibernate.annotations.Type; +import org.joda.time.DateTime; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.saml.saml2.common.CacheableSAMLObject; +import org.opensaml.saml.saml2.common.TimeBoundSAMLObject; +import org.opensaml.xmlsec.signature.Signature; +import org.opensaml.xmlsec.signature.SignableXMLObject; + +import javax.annotation.Nullable; +import javax.persistence.CascadeType; +import javax.persistence.MappedSuperclass; +import javax.persistence.OneToOne; +import java.util.List; + + +@MappedSuperclass +public abstract class AbstractDescriptor extends AbstractAttributeExtensibleXMLObject implements CacheableSAMLObject, TimeBoundSAMLObject, SignableXMLObject { + + private boolean isValid; + + private Long cacheDuration; + + @Type(type = "org.jadira.usertype.dateandtime.joda.PersistentDateTime") + private DateTime validUntil; + + private boolean isSigned; + + private String signatureReferenceID; + + @OneToOne(cascade = CascadeType.ALL) + private Extensions extensions; + + @Override + public boolean isValid() { + return isValid; + } + + public void setIsValid(boolean isValid) { + this.isValid = isValid; + } + + @Override + public Long getCacheDuration() { + return cacheDuration; + } + + @Override + public void setCacheDuration(Long cacheDuration) { + this.cacheDuration = cacheDuration; + } + + @Override + public DateTime getValidUntil() { + return validUntil; + } + + @Override + public void setValidUntil(DateTime validUntil) { + this.validUntil = validUntil; + } + + @Override + public boolean isSigned() { + return isSigned; + } + + public void setIsSigned(boolean isSigned) { + this.isSigned = isSigned; + } + + public Extensions getExtensions() { + return extensions; + } + + public void setExtensions(org.opensaml.saml.saml2.metadata.Extensions extensions) { + this.extensions = (Extensions)extensions; + } + + public String getSignatureReferenceID() { + return signatureReferenceID; + } + + public void setSignatureReferenceID(String signatureReferenceID) { + this.signatureReferenceID = signatureReferenceID; + } + + @Nullable + @Override + public Signature getSignature() { + return null; //TODO signing xml, so generate xml and pass through signing method here or dto layer? + } + + @Override + public void setSignature(@Nullable Signature signature) { + //TODO see above need to generate xml and pass through signing method here or in dto layer? + } + + @Nullable + public List getOrderedChildren() { + + return null; //TODO ? + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractElementExtensibleXMLObject.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractElementExtensibleXMLObject.java new file mode 100644 index 000000000..831501ecd --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractElementExtensibleXMLObject.java @@ -0,0 +1,39 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import org.opensaml.core.xml.ElementExtensibleXMLObject; +import org.opensaml.core.xml.XMLObject; + +import javax.annotation.Nonnull; +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.Inheritance; +import javax.persistence.InheritanceType; +import javax.persistence.OneToMany; +import javax.xml.namespace.QName; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + + +@Entity +@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) +public abstract class AbstractElementExtensibleXMLObject extends AbstractXMLObject implements ElementExtensibleXMLObject { + @OneToMany(cascade = CascadeType.ALL) + private List unknownXMLObjects = new ArrayList<>(); + + @Nonnull + @Override + public List getUnknownXMLObjects() { + return (List) (List) this.unknownXMLObjects; + } + + @Nonnull + @Override + public List getUnknownXMLObjects(@Nonnull QName qName) { + return this.unknownXMLObjects.stream().filter(p -> p.getElementQName().equals(qName)).collect(Collectors.toList()); + } + + public void addUnknownXMLObject(AbstractXMLObject xmlObject) { + this.unknownXMLObjects.add(xmlObject); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractLangBearingURL.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractLangBearingURL.java new file mode 100644 index 000000000..7f66a06c6 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractLangBearingURL.java @@ -0,0 +1,27 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import org.opensaml.saml.saml2.metadata.LocalizedURI; + +import javax.annotation.Nullable; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Inheritance; +import javax.persistence.InheritanceType; + +@Entity +@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) +abstract class AbstractLangBearingURL extends XSURI implements LocalizedURI { + @Column(name = "informationUrlXmlLang") + private String xmlLang; + + @Nullable + @Override + public String getXMLLang() { + return this.xmlLang; + } + + @Override + public void setXMLLang(@Nullable String xmlLang) { + this.xmlLang = xmlLang; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractXMLObject.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractXMLObject.java new file mode 100644 index 000000000..b905cea1a --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractXMLObject.java @@ -0,0 +1,224 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import net.shibboleth.utilities.java.support.collection.LockableClassToInstanceMultiMap; +import net.shibboleth.utilities.java.support.xml.QNameSupport; +import org.opensaml.core.xml.Namespace; +import org.opensaml.core.xml.NamespaceManager; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.schema.XSBooleanValue; +import org.opensaml.core.xml.util.IDIndex; +import org.w3c.dom.Element; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.persistence.Entity; +import javax.persistence.Inheritance; +import javax.persistence.InheritanceType; +import javax.persistence.Transient; +import javax.xml.namespace.QName; +import java.util.List; +import java.util.Set; + + +/** + * This covers both SAMLObject and XMLObject + */ +@Entity +@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) +public abstract class AbstractXMLObject extends AbstractAuditable implements XMLObject { + + private String namespaceURI; + private String elementLocalName; + private String namespacePrefix; + + private String schemaTypeNamespaceURI; + private String schemaTypeElementLocalName; + private String schemaTypeNamespacePrefix; + + //TODO all this class + + public void detach() { + + } + + @Nullable + public Element getDOM() { + return null; //convert this class using opensaml stuff + } + + public String getNamespaceURI() { + return namespaceURI; + } + + public void setNamespaceURI(String namespaceURI) { + this.namespaceURI = namespaceURI; + } + + public String getElementLocalName() { + return elementLocalName; + } + + public void setElementLocalName(String elementLocalName) { + this.elementLocalName = elementLocalName; + } + + public String getNamespacePrefix() { + return namespacePrefix; + } + + public void setNamespacePrefix(String namespacePrefix) { + this.namespacePrefix = namespacePrefix; + } + + @Transient + public QName getElementQName() { + return QNameSupport.constructQName(this.getNamespaceURI(), this.getElementLocalName(), this.getNamespacePrefix()); + } + + @Nullable + public IDIndex getIDIndex() { + //Is this OK? + //return new IDIndex(this); + + return null; + } + + @Transient + public NamespaceManager getNamespaceManager() { + return new NamespaceManager(this); + } + + @Transient + public Set getNamespaces() { + return this.getNamespaceManager().getNamespaces(); + } + + @Nullable + public String getNoNamespaceSchemaLocation() { + return null; + } + + @Nullable + public List getOrderedChildren() { + return null; + } + + @Nullable + public XMLObject getParent() { + return null; + } + + @Nullable + public String getSchemaLocation() { + return null; + } + + @Nullable + public QName getSchemaType() { + if (this.schemaTypeElementLocalName == null) { + return null; + } + return QNameSupport.constructQName(this.schemaTypeNamespaceURI, this.schemaTypeElementLocalName, this.schemaTypeNamespacePrefix); + } + + public void setSchemaType(QName schemaType) { + if (schemaType != null) { + this.schemaTypeNamespaceURI = schemaType.getNamespaceURI(); + this.schemaTypeElementLocalName = schemaType.getLocalPart(); + this.schemaTypeNamespacePrefix = schemaType.getPrefix(); + } + } + + public boolean hasChildren() { + return false; + } + + public boolean hasParent() { + return false; + } + + public void releaseChildrenDOM(boolean b) { + + } + + public void releaseDOM() { + + } + + public void releaseParentDOM(boolean b) { + + } + + @Nullable + public XMLObject resolveID(@Nonnull String s) { + return null; + } + + @Nullable + public XMLObject resolveIDFromRoot(@Nonnull String s) { + return null; + } + + public void setDOM(@Nullable Element element) { + + } + + public void setNoNamespaceSchemaLocation(@Nullable String s) { + + } + + public void setParent(@Nullable XMLObject xmlObject) { + + } + + public void setSchemaLocation(@Nullable String s) { + + } + + @Nullable + public Boolean isNil() { + return null; + } + + @Nullable + public XSBooleanValue isNilXSBoolean() { + return null; + } + + public void setNil(@Nullable Boolean aBoolean) { + + } + + public void setNil(@Nullable XSBooleanValue xsBooleanValue) { + + } + + @Nonnull + public LockableClassToInstanceMultiMap getObjectMetadata() { + return null; + } + + public String getSchemaTypeNamespaceURI() { + return schemaTypeNamespaceURI; + } + + public void setSchemaTypeNamespaceURI(String schemaTypeNamespaceURI) { + this.schemaTypeNamespaceURI = schemaTypeNamespaceURI; + } + + public String getSchemaTypeElementLocalName() { + return schemaTypeElementLocalName; + } + + public void setSchemaTypeElementLocalName(String schemaTypeElementLocalName) { + this.schemaTypeElementLocalName = schemaTypeElementLocalName; + } + + public String getSchemaTypeNamespacePrefix() { + return schemaTypeNamespacePrefix; + } + + public void setSchemaTypeNamespacePrefix(String schemaTypeNamespacePrefix) { + this.schemaTypeNamespacePrefix = schemaTypeNamespacePrefix; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AdditionalMetadataLocation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AdditionalMetadataLocation.java new file mode 100644 index 000000000..8aca6caba --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AdditionalMetadataLocation.java @@ -0,0 +1,32 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Entity; + + +@Entity +public class AdditionalMetadataLocation extends AbstractXMLObject implements org.opensaml.saml.saml2.metadata.AdditionalMetadataLocation { + + private String locationURI; + + private String namespaceURI; + + @Override + public String getLocationURI() { + return locationURI; + } + + @Override + public void setLocationURI(String locationURI) { + this.locationURI = locationURI; + } + + @Override + public String getNamespaceURI() { + return namespaceURI; + } + + @Override + public void setNamespaceURI(String namespaceURI) { + this.namespaceURI = namespaceURI; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AffiliateMember.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AffiliateMember.java new file mode 100644 index 000000000..0039047bf --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AffiliateMember.java @@ -0,0 +1,20 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Entity; + + +@Entity +public class AffiliateMember extends AbstractXMLObject implements org.opensaml.saml.saml2.metadata.AffiliateMember { + + private String localId; + + @Override + public String getID() { + return this.localId; + } + + @Override + public void setID(String id) { + this.localId = id; + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AffiliationDescriptor.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AffiliationDescriptor.java new file mode 100644 index 000000000..5122a00f9 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AffiliationDescriptor.java @@ -0,0 +1,63 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import com.google.common.collect.Lists; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.OneToMany; +import java.util.List; + + +@Entity +public class AffiliationDescriptor extends AbstractDescriptor implements org.opensaml.saml.saml2.metadata.AffiliationDescriptor { + + private String ownerId; + private String localId; + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "affildesc_affilmemb_id") + private List affiliateMembers; + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "affildesc_keydesc_id") + private List keyDescriptors; + + @Override + public String getID() { + return localId; + } + + @Override + public void setID(String id) { + this.localId = id; + } + + @Override + public String getOwnerID() { + return ownerId; + } + + @Override + public void setOwnerID(String ownerId) { + this.ownerId = ownerId; + } + + @Override + public List getMembers() { + return Lists.newArrayList(affiliateMembers); + } + + public void setAffiliateMembers(List affiliateMembers) { + this.affiliateMembers = affiliateMembers; + } + + @Override + public List getKeyDescriptors() { + return Lists.newArrayList(keyDescriptors); + } + + public void setKeyDescriptors(List keyDescriptors) { + this.keyDescriptors = keyDescriptors; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ArtifactResolutionService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ArtifactResolutionService.java new file mode 100644 index 000000000..950e4a6d7 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ArtifactResolutionService.java @@ -0,0 +1,8 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Entity; + + +@Entity +public class ArtifactResolutionService extends IndexedEndpoint implements org.opensaml.saml.saml2.metadata.ArtifactResolutionService { +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AssertionConsumerService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AssertionConsumerService.java new file mode 100644 index 000000000..905cb7d1b --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AssertionConsumerService.java @@ -0,0 +1,8 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Entity; + + +@Entity +public class AssertionConsumerService extends IndexedEndpoint implements org.opensaml.saml.saml2.metadata.AssertionConsumerService { +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AssertionIDRequestService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AssertionIDRequestService.java new file mode 100644 index 000000000..9c88846b6 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AssertionIDRequestService.java @@ -0,0 +1,8 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Entity; + + +@Entity +public class AssertionIDRequestService extends Endpoint implements org.opensaml.saml.saml2.metadata.AssertionIDRequestService { +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Attribute.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Attribute.java new file mode 100644 index 000000000..eb70609d5 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Attribute.java @@ -0,0 +1,78 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import org.opensaml.core.xml.XMLObject; + +import javax.annotation.Nullable; +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.OneToMany; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + + +@Entity +public class Attribute extends AbstractAttributeExtensibleXMLObject implements org.opensaml.saml.saml2.core.Attribute { + + private String name; + + private String nameFormat; + + private String friendlyName; + + @OneToMany(cascade = CascadeType.ALL) + private List attributeValues = new ArrayList<>(); + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public String getNameFormat() { + return nameFormat; + } + + @Override + public void setNameFormat(String nameFormat) { + this.nameFormat = nameFormat; + } + + @Override + public String getFriendlyName() { + return friendlyName; + } + + @Override + public void setFriendlyName(String friendlyName) { + this.friendlyName = friendlyName; + } + + @Override + public List getAttributeValues() { + return (List) (List) this.attributeValues; + } + + public void setAttributeValues(List attributeValues) { + this.attributeValues = attributeValues; + } + + public void addAttributeValue(AbstractXMLObject attributeValue) { + this.attributeValues.add(attributeValue); + } + + @Nullable + @Override + public List getOrderedChildren() { + ArrayList children = new ArrayList<>(); + + children.addAll(attributeValues); + + return Collections.unmodifiableList(children); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeAuthorityDescriptor.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeAuthorityDescriptor.java new file mode 100644 index 000000000..6cbd37fc7 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeAuthorityDescriptor.java @@ -0,0 +1,80 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import com.google.common.collect.Lists; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.OneToMany; +import java.util.List; + + +@Entity +public class AttributeAuthorityDescriptor extends RoleDescriptor implements org.opensaml.saml.saml2.metadata.AttributeAuthorityDescriptor { + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "attribauthdesc_attribserv_id") + private List attributeServices; + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "attribauthdesc_assertidreqservc_id") + private List assertionIDRequestServices; + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "attribauthdesc_nameidfrmt_id") + private List nameIDFormats; + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "attribauthdesc_attribprofile_id") + private List attributeProfiles; + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "attribauthdesc_attrib_id") + private List attributes; + + + @Override + public List getAttributeServices() { + return Lists.newArrayList(attributeServices); + } + + public void setAttributeServices(List attributeServices) { + this.attributeServices = attributeServices; + } + + @Override + public List getAssertionIDRequestServices() { + return Lists.newArrayList(assertionIDRequestServices); + } + + public void setAssertionIDRequestServices(List assertionIDRequestServices) { + this.assertionIDRequestServices = assertionIDRequestServices; + } + + @Override + public List getNameIDFormats() { + return Lists.newArrayList(nameIDFormats); + } + + public void setNameIDFormats(List nameIDFormats) { + this.nameIDFormats = nameIDFormats; + } + + @Override + public List getAttributeProfiles() { + return Lists.newArrayList(attributeProfiles); + } + + public void setAttributeProfiles(List attributeProfiles) { + this.attributeProfiles = attributeProfiles; + } + + @Override + public List getAttributes() { + return Lists.newArrayList(attributes); + } + + public void setAttributes(List attributes) { + this.attributes = attributes; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeConsumingService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeConsumingService.java new file mode 100644 index 000000000..7eef7dbd8 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeConsumingService.java @@ -0,0 +1,88 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import org.opensaml.core.xml.schema.XSBooleanValue; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.OneToMany; +import java.util.ArrayList; +import java.util.List; + + +@Entity +public class AttributeConsumingService extends AbstractXMLObject implements org.opensaml.saml.saml2.metadata.AttributeConsumingService { + + private int acsIndex; + + private boolean isDefault; + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "attribconsserv_servicename_id") + private List serviceNames = new ArrayList<>(); + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "attribconsserv_servicedesc_id") + private List serviceDescriptions = new ArrayList<>(); + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "attribconsserv_requestedattrib_id") + private List requestedAttributes = new ArrayList<>(); + + @Override + public int getIndex() { + return acsIndex; + } + + @Override + public void setIndex(int index) { + this.acsIndex = index; + } + + @Override + public Boolean isDefault() { + return isDefault; + } + + @Override + public XSBooleanValue isDefaultXSBoolean() { + return null; //TODO? + } + + @Override + public void setIsDefault(Boolean isDefault) { + this.isDefault = isDefault; + } + + @Override + public void setIsDefault(XSBooleanValue xsBooleanValue) { + //TODO? + } + + @Override + public List getNames() { + return (List) (List) this.serviceNames; + } + + public void setServiceNames(List serviceNames) { + this.serviceNames = serviceNames; + } + + @Override + public List getDescriptions() { + return (List) (List) this.serviceDescriptions; + } + + public void setServiceDescriptions(List serviceDescriptions) { + this.serviceDescriptions = serviceDescriptions; + } + + @Override + public List getRequestAttributes() { + return (List) (List) this.requestedAttributes; + } + + public void setRequestedAttributes(List requestedAttributes) { + this.requestedAttributes = requestedAttributes; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeProfile.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeProfile.java new file mode 100644 index 000000000..9883f8740 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeProfile.java @@ -0,0 +1,20 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Entity; + + +@Entity +public class AttributeProfile extends AbstractXMLObject implements org.opensaml.saml.saml2.metadata.AttributeProfile { + + private String profileURI; + + @Override + public String getProfileURI() { + return profileURI; + } + + @Override + public void setProfileURI(String profileURI) { + this.profileURI = profileURI; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeService.java new file mode 100644 index 000000000..07281118d --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeService.java @@ -0,0 +1,8 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Entity; + +@Entity +public class AttributeService extends Endpoint implements org.opensaml.saml.saml2.metadata.AttributeService { + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeValue.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeValue.java new file mode 100644 index 000000000..bef88bfa7 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeValue.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Entity; + +@Entity +public class AttributeValue extends AbstractXMLObject implements org.opensaml.saml.saml2.core.AttributeValue { +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Auditable.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Auditable.java new file mode 100644 index 000000000..367520433 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Auditable.java @@ -0,0 +1,21 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.time.LocalDateTime; + + +public interface Auditable extends Serializable { + + Long getAudId(); + + void setAudId(@NotNull final Long id); + + LocalDateTime getCreatedDate(); + + void setCreatedDate(@NotNull final LocalDateTime createdDate); + + LocalDateTime getModifiedDate(); + + void setModifiedDate(final LocalDateTime modifiedDate); +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AuthnAuthorityDescriptor.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AuthnAuthorityDescriptor.java new file mode 100644 index 000000000..33ff25f6a --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AuthnAuthorityDescriptor.java @@ -0,0 +1,53 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import com.google.common.collect.Lists; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.OneToMany; +import java.util.List; + + +@Entity +public class AuthnAuthorityDescriptor extends RoleDescriptor implements org.opensaml.saml.saml2.metadata.AuthnAuthorityDescriptor { + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "authnauthdesc_authnqueryserv_id") + private List authnQueryServices; + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "authnauthdesc_assertidreqserv_id") + private List assertionIDRequestServices; + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "authnauthdesc_nameidfmt_id") + private List nameIDFormats; + + @Override + public List getAuthnQueryServices() { + return Lists.newArrayList(authnQueryServices); + } + + public void setAuthnQueryServices(List authnQueryServices) { + this.authnQueryServices = authnQueryServices; + } + + @Override + public List getAssertionIDRequestServices() { + return Lists.newArrayList(assertionIDRequestServices); + } + + public void setAssertionIDRequestServices(List assertionIDRequestServices) { + this.assertionIDRequestServices = assertionIDRequestServices; + } + + @Override + public List getNameIDFormats() { + return Lists.newArrayList(nameIDFormats); + } + + public void setNameIDFormats(List nameIDFormats) { + this.nameIDFormats = nameIDFormats; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AuthnQueryService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AuthnQueryService.java new file mode 100644 index 000000000..2fe2e7b11 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AuthnQueryService.java @@ -0,0 +1,8 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Entity; + + +@Entity +public class AuthnQueryService extends Endpoint implements org.opensaml.saml.saml2.metadata.AuthnQueryService { +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AuthzService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AuthzService.java new file mode 100644 index 000000000..07d7ef713 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AuthzService.java @@ -0,0 +1,8 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Entity; + + +@Entity +public class AuthzService extends Endpoint implements org.opensaml.saml.saml2.metadata.AuthzService { +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Company.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Company.java new file mode 100644 index 000000000..93d93d0d3 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Company.java @@ -0,0 +1,20 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Entity; + + +@Entity +public class Company extends AbstractXMLObject implements org.opensaml.saml.saml2.metadata.Company { + + private String name; + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + this.name = name; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ContactPerson.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ContactPerson.java new file mode 100644 index 000000000..84c7c577b --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ContactPerson.java @@ -0,0 +1,141 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import org.opensaml.core.xml.XMLObject; +import org.opensaml.saml.saml2.metadata.ContactPersonTypeEnumeration; + +import javax.annotation.Nullable; +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; + + +@Entity +public class ContactPerson extends AbstractAttributeExtensibleXMLObject implements org.opensaml.saml.saml2.metadata.ContactPerson { + + private String contactPersonType; + + @OneToOne(cascade = CascadeType.ALL) + private Extensions extensions; + + @OneToOne(cascade = CascadeType.ALL) + private Company company; + + @OneToOne(cascade = CascadeType.ALL) + private GivenName givenName; + + @OneToOne(cascade = CascadeType.ALL) + private SurName surName; + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "contactpersn_emailaddr_id") + private List emailAddresses = new ArrayList<>(); + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "contactpersn_telenmbr_id") + private List telephoneNumbers = new ArrayList<>(); + + @Override + @Transient + public org.opensaml.saml.saml2.metadata.ContactPersonTypeEnumeration getType() { + if(this.contactPersonType != null) { + try { + return (ContactPersonTypeEnumeration) + ContactPersonTypeEnumeration.class.getField(this.contactPersonType.toUpperCase()).get(null); + } + catch (NoSuchFieldException | IllegalAccessException e) { + return null; + } + } + return null; + } + + @Override + public void setType(org.opensaml.saml.saml2.metadata.ContactPersonTypeEnumeration contactPersonTypeEnumeration) { + this.contactPersonType = contactPersonTypeEnumeration.toString(); + } + + public void setType(String type) { + this.contactPersonType = type; + } + + @Override + public Extensions getExtensions() { + return extensions; + } + + @Override + public void setExtensions(org.opensaml.saml.saml2.metadata.Extensions extensions) { + this.extensions = (Extensions) extensions; + } + + @Override + public Company getCompany() { + return company; + } + + @Override + public void setCompany(org.opensaml.saml.saml2.metadata.Company company) { + this.company = (Company) company; + } + + @Override + public GivenName getGivenName() { + return givenName; + } + + @Override + public void setGivenName(org.opensaml.saml.saml2.metadata.GivenName givenName) { + this.givenName = (GivenName) givenName; + } + + @Override + public SurName getSurName() { + return surName; + } + + @Override + public void setSurName(org.opensaml.saml.saml2.metadata.SurName surName) { + this.surName = (SurName) surName; + } + + @Override + public List getEmailAddresses() { + return (List) (List) this.emailAddresses; + } + + public void setEmailAddresses(List emailAddresses) { + this.emailAddresses = emailAddresses; + } + + @Override + public List getTelephoneNumbers() { + return (List) (List) this.telephoneNumbers; + } + + public void setTelephoneNumbers(List telephoneNumbers) { + this.telephoneNumbers = telephoneNumbers; + } + + public void addEmailAddress(EmailAddress emailAddress) { + this.emailAddresses.add(emailAddress); + } + + @Nullable + @Override + public List getOrderedChildren() { + List list = new ArrayList<>(); + + list.add(this.extensions); + list.add(this.company); + list.add(this.givenName); + list.add(this.surName); + list.addAll(this.emailAddresses); + list.addAll(this.telephoneNumbers); + + if (list.size() == 0) { + return null; + } + + return list; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Description.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Description.java new file mode 100644 index 000000000..ecd06580f --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Description.java @@ -0,0 +1,37 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.annotation.Nullable; +import javax.persistence.Column; +import javax.persistence.Entity; + +@Entity +public class Description extends AbstractXMLObject implements org.opensaml.saml.ext.saml2mdui.Description { + + @Column(name = "descriptionXMLLang") + private String xmlLang; + + @Column(name = "descriptionValue") + private String value; + + @Nullable + @Override + public String getXMLLang() { + return this.xmlLang; + } + + @Override + public void setXMLLang(@Nullable String xmlLang) { + this.xmlLang = xmlLang; + } + + @Nullable + @Override + public String getValue() { + return this.value; + } + + @Override + public void setValue(@Nullable String value) { + this.value = value; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/DisplayName.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/DisplayName.java new file mode 100644 index 000000000..c5f394bf5 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/DisplayName.java @@ -0,0 +1,37 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.annotation.Nullable; +import javax.persistence.Column; +import javax.persistence.Entity; + +@Entity +public class DisplayName extends AbstractXMLObject implements org.opensaml.saml.ext.saml2mdui.DisplayName { + + @Column(name = "displayNameXMLLan") + private String xmlLang; + + @Column(name = "displayNameValue") + private String value; + + @Nullable + @Override + public String getXMLLang() { + return this.xmlLang; + } + + @Override + public void setXMLLang(@Nullable String xmlLang) { + this.xmlLang = xmlLang; + } + + @Nullable + @Override + public String getValue() { + return this.value; + } + + @Override + public void setValue(@Nullable String value) { + this.value = value; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EmailAddress.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EmailAddress.java new file mode 100644 index 000000000..59228e45a --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EmailAddress.java @@ -0,0 +1,19 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Entity; + +@Entity +public class EmailAddress extends AbstractXMLObject implements org.opensaml.saml.saml2.metadata.EmailAddress { + + private String address; + + @Override + public String getAddress() { + return address; + } + + @Override + public void setAddress(String address) { + this.address = address; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EncryptionMethod.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EncryptionMethod.java new file mode 100644 index 000000000..7fb36e6f1 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EncryptionMethod.java @@ -0,0 +1,54 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import org.opensaml.xmlsec.encryption.KeySize; +import org.opensaml.xmlsec.encryption.OAEPparams; + +import javax.annotation.Nullable; +import javax.persistence.Embedded; +import javax.persistence.Entity; + +@Entity +public class EncryptionMethod extends AbstractElementExtensibleXMLObject implements org.opensaml.saml.saml2.metadata.EncryptionMethod { + + private String algorithm; + + @Embedded + private KeySize keySize; + + @Embedded + private OAEPparams oaePparams; + + + @Nullable + @Override + public String getAlgorithm() { + return algorithm; + } + + @Override + public void setAlgorithm(@Nullable String algorithm) { + this.algorithm = algorithm; + } + + @Nullable + @Override + public org.opensaml.xmlsec.encryption.KeySize getKeySize() { + return keySize; + } + + @Override + public void setKeySize(@Nullable org.opensaml.xmlsec.encryption.KeySize keySize) { + this.keySize = keySize; + } + + @Nullable + @Override + public org.opensaml.xmlsec.encryption.OAEPparams getOAEPparams() { + return oaePparams; + } + + @Override + public void setOAEPparams(@Nullable org.opensaml.xmlsec.encryption.OAEPparams oaePparams) { + this.oaePparams = oaePparams; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Endpoint.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Endpoint.java new file mode 100644 index 000000000..94afb1935 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Endpoint.java @@ -0,0 +1,73 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import org.opensaml.core.xml.XMLObject; + +import javax.annotation.Nonnull; +import javax.persistence.Entity; +import javax.xml.namespace.QName; +import java.util.List; + + +/** + * Note: This also should extend AbstractElementExtensibleXMLObject, but we can't in Java. + */ +@Entity +public class Endpoint extends AbstractAttributeExtensibleXMLObject implements org.opensaml.saml.saml2.metadata.Endpoint { + + private String binding; + + private String location; + + private String responseLocation; + + @Override + public String getBinding() { + return binding; + } + + @Override + public void setBinding(String binding) { + this.binding = binding; + } + + @Override + public String getLocation() { + return location; + } + + @Override + public void setLocation(String location) { + this.location = location; + } + + @Override + public String getResponseLocation() { + return responseLocation; + } + + @Override + public void setResponseLocation(String responseLocation) { + this.responseLocation = responseLocation; + } + + /** + * This should come from AbstractElementExtensibleXMLObject + * @return + */ + @Nonnull + @Override + public List getUnknownXMLObjects() { + return null; //TODO + } + + /** + * This also should come from AbstractElementExtensibleXMLObject + * @param qName + * @return + */ + @Nonnull + @Override + public List getUnknownXMLObjects(@Nonnull QName qName) { + return null; //TODO + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityAttributes.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityAttributes.java new file mode 100644 index 000000000..4740aeb7f --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityAttributes.java @@ -0,0 +1,52 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import org.opensaml.core.xml.XMLObject; +import org.opensaml.saml.saml2.core.Assertion; + +import javax.annotation.Nullable; +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.OneToMany; +import javax.persistence.Transient; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Entity +public class EntityAttributes extends AbstractElementExtensibleXMLObject implements org.opensaml.saml.ext.saml2mdattr.EntityAttributes { + + @OneToMany(cascade = CascadeType.ALL) + private List attributes = new ArrayList<>(); + + @Transient // TODO: check to make sure this won't ever be used + private List assertions = new ArrayList<>(); + + @Override + public List getAttributes() { + return (List) (List) this.attributes; + } + + public void addAttribute(Attribute attribute) { + this.attributes.add(attribute); + } + + @Override + public List getAssertions() { + return this.assertions; + } + + @Nullable + @Override + public List getOrderedChildren() { + ArrayList children = new ArrayList<>(); + + if (this.getAssertions().size() == 0 && this.getAttributes().size() == 0) { + return null; + } + + children.addAll(this.getAttributes()); + children.addAll(this.getAssertions()); + + return Collections.unmodifiableList(children); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityAttributesBuilder.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityAttributesBuilder.java new file mode 100644 index 000000000..7a85be1e5 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityAttributesBuilder.java @@ -0,0 +1,24 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import edu.internet2.tier.shibboleth.admin.ui.opensaml.xml.AbstractSAMLObjectBuilder; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class EntityAttributesBuilder extends AbstractSAMLObjectBuilder { + @Nonnull + @Override + public EntityAttributes buildObject() { + return buildObject(EntityAttributes.DEFAULT_ELEMENT_NAME.getNamespaceURI(), EntityAttributes.DEFAULT_ELEMENT_NAME.getLocalPart(), EntityAttributes.DEFAULT_ELEMENT_NAME.getPrefix()); + } + + @Nonnull + @Override + public EntityAttributes buildObject(@Nullable String namespaceURI, @Nonnull String localName, @Nullable String namespacePrefix) { + EntityAttributes o = new EntityAttributes(); + o.setNamespaceURI(namespaceURI); + o.setElementLocalName(localName); + o.setNamespacePrefix(namespacePrefix); + return o; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptor.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptor.java new file mode 100644 index 000000000..c81d1ed43 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptor.java @@ -0,0 +1,259 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import com.google.common.base.MoreObjects; +import com.google.common.collect.Lists; + +import org.opensaml.core.xml.XMLObject; +import org.springframework.util.StringUtils; + +import javax.annotation.Nullable; + +import javax.persistence.JoinColumn; +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.OneToMany; +import javax.persistence.OneToOne; +import javax.persistence.Transient; + +import javax.xml.namespace.QName; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + + +@Entity +public class EntityDescriptor extends AbstractDescriptor implements org.opensaml.saml.saml2.metadata.EntityDescriptor { + private String localId; + + private String entityID; + + private String serviceProviderName; + + private boolean serviceEnabled; + + private String resourceId; + + @OneToOne(cascade = CascadeType.ALL) + private Organization organization; + + @OneToMany(cascade = CascadeType.ALL) + private List contactPersons = new ArrayList<>(); + + @OneToMany(cascade = CascadeType.ALL) + private List roleDescriptors; + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "entitydesc_addlmetdatlocations_id") + private List additionalMetadataLocations = new ArrayList<>(); + + @OneToOne(cascade = CascadeType.ALL) + private AuthnAuthorityDescriptor authnAuthorityDescriptor; + + @OneToOne(cascade = CascadeType.ALL) + private AttributeAuthorityDescriptor attributeAuthorityDescriptor; + + @OneToOne(cascade = CascadeType.ALL) + private PDPDescriptor pdpDescriptor; + + @OneToOne(cascade = CascadeType.ALL) + private AffiliationDescriptor affiliationDescriptor; + + public EntityDescriptor() { + super(); + this.resourceId = UUID.randomUUID().toString(); + } + + //getters and setters + @Override + public String getID() { + return this.localId; + } + + @Override + public void setID(String id) { + this.localId = id; + } + + @Override + public String getEntityID() { + return entityID; + } + + @Override + public void setEntityID(String entityID) { + this.entityID = entityID; + } + + public String getServiceProviderName() { + return serviceProviderName; + } + + public void setServiceProviderName(String serviceProviderName) { + this.serviceProviderName = serviceProviderName; + } + + public boolean isServiceEnabled() { + return serviceEnabled; + } + + public void setServiceEnabled(boolean serviceEnabled) { + this.serviceEnabled = serviceEnabled; + } + + public String getResourceId() { + return resourceId; + } + + public void setResourceId(String resourceId) { + this.resourceId = resourceId; + } + + @Override + public List getRoleDescriptors() { + // TODO: we're lazy now, but might need to change in the future. Also, this break concurrency, so... + if (this.roleDescriptors == null) { + this.roleDescriptors = new ArrayList<>(); + } + + return (List) (List) this.roleDescriptors; + } + + public void setRoleDescriptors(List roleDescriptors) { + this.roleDescriptors = roleDescriptors; + } + + @Override + public List getRoleDescriptors(QName qName) { + return this.getRoleDescriptors() + .stream() + .filter(p -> p.getElementQName().equals(qName)) + .collect(Collectors.toList()); + } + + @Override + public List getRoleDescriptors(QName qName, String s) { + return this.getRoleDescriptors() + .stream() + .filter(p -> p.getElementQName().equals(qName) && p.isSupportedProtocol(s)) + .collect(Collectors.toList()); + } + + @Override + @Transient + public IDPSSODescriptor getIDPSSODescriptor(String s) { + return (IDPSSODescriptor) this.getRoleDescriptors() + .stream() + .filter(p -> p instanceof org.opensaml.saml.saml2.metadata.IDPSSODescriptor && (StringUtils.isEmpty(s) ? true :p.isSupportedProtocol(s))) + .findFirst() + .orElse(null); + } + + @Override + @Transient + public SPSSODescriptor getSPSSODescriptor(String s) { + return (SPSSODescriptor) this.getRoleDescriptors() + .stream() + .filter(p -> p instanceof org.opensaml.saml.saml2.metadata.SPSSODescriptor && (StringUtils.isEmpty(s) ? true :p.isSupportedProtocol(s))) + .findFirst() + .orElse(null); + } + + @Override + public AuthnAuthorityDescriptor getAuthnAuthorityDescriptor(String s) { + return authnAuthorityDescriptor; + } + + public void setAuthnAuthorityDescriptor(AuthnAuthorityDescriptor authnAuthorityDescriptor) { + this.authnAuthorityDescriptor = authnAuthorityDescriptor; + } + + @Override + public AttributeAuthorityDescriptor getAttributeAuthorityDescriptor(String s) { + return attributeAuthorityDescriptor; + } + + public void setAttributeAuthorityDescriptor(AttributeAuthorityDescriptor attributeAuthorityDescriptor) { + this.attributeAuthorityDescriptor = attributeAuthorityDescriptor; + } + + @Override + public PDPDescriptor getPDPDescriptor(String s) { + return pdpDescriptor; + } + + public void setPdpDescriptor(PDPDescriptor pdpDescriptor) { + this.pdpDescriptor = pdpDescriptor; + } + + @Override + public AffiliationDescriptor getAffiliationDescriptor() { + return affiliationDescriptor; + } + + @Override + public void setAffiliationDescriptor(org.opensaml.saml.saml2.metadata.AffiliationDescriptor affiliationDescriptor) { + this.affiliationDescriptor = (AffiliationDescriptor) affiliationDescriptor; + } + + @Override + public org.opensaml.saml.saml2.metadata.Organization getOrganization() { + return organization; + } + + @Override + public void setOrganization(org.opensaml.saml.saml2.metadata.Organization organization) { + this.organization = (Organization) organization; + } + + @Override + public List getContactPersons() { + return (List) (List) this.contactPersons; + } + + public void addContactPerson(ContactPerson contactPerson) { + this.contactPersons.add(contactPerson); + } + + public void setContactPersons(List contactPersons) { + this.contactPersons = contactPersons; + } + + @Override + public List getAdditionalMetadataLocations() { + return Lists.newArrayList(additionalMetadataLocations); + } + + public void setAdditionalMetadataLocations(List additionalMetadataLocations) { + this.additionalMetadataLocations = additionalMetadataLocations; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("entityID", entityID) + // .add("organization", organization) + .add("id", id) + .toString(); + } + + @Nullable + @Override + public List getOrderedChildren() { + final ArrayList children = new ArrayList<>(); + + if (getSignature() != null) { + children.add(getSignature()); + } + children.add(getExtensions()); + children.addAll(this.getRoleDescriptors()); + children.add(getAffiliationDescriptor()); + children.add(getOrganization()); + children.addAll(this.getContactPersons()); + children.addAll(this.getAdditionalMetadataLocations()); + + return Collections.unmodifiableList(children); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Extensions.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Extensions.java new file mode 100644 index 000000000..3494a3495 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Extensions.java @@ -0,0 +1,18 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import org.opensaml.core.xml.XMLObject; + +import javax.annotation.Nullable; +import javax.persistence.Entity; +import java.util.Collections; +import java.util.List; + + +@Entity +public class Extensions extends AbstractElementExtensibleXMLObject implements org.opensaml.saml.saml2.metadata.Extensions { + @Nullable + @Override + public List getOrderedChildren() { + return Collections.unmodifiableList(this.getUnknownXMLObjects()); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/GivenName.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/GivenName.java new file mode 100644 index 000000000..85380d1b7 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/GivenName.java @@ -0,0 +1,19 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Entity; + +@Entity +public class GivenName extends AbstractXMLObject implements org.opensaml.saml.saml2.metadata.GivenName { + + private String name; + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + this.name = name; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/IDPSSODescriptor.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/IDPSSODescriptor.java new file mode 100644 index 000000000..de1f7410e --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/IDPSSODescriptor.java @@ -0,0 +1,102 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import com.google.common.collect.Lists; +import org.opensaml.core.xml.schema.XSBooleanValue; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.OneToMany; +import java.util.List; + + +@Entity +public class IDPSSODescriptor extends SSODescriptor implements org.opensaml.saml.saml2.metadata.IDPSSODescriptor { + + private boolean wantAuthnRequestsSigned; + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "idpssodesc_ssoserv_id") + private List singleSignOnServices; + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "idpssodesc_nameidmapserv_id") + private List nameIDMappingServices; + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "idpssodesc_asseridreqserv_id") + private List assertionIDRequestServices; + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "idpssodesc_attribprofile_id") + private List attributeProfiles; + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "idpssodesc_attrib_id") + private List attributes; + + @Override + public Boolean getWantAuthnRequestsSigned() { + return wantAuthnRequestsSigned; + } + + @Override + public XSBooleanValue getWantAuthnRequestsSignedXSBoolean() { + return null; //TODO + } + + @Override + public void setWantAuthnRequestsSigned(Boolean wantAuthnRequestsSigned) { + this.wantAuthnRequestsSigned = wantAuthnRequestsSigned; + } + + @Override + public void setWantAuthnRequestsSigned(XSBooleanValue xsBooleanValue) { + //TODO + } + + @Override + public List getSingleSignOnServices() { + return Lists.newArrayList(singleSignOnServices); + } + + public void setSingleSignOnServices(List singleSignOnServices) { + this.singleSignOnServices = singleSignOnServices; + } + + @Override + public List getNameIDMappingServices() { + return Lists.newArrayList(nameIDMappingServices); + } + + public void setNameIDMappingServices(List nameIDMappingServices) { + this.nameIDMappingServices = nameIDMappingServices; + } + + @Override + public List getAssertionIDRequestServices() { + return Lists.newArrayList(assertionIDRequestServices); + } + + public void setAssertionIDRequestServices(List assertionIDRequestServices) { + this.assertionIDRequestServices = assertionIDRequestServices; + } + + @Override + public List getAttributeProfiles() { + return Lists.newArrayList(attributeProfiles); + } + + public void setAttributeProfiles(List attributeProfiles) { + this.attributeProfiles = attributeProfiles; + } + + @Override + public List getAttributes() { + return Lists.newArrayList(attributes); + } + + public void setAttributes(List attributes) { + this.attributes = attributes; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/IndexedEndpoint.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/IndexedEndpoint.java new file mode 100644 index 000000000..d3ea44b3c --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/IndexedEndpoint.java @@ -0,0 +1,47 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import org.opensaml.core.xml.schema.XSBooleanValue; + +import javax.persistence.Entity; + + +@Entity +public class IndexedEndpoint extends Endpoint implements org.opensaml.saml.saml2.metadata.IndexedEndpoint { + + private Integer endpointIndex; + + private Boolean isDefault; + + @Override + public Integer getIndex() { + return endpointIndex; + } + + @Override + public void setIndex(Integer index) { + this.endpointIndex = index; + } + + @Override + public Boolean isDefault() { + return isDefault; + } + + @Override + public XSBooleanValue isDefaultXSBoolean() { + if (this.isDefault == null || !this.isDefault) { + return null; + } + return XSBooleanValue.valueOf("true"); + } + + @Override + public void setIsDefault(Boolean aBoolean) { + this.isDefault = aBoolean; + } + + @Override + public void setIsDefault(XSBooleanValue xsBooleanValue) { + this.isDefault = xsBooleanValue.getValue(); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/InformationURL.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/InformationURL.java new file mode 100644 index 000000000..3bf42dcaa --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/InformationURL.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Entity; + +@Entity +public class InformationURL extends AbstractLangBearingURL implements org.opensaml.saml.ext.saml2mdui.InformationURL { +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/KeyDescriptor.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/KeyDescriptor.java new file mode 100644 index 000000000..d95d40f3b --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/KeyDescriptor.java @@ -0,0 +1,83 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import com.google.common.collect.Lists; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.security.credential.UsageType; +import org.opensaml.xmlsec.signature.KeyInfo; + +import javax.annotation.Nullable; +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; + +@Entity +public class KeyDescriptor extends AbstractXMLObject implements org.opensaml.saml.saml2.metadata.KeyDescriptor { + + @Column(name = "keyDescriptorName") + private String name; + private String usageType; + + @OneToOne(cascade = CascadeType.ALL) + private edu.internet2.tier.shibboleth.admin.ui.domain.KeyInfo keyInfo; + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "keydesc_encryptionmethod_id") + private List encryptionMethods; + + @Override + public UsageType getUse() { + if (this.usageType == null) { + return null; + } + return UsageType.valueOf(this.usageType.toUpperCase()); + } + + @Override + public void setUse(UsageType usageType) { + this.usageType = usageType.toString().toLowerCase(); + } + + public void setUsageType(String usageType) { + this.usageType = usageType; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public org.opensaml.xmlsec.signature.KeyInfo getKeyInfo() { + return keyInfo; + } + + @Override + public void setKeyInfo(KeyInfo keyInfo) { + this.keyInfo = (edu.internet2.tier.shibboleth.admin.ui.domain.KeyInfo) keyInfo; //TODO + } + + @Override + public List getEncryptionMethods() { + return Lists.newArrayList(encryptionMethods); + } + + public void setEncryptionMethods(List encryptionMethods) { + this.encryptionMethods = encryptionMethods; + } + + @Nullable + @Override + public List getOrderedChildren() { + ArrayList children = new ArrayList<>(); + + children.add(this.keyInfo); + if (this.encryptionMethods != null) { + children.addAll(this.encryptionMethods); + } + + return children; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/KeyInfo.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/KeyInfo.java new file mode 100644 index 000000000..a996749f4 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/KeyInfo.java @@ -0,0 +1,141 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import org.opensaml.core.xml.XMLObject; +import org.opensaml.xmlsec.encryption.AgreementMethod; +import org.opensaml.xmlsec.encryption.EncryptedKey; +import org.opensaml.xmlsec.signature.DEREncodedKeyValue; +import org.opensaml.xmlsec.signature.KeyInfoReference; +import org.opensaml.xmlsec.signature.KeyName; +import org.opensaml.xmlsec.signature.KeyValue; +import org.opensaml.xmlsec.signature.MgmtData; +import org.opensaml.xmlsec.signature.PGPData; +import org.opensaml.xmlsec.signature.RetrievalMethod; +import org.opensaml.xmlsec.signature.SPKIData; +import org.opensaml.xmlsec.signature.X509Data; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.OneToMany; +import javax.xml.namespace.QName; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +@Entity +public class KeyInfo extends AbstractXMLObject implements org.opensaml.xmlsec.signature.KeyInfo { + + @OneToMany(cascade = CascadeType.ALL) + List xmlObjects = new ArrayList<>(); + + + @Nullable + @Override + public String getID() { + return null; + } + + @Override + public void setID(@Nullable String newID) { + + } + + @Nonnull + @Override + public List getXMLObjects() { + return (List) (List) this.xmlObjects; + } + + @Nonnull + @Override + public List getXMLObjects(@Nonnull QName typeOrName) { + return this.getXMLObjects().stream().filter(p -> p.getElementQName().equals(typeOrName) || p.getSchemaType().equals(typeOrName)).collect(Collectors.toList()); + } + + @Nonnull + @Override + public List getKeyNames() { + return Collections.emptyList(); + } + + @Nonnull + @Override + public List getKeyValues() { + return Collections.emptyList(); + } + + @Nonnull + @Override + public List getDEREncodedKeyValues() { + return Collections.emptyList(); + } + + @Nonnull + @Override + public List getRetrievalMethods() { + return Collections.emptyList(); + } + + @Nonnull + @Override + public List getKeyInfoReferences() { + return Collections.emptyList(); + } + + @Nonnull + @Override + public List getX509Datas() { + return Arrays.asList(this.xmlObjects.stream().filter(i -> i instanceof X509Data).toArray(X509Data[]::new)); + } + + public void addX509Data(edu.internet2.tier.shibboleth.admin.ui.domain.X509Data x509Data) { + this.xmlObjects.add(x509Data); + } + + @Nonnull + @Override + public List getPGPDatas() { + return Collections.emptyList(); + } + + @Nonnull + @Override + public List getSPKIDatas() { + return Collections.emptyList(); + } + + @Nonnull + @Override + public List getMgmtDatas() { + return Collections.emptyList(); + } + + @Nonnull + @Override + public List getAgreementMethods() { + return Collections.emptyList(); + } + + @Nonnull + @Override + public List getEncryptedKeys() { + return Collections.emptyList(); + } + + @Nullable + @Override + public List getOrderedChildren() { + ArrayList children = new ArrayList<>(); + + children.addAll(this.getXMLObjects()); + + if (children.size() == 0) { + return null; + } + + return children; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/KeyName.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/KeyName.java new file mode 100644 index 000000000..90acbeb4d --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/KeyName.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Entity; + +@Entity +public class KeyName extends XSString implements org.opensaml.xmlsec.signature.KeyName { +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Keywords.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Keywords.java new file mode 100644 index 000000000..ba8b15c5f --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Keywords.java @@ -0,0 +1,34 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.annotation.Nullable; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import java.util.List; + +@Entity +public class Keywords extends AbstractXMLObject implements org.opensaml.saml.ext.saml2mdui.Keywords { + @ElementCollection + private List keywords; + private String xmlLang; + + @Override + public List getKeywords() { + return this.keywords; + } + + @Override + public void setKeywords(List keywords) { + this.keywords = keywords; + } + + @Nullable + @Override + public String getXMLLang() { + return this.xmlLang; + } + + @Override + public void setXMLLang(@Nullable String xmlLang) { + this.xmlLang = xmlLang; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/LocalizedName.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/LocalizedName.java new file mode 100644 index 000000000..2aa493293 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/LocalizedName.java @@ -0,0 +1,34 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.annotation.Nullable; +import javax.persistence.MappedSuperclass; + +@MappedSuperclass +public class LocalizedName extends AbstractXMLObject implements org.opensaml.saml.saml2.metadata.LocalizedName { + + private String xMLLang; + + private String value; + + @Nullable + @Override + public String getXMLLang() { + return xMLLang; + } + + @Override + public void setXMLLang(@Nullable String xmllang) { + this.xMLLang = xmllang; + } + + @Nullable + @Override + public String getValue() { + return value; + } + + @Override + public void setValue(@Nullable String value) { + this.value = value; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Logo.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Logo.java new file mode 100644 index 000000000..bc836c5d5 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Logo.java @@ -0,0 +1,61 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.annotation.Nullable; +import javax.persistence.Column; +import javax.persistence.Entity; + +@Entity +public class Logo extends AbstractXMLObject implements org.opensaml.saml.ext.saml2mdui.Logo { + @Column(name = "logUrl") + private String url; + + @Column(name = "logoHieght") + private int height; + + @Column(name = "logoWidth") + private int width; + + @Column(name = "logoXmlLang") + private String xmlLang; + + @Override + public String getURL() { + return this.url; + } + + @Override + public void setURL(String url) { + this.url = url; + } + + @Override + public Integer getHeight() { + return this.height; + } + + @Override + public void setHeight(Integer height) { + this.height = height; + } + + @Override + public Integer getWidth() { + return this.width; + } + + @Override + public void setWidth(Integer width) { + this.width = width; + } + + @Nullable + @Override + public String getXMLLang() { + return this.xmlLang; + } + + @Override + public void setXMLLang(@Nullable String xmlLang) { + this.xmlLang = xmlLang; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ManageNameIDService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ManageNameIDService.java new file mode 100644 index 000000000..0ec74a11e --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ManageNameIDService.java @@ -0,0 +1,8 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Entity; + +@Entity +public class ManageNameIDService extends Endpoint implements org.opensaml.saml.saml2.metadata.ManageNameIDService { + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/NameIDFormat.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/NameIDFormat.java new file mode 100644 index 000000000..a9952d4f5 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/NameIDFormat.java @@ -0,0 +1,20 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Entity; + +@Entity +public class NameIDFormat extends AbstractXMLObject implements org.opensaml.saml.saml2.metadata.NameIDFormat { + + private String format; + + + @Override + public String getFormat() { + return format; + } + + @Override + public void setFormat(String format) { + this.format = format; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/NameIDMappingService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/NameIDMappingService.java new file mode 100644 index 000000000..02e6de8f7 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/NameIDMappingService.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Entity; + +@Entity +public class NameIDMappingService extends Endpoint implements org.opensaml.saml.saml2.metadata.NameIDMappingService { +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Organization.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Organization.java new file mode 100644 index 000000000..5e65e8b03 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Organization.java @@ -0,0 +1,80 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import org.opensaml.core.xml.XMLObject; + +import javax.annotation.Nullable; +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; + + +@Entity +public class Organization extends AbstractAttributeExtensibleXMLObject implements org.opensaml.saml.saml2.metadata.Organization { + + @OneToOne(cascade = CascadeType.ALL) + private Extensions extensions; + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "org_orgname_id") + private List organizationNames = new ArrayList<>(); + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "org_orgdisplayname_id") + private List organizationDisplayNames = new ArrayList<>(); + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "org_orgurl_id") + private List organizationURLs = new ArrayList<>(); + + @Override + public org.opensaml.saml.saml2.metadata.Extensions getExtensions() { + return extensions; + } + + @Override + public void setExtensions(org.opensaml.saml.saml2.metadata.Extensions extensions) { + this.extensions = (Extensions) extensions; + } + + @Override + public List getOrganizationNames() { + return (List) (List) organizationNames; + } + + public void setOrganizationNames(List organizationNames) { + this.organizationNames = organizationNames; + } + + @Override + public List getDisplayNames() { + return (List) (List) organizationDisplayNames; + } + + public void setOrganizationDisplayNames(List organizationDisplayNames) { + this.organizationDisplayNames = organizationDisplayNames; + } + + @Override + public List getURLs() { + return (List) (List) organizationURLs; + } + + public void setOrganizationURLs(List organizationURLs) { + this.organizationURLs = organizationURLs; + } + + @Nullable + @Override + public List getOrderedChildren() { + final ArrayList children = new ArrayList<>(); + + children.add(this.extensions); + children.addAll(this.organizationNames); + children.addAll(this.organizationDisplayNames); + children.addAll(this.organizationURLs); + + return children; + } +} + + diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/OrganizationDisplayName.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/OrganizationDisplayName.java new file mode 100644 index 000000000..4e024b9e5 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/OrganizationDisplayName.java @@ -0,0 +1,8 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Entity; + +@Entity +public class OrganizationDisplayName extends LocalizedName implements org.opensaml.saml.saml2.metadata.OrganizationDisplayName { + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/OrganizationName.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/OrganizationName.java new file mode 100644 index 000000000..4d058ebc8 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/OrganizationName.java @@ -0,0 +1,8 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Entity; + +@Entity +public class OrganizationName extends LocalizedName implements org.opensaml.saml.saml2.metadata.OrganizationName { + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/OrganizationURL.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/OrganizationURL.java new file mode 100644 index 000000000..59aa89800 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/OrganizationURL.java @@ -0,0 +1,34 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.annotation.Nullable; +import javax.persistence.Entity; + +@Entity +public class OrganizationURL extends AbstractXMLObject implements org.opensaml.saml.saml2.metadata.OrganizationURL { + + private String xMLLang; + + private String value; + + @Nullable + @Override + public String getXMLLang() { + return xMLLang; //TODO implement langbearing used x2 + } + + @Override + public void setXMLLang(@Nullable String xmlLang) { + this.xMLLang = xmlLang; + } + + @Nullable + @Override + public String getValue() { + return value; + } + + @Override + public void setValue(@Nullable String value) { + this.value = value; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/PDPDescriptor.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/PDPDescriptor.java new file mode 100644 index 000000000..38b3aa161 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/PDPDescriptor.java @@ -0,0 +1,53 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import com.google.common.collect.Lists; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.OneToMany; +import java.util.List; + + +@Entity +public class PDPDescriptor extends RoleDescriptor implements org.opensaml.saml.saml2.metadata.PDPDescriptor { + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "pdpdesc_authzserv_id") + private List authzServices; + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "pdpdesc_assertidreqserv_id") + private List assertionIDRequestServices; + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "pdpdesc_nameidfmt_id") + private List nameIDFormats; + + @Override + public List getAuthzServices() { + return Lists.newArrayList(authzServices); + } + + public void setAuthzServices(List authzServices) { + this.authzServices = authzServices; + } + + @Override + public List getAssertionIDRequestServices() { + return Lists.newArrayList(assertionIDRequestServices); + } + + public void setAssertionIDRequestServices(List assertionIDRequestServices) { + this.assertionIDRequestServices = assertionIDRequestServices; + } + + @Override + public List getNameIDFormats() { + return Lists.newArrayList(nameIDFormats); + } + + public void setNameIDFormats(List nameIDFormats) { + this.nameIDFormats = nameIDFormats; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/PrivacyStatementURL.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/PrivacyStatementURL.java new file mode 100644 index 000000000..c3f14bfd7 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/PrivacyStatementURL.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Entity; + +@Entity +public class PrivacyStatementURL extends AbstractLangBearingURL implements org.opensaml.saml.ext.saml2mdui.PrivacyStatementURL { +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RequestedAttribute.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RequestedAttribute.java new file mode 100644 index 000000000..1a841a47e --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RequestedAttribute.java @@ -0,0 +1,31 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import org.opensaml.core.xml.schema.XSBooleanValue; + +import javax.persistence.Entity; + +@Entity +public class RequestedAttribute extends Attribute implements org.opensaml.saml.saml2.metadata.RequestedAttribute { + + private boolean isRequired; + + @Override + public Boolean isRequired() { + return isRequired; + } + + @Override + public XSBooleanValue isRequiredXSBoolean() { + return null; //TODO? + } + + @Override + public void setIsRequired(Boolean isRequired) { + this.isRequired = isRequired; + } + + @Override + public void setIsRequired(XSBooleanValue xsBooleanValue) { + //TODO? + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RoleDescriptor.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RoleDescriptor.java new file mode 100644 index 000000000..a2a111e12 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RoleDescriptor.java @@ -0,0 +1,193 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.util.AttributeMap; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.OneToMany; +import javax.persistence.Transient; +import javax.persistence.ElementCollection; +import javax.persistence.JoinColumn; +import javax.persistence.OneToOne; + +import javax.xml.namespace.QName; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +@Entity +public class RoleDescriptor extends AbstractDescriptor implements org.opensaml.saml.saml2.metadata.RoleDescriptor { + + @ElementCollection + private List supportedProtocols = new ArrayList<>(); + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "roledesc_keydesc_id") + private List keyDescriptors = new ArrayList<>(); // TODO: implement + + @OneToOne(cascade = CascadeType.ALL) + private Organization organization; + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "roledesc_contactperson_id") + private List contactPersons = new ArrayList<>(); // TODO: implement + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "roledesc_endpoint_id") + private List endpoints = new ArrayList<>(); + + private boolean isSupportedProtocol; + + private String errorURL; + + private String localId; + + @Transient + private AttributeMap unknownAttributes; + + public RoleDescriptor() { + this.unknownAttributes = new AttributeMap(this); + } + + @Override + public String getID() { + return this.localId; + } + + @Override + public void setID(String id) { + this.localId = id; + } + + @Override + public List getSupportedProtocols() { + return supportedProtocols; + } + + public void setSupportedProtocols(List supportedProtocols) { + this.supportedProtocols = supportedProtocols; + } + + @Override + public boolean isSupportedProtocol(String s) { + return isSupportedProtocol; + } + + public void setIsSupportedProtocol(boolean isSupportedProtocol) { + this.isSupportedProtocol = isSupportedProtocol; + } + + @Override + public void addSupportedProtocol(String supportedProtocol) { + supportedProtocols.add(supportedProtocol); + } + + @Override + public void removeSupportedProtocol(String supportedProtocol) { + //TODO + } + + @Override + public void removeSupportedProtocols(Collection collection) { + //TODO + } + + @Override + public void removeAllSupportedProtocols() { + //TODO + } + + @Override + public String getErrorURL() { + return errorURL; + } + + @Override + public void setErrorURL(String errorURL) { + this.errorURL = errorURL; + } + + @Override + public List getKeyDescriptors() { + return (List) (List) this.keyDescriptors; + } + + public void addKeyDescriptor(KeyDescriptor keyDescriptor) { + this.keyDescriptors.add(keyDescriptor); + } + + public void setKeyDescriptors(List keyDescriptors) { + this.keyDescriptors = keyDescriptors; + } + + @Override + public Organization getOrganization() { + return organization; + } + + @Override + public void setOrganization(org.opensaml.saml.saml2.metadata.Organization organization) { + this.organization = (Organization) organization; + } + + @Override + public List getContactPersons() { + return (List) (List) this.contactPersons; + } + + public void setContactPersons(List contactPersons) { + this.contactPersons = contactPersons; + } + + @Override + public List getEndpoints() { + return (List) (List) this.endpoints; + } + + public void setEndpoints(List endpoints) { + this.endpoints = endpoints; + } + + @Override + public List getEndpoints(QName qName) { + return null; //TODO? + } + + @Nonnull + @Override + public AttributeMap getUnknownAttributes() { + return unknownAttributes; + } + + public void setUnknownAttributes(AttributeMap unknownAttributes) { + this.unknownAttributes = unknownAttributes; + } + + @Nullable + @Override + @Transient + public List getOrderedChildren() { + ArrayList children = new ArrayList<>(); + + if (getSignature() != null) { + children.add(getSignature()); + } + + if (getExtensions() != null) { + children.add(getExtensions()); + } + children.addAll(getKeyDescriptors()); + if (organization != null) { + children.add(getOrganization()); + } + children.addAll(getContactPersons()); + + return Collections.unmodifiableList(children); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RoleDescriptorResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RoleDescriptorResolver.java new file mode 100644 index 000000000..5fa37ba63 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RoleDescriptorResolver.java @@ -0,0 +1,47 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import net.shibboleth.utilities.java.support.resolver.CriteriaSet; +import net.shibboleth.utilities.java.support.resolver.ResolverException; +import org.opensaml.saml.saml2.metadata.RoleDescriptor; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.persistence.Entity; + +@Entity +public class RoleDescriptorResolver extends AbstractAuditable implements org.opensaml.saml.metadata.resolver.RoleDescriptorResolver { + private String localId; + + private boolean isRequireValidMetadata; + + @Override + public String getId() { + return this.localId; + } + + public void setID(String id) { + this.localId = id; + } + + @Override + public boolean isRequireValidMetadata() { + return isRequireValidMetadata; + } + + @Override + public void setRequireValidMetadata(boolean isRequireValidMetadata) { + this.isRequireValidMetadata = isRequireValidMetadata; + } + + @Nonnull + @Override + public Iterable resolve(@Nullable CriteriaSet criteria) throws ResolverException { + return null; //TODO pull role descriptors from db based on criteria? + } + + @Nullable + @Override + public RoleDescriptor resolveSingle(@Nullable CriteriaSet criteria) throws ResolverException { + return null; //TODO pull role descriptor from db based on criteria? + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SPSSODescriptor.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SPSSODescriptor.java new file mode 100644 index 000000000..9300d9d3f --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SPSSODescriptor.java @@ -0,0 +1,115 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import com.google.common.collect.Lists; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.schema.XSBooleanValue; + +import javax.annotation.Nullable; +import javax.persistence.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Entity +public class SPSSODescriptor extends SSODescriptor implements org.opensaml.saml.saml2.metadata.SPSSODescriptor { + + private Boolean isAuthnRequestsSigned; + + private Boolean wantAssertionsSigned; + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "spssodesc_attribconsserv_id") + private List attributeConsumingServices = new ArrayList<>(); + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "spssodesc_assertconsserv_id") + private List assertionConsumerServices = new ArrayList<>(); + + + @Override + public Boolean isAuthnRequestsSigned() { + return isAuthnRequestsSigned; + } + + @Override + public XSBooleanValue isAuthnRequestsSignedXSBoolean() { + if (this.isAuthnRequestsSigned == null) { + return null; + } + return XSBooleanValue.valueOf(Boolean.toString(isAuthnRequestsSigned)); + } + + @Override + public void setAuthnRequestsSigned(Boolean isAuthnRequestsSigned) { + this.isAuthnRequestsSigned = isAuthnRequestsSigned; + } + + @Override + public void setAuthnRequestsSigned(XSBooleanValue xsBooleanValue) { + //TODO? + } + + @Override + public Boolean getWantAssertionsSigned() { + return wantAssertionsSigned; + } + + @Override + public XSBooleanValue getWantAssertionsSignedXSBoolean() { + if (this.wantAssertionsSigned == null) { + return null; + } + return XSBooleanValue.valueOf(Boolean.toString(wantAssertionsSigned)); + } + + @Override + public void setWantAssertionsSigned(Boolean wantAssertionsSigned) { + this.wantAssertionsSigned = wantAssertionsSigned; + } + + @Override + public void setWantAssertionsSigned(XSBooleanValue xsBooleanValue) { + //TODO? + } + + @Override + public List getAssertionConsumerServices() { + return (List)(List)this.assertionConsumerServices; + } + + public void setAssertionConsumerServices(List assertionConsumerServices) { + this.assertionConsumerServices = assertionConsumerServices; + } + + @Override + public AssertionConsumerService getDefaultAssertionConsumerService() { + return null; //TODO? + } + + @Override + public List getAttributeConsumingServices() { + return Lists.newArrayList(attributeConsumingServices); + } + + public void setAttributeConsumingServices(List attributeConsumingServices) { + this.attributeConsumingServices = attributeConsumingServices; + } + + @Override + public AttributeConsumingService getDefaultAttributeConsumingService() { + return null; //TODO? + } + + @Nullable + @Override + @Transient + public List getOrderedChildren() { + ArrayList children = new ArrayList<>(); + + children.addAll(super.getOrderedChildren()); + children.addAll(this.getAssertionConsumerServices()); + children.addAll(this.getAttributeConsumingServices()); + + return Collections.unmodifiableList(children); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SSODescriptor.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SSODescriptor.java new file mode 100644 index 000000000..04e62fb02 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SSODescriptor.java @@ -0,0 +1,90 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import com.google.common.collect.Lists; +import org.opensaml.core.xml.XMLObject; + +import javax.annotation.Nullable; +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.OneToMany; +import javax.persistence.Transient; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Entity +public class SSODescriptor extends RoleDescriptor implements org.opensaml.saml.saml2.metadata.SSODescriptor { + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "ssodesc_artifctresserv_id") + private List artifactResolutionServices = new ArrayList<>(); + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "ssodesc_singlelogoutserv_id") + private List singleLogoutServices = new ArrayList<>(); + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "ssodesc_managenameidserv_id") + private List manageNameIDServices = new ArrayList<>(); + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "ssodesc_nameidfmt_id") + private List nameIDFormats = new ArrayList<>(); + + @Override + public List getArtifactResolutionServices() { + return Lists.newArrayList(artifactResolutionServices); + } + + public void setArtifactResolutionServices(List artifactResolutionServices) { + this.artifactResolutionServices = artifactResolutionServices; + } + + @Override + public ArtifactResolutionService getDefaultArtifactResolutionService() { + return null; //TODO? + } + + @Override + public List getSingleLogoutServices() { + return (List) (List) this.singleLogoutServices; + } + + public void setSingleLogoutServices(List singleLogoutServices) { + this.singleLogoutServices = singleLogoutServices; + } + + @Override + public List getManageNameIDServices() { + return Lists.newArrayList(manageNameIDServices); + } + + public void setManageNameIDServices(List manageNameIDServices) { + this.manageNameIDServices = manageNameIDServices; + } + + @Override + public List getNameIDFormats() { + return (List) (List) nameIDFormats; + } + + public void setNameIDFormats(List nameIDFormats) { + this.nameIDFormats = nameIDFormats; + } + + @Nullable + @Override + @Transient + public List getOrderedChildren() { + ArrayList children = new ArrayList<>(); + + children.addAll(super.getOrderedChildren()); + children.addAll(this.getArtifactResolutionServices()); + children.addAll(this.getSingleLogoutServices()); + children.addAll(this.getManageNameIDServices()); + children.addAll(this.getNameIDFormats()); + + return Collections.unmodifiableList(children); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ServiceDescription.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ServiceDescription.java new file mode 100644 index 000000000..e6365f1f3 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ServiceDescription.java @@ -0,0 +1,8 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Entity; + +@Entity +public class ServiceDescription extends LocalizedName implements org.opensaml.saml.saml2.metadata.ServiceDescription { + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ServiceName.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ServiceName.java new file mode 100644 index 000000000..9e5f3c5ac --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ServiceName.java @@ -0,0 +1,8 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Entity; + +@Entity +public class ServiceName extends LocalizedName implements org.opensaml.saml.saml2.metadata.ServiceName { + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SingleLogoutService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SingleLogoutService.java new file mode 100644 index 000000000..6947fa4f2 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SingleLogoutService.java @@ -0,0 +1,8 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Entity; + +@Entity +public class SingleLogoutService extends Endpoint implements org.opensaml.saml.saml2.metadata.SingleLogoutService { + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SingleSignOnService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SingleSignOnService.java new file mode 100644 index 000000000..41659269b --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SingleSignOnService.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Entity; + +@Entity +public class SingleSignOnService extends Endpoint implements org.opensaml.saml.saml2.metadata.SingleSignOnService { +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SurName.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SurName.java new file mode 100644 index 000000000..535c05071 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SurName.java @@ -0,0 +1,19 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Entity; + +@Entity +public class SurName extends AbstractXMLObject implements org.opensaml.saml.saml2.metadata.SurName { + + private String name; + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + this.name = name; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/TelephoneNumber.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/TelephoneNumber.java new file mode 100644 index 000000000..03cd97a97 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/TelephoneNumber.java @@ -0,0 +1,19 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Entity; + +@Entity +public class TelephoneNumber extends AbstractXMLObject implements org.opensaml.saml.saml2.metadata.TelephoneNumber { + + private String number; + + @Override + public String getNumber() { + return number; + } + + @Override + public void setNumber(String number) { + this.number = number; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/UIInfo.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/UIInfo.java new file mode 100644 index 000000000..cc694b210 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/UIInfo.java @@ -0,0 +1,100 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import org.hibernate.annotations.Cascade; +import org.hibernate.annotations.CascadeType; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.saml.ext.saml2mdui.Description; +import org.opensaml.saml.ext.saml2mdui.DisplayName; +import org.opensaml.saml.ext.saml2mdui.InformationURL; +import org.opensaml.saml.ext.saml2mdui.Keywords; +import org.opensaml.saml.ext.saml2mdui.Logo; +import org.opensaml.saml.ext.saml2mdui.PrivacyStatementURL; + +import javax.annotation.Nullable; +import javax.persistence.Entity; +import javax.persistence.OneToMany; +import javax.xml.namespace.QName; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Entity +public class UIInfo extends AbstractXMLObject implements org.opensaml.saml.ext.saml2mdui.UIInfo { + @OneToMany + @Cascade(CascadeType.ALL) + private List xmlObjects = new ArrayList<>(); + + @Override + public List getDisplayNames() { + return (List) (List) this.xmlObjects.stream().filter(p -> p instanceof DisplayName).map(p -> (DisplayName) p).collect(Collectors.toList()); + } + + public void addDisplayName(edu.internet2.tier.shibboleth.admin.ui.domain.DisplayName displayName) { + this.xmlObjects.add(displayName); + } + + @Override + public List getKeywords() { + return this.xmlObjects.stream().filter(p -> p instanceof Keywords).map(p -> (Keywords) p).collect(Collectors.toList()); + } + + public void addKeywords(edu.internet2.tier.shibboleth.admin.ui.domain.Keywords keywords) { + this.xmlObjects.add(keywords); + } + + @Override + public List getDescriptions() { + return this.xmlObjects.stream().filter(p -> p instanceof Description).map(p -> (Description) p).collect(Collectors.toList()); + } + + public void addDescription(edu.internet2.tier.shibboleth.admin.ui.domain.Description description) { + this.xmlObjects.add(description); + } + + @Override + public List getLogos() { + return this.xmlObjects.stream().filter(p -> p instanceof Logo).map(p -> (Logo) p).collect(Collectors.toList()); + } + + public void addLog(edu.internet2.tier.shibboleth.admin.ui.domain.Logo logo) { + this.xmlObjects.add(logo); + } + + @Override + public List getInformationURLs() { + return this.xmlObjects.stream().filter(p -> p instanceof InformationURL).map(p -> (InformationURL) p).collect(Collectors.toList()); + } + + public void addInformationURL(edu.internet2.tier.shibboleth.admin.ui.domain.InformationURL informationURL) { + this.xmlObjects.add(informationURL); + } + + @Override + public List getPrivacyStatementURLs() { + return this.xmlObjects.stream().filter(p -> p instanceof PrivacyStatementURL).map(p -> (PrivacyStatementURL) p).collect(Collectors.toList()); + } + + public void addPrivacyStatementURL(edu.internet2.tier.shibboleth.admin.ui.domain.PrivacyStatementURL privacyStatementURL) { + this.xmlObjects.add(privacyStatementURL); + } + + @Override + public List getXMLObjects() { + return (List) (List) this.xmlObjects; + } + + @Override + public List getXMLObjects(QName typeOrName) { + return ((List) (List) this.xmlObjects).stream().filter(p -> p.getSchemaType().equals(typeOrName) || p.getElementQName().equals(typeOrName)).collect(Collectors.toList()); + } + + @Nullable + @Override + public List getOrderedChildren() { + List children = new ArrayList<>(); + + children.addAll(this.getXMLObjects()); + + return children; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/X509Certificate.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/X509Certificate.java new file mode 100644 index 000000000..b93451899 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/X509Certificate.java @@ -0,0 +1,24 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.annotation.Nullable; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Lob; + +@Entity +public class X509Certificate extends AbstractXMLObject implements org.opensaml.xmlsec.signature.X509Certificate { + @Column(name = "x509CertificateValue") + @Lob + private String value; + + @Nullable + @Override + public String getValue() { + return this.value; + } + + @Override + public void setValue(@Nullable String value) { + this.value = value; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/X509Data.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/X509Data.java new file mode 100644 index 000000000..e2f257ccb --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/X509Data.java @@ -0,0 +1,92 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import org.opensaml.core.xml.XMLObject; +import org.opensaml.xmlsec.signature.X509CRL; +import org.opensaml.xmlsec.signature.X509Certificate; +import org.opensaml.xmlsec.signature.X509Digest; +import org.opensaml.xmlsec.signature.X509IssuerSerial; +import org.opensaml.xmlsec.signature.X509SKI; +import org.opensaml.xmlsec.signature.X509SubjectName; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.OneToMany; +import javax.xml.namespace.QName; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@Entity +public class X509Data extends AbstractXMLObject implements org.opensaml.xmlsec.signature.X509Data { + @OneToMany(cascade = CascadeType.ALL) + List xmlObjects = new ArrayList<>(); + + @Nonnull + @Override + public List getXMLObjects() { + return (List) (List) this.xmlObjects; + } + + @Nonnull + @Override + public List getXMLObjects(@Nonnull QName typeOrName) { + return this.xmlObjects.stream().filter(i -> i.getElementQName().equals(typeOrName) || i.getSchemaType().equals(typeOrName)).collect(Collectors.toList()); + } + + public void setXmlObjects(List xmlObjects) { + this.xmlObjects = xmlObjects; + } + + @Nonnull + @Override + public List getX509IssuerSerials() { + return null; + } + + @Nonnull + @Override + public List getX509SKIs() { + return null; + } + + @Nonnull + @Override + public List getX509SubjectNames() { + return null; + } + + @Nonnull + @Override + public List getX509Certificates() { + return Arrays.asList(this.xmlObjects.stream().filter(i -> i instanceof org.opensaml.xmlsec.signature.X509Certificate).toArray(org.opensaml.xmlsec.signature.X509Certificate[]::new)); + } + + public void addX509Certificate(edu.internet2.tier.shibboleth.admin.ui.domain.X509Certificate x509Certificate) { + this.xmlObjects.add(x509Certificate); + } + + @Nonnull + @Override + public List getX509CRLs() { + return null; + } + + @Nonnull + @Override + public List getX509Digests() { + return null; + } + + @Nullable + @Override + public List getOrderedChildren() { + List children = new ArrayList<>(); + + children.addAll(this.getX509Certificates()); + + return children; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSAny.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSAny.java new file mode 100644 index 000000000..9dd447277 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSAny.java @@ -0,0 +1,39 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import org.opensaml.core.xml.util.AttributeMap; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.persistence.Entity; +import javax.persistence.Transient; + +@Entity +public class XSAny extends AbstractElementExtensibleXMLObject implements org.opensaml.core.xml.schema.XSAny { + + private String textContext; + + //TODO: implement. this at the underlying level is a just a Map + @Transient + private AttributeMap unknownAttributes; + + protected XSAny() { + this.unknownAttributes = new AttributeMap(this); + } + + @Nullable + @Override + public String getTextContent() { + return this.textContext; + } + + @Override + public void setTextContent(@Nullable String newContent) { + this.textContext = newContent; + } + + @Nonnull + @Override + public AttributeMap getUnknownAttributes() { + return this.unknownAttributes; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSBase64Binary.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSBase64Binary.java new file mode 100644 index 000000000..a0df05fde --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSBase64Binary.java @@ -0,0 +1,20 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.annotation.Nullable; +import javax.persistence.Entity; + +@Entity +public class XSBase64Binary extends AbstractXMLObject implements org.opensaml.core.xml.schema.XSBase64Binary { + private String b64value; + + @Nullable + @Override + public String getValue() { + return this.b64value; + } + + @Override + public void setValue(@Nullable String newValue) { + this.b64value = newValue; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSBoolean.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSBoolean.java new file mode 100644 index 000000000..ab3f88cb8 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSBoolean.java @@ -0,0 +1,32 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import org.opensaml.core.xml.schema.XSBooleanValue; + +import javax.annotation.Nullable; +import javax.persistence.Entity; +import javax.persistence.Transient; + +@Entity +public class XSBoolean extends AbstractXMLObject implements org.opensaml.core.xml.schema.XSBoolean { + private String storedValue; + + @Nullable + @Override + @Transient + public XSBooleanValue getValue() { + return XSBooleanValue.valueOf(this.storedValue); + } + + @Override + public void setValue(@Nullable XSBooleanValue value) { + this.storedValue = value.getValue().toString(); + } + + public String getStoredValue() { + return storedValue; + } + + public void setStoredValue(String storedValue) { + this.storedValue = storedValue; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSDateTime.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSDateTime.java new file mode 100644 index 000000000..c88f75917 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSDateTime.java @@ -0,0 +1,47 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import org.joda.time.DateTime; +import org.joda.time.chrono.ISOChronology; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.persistence.Entity; +import javax.persistence.Transient; + +@Entity +public class XSDateTime extends AbstractXMLObject implements org.opensaml.core.xml.schema.XSDateTime { + + private DateTime dateTime; + + @Transient + private DateTimeFormatter dateTimeFormatter; + + protected XSDateTime() { + this.dateTimeFormatter = ISODateTimeFormat.dateTime().withChronology(ISOChronology.getInstanceUTC()); + } + + @Nullable + @Override + //TODO: find good way to persist + public DateTime getValue() { + return this.dateTime; + } + + @Override + public void setValue(@Nullable DateTime newValue) { + this.dateTime = newValue; + } + + @Nonnull + @Override + public DateTimeFormatter getDateTimeFormatter() { + return this.dateTimeFormatter; + } + + @Override + public void setDateTimeFormatter(@Nonnull DateTimeFormatter newFormatter) { + this.dateTimeFormatter = newFormatter; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSInteger.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSInteger.java new file mode 100644 index 000000000..82ffc6329 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSInteger.java @@ -0,0 +1,20 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.annotation.Nullable; +import javax.persistence.Entity; + +@Entity +public class XSInteger extends AbstractXMLObject implements org.opensaml.core.xml.schema.XSInteger { + private int intValue; + + @Nullable + @Override + public Integer getValue() { + return this.intValue; + } + + @Override + public void setValue(@Nullable Integer newValue) { + this.intValue = newValue; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSQName.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSQName.java new file mode 100644 index 000000000..e17a93863 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSQName.java @@ -0,0 +1,25 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import net.shibboleth.utilities.java.support.xml.QNameSupport; + +import javax.annotation.Nullable; +import javax.persistence.Entity; +import javax.xml.namespace.QName; +import java.beans.Transient; + +@Entity +public class XSQName extends AbstractXMLObject implements org.opensaml.core.xml.schema.XSQName { + @Nullable + @Override + @Transient + public QName getValue() { + return QNameSupport.constructQName(this.getNamespaceURI(), this.getElementLocalName(), this.getNamespacePrefix()); + } + + @Override + public void setValue(@Nullable QName newValue) { + this.setNamespaceURI(newValue.getNamespaceURI()); + this.setElementLocalName(newValue.getLocalPart()); + this.setNamespacePrefix(newValue.getPrefix()); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSString.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSString.java new file mode 100644 index 000000000..5ad9264f6 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSString.java @@ -0,0 +1,20 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.annotation.Nullable; +import javax.persistence.Entity; + +@Entity +public class XSString extends AbstractXMLObject implements org.opensaml.core.xml.schema.XSString { + private String xsStringvalue; + + @Nullable + @Override + public String getValue() { + return this.xsStringvalue; + } + + @Override + public void setValue(@Nullable String newValue) { + this.xsStringvalue = newValue; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSURI.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSURI.java new file mode 100644 index 000000000..8e2a6c02c --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSURI.java @@ -0,0 +1,22 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.annotation.Nullable; +import javax.persistence.Column; +import javax.persistence.Entity; + +@Entity +public class XSURI extends AbstractXMLObject implements org.opensaml.core.xml.schema.XSURI { + @Column(name = "xsuriValue") + private String value; + + @Nullable + @Override + public String getValue() { + return this.value; + } + + @Override + public void setValue(@Nullable String value) { + this.value = value; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/AssertionConsumerServiceRepresentation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/AssertionConsumerServiceRepresentation.java new file mode 100644 index 000000000..17b50b11b --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/AssertionConsumerServiceRepresentation.java @@ -0,0 +1,38 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.frontend; + +import java.io.Serializable; + +public class AssertionConsumerServiceRepresentation implements Serializable { + + private static final long serialVersionUID = 7610150456756113460L; + + private String locationUrl; + + private String binding; + + private boolean makeDefault; + + public String getLocationUrl() { + return locationUrl; + } + + public void setLocationUrl(String locationUrl) { + this.locationUrl = locationUrl; + } + + public String getBinding() { + return binding; + } + + public void setBinding(String binding) { + this.binding = binding; + } + + public boolean isMakeDefault() { + return makeDefault; + } + + public void setMakeDefault(boolean makeDefault) { + this.makeDefault = makeDefault; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/AttributeReleaseRepresentation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/AttributeReleaseRepresentation.java new file mode 100644 index 000000000..bfa7913a1 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/AttributeReleaseRepresentation.java @@ -0,0 +1,138 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.frontend; + +import java.io.Serializable; + +public class AttributeReleaseRepresentation implements Serializable { + + private static final long serialVersionUID = -6112130016302634264L; + + private boolean eduPersonPrincipalName; + + private boolean uid; + + private boolean mail; + + private boolean surname; + + private boolean givenName; + + private boolean displayName; + + private boolean eduPersonAffiliation; + + private boolean eduPersonScopedAffiliation; + + private boolean eduPersonPrimaryAffiliation; + + private boolean eduPersonEntitlement; + + private boolean eduPersonAssurance; + + private boolean eduPersonUniqueId; + + private boolean employeeNumber; + + public boolean isEduPersonPrincipalName() { + return eduPersonPrincipalName; + } + + public void setEduPersonPrincipalName(boolean eduPersonPrincipalName) { + this.eduPersonPrincipalName = eduPersonPrincipalName; + } + + public boolean isUid() { + return uid; + } + + public void setUid(boolean uid) { + this.uid = uid; + } + + public boolean isMail() { + return mail; + } + + public void setMail(boolean mail) { + this.mail = mail; + } + + public boolean isSurname() { + return surname; + } + + public void setSurname(boolean surname) { + this.surname = surname; + } + + public boolean isGivenName() { + return givenName; + } + + public void setGivenName(boolean givenName) { + this.givenName = givenName; + } + + public boolean isDisplayName() { + return displayName; + } + + public void setDisplayName(boolean displayName) { + this.displayName = displayName; + } + + public boolean isEduPersonAffiliation() { + return eduPersonAffiliation; + } + + public void setEduPersonAffiliation(boolean eduPersonAffiliation) { + this.eduPersonAffiliation = eduPersonAffiliation; + } + + public boolean isEduPersonScopedAffiliation() { + return eduPersonScopedAffiliation; + } + + public void setEduPersonScopedAffiliation(boolean eduPersonScopedAffiliation) { + this.eduPersonScopedAffiliation = eduPersonScopedAffiliation; + } + + public boolean isEduPersonPrimaryAffiliation() { + return eduPersonPrimaryAffiliation; + } + + public void setEduPersonPrimaryAffiliation(boolean eduPersonPrimaryAffiliation) { + this.eduPersonPrimaryAffiliation = eduPersonPrimaryAffiliation; + } + + public boolean isEduPersonEntitlement() { + return eduPersonEntitlement; + } + + public void setEduPersonEntitlement(boolean eduPersonEntitlement) { + this.eduPersonEntitlement = eduPersonEntitlement; + } + + public boolean isEduPersonAssurance() { + return eduPersonAssurance; + } + + public void setEduPersonAssurance(boolean eduPersonAssurance) { + this.eduPersonAssurance = eduPersonAssurance; + } + + public boolean isEduPersonUniqueId() { + return eduPersonUniqueId; + } + + public void setEduPersonUniqueId(boolean eduPersonUniqueId) { + this.eduPersonUniqueId = eduPersonUniqueId; + } + + public boolean isEmployeeNumber() { + return employeeNumber; + } + + public void setEmployeeNumber(boolean employeeNumber) { + this.employeeNumber = employeeNumber; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/ContactRepresentation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/ContactRepresentation.java new file mode 100644 index 000000000..9c30f3ecb --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/ContactRepresentation.java @@ -0,0 +1,58 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.frontend; + +import java.io.Serializable; + +public class ContactRepresentation implements Serializable { + + private static final long serialVersionUID = -6030201796077709908L; + + private String type; + + private String name; + + private String emailAddress; + + private String displayName; + + private String url; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmailAddress() { + return emailAddress; + } + + public void setEmailAddress(String emailAddress) { + this.emailAddress = emailAddress; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java new file mode 100644 index 000000000..3bab8e4b0 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java @@ -0,0 +1,196 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.frontend; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +public class EntityDescriptorRepresentation implements Serializable { + + + public EntityDescriptorRepresentation() { + } + + public EntityDescriptorRepresentation(String id, + String entityId, + String serviceProviderName, + boolean serviceEnabled, + LocalDateTime createdDate, + LocalDateTime modifiedDate) { + this.id = id; + this.entityId = entityId; + this.serviceProviderName = serviceProviderName; + this.serviceEnabled = serviceEnabled; + this.createdDate = createdDate; + this.modifiedDate = modifiedDate; + } + + private static final long serialVersionUID = 7753435553892353966L; + + private String id; + + @NotNull + private String serviceProviderName; + + @NotNull + private String entityId; + + private OrganizationRepresentation organization; + + private List contacts; + + private MduiRepresentation mdui; + + private ServiceProviderSsoDescriptorRepresentation serviceProviderSsoDescriptor; + + private List logoutEndpoints; + + private SecurityInfoRepresentation securityInfo; + + private List assertionConsumerServices; + + private boolean serviceEnabled; + + private LocalDateTime createdDate; + + private LocalDateTime modifiedDate; + + private RelyingPartyOverridesRepresentation relyingPartyOverrides; + + private List attributeRelease; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getServiceProviderName() { + return serviceProviderName; + } + + public void setServiceProviderName(String serviceProviderName) { + this.serviceProviderName = serviceProviderName; + } + + public String getEntityId() { + return entityId; + } + + public void setEntityId(String entityId) { + this.entityId = entityId; + } + + public OrganizationRepresentation getOrganization() { + return organization; + } + + public void setOrganization(OrganizationRepresentation organization) { + this.organization = organization; + } + + public List getContacts() { + return contacts; + } + + public void setContacts(List contacts) { + this.contacts = contacts; + } + + public MduiRepresentation getMdui() { + return mdui; + } + + public void setMdui(MduiRepresentation mdui) { + this.mdui = mdui; + } + + public ServiceProviderSsoDescriptorRepresentation getServiceProviderSsoDescriptor() { + return this.getServiceProviderSsoDescriptor(false); + } + + public ServiceProviderSsoDescriptorRepresentation getServiceProviderSsoDescriptor(boolean create) { + if (create && this.serviceProviderSsoDescriptor == null) { + this.serviceProviderSsoDescriptor = new ServiceProviderSsoDescriptorRepresentation(); + } + return this.serviceProviderSsoDescriptor; + } + + public void setServiceProviderSsoDescriptor(ServiceProviderSsoDescriptorRepresentation serviceProviderSsoDescriptor) { + this.serviceProviderSsoDescriptor = serviceProviderSsoDescriptor; + } + + public List getLogoutEndpoints() { + return this.getLogoutEndpoints(false); + } + + public List getLogoutEndpoints(boolean create) { + if (create && this.logoutEndpoints == null) { + this.logoutEndpoints = new ArrayList<>(); + } + return logoutEndpoints; + } + + public void setLogoutEndpoints(List logoutEndpoints) { + this.logoutEndpoints = logoutEndpoints; + } + + public SecurityInfoRepresentation getSecurityInfo() { + return securityInfo; + } + + public void setSecurityInfo(SecurityInfoRepresentation securityInfo) { + this.securityInfo = securityInfo; + } + + public List getAssertionConsumerServices() { + return assertionConsumerServices; + } + + public void setAssertionConsumerServices(List assertionConsumerServices) { + this.assertionConsumerServices = assertionConsumerServices; + } + + public boolean isServiceEnabled() { + return serviceEnabled; + } + + public void setServiceEnabled(boolean serviceEnabled) { + this.serviceEnabled = serviceEnabled; + } + + public String getCreatedDate() { + return createdDate != null ? createdDate.toString() : null; + } + + public void setCreatedDate(LocalDateTime createdDate) { + this.createdDate = createdDate; + } + + public String getModifiedDate() { + return modifiedDate != null ? modifiedDate.toString() : null; + } + + public void setModifiedDate(LocalDateTime modifiedDate) { + this.modifiedDate = modifiedDate; + } + + public RelyingPartyOverridesRepresentation getRelyingPartyOverrides() { + return relyingPartyOverrides; + } + + public void setRelyingPartyOverrides(RelyingPartyOverridesRepresentation relyingPartyOverrides) { + this.relyingPartyOverrides = relyingPartyOverrides; + } + + public List getAttributeRelease() { + return attributeRelease; + } + + public void setAttributeRelease(List attributeRelease) { + this.attributeRelease = attributeRelease; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/KeyDescriptorRepresentation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/KeyDescriptorRepresentation.java new file mode 100644 index 000000000..f56f5ad5b --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/KeyDescriptorRepresentation.java @@ -0,0 +1,38 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.frontend; + +import java.io.Serializable; + +public class KeyDescriptorRepresentation implements Serializable { + + private static final long serialVersionUID = -2397547851045884034L; + + private boolean x509CertificateAvailable; + + private boolean authenticationRequestsSigned; + + private String x509Certificate; + + public boolean isX509CertificateAvailable() { + return x509CertificateAvailable; + } + + public void setX509CertificateAvailable(boolean x509CertificateAvailable) { + this.x509CertificateAvailable = x509CertificateAvailable; + } + + public boolean isAuthenticationRequestsSigned() { + return authenticationRequestsSigned; + } + + public void setAuthenticationRequestsSigned(boolean authenticationRequestsSigned) { + this.authenticationRequestsSigned = authenticationRequestsSigned; + } + + public String getX509Certificate() { + return x509Certificate; + } + + public void setX509Certificate(String x509Certificate) { + this.x509Certificate = x509Certificate; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/LogoutEndpointRepresentation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/LogoutEndpointRepresentation.java new file mode 100644 index 000000000..bf0a6cb1f --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/LogoutEndpointRepresentation.java @@ -0,0 +1,28 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.frontend; + +import java.io.Serializable; + +public class LogoutEndpointRepresentation implements Serializable { + + private static final long serialVersionUID = 8630217698477344178L; + + private String url; + + private String bindingType; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getBindingType() { + return bindingType; + } + + public void setBindingType(String bindingType) { + this.bindingType = bindingType; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/MduiRepresentation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/MduiRepresentation.java new file mode 100644 index 000000000..ac907f7b3 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/MduiRepresentation.java @@ -0,0 +1,78 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.frontend; + +import java.io.Serializable; + +public class MduiRepresentation implements Serializable { + + private static final long serialVersionUID = -3691809604832660384L; + + private String displayName; + + private String informationUrl; + + private String privacyStatementUrl; + + private String description; + + private String logoUrl; + + private int logoHeight; + + private int logoWidth; + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String getInformationUrl() { + return informationUrl; + } + + public void setInformationUrl(String informationUrl) { + this.informationUrl = informationUrl; + } + + public String getPrivacyStatementUrl() { + return privacyStatementUrl; + } + + public void setPrivacyStatementUrl(String privacyStatementUrl) { + this.privacyStatementUrl = privacyStatementUrl; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getLogoUrl() { + return logoUrl; + } + + public void setLogoUrl(String logoUrl) { + this.logoUrl = logoUrl; + } + + public int getLogoHeight() { + return logoHeight; + } + + public void setLogoHeight(int logoHeight) { + this.logoHeight = logoHeight; + } + + public int getLogoWidth() { + return logoWidth; + } + + public void setLogoWidth(int logoWidth) { + this.logoWidth = logoWidth; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/OrganizationRepresentation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/OrganizationRepresentation.java new file mode 100644 index 000000000..00d98797c --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/OrganizationRepresentation.java @@ -0,0 +1,38 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.frontend; + +import java.io.Serializable; + +public class OrganizationRepresentation implements Serializable { + + private static final long serialVersionUID = 802722455433573538L; + + private String name; + + private String displayName; + + private String url; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/RelyingPartyOverridesRepresentation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/RelyingPartyOverridesRepresentation.java new file mode 100644 index 000000000..d3487b556 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/RelyingPartyOverridesRepresentation.java @@ -0,0 +1,100 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.frontend; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +public class RelyingPartyOverridesRepresentation implements Serializable { + + private static final long serialVersionUID = 2439457246884861580L; + + private boolean signAssertion; + + private boolean dontSignResponse; + + private boolean turnOffEncryption; + + private boolean useSha; + + private boolean ignoreAuthenticationMethod; + + private boolean omitNotBefore; + + private String responderId; + + private List nameIdFormats = new ArrayList<>(); + + private List authenticationMethods = new ArrayList<>(); + + public boolean isSignAssertion() { + return signAssertion; + } + + public void setSignAssertion(boolean signAssertion) { + this.signAssertion = signAssertion; + } + + public boolean isDontSignResponse() { + return dontSignResponse; + } + + public void setDontSignResponse(boolean dontSignResponse) { + this.dontSignResponse = dontSignResponse; + } + + public boolean isTurnOffEncryption() { + return turnOffEncryption; + } + + public void setTurnOffEncryption(boolean turnOffEncryption) { + this.turnOffEncryption = turnOffEncryption; + } + + public boolean isUseSha() { + return useSha; + } + + public void setUseSha(boolean useSha) { + this.useSha = useSha; + } + + public boolean isIgnoreAuthenticationMethod() { + return ignoreAuthenticationMethod; + } + + public void setIgnoreAuthenticationMethod(boolean ignoreAuthenticationMethod) { + this.ignoreAuthenticationMethod = ignoreAuthenticationMethod; + } + + public boolean isOmitNotBefore() { + return omitNotBefore; + } + + public void setOmitNotBefore(boolean omitNotBefore) { + this.omitNotBefore = omitNotBefore; + } + + public String getResponderId() { + return responderId; + } + + public void setResponderId(String responderId) { + this.responderId = responderId; + } + + public List getNameIdFormats() { + return nameIdFormats; + } + + public void setNameIdFormats(List nameIdFormats) { + this.nameIdFormats = nameIdFormats; + } + + public List getAuthenticationMethods() { + return authenticationMethods; + } + + public void setAuthenticationMethods(List authenticationMethods) { + this.authenticationMethods = authenticationMethods; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/SecurityInfoRepresentation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/SecurityInfoRepresentation.java new file mode 100644 index 000000000..c2e5a2f9f --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/SecurityInfoRepresentation.java @@ -0,0 +1,86 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.frontend; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +public class SecurityInfoRepresentation implements Serializable { + + private static final long serialVersionUID = 9016350010045719454L; + + private boolean x509CertificateAvailable; + + private boolean authenticationRequestsSigned; + + private boolean wantAssertionsSigned; + + private List x509Certificates = new ArrayList<>(); + + public boolean isX509CertificateAvailable() { + return x509CertificateAvailable; + } + + public void setX509CertificateAvailable(boolean x509CertificateAvailable) { + this.x509CertificateAvailable = x509CertificateAvailable; + } + + public boolean isAuthenticationRequestsSigned() { + return authenticationRequestsSigned; + } + + public void setAuthenticationRequestsSigned(boolean authenticationRequestsSigned) { + this.authenticationRequestsSigned = authenticationRequestsSigned; + } + + public boolean isWantAssertionsSigned() { + return wantAssertionsSigned; + } + + public void setWantAssertionsSigned(boolean wantAssertionsSigned) { + this.wantAssertionsSigned = wantAssertionsSigned; + } + + public List getX509Certificates() { + return x509Certificates; + } + + public void setX509Certificates(List x509Certificates) { + this.x509Certificates = x509Certificates; + } + + public static class X509CertificateRepresentation implements Serializable { + + private static final long serialVersionUID = -4893206348572998788L; + + private String name; + + //TODO refactor into Enum? + private String type; + + private String value; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/ServiceProviderSsoDescriptorRepresentation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/ServiceProviderSsoDescriptorRepresentation.java new file mode 100644 index 000000000..d20ec97ae --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/ServiceProviderSsoDescriptorRepresentation.java @@ -0,0 +1,31 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.frontend; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +public class ServiceProviderSsoDescriptorRepresentation implements Serializable { + + + private static final long serialVersionUID = 8366502466924209389L; + + private String protocolSupportEnum; + + private List nameIdFormats = new ArrayList<>(); + + public String getProtocolSupportEnum() { + return protocolSupportEnum; + } + + public void setProtocolSupportEnum(String protocolSupportEnum) { + this.protocolSupportEnum = protocolSupportEnum; + } + + public List getNameIdFormats() { + return nameIdFormats; + } + + public void setNameIdFormats(List nameIdFormats) { + this.nameIdFormats = nameIdFormats; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/OpenSamlObjects.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/OpenSamlObjects.java new file mode 100644 index 000000000..e431c1ea9 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/OpenSamlObjects.java @@ -0,0 +1,127 @@ +package edu.internet2.tier.shibboleth.admin.ui.opensaml; + +import com.google.common.io.ByteSource; +import edu.internet2.tier.shibboleth.admin.ui.opensaml.config.InitializationService; +import net.shibboleth.utilities.java.support.component.ComponentInitializationException; +import net.shibboleth.utilities.java.support.xml.BasicParserPool; +import net.shibboleth.utilities.java.support.xml.ParserPool; +import org.opensaml.core.config.ConfigurationService; +import org.opensaml.core.config.InitializationException; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.XMLObjectBuilderFactory; +import org.opensaml.core.xml.config.XMLObjectProviderRegistry; +import org.opensaml.core.xml.io.Marshaller; +import org.opensaml.core.xml.io.MarshallerFactory; +import org.opensaml.core.xml.io.MarshallingException; +import org.opensaml.core.xml.io.Unmarshaller; +import org.opensaml.core.xml.io.UnmarshallerFactory; +import org.opensaml.saml.saml2.metadata.EntityDescriptor; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.annotation.PostConstruct; +import javax.xml.namespace.QName; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; + +public class OpenSamlObjects { + + private XMLObjectBuilderFactory builderFactory; + + private MarshallerFactory marshallerFactory; + + private UnmarshallerFactory unmarshallerFactory; + + private BasicParserPool parserPool = new BasicParserPool(); + + public XMLObjectBuilderFactory getBuilderFactory() { + return this.builderFactory; + } + + public MarshallerFactory getMarshallerFactory() { + return this.marshallerFactory; + } + + public UnmarshallerFactory getUnmarshallerFactory() { + return this.unmarshallerFactory; + } + + public ParserPool getParserPool() { + return parserPool; + } + + /** + * Initialize opensaml. + */ + @PostConstruct + public void init() throws ComponentInitializationException { + try { + InitializationService.initialize(); + } catch (final InitializationException e) { + throw new IllegalArgumentException("Exception initializing OpenSAML", e); + } + + XMLObjectProviderRegistry registry; + synchronized (ConfigurationService.class) { + registry = ConfigurationService.get(XMLObjectProviderRegistry.class); + if (registry == null) { + registry = new XMLObjectProviderRegistry(); + ConfigurationService.register(XMLObjectProviderRegistry.class, registry); + } + } + this.parserPool.initialize(); + registry.setParserPool(this.parserPool); + this.builderFactory = registry.getBuilderFactory(); + this.marshallerFactory = registry.getMarshallerFactory(); + this.unmarshallerFactory = registry.getUnmarshallerFactory(); + } + + public String marshalToXmlString(XMLObject ed) throws MarshallingException { + Marshaller marshaller = this.marshallerFactory.getMarshaller(ed); + String entityDescriptorXmlString = null; + if (marshaller != null) { + try (StringWriter writer = new StringWriter()) { + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + transformer.transform(new DOMSource(marshaller.marshall(ed)), new StreamResult(writer)); + entityDescriptorXmlString = writer.toString(); + } catch (TransformerException | IOException e) { + e.printStackTrace(); + } + } + + if (entityDescriptorXmlString == null) { + //Figure out the best way to deal with this case + throw new RuntimeException("Unable to marshal EntityDescriptor"); + } + return entityDescriptorXmlString; + } + + public EntityDescriptor unmarshalFromXml(byte[] entityDescriptorXml) throws Exception { + try (InputStream edIs = ByteSource.wrap(entityDescriptorXml).openBufferedStream()) { + Document doc = this.parserPool.parse(edIs); + Element e = doc.getDocumentElement(); + Unmarshaller unmarshaller = this.unmarshallerFactory.getUnmarshaller(e); + if (unmarshaller != null) { + //Cast here could be dangerous, but we want to make sure that only EntityDescriptor representation is being POSTed (somehow) + return EntityDescriptor.class.cast(unmarshaller.unmarshall(e)); + } + return null; + } + } + + // TODO: yeah, I'm not happy with this... + public T buildDefaultInstanceOfType(Class type) { + try { + QName defaultElementName = (QName) type.getField("DEFAULT_ELEMENT_NAME").get(null); + return (T) this.getBuilderFactory().getBuilder(defaultElementName).buildObject(defaultElementName); + } catch (IllegalAccessException | NoSuchFieldException e) { + throw new RuntimeException("there was a problem building an instance", e); + } + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/config/InitializationService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/config/InitializationService.java new file mode 100644 index 000000000..38bdb6784 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/config/InitializationService.java @@ -0,0 +1,24 @@ +package edu.internet2.tier.shibboleth.admin.ui.opensaml.config; + +import org.opensaml.core.config.InitializationException; +import org.opensaml.core.config.Initializer; +import org.opensaml.core.xml.config.XMLObjectProviderInitializer; + +import java.util.ServiceLoader; + +//TODO: this is a crutch until a better way of skipping something is available +public class InitializationService { + + protected InitializationService() { + } + + public static synchronized void initialize() throws InitializationException { + final ServiceLoader serviceLoader = ServiceLoader.load(Initializer.class); + for (Initializer initializer : serviceLoader) { + if (initializer.getClass().equals(org.opensaml.saml.config.impl.XMLObjectProviderInitializer.class) || initializer.getClass().equals(XMLObjectProviderInitializer.class) || initializer.getClass().equals(org.opensaml.xmlsec.config.impl.XMLObjectProviderInitializer.class)) { + continue; + } + initializer.init(); + } + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/config/JPAXMLObjectProviderInitializer.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/config/JPAXMLObjectProviderInitializer.java new file mode 100644 index 000000000..f21fdc93d --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/config/JPAXMLObjectProviderInitializer.java @@ -0,0 +1,19 @@ +package edu.internet2.tier.shibboleth.admin.ui.opensaml.config; + +import org.opensaml.core.xml.config.AbstractXMLObjectProviderInitializer; + +public class JPAXMLObjectProviderInitializer extends AbstractXMLObjectProviderInitializer { + + @Override + protected String[] getConfigResources() { + return new String[]{ + "/jpa-default-config.xml", + "/jpa-saml2-metadata-config.xml", + "/jpa-saml2-metadata-attr-config.xml", + "/jpa-saml2-assertion-config.xml", + "/jpa-schema-config.xml", + "/jpa-saml2-metadata-ui-config.xml", + "/jpa-signature-config.xml" + }; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/xml/AbstractSAMLObjectBuilder.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/xml/AbstractSAMLObjectBuilder.java new file mode 100644 index 000000000..b97f1db03 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/xml/AbstractSAMLObjectBuilder.java @@ -0,0 +1,40 @@ +/* + * Licensed to the University Corporation for Advanced Internet Development, + * Inc. (UCAID) under one or more contributor license agreements. See the + * NOTICE file distributed with this work for additional information regarding + * copyright ownership. The UCAID licenses this file to You under the Apache + * License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package edu.internet2.tier.shibboleth.admin.ui.opensaml.xml; + +import javax.annotation.Nonnull; + +import org.opensaml.saml.common.SAMLObject; +import org.opensaml.saml.common.SAMLObjectBuilder; + +/** + * Base builder for {@link org.opensaml.saml.common.SAMLObject}s. + * + * @param the SAML object type built + */ +public abstract class AbstractSAMLObjectBuilder extends + AbstractXMLObjectBuilder implements SAMLObjectBuilder { + + /** + * Builds a SAMLObject using the default name and namespace information provided SAML specifications. + * + * @return built SAMLObject + */ + @Nonnull + public abstract SAMLObjectType buildObject(); +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/xml/AbstractXMLObjectBuilder.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/xml/AbstractXMLObjectBuilder.java new file mode 100644 index 000000000..1b49c3b56 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/xml/AbstractXMLObjectBuilder.java @@ -0,0 +1,95 @@ +/* + * Licensed to the University Corporation for Advanced Internet Development, + * Inc. (UCAID) under one or more contributor license agreements. See the + * NOTICE file distributed with this work for additional information regarding + * copyright ownership. The UCAID licenses this file to You under the Apache + * License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package edu.internet2.tier.shibboleth.admin.ui.opensaml.xml; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.xml.namespace.QName; + +import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractXMLObject; +import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty; +import net.shibboleth.utilities.java.support.xml.DOMTypeSupport; + +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.XMLObjectBuilder; +import org.w3c.dom.Element; + +/** + * Base implementation for XMLObject builders. + *

+ * Note: This class only works with {@link org.opensaml.core.xml.AbstractXMLObject}s + * + * @param the XMLObject type that this builder produces + */ +public abstract class AbstractXMLObjectBuilder implements + XMLObjectBuilder { + + /** + * {@inheritDoc} + */ + @Nonnull + public XMLObjectType buildObject(@Nonnull final QName objectName) { + return buildObject(objectName.getNamespaceURI(), objectName.getLocalPart(), objectName.getPrefix()); + } + + /** + * {@inheritDoc} + */ + @Nonnull + public XMLObjectType buildObject(@Nonnull final QName objectName, @Nullable final QName schemaType) { + return buildObject(objectName.getNamespaceURI(), objectName.getLocalPart(), objectName.getPrefix(), schemaType); + } + + /** + * {@inheritDoc} + */ + @Nonnull + public abstract XMLObjectType buildObject(@Nullable final String namespaceURI, + @Nonnull @NotEmpty final String localName, @Nullable final String namespacePrefix); + + /** + * {@inheritDoc} + */ + @Nonnull + public XMLObjectType buildObject(@Nullable final String namespaceURI, @Nonnull final String localName, + @Nullable final String namespacePrefix, @Nullable final QName schemaType) { + final XMLObjectType xmlObject; + + xmlObject = buildObject(namespaceURI, localName, namespacePrefix); + ((AbstractXMLObject) xmlObject).setSchemaType(schemaType); + + return xmlObject; + } + + /** + * {@inheritDoc} + */ + @Nonnull + public XMLObjectType buildObject(@Nonnull final Element element) { + final XMLObjectType xmlObject; + + final String localName = element.getLocalName(); + final String nsURI = element.getNamespaceURI(); + final String nsPrefix = element.getPrefix(); + final QName schemaType = DOMTypeSupport.getXSIType(element); + + xmlObject = buildObject(nsURI, localName, nsPrefix, schemaType); + + return xmlObject; + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java new file mode 100644 index 000000000..be729d489 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java @@ -0,0 +1,24 @@ +package edu.internet2.tier.shibboleth.admin.ui.repository; + +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; + +import java.util.stream.Stream; + + +/** + * Repository to manage {@link EntityDescriptor} instances. + */ +public interface EntityDescriptorRepository extends CrudRepository { + + EntityDescriptor findByEntityID(String entityId); + + EntityDescriptor findByResourceId(String resourceId); + + Stream findAllByServiceEnabled(boolean serviceEnabled); + + @Query("select e from EntityDescriptor e") + Stream findAllByCustomQueryAndStream(); + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/RoleDescriptorResolverRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/RoleDescriptorResolverRepository.java new file mode 100644 index 000000000..c2cbef436 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/RoleDescriptorResolverRepository.java @@ -0,0 +1,12 @@ +package edu.internet2.tier.shibboleth.admin.ui.repository; + +import edu.internet2.tier.shibboleth.admin.ui.domain.RoleDescriptorResolver; +import org.springframework.data.repository.CrudRepository; + + +/** + * Repository to manage {@link RoleDescriptorResolver} instances. + */ +public interface RoleDescriptorResolverRepository extends CrudRepository { + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasks.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasks.java new file mode 100644 index 000000000..de1c2472b --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasks.java @@ -0,0 +1,129 @@ +package edu.internet2.tier.shibboleth.admin.ui.scheduled; + +import com.google.common.collect.Sets; +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor; +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; +import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; +import org.bouncycastle.util.encoders.Hex; +import org.opensaml.core.xml.io.MarshallingException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Set; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toSet; + + +/** + * This class is wrapped with Spring's scheduling facility to produce background scheduled periodic tasks pertaining to + * generating, cleaning, etc. of entity descriptor files conforming to Shibboleth's LocalDynamicMetadataProvider naming + * specification. + * + * @since 1.0 + */ +public class EntityDescriptorFilesScheduledTasks { + + private static final Logger LOGGER = LoggerFactory.getLogger(EntityDescriptorFilesScheduledTasks.class); + + private String metadataDirName; + + private EntityDescriptorRepository entityDescriptorRepository; + + private OpenSamlObjects openSamlObjects; + + private static final String SHA1HEX_FILENAME_TEMPLATE = "%s.xml"; + + private static final String TARGET_FILE_TEMPLATE = "%s/%s"; + + public EntityDescriptorFilesScheduledTasks(String metadataDirName, + EntityDescriptorRepository entityDescriptorRepository, + OpenSamlObjects openSamlObjects) { + this.metadataDirName = metadataDirName; + this.entityDescriptorRepository = entityDescriptorRepository; + this.openSamlObjects = openSamlObjects; + } + + @Scheduled(fixedRateString = "${shibui.taskRunRate:30000}") + @Transactional(readOnly = true) + public void generateEntityDescriptorFiles() throws MarshallingException { + this.entityDescriptorRepository.findAllByServiceEnabled(true) + .forEach(ed -> { + Path targetFilePath = targetFilePathFor(toSha1HexString(ed.getEntityID())); + if (Files.exists(targetFilePath)) { + LOGGER.info("Overwriting entity descriptor file [{}] for entity id [{}]", targetFilePath, ed.getEntityID()); + } else { + LOGGER.info("Generating entity descriptor file [{}] for entity id [{}]", targetFilePath, ed.getEntityID()); + } + + try { + String xmlContent = this.openSamlObjects.marshalToXmlString(ed); + Files.write(targetFilePath, xmlContent.getBytes()); + } catch (MarshallingException | IOException e) { + //TODO: any other better way to handle it? + LOGGER.error("Error marshalling entity descriptor into a file {} - {}", ed.getEntityID(), e.getMessage()); + } + }); + } + + @Scheduled(fixedDelayString = "${shibui.taskDelayRate:30000}") + @Transactional(readOnly = true) + public void removeDanglingEntityDescriptorFiles() { + Path targetDirPath = Paths.get(this.metadataDirName); + try (Stream directoryStream = Files.list(targetDirPath)) { + Set allFilesWithoutExt = directoryStream + .filter(it -> it.toString().endsWith(".xml")) + .map(Path::getFileName) + .map(Path::toString) + .map(it -> it.substring(0, it.indexOf("."))) + .collect(toSet()); + + Set enabledEidsSha1Hashes = this.entityDescriptorRepository.findAllByServiceEnabled(true) + .map(EntityDescriptor::getEntityID) + .map(EntityDescriptorFilesScheduledTasks::toSha1HexString) + .collect(toSet()); + + Set fileNamesToDelete = Sets.difference(allFilesWithoutExt, enabledEidsSha1Hashes); + + fileNamesToDelete.forEach(fName -> { + Path targetFilePath = targetFilePathFor(fName); + try { + LOGGER.info("Deleting dangling metadata file [{}]", targetFilePath); + Files.delete(targetFilePath); + } catch (IOException e) { + //TODO: any other better way to handle it? + LOGGER.error("Unable to delete file [{}} - {}", targetFilePath, e.getMessage()); + } + }); + } catch (IOException e) { + //TODO: any other better way to handle it? + LOGGER.error("Error manipulating files in [{}] - {}", targetDirPath, e.getMessage()); + } + } + + private static String toSha1HexString(final String sourceString) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + byte[] hash = digest.digest(sourceString.getBytes(StandardCharsets.UTF_8)); + return Hex.toHexString(hash); + } catch (NoSuchAlgorithmException e) { + //Should never get here? + throw new IllegalStateException(e.getMessage()); + } + } + + private Path targetFilePathFor(String sha1HexBaseFilename) { + String filename = String.format(SHA1HEX_FILENAME_TEMPLATE, sha1HexBaseFilename); + String filenamePath = String.format(TARGET_FILE_TEMPLATE, this.metadataDirName, filename); + return Paths.get(filenamePath); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java new file mode 100644 index 000000000..65f5971e0 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java @@ -0,0 +1,36 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; +import org.opensaml.saml.saml2.metadata.EntityDescriptor; + +/** + * Main backend facade API that defines operations pertaining to manipulating {@link EntityDescriptor} state. + * + * @since 1.0 + */ +public interface EntityDescriptorService { + + /** + * Map from front-end data representation of entity descriptor to opensaml implementation of entity descriptor model + * + * @param representation of entity descriptor coming from front end layer + * @return EntityDescriptor + */ + EntityDescriptor createDescriptorFromRepresentation(final EntityDescriptorRepresentation representation); + + /** + * Map from opensaml implementation of entity descriptor model to front-end data representation of entity descriptor + * + * @param entityDescriptor opensaml model + * @return EntityDescriptorRepresentation + */ + EntityDescriptorRepresentation createRepresentationFromDescriptor(final EntityDescriptor entityDescriptor); + + /** + * Update an instance of entity descriptor with information from the front-end representation + * + * @param entityDescriptor opensaml model instance to update + * @param representation front end representation to use to update + */ + void updateDescriptorFromRepresentation(final EntityDescriptor entityDescriptor, final EntityDescriptorRepresentation representation); +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java new file mode 100644 index 000000000..56255c995 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java @@ -0,0 +1,606 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + + +import com.google.common.base.Strings; +import edu.internet2.tier.shibboleth.admin.ui.domain.AssertionConsumerService; +import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute; +import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBuilder; +import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeValue; +import edu.internet2.tier.shibboleth.admin.ui.domain.ContactPerson; +import edu.internet2.tier.shibboleth.admin.ui.domain.ContactPersonBuilder; +import edu.internet2.tier.shibboleth.admin.ui.domain.Description; +import edu.internet2.tier.shibboleth.admin.ui.domain.DisplayName; +import edu.internet2.tier.shibboleth.admin.ui.domain.EmailAddress; +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributes; +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesBuilder; +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor; +import edu.internet2.tier.shibboleth.admin.ui.domain.Extensions; +import edu.internet2.tier.shibboleth.admin.ui.domain.GivenName; +import edu.internet2.tier.shibboleth.admin.ui.domain.InformationURL; +import edu.internet2.tier.shibboleth.admin.ui.domain.KeyDescriptor; +import edu.internet2.tier.shibboleth.admin.ui.domain.Logo; +import edu.internet2.tier.shibboleth.admin.ui.domain.NameIDFormat; +import edu.internet2.tier.shibboleth.admin.ui.domain.Organization; +import edu.internet2.tier.shibboleth.admin.ui.domain.OrganizationDisplayName; +import edu.internet2.tier.shibboleth.admin.ui.domain.OrganizationName; +import edu.internet2.tier.shibboleth.admin.ui.domain.OrganizationURL; +import edu.internet2.tier.shibboleth.admin.ui.domain.PrivacyStatementURL; +import edu.internet2.tier.shibboleth.admin.ui.domain.SPSSODescriptor; +import edu.internet2.tier.shibboleth.admin.ui.domain.SingleLogoutService; +import edu.internet2.tier.shibboleth.admin.ui.domain.UIInfo; +import edu.internet2.tier.shibboleth.admin.ui.domain.XSAny; +import edu.internet2.tier.shibboleth.admin.ui.domain.XSBoolean; +import edu.internet2.tier.shibboleth.admin.ui.domain.XSString; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.AssertionConsumerServiceRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ContactRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.LogoutEndpointRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.MduiRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.OrganizationRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.RelyingPartyOverridesRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.SecurityInfoRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ServiceProviderSsoDescriptorRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; +import edu.internet2.tier.shibboleth.admin.util.MDDCConstants; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.schema.XSBooleanValue; +import org.opensaml.xmlsec.signature.KeyInfo; +import org.opensaml.xmlsec.signature.X509Certificate; +import org.opensaml.xmlsec.signature.X509Data; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Default implementation of {@link EntityDescriptorService} + * + * @since 1.0 + */ +public class JPAEntityDescriptorServiceImpl implements EntityDescriptorService { + + private static final Logger LOGGER = LoggerFactory.getLogger(JPAEntityDescriptorServiceImpl.class); + + @Autowired + private OpenSamlObjects openSamlObjects; + + public JPAEntityDescriptorServiceImpl() { + } + + public JPAEntityDescriptorServiceImpl(OpenSamlObjects openSamlObjects) { + this.openSamlObjects = openSamlObjects; + } + + @Override + public EntityDescriptor createDescriptorFromRepresentation(final EntityDescriptorRepresentation representation) { + EntityDescriptor ed = openSamlObjects.buildDefaultInstanceOfType(EntityDescriptor.class); + ed.setEntityID(representation.getEntityId()); + + // setup SPSSODescriptor + if (representation.getServiceProviderSsoDescriptor() != null) { + SPSSODescriptor spssoDescriptor = getSPSSODescriptorFromEntityDescriptor(ed); + + if (!Strings.isNullOrEmpty(representation.getServiceProviderSsoDescriptor().getProtocolSupportEnum())) { + spssoDescriptor.setSupportedProtocols( + Arrays.stream(representation.getServiceProviderSsoDescriptor().getProtocolSupportEnum().split(",")).map(p -> MDDCConstants.PROTOCOL_BINDINGS.get(p.trim())).collect(Collectors.toList()) + ); + } + + + if (representation.getServiceProviderSsoDescriptor() != null && representation.getServiceProviderSsoDescriptor().getNameIdFormats() != null && representation.getServiceProviderSsoDescriptor().getNameIdFormats().size() > 0) { + for (String nameidFormat : representation.getServiceProviderSsoDescriptor().getNameIdFormats()) { + NameIDFormat nameIDFormat = openSamlObjects.buildDefaultInstanceOfType(NameIDFormat.class); + + nameIDFormat.setFormat(nameidFormat); + + spssoDescriptor.getNameIDFormats().add(nameIDFormat); + } + } + } + + ed.setServiceProviderName(representation.getServiceProviderName()); + ed.setServiceEnabled(representation.isServiceEnabled()); + + // set up organization + if (representation.getOrganization() != null && representation.getOrganization().getName() != null && representation.getOrganization().getDisplayName() != null && representation.getOrganization().getUrl() != null) { + OrganizationRepresentation organizationRepresentation = representation.getOrganization(); + Organization organization = openSamlObjects.buildDefaultInstanceOfType(Organization.class); + + OrganizationName organizationName = openSamlObjects.buildDefaultInstanceOfType(OrganizationName.class); + organizationName.setXMLLang("en"); + organizationName.setValue(organizationRepresentation.getName()); + organization.getOrganizationNames().add(organizationName); + + OrganizationDisplayName organizationDisplayName = openSamlObjects.buildDefaultInstanceOfType(OrganizationDisplayName.class); + organizationDisplayName.setXMLLang("en"); + organizationDisplayName.setValue(organizationRepresentation.getDisplayName()); + organization.getDisplayNames().add(organizationDisplayName); + + OrganizationURL organizationURL = openSamlObjects.buildDefaultInstanceOfType(OrganizationURL.class); + organizationURL.setXMLLang("en"); + organizationURL.setValue(organizationRepresentation.getUrl()); + organization.getURLs().add(organizationURL); + + ed.setOrganization(organization); + } + + // set up contacts + if (representation.getContacts() != null && representation.getContacts().size() > 0) { + for (ContactRepresentation contactRepresentation : representation.getContacts()) { + ContactPerson contactPerson = ((ContactPersonBuilder) openSamlObjects.getBuilderFactory().getBuilder(ContactPerson.DEFAULT_ELEMENT_NAME)).buildObject(); + + contactPerson.setType(contactRepresentation.getType()); + + GivenName givenName = openSamlObjects.buildDefaultInstanceOfType(GivenName.class); + givenName.setName(contactRepresentation.getName()); + contactPerson.setGivenName(givenName); + + EmailAddress emailAddress = openSamlObjects.buildDefaultInstanceOfType(EmailAddress.class); + emailAddress.setAddress(contactRepresentation.getEmailAddress()); + contactPerson.addEmailAddress(emailAddress); + + ed.addContactPerson(contactPerson); + } + } + + // set up mdui + if (representation.getMdui() != null) { + MduiRepresentation mduiRepresentation = representation.getMdui(); + + if (!Strings.isNullOrEmpty(mduiRepresentation.getDisplayName())) { + DisplayName displayName = openSamlObjects.buildDefaultInstanceOfType(DisplayName.class); + getUIInfo(ed).addDisplayName(displayName); + displayName.setValue(mduiRepresentation.getDisplayName()); + displayName.setXMLLang("en"); + } + + if (!Strings.isNullOrEmpty(mduiRepresentation.getInformationUrl())) { + InformationURL informationURL = openSamlObjects.buildDefaultInstanceOfType(InformationURL.class); + getUIInfo(ed).addInformationURL(informationURL); + informationURL.setValue(mduiRepresentation.getInformationUrl()); + informationURL.setXMLLang("en"); + } + + if (!Strings.isNullOrEmpty(mduiRepresentation.getPrivacyStatementUrl())) { + PrivacyStatementURL privacyStatementURL = openSamlObjects.buildDefaultInstanceOfType(PrivacyStatementURL.class); + getUIInfo(ed).addPrivacyStatementURL(privacyStatementURL); + privacyStatementURL.setValue(mduiRepresentation.getPrivacyStatementUrl()); + privacyStatementURL.setXMLLang("en"); + } + + if (!Strings.isNullOrEmpty(mduiRepresentation.getDescription())) { + Description description = openSamlObjects.buildDefaultInstanceOfType(Description.class); + getUIInfo(ed).addDescription(description); + description.setValue(mduiRepresentation.getDescription()); + description.setXMLLang("en"); + } + + if (!Strings.isNullOrEmpty(mduiRepresentation.getLogoUrl())) { + Logo logo = openSamlObjects.buildDefaultInstanceOfType(Logo.class); + getUIInfo(ed).addLog(logo); + logo.setURL(mduiRepresentation.getLogoUrl()); + logo.setHeight(mduiRepresentation.getLogoHeight()); + logo.setWidth(mduiRepresentation.getLogoWidth()); + logo.setXMLLang("en"); + } + } + + // setup security + if (representation.getSecurityInfo() != null) { + SecurityInfoRepresentation securityInfoRepresentation = representation.getSecurityInfo(); + if (securityInfoRepresentation.isAuthenticationRequestsSigned()) { + getSPSSODescriptorFromEntityDescriptor(ed).setAuthnRequestsSigned(true); + } + if (securityInfoRepresentation.isWantAssertionsSigned()) { + getSPSSODescriptorFromEntityDescriptor(ed).setWantAssertionsSigned(true); + } + if (securityInfoRepresentation.isX509CertificateAvailable()) { + for (SecurityInfoRepresentation.X509CertificateRepresentation x509CertificateRepresentation : securityInfoRepresentation.getX509Certificates()) { + KeyDescriptor keyDescriptor = createKeyDescriptor(x509CertificateRepresentation.getName(), x509CertificateRepresentation.getType(), x509CertificateRepresentation.getValue()); + getSPSSODescriptorFromEntityDescriptor(ed).addKeyDescriptor(keyDescriptor); + } + } + } + + // setup ACSs + if (representation.getAssertionConsumerServices() != null && representation.getAssertionConsumerServices().size() > 0) { + for (AssertionConsumerServiceRepresentation acsRepresentation : representation.getAssertionConsumerServices()) { + AssertionConsumerService assertionConsumerService = openSamlObjects.buildDefaultInstanceOfType(AssertionConsumerService.class); + getSPSSODescriptorFromEntityDescriptor(ed).getAssertionConsumerServices().add(assertionConsumerService); + if (acsRepresentation.isMakeDefault()) { + assertionConsumerService.setIsDefault(true); + } + assertionConsumerService.setBinding(acsRepresentation.getBinding()); + assertionConsumerService.setLocation(acsRepresentation.getLocationUrl()); + } + } + + // setup logout + if (representation.getLogoutEndpoints() != null && !representation.getLogoutEndpoints().isEmpty()) { + for (LogoutEndpointRepresentation logoutEndpointRepresentation : representation.getLogoutEndpoints()) { + SingleLogoutService singleLogoutService = openSamlObjects.buildDefaultInstanceOfType(SingleLogoutService.class); + singleLogoutService.setBinding(logoutEndpointRepresentation.getBindingType()); + singleLogoutService.setLocation(logoutEndpointRepresentation.getUrl()); + + getSPSSODescriptorFromEntityDescriptor(ed).getSingleLogoutServices().add(singleLogoutService); + } + } + + if (representation.getRelyingPartyOverrides() != null || (representation.getAttributeRelease() != null && representation.getAttributeRelease().size() > 0)) { + if (representation.getRelyingPartyOverrides() != null) { + // Let's do the overrides + RelyingPartyOverridesRepresentation overridesRepresentation = representation.getRelyingPartyOverrides(); + if (overridesRepresentation.isSignAssertion()) { + getEntityAttributes(ed).getAttributes().add(createAttributeWithBooleanValue(MDDCConstants.SIGN_ASSERTIONS, MDDCConstants.SIGN_ASSERTIONS_FN, true)); + } + if (overridesRepresentation.isDontSignResponse()) { + getEntityAttributes(ed).getAttributes().add(createAttributeWithBooleanValue(MDDCConstants.SIGN_RESPONSES, MDDCConstants.SIGN_RESPONSES_FN, false)); + } + if (overridesRepresentation.isTurnOffEncryption()) { + getEntityAttributes(ed).getAttributes().add(createAttributeWithBooleanValue(MDDCConstants.ENCRYPT_ASSERTIONS, MDDCConstants.ENCRYPT_ASSERTIONS_FN, false)); + } + if (overridesRepresentation.isUseSha()) { + getEntityAttributes(ed).getAttributes().add(createAttributeWithArbitraryValues(MDDCConstants.SECURITY_CONFIGURATION, MDDCConstants.SECURITY_CONFIGURATION_FN, "shibboleth.SecurityConfiguration.SHA1")); + } + if (overridesRepresentation.isIgnoreAuthenticationMethod()) { + // this is actually going to be wrong, but it will work for the time being. this should be a bitmask value that we calculate + // TODO: fix + getEntityAttributes(ed).getAttributes().add(createAttributeWithArbitraryValues(MDDCConstants.DISALLOWED_FEATURES, MDDCConstants.DISALLOWED_FEATURES_FN, "0x1")); + } + if (overridesRepresentation.isOmitNotBefore()) { + getEntityAttributes(ed).getAttributes().add(createAttributeWithBooleanValue(MDDCConstants.INCLUDE_CONDITIONS_NOT_BEFORE, MDDCConstants.INCLUDE_CONDITIONS_NOT_BEFORE_FN, false)); + } + if (overridesRepresentation.getResponderId() != null && !"".equals(overridesRepresentation.getResponderId())) { + getEntityAttributes(ed).getAttributes().add(createAttributeWithArbitraryValues(MDDCConstants.RESPONDER_ID, MDDCConstants.RESPONDER_ID_FN, overridesRepresentation.getResponderId())); + } + if (overridesRepresentation.getNameIdFormats() != null && overridesRepresentation.getNameIdFormats().size() > 0) { + getEntityAttributes(ed).getAttributes().add(createAttributeWithArbitraryValues(MDDCConstants.NAME_ID_FORMAT_PRECEDENCE, MDDCConstants.NAME_ID_FORMAT_PRECEDENCE_FN, overridesRepresentation.getNameIdFormats())); + } + if (overridesRepresentation.getAuthenticationMethods() != null && overridesRepresentation.getAuthenticationMethods().size() > 0) { + getEntityAttributes(ed).getAttributes().add(createAttributeWithArbitraryValues(MDDCConstants.DEFAULT_AUTHENTICATION_METHODS, MDDCConstants.DEFAULT_AUTHENTICATION_METHODS_FN, overridesRepresentation.getAuthenticationMethods())); + } + } + + // let's map the attribute release + if (representation.getAttributeRelease() != null && representation.getAttributeRelease().size() > 0) { + Attribute attribute = ((AttributeBuilder) openSamlObjects.getBuilderFactory().getBuilder(Attribute.DEFAULT_ELEMENT_NAME)).buildObject(); + getEntityAttributes(ed).addAttribute(attribute); + + attribute.setName(MDDCConstants.RELEASE_ATTRIBUTES); + + for (String attributeRelease : representation.getAttributeRelease()) { + XSString xsString = (XSString) openSamlObjects.getBuilderFactory().getBuilder(XSString.TYPE_NAME).buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME); + xsString.setValue(attributeRelease); + attribute.getAttributeValues().add(xsString); + } + } + } + return ed; + } + + private SPSSODescriptor getSPSSODescriptorFromEntityDescriptor(EntityDescriptor entityDescriptor) { + if (entityDescriptor.getSPSSODescriptor("") == null) { + SPSSODescriptor spssoDescriptor = openSamlObjects.buildDefaultInstanceOfType(SPSSODescriptor.class); + entityDescriptor.getRoleDescriptors().add(spssoDescriptor); + } + return entityDescriptor.getSPSSODescriptor(""); + } + + private Attribute createAttributeWithBooleanValue(String name, String friendlyName, Boolean value) { + Attribute attribute = ((AttributeBuilder) openSamlObjects.getBuilderFactory().getBuilder(Attribute.DEFAULT_ELEMENT_NAME)).buildObject(); + attribute.setName(name); + attribute.setFriendlyName(friendlyName); + attribute.setNameFormat("urn:oasis:names:tc:SAML:2.0:attrname-format:uri"); + + XSBoolean xsBoolean = (XSBoolean) openSamlObjects.getBuilderFactory().getBuilder(XSBoolean.TYPE_NAME).buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSBoolean.TYPE_NAME); + xsBoolean.setValue(XSBooleanValue.valueOf(value.toString())); + + attribute.getAttributeValues().add(xsBoolean); + return attribute; + } + + private Attribute createAttributeWithArbitraryValues(String name, String friendlyName, String... values) { + Attribute attribute = ((AttributeBuilder) openSamlObjects.getBuilderFactory().getBuilder(Attribute.DEFAULT_ELEMENT_NAME)).buildObject(); + attribute.setName(name); + attribute.setFriendlyName(friendlyName); + attribute.setNameFormat("urn:oasis:names:tc:SAML:2.0:attrname-format:uri"); + + for (String value : values) { + XSAny xsAny = (XSAny) openSamlObjects.getBuilderFactory().getBuilder(XSAny.TYPE_NAME).buildObject(AttributeValue.DEFAULT_ELEMENT_NAME); + xsAny.setTextContent(value); + attribute.getAttributeValues().add(xsAny); + } + + return attribute; + } + + private Attribute createAttributeWithArbitraryValues(String name, String friendlyName, List values) { + return createAttributeWithArbitraryValues(name, friendlyName, values.toArray(new String[]{})); + } + + private KeyDescriptor createKeyDescriptor(String name, String type, String value) { + KeyDescriptor keyDescriptor = openSamlObjects.buildDefaultInstanceOfType(KeyDescriptor.class); + + if (!Strings.isNullOrEmpty(name)) { + keyDescriptor.setName(name); + } + + if (!"both".equals(type)) { + keyDescriptor.setUsageType(type); + } + + KeyInfo keyInfo = openSamlObjects.buildDefaultInstanceOfType(KeyInfo.class); + keyDescriptor.setKeyInfo(keyInfo); + + X509Data x509Data = openSamlObjects.buildDefaultInstanceOfType(X509Data.class); + keyInfo.getXMLObjects().add(x509Data); + + X509Certificate x509Certificate = openSamlObjects.buildDefaultInstanceOfType(X509Certificate.class); + x509Data.getXMLObjects().add(x509Certificate); + x509Certificate.setValue(value); + + return keyDescriptor; + } + + private EntityAttributes getEntityAttributes(EntityDescriptor ed) { + Extensions extensions = ed.getExtensions(); + if (extensions == null) { + extensions = openSamlObjects.buildDefaultInstanceOfType(Extensions.class); + ed.setExtensions(extensions); + } + + EntityAttributes entityAttributes; + if (extensions.getUnknownXMLObjects(EntityAttributes.DEFAULT_ELEMENT_NAME).size() > 0) { + entityAttributes = (EntityAttributes) extensions.getUnknownXMLObjects(EntityAttributes.DEFAULT_ELEMENT_NAME).get(0); + } else { + entityAttributes = ((EntityAttributesBuilder) openSamlObjects.getBuilderFactory().getBuilder(EntityAttributes.DEFAULT_ELEMENT_NAME)).buildObject(); + extensions.getUnknownXMLObjects().add(entityAttributes); + } + return entityAttributes; + } + + private UIInfo getUIInfo(EntityDescriptor ed) { + Extensions extensions = getSPSSODescriptorFromEntityDescriptor(ed).getExtensions(); + if (extensions == null) { + extensions = openSamlObjects.buildDefaultInstanceOfType(Extensions.class); + ed.getSPSSODescriptor("").setExtensions(extensions); + } + + UIInfo uiInfo; + if (extensions.getUnknownXMLObjects(UIInfo.DEFAULT_ELEMENT_NAME).size() > 0) { + uiInfo = (UIInfo) extensions.getUnknownXMLObjects(UIInfo.DEFAULT_ELEMENT_NAME).get(0); + } else { + uiInfo = openSamlObjects.buildDefaultInstanceOfType(UIInfo.class); + extensions.getUnknownXMLObjects().add(uiInfo); + } + return uiInfo; + } + + //TODO: implement + @Override + public EntityDescriptorRepresentation createRepresentationFromDescriptor(org.opensaml.saml.saml2.metadata.EntityDescriptor entityDescriptor) { + EntityDescriptor ed = (EntityDescriptor) entityDescriptor; + EntityDescriptorRepresentation representation = new EntityDescriptorRepresentation(); + + representation.setId(ed.getResourceId()); + representation.setEntityId(ed.getEntityID()); + representation.setServiceProviderName(ed.getServiceProviderName()); + representation.setServiceEnabled(ed.isServiceEnabled()); + representation.setCreatedDate(ed.getCreatedDate()); + representation.setModifiedDate(ed.getModifiedDate()); + + if (ed.getSPSSODescriptor("") != null && ed.getSPSSODescriptor("").getSupportedProtocols().size() > 0) { + ServiceProviderSsoDescriptorRepresentation serviceProviderSsoDescriptorRepresentation = representation.getServiceProviderSsoDescriptor(true); + serviceProviderSsoDescriptorRepresentation.setProtocolSupportEnum(String.join(",", ed.getSPSSODescriptor("").getSupportedProtocols().stream().map(p -> MDDCConstants.PROTOCOL_BINDINGS.get(p)).collect(Collectors.toList()))); + } + + if (ed.getSPSSODescriptor("") != null && ed.getSPSSODescriptor("").getNameIDFormats().size() > 0) { + ServiceProviderSsoDescriptorRepresentation serviceProviderSsoDescriptorRepresentation = representation.getServiceProviderSsoDescriptor(true); + serviceProviderSsoDescriptorRepresentation.setNameIdFormats( + ed.getSPSSODescriptor("").getNameIDFormats().stream().map(p -> p.getFormat()).collect(Collectors.toList()) + ); + } + + if (ed.getOrganization() != null) { + // set up organization + OrganizationRepresentation organizationRepresentation = new OrganizationRepresentation(); + organizationRepresentation.setName(ed.getOrganization().getOrganizationNames().get(0).getValue()); + organizationRepresentation.setDisplayName(ed.getOrganization().getDisplayNames().get(0).getValue()); + organizationRepresentation.setUrl(ed.getOrganization().getURLs().get(0).getValue()); + representation.setOrganization(organizationRepresentation); + } + + if (ed.getContactPersons() != null && ed.getContactPersons().size() > 0) { + // set up contact persons + List contactRepresentations = new ArrayList<>(); + for (org.opensaml.saml.saml2.metadata.ContactPerson contactPerson : ed.getContactPersons()) { + ContactRepresentation contactRepresentation = new ContactRepresentation(); + + if (contactPerson.getType() != null) { + contactRepresentation.setType(contactPerson.getType().toString()); + } + if (contactPerson.getGivenName() != null) { + contactRepresentation.setName(contactPerson.getGivenName().getName()); + } + if (contactPerson.getEmailAddresses() != null && contactPerson.getEmailAddresses().size() > 0) { + contactRepresentation.setEmailAddress(contactPerson.getEmailAddresses().get(0).getAddress()); + } + + contactRepresentations.add(contactRepresentation); + } + representation.setContacts(contactRepresentations); + } + + // set up MDUI + if (ed.getSPSSODescriptor("") != null && ed.getSPSSODescriptor("").getExtensions() != null && ed.getSPSSODescriptor("").getExtensions().getUnknownXMLObjects(UIInfo.DEFAULT_ELEMENT_NAME).size() == 1) { + UIInfo uiInfo = (UIInfo) ed.getSPSSODescriptor("").getExtensions().getUnknownXMLObjects(UIInfo.DEFAULT_ELEMENT_NAME).get(0); + MduiRepresentation mduiRepresentation = new MduiRepresentation(); + representation.setMdui(mduiRepresentation); + + if (uiInfo.getDisplayNames().size() > 0) { + mduiRepresentation.setDisplayName(uiInfo.getDisplayNames().get(0).getValue()); + } + if (uiInfo.getInformationURLs().size() > 0) { + mduiRepresentation.setInformationUrl(uiInfo.getInformationURLs().get(0).getValue()); + } + if (uiInfo.getPrivacyStatementURLs().size() > 0) { + mduiRepresentation.setPrivacyStatementUrl(uiInfo.getPrivacyStatementURLs().get(0).getValue()); + } + if (uiInfo.getDescriptions().size() > 0) { + mduiRepresentation.setDescription(uiInfo.getDescriptions().get(0).getValue()); + } + if (uiInfo.getLogos().size() > 0) { + org.opensaml.saml.ext.saml2mdui.Logo logo = uiInfo.getLogos().get(0); + mduiRepresentation.setLogoUrl(logo.getURL()); + mduiRepresentation.setLogoHeight(logo.getHeight()); + mduiRepresentation.setLogoWidth(logo.getWidth()); + } + } + + // set up security + // TODO: cleanup, probably use a lazy initializer + SecurityInfoRepresentation securityInfoRepresentation = representation.getSecurityInfo(); + if (ed.getSPSSODescriptor("") != null && ed.getSPSSODescriptor("").getWantAssertionsSigned() != null && ed.getSPSSODescriptor("").getWantAssertionsSigned()) { + if (securityInfoRepresentation == null) { + securityInfoRepresentation = new SecurityInfoRepresentation(); + representation.setSecurityInfo(securityInfoRepresentation); + } + securityInfoRepresentation.setWantAssertionsSigned(true); + } + if (ed.getSPSSODescriptor("") != null && ed.getSPSSODescriptor("").isAuthnRequestsSigned() != null && ed.getSPSSODescriptor("").isAuthnRequestsSigned()) { + if (securityInfoRepresentation == null) { + securityInfoRepresentation = new SecurityInfoRepresentation(); + representation.setSecurityInfo(securityInfoRepresentation); + } + securityInfoRepresentation.setAuthenticationRequestsSigned(true); + } + if (ed.getSPSSODescriptor("") != null && ed.getSPSSODescriptor("").getKeyDescriptors().size() > 0) { + if (securityInfoRepresentation == null) { + securityInfoRepresentation = new SecurityInfoRepresentation(); + representation.setSecurityInfo(securityInfoRepresentation); + } + securityInfoRepresentation.setX509CertificateAvailable(true); + for (org.opensaml.saml.saml2.metadata.KeyDescriptor keyDescriptor : ed.getSPSSODescriptor("").getKeyDescriptors()) { + SecurityInfoRepresentation.X509CertificateRepresentation x509CertificateRepresentation = new SecurityInfoRepresentation.X509CertificateRepresentation(); + x509CertificateRepresentation.setName(((KeyDescriptor) keyDescriptor).getName()); + //TODO: check this. assume that if no value is set, it's used for both + if (keyDescriptor.getUse() != null) { + x509CertificateRepresentation.setType(keyDescriptor.getUse().toString().toLowerCase()); + } else { + x509CertificateRepresentation.setType("both"); + } + x509CertificateRepresentation.setValue(keyDescriptor.getKeyInfo().getX509Datas().get(0).getX509Certificates().get(0).getValue()); + securityInfoRepresentation.getX509Certificates().add(x509CertificateRepresentation); + } + } + + // set up ACSs + if (ed.getSPSSODescriptor("") != null && ed.getSPSSODescriptor("").getAssertionConsumerServices().size() > 0) { + if (representation.getAssertionConsumerServices() == null) { + representation.setAssertionConsumerServices(new ArrayList<>()); + } + for (org.opensaml.saml.saml2.metadata.AssertionConsumerService assertionConsumerService : ed.getSPSSODescriptor("").getAssertionConsumerServices()) { + AssertionConsumerServiceRepresentation assertionConsumerServiceRepresentation = new AssertionConsumerServiceRepresentation(); + + Boolean isDefault = assertionConsumerService.isDefault(); + assertionConsumerServiceRepresentation.setMakeDefault(isDefault == null ? false : isDefault); + assertionConsumerServiceRepresentation.setBinding(assertionConsumerService.getBinding()); + assertionConsumerServiceRepresentation.setLocationUrl(assertionConsumerService.getLocation()); + + representation.getAssertionConsumerServices().add(assertionConsumerServiceRepresentation); + } + } + + // set up logout endpoints + if (ed.getSPSSODescriptor("") != null && !ed.getSPSSODescriptor("").getSingleLogoutServices().isEmpty()) { + for (org.opensaml.saml.saml2.metadata.SingleLogoutService singleLogoutService : ed.getSPSSODescriptor("").getSingleLogoutServices()) { + LogoutEndpointRepresentation logoutEndpointRepresentation = new LogoutEndpointRepresentation(); + logoutEndpointRepresentation.setBindingType(singleLogoutService.getBinding()); + logoutEndpointRepresentation.setUrl(singleLogoutService.getLocation()); + representation.getLogoutEndpoints(true).add(logoutEndpointRepresentation); + } + } + + // set up extensions + if (ed.getExtensions() != null && ed.getExtensions().getUnknownXMLObjects(EntityAttributes.DEFAULT_ELEMENT_NAME) != null && ed.getExtensions().getUnknownXMLObjects(EntityAttributes.DEFAULT_ELEMENT_NAME).size() == 1) { + // we have entity attributes (hopefully), so should have overrides + RelyingPartyOverridesRepresentation relyingPartyOverridesRepresentation = new RelyingPartyOverridesRepresentation(); + representation.setRelyingPartyOverrides(relyingPartyOverridesRepresentation); + + for (org.opensaml.saml.saml2.core.Attribute attribute : ((EntityAttributes) ed.getExtensions().getUnknownXMLObjects(EntityAttributes.DEFAULT_ELEMENT_NAME).get(0)).getAttributes()) { + Attribute jpaAttribute = (Attribute) attribute; + // TODO: this is going to get real ugly real quick. clean it up, future Jj! + switch (jpaAttribute.getName()) { + case MDDCConstants.SIGN_ASSERTIONS: + relyingPartyOverridesRepresentation.setSignAssertion(getBooleanValueOfAttribute(jpaAttribute)); + break; + case MDDCConstants.SIGN_RESPONSES: + relyingPartyOverridesRepresentation.setDontSignResponse(!getBooleanValueOfAttribute(jpaAttribute)); + break; + case MDDCConstants.ENCRYPT_ASSERTIONS: + relyingPartyOverridesRepresentation.setTurnOffEncryption(!getBooleanValueOfAttribute(jpaAttribute)); + break; + case MDDCConstants.SECURITY_CONFIGURATION: + if (getStringListValueOfAttribute(jpaAttribute).contains("shibboleth.SecurityConfiguration.SHA1")) { + relyingPartyOverridesRepresentation.setUseSha(true); + } + break; + case MDDCConstants.DISALLOWED_FEATURES: + if ((Integer.decode(getStringListValueOfAttribute(jpaAttribute).get(0)) & 0x1) == 0x1) { + relyingPartyOverridesRepresentation.setIgnoreAuthenticationMethod(true); + } + break; + case MDDCConstants.INCLUDE_CONDITIONS_NOT_BEFORE: + relyingPartyOverridesRepresentation.setOmitNotBefore(!getBooleanValueOfAttribute(jpaAttribute)); + break; + case MDDCConstants.RESPONDER_ID: + relyingPartyOverridesRepresentation.setResponderId(getStringListValueOfAttribute(jpaAttribute).get(0)); + break; + case MDDCConstants.NAME_ID_FORMAT_PRECEDENCE: + relyingPartyOverridesRepresentation.setNameIdFormats(getStringListValueOfAttribute(jpaAttribute)); + break; + case MDDCConstants.DEFAULT_AUTHENTICATION_METHODS: + relyingPartyOverridesRepresentation.setAuthenticationMethods(getStringListValueOfAttribute(jpaAttribute)); + break; + case MDDCConstants.RELEASE_ATTRIBUTES: + representation.setAttributeRelease(getStringListOfAttributeValues(attribute.getAttributeValues())); + break; + default: + break; + } + } + } + + return representation; + } + + private boolean getBooleanValueOfAttribute(Attribute attribute) { + return ((XSBoolean) attribute.getAttributeValues().get(0)).getValue().getValue(); + } + + private List getStringListValueOfAttribute(Attribute attribute) { + return getStringListOfAttributeValues(attribute.getAttributeValues()); + } + + private List getStringListOfAttributeValues(List attributeValues) { + List stringAttributeValues = new ArrayList<>(); + for (XMLObject attributeValue : attributeValues) { + if (attributeValue instanceof org.opensaml.core.xml.schema.XSString) { + stringAttributeValues.add(((org.opensaml.core.xml.schema.XSString) attributeValue).getValue()); + } else if (attributeValue instanceof org.opensaml.core.xml.schema.XSAny) { + stringAttributeValues.add(((org.opensaml.core.xml.schema.XSAny) attributeValue).getTextContent()); + } + } + return stringAttributeValues; + } + + @Override + public void updateDescriptorFromRepresentation(org.opensaml.saml.saml2.metadata.EntityDescriptor entityDescriptor, EntityDescriptorRepresentation representation) { + // TODO: implement + throw new UnsupportedOperationException("not yet implemented"); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/MDDCConstants.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/MDDCConstants.java new file mode 100644 index 000000000..2396eb30d --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/MDDCConstants.java @@ -0,0 +1,53 @@ +package edu.internet2.tier.shibboleth.admin.util; + +import org.opensaml.saml.common.xml.SAMLConstants; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class MDDCConstants { + public static final String RELEASE_ATTRIBUTES = "http://shibboleth.net/ns/attributes/releaseAllValues"; + + public static final String SIGN_ASSERTIONS = "http://shibboleth.net/ns/profiles/saml2/sso/browser/signAssertions"; + public static final String SIGN_ASSERTIONS_FN = "signAssertions"; + + public static final String SIGN_RESPONSES = "http://shibboleth.net/ns/profiles/saml2/sso/browser/signResponses"; + public static final String SIGN_RESPONSES_FN = "signResponses"; + + public static final String ENCRYPT_ASSERTIONS = "http://shibboleth.net/ns/profiles/encryptAssertions"; + public static final String ENCRYPT_ASSERTIONS_FN = "encryptAssertions"; + + public static final String SECURITY_CONFIGURATION = "http://shibboleth.net/ns/profiles/securityConfiguration"; + public static final String SECURITY_CONFIGURATION_FN = "securityConfiguration"; + + public static final String DISALLOWED_FEATURES = "http://shibboleth.net/ns/profiles/disallowedFeatures"; + public static final String DISALLOWED_FEATURES_FN = "disallowedFeatures"; + + public static final String INCLUDE_CONDITIONS_NOT_BEFORE = "http://shibboleth.net/ns/profiles/includeConditionsNotBefore"; + public static final String INCLUDE_CONDITIONS_NOT_BEFORE_FN = "includeConditionsNotBefore"; + + public static final String RESPONDER_ID = "http://shibboleth.net/ns/profiles/responderId"; + public static final String RESPONDER_ID_FN = "responderId"; + + public static final String NAME_ID_FORMAT_PRECEDENCE = "http://shibboleth.net/ns/profiles/nameIDFormatPrecedence"; + public static final String NAME_ID_FORMAT_PRECEDENCE_FN = "nameIDFormatPrecedence"; + + public static final String DEFAULT_AUTHENTICATION_METHODS = "http://shibboleth.net/ns/profiles/defaultAuthenticationMethods"; + public static final String DEFAULT_AUTHENTICATION_METHODS_FN = "defaultAuthenticationMethods"; + + public static final Map PROTOCOL_BINDINGS; + + static { + Map map = new HashMap<>(); + //TODO: this may not be right + map.put("SAML 2", SAMLConstants.SAML20P_NS); + map.put("SAML 1.1", SAMLConstants.SAML11P_NS); + + // TODO: right now we're lazy 'cause this is small. reevaluate later + map.put(SAMLConstants.SAML20P_NS, "SAML 2"); + map.put(SAMLConstants.SAML11P_NS, "SAML 1.1"); + + PROTOCOL_BINDINGS = Collections.unmodifiableMap(map); + } +} diff --git a/backend/src/main/resources/META-INF/services/org.opensaml.core.config.Initializer b/backend/src/main/resources/META-INF/services/org.opensaml.core.config.Initializer new file mode 100644 index 000000000..299b3ef81 --- /dev/null +++ b/backend/src/main/resources/META-INF/services/org.opensaml.core.config.Initializer @@ -0,0 +1 @@ +edu.internet2.tier.shibboleth.admin.ui.opensaml.config.JPAXMLObjectProviderInitializer \ No newline at end of file diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties new file mode 100644 index 000000000..458c2e0c4 --- /dev/null +++ b/backend/src/main/resources/application.properties @@ -0,0 +1,55 @@ +# Server Configuration +#server.port=8080 + +# Logging Configuration +#logging.config=classpath:log4j2.xml +#logging.level.org.springframework.web=ERROR + +# Database Credentials +spring.datasource.username=shibui +spring.datasource.password=shibui + +# Database Configuration H2 +spring.datasource.url=jdbc:h2:mem:shibui;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE +spring.datasource.platform=h2 +spring.datasource.driverClassName=org.h2.Driver +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.h2.console.enabled=true + + +# Database Configuration PostgreSQL +#spring.datasource.url=jdbc:postgresql://localhost:5432/shibui +#spring.datasource.driverClassName=org.postgresql.Driver +#spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect + +#Maria/MySQL DB +#spring.datasource.url=jdbc:mariadb://localhost:3306/shibui +#spring.datasource.driverClassName=org.mariadb.jdbc.Driver +#spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect + +#Tomcat specific DataSource props. Do we need these? +#spring.datasource.tomcat.maxActive=100 +#spring.datasource.tomcat.minIdle=10 +#spring.datasource.tomcat.maxIdle=10 +#spring.datasource.tomcat.initialSize=50 +#spring.datasource.tomcat.validationQuery=select 1 + +# Liquibase properties +liquibase.enabled=false +#liquibase.change-log=classpath:edu/internet2/tier/shibboleth/admin/ui/database/masterchangelog.xml + +# Hibernate properties +# for production never ever use create, create-drop. It's BEST to use validate +spring.jpa.hibernate.ddl-auto=create +spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl +spring.jpa.show-sql=false +spring.jpa.properties.hibernate.format_sql=false + +spring.jpa.hibernate.use-new-id-generator-mappings=true + +shibui.metadata-dir=/opt/shibboleth-idp/metadata/generated +shibui.logout-url=/dashboard + +spring.profiles.active=default + +#shibui.default-password= \ No newline at end of file diff --git a/backend/src/main/resources/edu/internet2/tier/shibboleth/admin/ui/database/changesets/000001.xml b/backend/src/main/resources/edu/internet2/tier/shibboleth/admin/ui/database/changesets/000001.xml new file mode 100644 index 000000000..dc6317d63 --- /dev/null +++ b/backend/src/main/resources/edu/internet2/tier/shibboleth/admin/ui/database/changesets/000001.xml @@ -0,0 +1,560 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/src/main/resources/edu/internet2/tier/shibboleth/admin/ui/database/masterchangelog.xml b/backend/src/main/resources/edu/internet2/tier/shibboleth/admin/ui/database/masterchangelog.xml new file mode 100644 index 000000000..4dee475a8 --- /dev/null +++ b/backend/src/main/resources/edu/internet2/tier/shibboleth/admin/ui/database/masterchangelog.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/backend/src/main/resources/jpa-default-config.xml b/backend/src/main/resources/jpa-default-config.xml new file mode 100644 index 000000000..9b92ce8f4 --- /dev/null +++ b/backend/src/main/resources/jpa-default-config.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/jpa-saml2-assertion-config.xml b/backend/src/main/resources/jpa-saml2-assertion-config.xml new file mode 100644 index 000000000..2cf8d5dd5 --- /dev/null +++ b/backend/src/main/resources/jpa-saml2-assertion-config.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/jpa-saml2-metadata-attr-config.xml b/backend/src/main/resources/jpa-saml2-metadata-attr-config.xml new file mode 100644 index 000000000..485be2779 --- /dev/null +++ b/backend/src/main/resources/jpa-saml2-metadata-attr-config.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/jpa-saml2-metadata-config.xml b/backend/src/main/resources/jpa-saml2-metadata-config.xml new file mode 100644 index 000000000..d2e17c2f8 --- /dev/null +++ b/backend/src/main/resources/jpa-saml2-metadata-config.xml @@ -0,0 +1,374 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/jpa-saml2-metadata-ui-config.xml b/backend/src/main/resources/jpa-saml2-metadata-ui-config.xml new file mode 100644 index 000000000..1ab941897 --- /dev/null +++ b/backend/src/main/resources/jpa-saml2-metadata-ui-config.xml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/jpa-schema-config.xml b/backend/src/main/resources/jpa-schema-config.xml new file mode 100644 index 000000000..86233c041 --- /dev/null +++ b/backend/src/main/resources/jpa-schema-config.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/jpa-signature-config.xml b/backend/src/main/resources/jpa-signature-config.xml new file mode 100644 index 000000000..0a6696db5 --- /dev/null +++ b/backend/src/main/resources/jpa-signature-config.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/main/templates/AssertionBuilderTemplate.java b/backend/src/main/templates/AssertionBuilderTemplate.java new file mode 100644 index 000000000..b80c089ca --- /dev/null +++ b/backend/src/main/templates/AssertionBuilderTemplate.java @@ -0,0 +1,25 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import edu.internet2.tier.shibboleth.admin.ui.opensaml.xml.AbstractSAMLObjectBuilder; +import org.opensaml.saml.common.xml.SAMLConstants; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class {{TOKEN}}Builder extends AbstractSAMLObjectBuilder<{{TOKEN}}> { + @Nonnull + @Override + public {{TOKEN}} buildObject() { + return buildObject(SAMLConstants.SAML20_NS, org.opensaml.saml.saml2.core.{{TOKEN}}.DEFAULT_ELEMENT_LOCAL_NAME, SAMLConstants.SAML20_PREFIX); + } + + @Nonnull + @Override + public {{TOKEN}} buildObject(@Nullable String namespaceURI, @Nonnull String localName, @Nullable String namespacePrefix) { + {{TOKEN}} o = new {{TOKEN}}(); + o.setNamespaceURI(namespaceURI); + o.setElementLocalName(localName); + o.setNamespacePrefix(namespacePrefix); + return o; + } +} diff --git a/backend/src/main/templates/MetadataBuilderTemplate.java b/backend/src/main/templates/MetadataBuilderTemplate.java new file mode 100644 index 000000000..aa828e425 --- /dev/null +++ b/backend/src/main/templates/MetadataBuilderTemplate.java @@ -0,0 +1,25 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import edu.internet2.tier.shibboleth.admin.ui.opensaml.xml.AbstractSAMLObjectBuilder; +import org.opensaml.saml.common.xml.SAMLConstants; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class {{TOKEN}}Builder extends AbstractSAMLObjectBuilder<{{TOKEN}}> { + @Nonnull + @Override + public {{TOKEN}} buildObject() { + return buildObject(SAMLConstants.SAML20MD_NS, org.opensaml.saml.saml2.metadata.{{TOKEN}}.DEFAULT_ELEMENT_LOCAL_NAME, SAMLConstants.SAML20MD_PREFIX); + } + + @Nonnull + @Override + public {{TOKEN}} buildObject(@Nullable String namespaceURI, @Nonnull String localName, @Nullable String namespacePrefix) { + {{TOKEN}} o = new {{TOKEN}}(); + o.setNamespaceURI(namespaceURI); + o.setElementLocalName(localName); + o.setNamespacePrefix(namespacePrefix); + return o; + } +} diff --git a/backend/src/main/templates/MetadataUIBuilderTemplate.java b/backend/src/main/templates/MetadataUIBuilderTemplate.java new file mode 100644 index 000000000..708fe96e6 --- /dev/null +++ b/backend/src/main/templates/MetadataUIBuilderTemplate.java @@ -0,0 +1,25 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import edu.internet2.tier.shibboleth.admin.ui.opensaml.xml.AbstractSAMLObjectBuilder; +import org.opensaml.saml.common.xml.SAMLConstants; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class {{TOKEN}}Builder extends AbstractSAMLObjectBuilder<{{TOKEN}}> { + @Nonnull + @Override + public {{TOKEN}} buildObject() { + return buildObject(SAMLConstants.SAML20MDUI_NS, org.opensaml.saml.ext.saml2mdui.{{TOKEN}}.DEFAULT_ELEMENT_LOCAL_NAME, SAMLConstants.SAML20MDUI_PREFIX); + } + + @Nonnull + @Override + public {{TOKEN}} buildObject(@Nullable String namespaceURI, @Nonnull String localName, @Nullable String namespacePrefix) { + {{TOKEN}} o = new {{TOKEN}}(); + o.setNamespaceURI(namespaceURI); + o.setElementLocalName(localName); + o.setNamespacePrefix(namespacePrefix); + return o; + } +} diff --git a/backend/src/main/templates/XMLSecBuilderTemplate.java b/backend/src/main/templates/XMLSecBuilderTemplate.java new file mode 100644 index 000000000..60422614b --- /dev/null +++ b/backend/src/main/templates/XMLSecBuilderTemplate.java @@ -0,0 +1,27 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import edu.internet2.tier.shibboleth.admin.ui.opensaml.xml.AbstractXMLObjectBuilder; +import org.opensaml.xmlsec.signature.XMLSignatureBuilder; +import org.opensaml.xmlsec.signature.support.SignatureConstants; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class {{TOKEN}}Builder extends AbstractXMLObjectBuilder implements XMLSignatureBuilder { + public {{TOKEN}}Builder() {} + + @Nonnull + @Override + public {{TOKEN}} buildObject(@Nullable String namespaceURI, @Nonnull String localName, @Nullable String namespacePrefix) { + {{TOKEN}} o = new {{TOKEN}}(); + o.setNamespaceURI(namespaceURI); + o.setElementLocalName(localName); + o.setNamespacePrefix(namespacePrefix); + return o; + } + + public {{TOKEN}} buildObject() { + return buildObject(SignatureConstants.XMLSIG_NS, {{TOKEN}}.DEFAULT_ELEMENT_LOCAL_NAME, + SignatureConstants.XMLSIG_PREFIX); + } +} diff --git a/backend/src/main/templates/XSBuilderTemplate.java b/backend/src/main/templates/XSBuilderTemplate.java new file mode 100644 index 000000000..204aaed63 --- /dev/null +++ b/backend/src/main/templates/XSBuilderTemplate.java @@ -0,0 +1,20 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import edu.internet2.tier.shibboleth.admin.ui.opensaml.xml.AbstractXMLObjectBuilder; +import org.opensaml.saml.common.xml.SAMLConstants; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class {{TOKEN}}Builder extends AbstractXMLObjectBuilder<{{TOKEN}}> { + + @Nonnull + @Override + public {{TOKEN}} buildObject(@Nullable String namespaceURI, @Nonnull String localName, @Nullable String namespacePrefix) { + {{TOKEN}} o = new {{TOKEN}}(); + o.setNamespaceURI(namespaceURI); + o.setElementLocalName(localName); + o.setNamespacePrefix(namespacePrefix); + return o; + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy new file mode 100644 index 000000000..91779a493 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy @@ -0,0 +1,328 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository +import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import spock.lang.Specification +import spock.lang.Subject + +import java.time.LocalDateTime + +import static org.hamcrest.CoreMatchers.containsString +import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8 +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.* + +class EntityDescriptorControllerTests extends Specification { + + + def entityDescriptorRepository = Mock(EntityDescriptorRepository) + + def openSamlObjects = new OpenSamlObjects().with { + init() + it + } + + @Subject + def controller = new EntityDescriptorController ( + entityDescriptorRepository: entityDescriptorRepository, + openSamlObjects: openSamlObjects, + entityDescriptorService: new JPAEntityDescriptorServiceImpl(openSamlObjects) + ) + + + def mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + + def 'GET /EntityDescriptors with empty repository'() { + given: + def emptyRecordsFromRepository = [].stream() + def expectedEmptyListResponseBody = '[]' + def expectedResponseContentType = APPLICATION_JSON_UTF8 + def expectedHttpResponseStatus = status().isOk() + + when: + def result = mockMvc.perform(get('/api/EntityDescriptors')) + + then: + //One call to the repo expected + 1 * entityDescriptorRepository.findAllByCustomQueryAndStream() >> emptyRecordsFromRepository + result.andExpect(expectedHttpResponseStatus) + .andExpect(content().contentType(expectedResponseContentType)) + .andExpect(content().json(expectedEmptyListResponseBody)) + + } + + def 'GET /EntityDescriptors with 1 record in repository'() { + given: + def expectedCreationDate = '2017-10-23T11:11:11' + def oneRecordFromRepository = + [new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, + createdDate: LocalDateTime.parse(expectedCreationDate))].stream() + def expectedOneRecordListResponseBody = """ + [ + { + "id": "uuid-1", + "serviceProviderName": "sp1", + "entityId": "eid1", + "serviceEnabled": true, + "createdDate": "$expectedCreationDate", + "modifiedDate": null, + "organization": null, + "contacts": null, + "mdui": null, + "serviceProviderSsoDescriptor": null, + "logoutEndpoints": null, + "securityInfo": null, + "assertionConsumerServices": null, + "relyingPartyOverrides": null, + "attributeRelease": null + } + ] + """ + + def expectedResponseContentType = APPLICATION_JSON_UTF8 + def expectedHttpResponseStatus = status().isOk() + + when: + def result = mockMvc.perform(get('/api/EntityDescriptors')) + + then: + //One call to the repo expected + 1 * entityDescriptorRepository.findAllByCustomQueryAndStream() >> oneRecordFromRepository + result.andExpect(expectedHttpResponseStatus) + .andExpect(content().contentType(expectedResponseContentType)) + .andExpect(content().json(expectedOneRecordListResponseBody, true)) + + } + + def 'GET /EntityDescriptors with 2 records in repository'() { + given: + def expectedCreationDate = '2017-10-23T11:11:11' + def twoRecordsFromRepository = [new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', + serviceEnabled: true, + createdDate: LocalDateTime.parse(expectedCreationDate)), + new EntityDescriptor(resourceId: 'uuid-2', entityID: 'eid2', serviceProviderName: 'sp2', + serviceEnabled: false, + createdDate: LocalDateTime.parse(expectedCreationDate))].stream() + def expectedTwoRecordsListResponseBody = """ + [ + { + "id": "uuid-1", + "serviceProviderName": "sp1", + "entityId": "eid1", + "serviceEnabled": true, + "createdDate": "$expectedCreationDate", + "modifiedDate": null, + "organization": null, + "contacts": null, + "mdui": null, + "serviceProviderSsoDescriptor": null, + "logoutEndpoints": null, + "securityInfo": null, + "assertionConsumerServices": null, + "relyingPartyOverrides": null, + "attributeRelease": null + }, + { + "id": "uuid-2", + "serviceProviderName": "sp2", + "entityId": "eid2", + "serviceEnabled": false, + "createdDate": "$expectedCreationDate", + "modifiedDate": null, + "organization": null, + "contacts": null, + "mdui": null, + "serviceProviderSsoDescriptor": null, + "logoutEndpoints": null, + "securityInfo": null, + "assertionConsumerServices": null, + "relyingPartyOverrides": null, + "attributeRelease": null + } + ] + """ + + def expectedResponseContentType = APPLICATION_JSON_UTF8 + def expectedHttpResponseStatus = status().isOk() + + when: + def result = mockMvc.perform(get('/api/EntityDescriptors')) + + then: + //One call to the repo expected + 1 * entityDescriptorRepository.findAllByCustomQueryAndStream() >> twoRecordsFromRepository + result.andExpect(expectedHttpResponseStatus) + .andExpect(content().contentType(expectedResponseContentType)) + .andExpect(content().json(expectedTwoRecordsListResponseBody, true)) + + } + + def 'POST /EntityDescriptor and successfully create new record'() { + given: + def expectedCreationDate = '2017-10-23T11:11:11' + def expectedEntityId = 'https://shib' + def expectedSpName = 'sp1' + def expectedUUID = 'uuid-1' + def expectedResponseHeader = 'Location' + def expectedResponseHeaderValue = "/api/EntityDescriptor/$expectedUUID" + + def postedJsonBody = """ + { + "serviceProviderName": "$expectedSpName", + "entityId": "$expectedEntityId", + "organization": null, + "serviceEnabled": true, + "createdDate": null, + "modifiedDate": null, + "organization": null, + "contacts": null, + "mdui": null, + "serviceProviderSsoDescriptor": null, + "logoutEndpoints": null, + "securityInfo": null, + "assertionConsumerServices": null, + "relyingPartyOverrides": null, + "attributeRelease": null + } + """ + + def expectedJsonBody = """ + { + "id": "$expectedUUID", + "serviceProviderName": "$expectedSpName", + "entityId": "$expectedEntityId", + "organization": null, + "serviceEnabled": true, + "createdDate": "$expectedCreationDate", + "modifiedDate": null, + "organization": null, + "contacts": null, + "mdui": null, + "serviceProviderSsoDescriptor": null, + "logoutEndpoints": null, + "securityInfo": null, + "assertionConsumerServices": null, + "relyingPartyOverrides": null, + "attributeRelease": null + } + """ + + when: + def result = mockMvc.perform( + post('/api/EntityDescriptor') + .contentType(APPLICATION_JSON_UTF8) + .content(postedJsonBody)) + + then: + //Stub invocation of the repository returning null for non-existent record + 1 * entityDescriptorRepository.findByEntityID(expectedEntityId) >> null + + //Expect 1 invocation of repository save() with correct EntityDescriptor + 1 * entityDescriptorRepository.save({ + it.entityID == expectedEntityId && + it.serviceProviderName == expectedSpName && + it.serviceEnabled == true + }) >> new EntityDescriptor(resourceId: expectedUUID, entityID: expectedEntityId, serviceProviderName: expectedSpName, + serviceEnabled: true, + createdDate: LocalDateTime.parse(expectedCreationDate)) + + result.andExpect(status().isCreated()) + .andExpect(content().json(expectedJsonBody, true)) + .andExpect(header().string(expectedResponseHeader, containsString(expectedResponseHeaderValue))) + + } + + def 'POST /EntityDescriptor record already exists'() { + given: + def expectedEntityId = 'eid1' + def postedJsonBody = """ + { + "serviceProviderName": "sp1", + "entityId": "$expectedEntityId", + "organization": null, + "serviceEnabled": true, + "createdDate": null, + "modifiedDate": null, + "organization": null, + "contacts": null, + "mdui": null, + "serviceProviderSsoDescriptor": null, + "logoutEndpoints": null, + "securityInfo": null, + "assertionConsumerServices": null, + "relyingPartyOverrides": null, + "attributeRelease": null + } + """ + + when: + def result = mockMvc.perform( + post('/api/EntityDescriptor') + .contentType(APPLICATION_JSON_UTF8) + .content(postedJsonBody)) + + then: + //Stub invocation of the repository returning an existing record + 1 * entityDescriptorRepository.findByEntityID(expectedEntityId) >> new EntityDescriptor(entityID: expectedEntityId) + result.andExpect(status().isConflict()) + } + + def 'GET /EntityDescriptor/{resourceId} non-existent'() { + given: + def providedResourceId = 'uuid-1' + + when: + def result = mockMvc.perform(get("/api/EntityDescriptor/$providedResourceId")) + + then: + //No EntityDescriptor found + 1 * entityDescriptorRepository.findByResourceId(providedResourceId) >> null + result.andExpect(status().isNotFound()) + } + + def 'GET /EntityDescriptor/{resourceId} existing'() { + given: + def expectedCreationDate = '2017-10-23T11:11:11' + def providedResourceId = 'uuid-1' + def expectedSpName = 'sp1' + def expectedEntityId = 'eid1' + + def expectedJsonBody = """ + { + "id": "${providedResourceId}", + "serviceProviderName": "$expectedSpName", + "entityId": "$expectedEntityId", + "organization": null, + "serviceEnabled": true, + "createdDate": "$expectedCreationDate", + "modifiedDate": null, + "organization": null, + "contacts": null, + "mdui": null, + "serviceProviderSsoDescriptor": null, + "logoutEndpoints": null, + "securityInfo": null, + "assertionConsumerServices": null, + "relyingPartyOverrides": null, + "attributeRelease": null + } + """ + + when: + def result = mockMvc.perform(get("/api/EntityDescriptor/$providedResourceId")) + + then: + //EntityDescriptor found + 1 * entityDescriptorRepository.findByResourceId(providedResourceId) >> + new EntityDescriptor(resourceId: providedResourceId, entityID: expectedEntityId, serviceProviderName: expectedSpName, + serviceEnabled: true, + createdDate: LocalDateTime.parse(expectedCreationDate)) + + result.andExpect(status().isOk()) + .andExpect(content().json(expectedJsonBody, true)) + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/BuilderTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/BuilderTests.groovy new file mode 100644 index 000000000..0cb246991 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/BuilderTests.groovy @@ -0,0 +1,34 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain + +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import org.xmlunit.builder.DiffBuilder +import org.xmlunit.builder.Input +import org.xmlunit.diff.Diff +import spock.lang.Specification + +class BuilderTests extends Specification { + OpenSamlObjects openSamlObjects = new OpenSamlObjects().with { + init() + it + } + + def "simple builder test"() { + when: + def expected = '''''' + def output = openSamlObjects.marshalToXmlString(openSamlObjects.buildDefaultInstanceOfType(org.opensaml.saml.saml2.metadata.EntityDescriptor).with { + entityID = 'http://test.example.org/test1' + it + } + ) + + Diff diff = DiffBuilder.compare(Input.fromString(expected)).withTest(Input.fromString(output)).ignoreComments().ignoreWhitespace().build() + + then: + !diff.hasDifferences() + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/restapi/JsonSchemaGeneration.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/restapi/JsonSchemaGeneration.groovy new file mode 100644 index 000000000..01729e7df --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/restapi/JsonSchemaGeneration.groovy @@ -0,0 +1,24 @@ +package edu.internet2.tier.shibboleth.admin.ui.restapi + +import com.fasterxml.jackson.databind.ObjectMapper +import com.kjetland.jackson.jsonSchema.JsonSchemaGenerator +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation + +import spock.lang.Specification + +import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT + +class JsonSchemaGeneration extends Specification { + + /*def setupSpec() { + def om = new ObjectMapper().with {it.enable(INDENT_OUTPUT); it} + def schemaGenerator = new JsonSchemaGenerator(om) + def jsonSchema = schemaGenerator.generateJsonSchema(EntityDescriptorRepresentation) + + def jsonSchemaOutputFile = new File('src/test/resources/entity-descriptor-json-schema.json') + jsonSchemaOutputFile.delete() + jsonSchemaOutputFile.withWriter('UTF-8') { + it.write(jsonSchema.toString()) + } + }*/ +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy new file mode 100644 index 000000000..04a094ab6 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy @@ -0,0 +1,594 @@ +package edu.internet2.tier.shibboleth.admin.ui.service + +import com.fasterxml.jackson.databind.ObjectMapper +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.* +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import org.assertj.core.api.Assertions +import org.opensaml.security.credential.UsageType +import org.springframework.boot.test.json.JacksonTester +import org.xmlunit.builder.DiffBuilder +import org.xmlunit.builder.Input +import org.xmlunit.diff.DefaultNodeMatcher +import org.xmlunit.diff.ElementSelectors +import spock.lang.Specification + +class JPAEntityDescriptorServiceImplTests extends Specification { + OpenSamlObjects openSamlObjects = new OpenSamlObjects().with { + init() + it + } + + def service = new JPAEntityDescriptorServiceImpl(openSamlObjects) + + JacksonTester jacksonTester + + def setup() { + JacksonTester.initFields(this, new ObjectMapper()) + } + + + + def "simple Entity Descriptor"() { + when: + def expected = '''''' + + def test = openSamlObjects.marshalToXmlString(service.createDescriptorFromRepresentation(new EntityDescriptorRepresentation().with { + it.setEntityId('http://test.example.org/test1') + it + })) + + def diff = DiffBuilder.compare(Input.fromString(expected)).withTest(Input.fromString(test)).ignoreComments().build() + + then: + !diff.hasDifferences() + } + + def "test organization setting"() { + when: + def expected = ''' + + + name + display name + http://test.example.org + + + ''' + + def test = openSamlObjects.marshalToXmlString(service.createDescriptorFromRepresentation(new EntityDescriptorRepresentation().with { + it.entityId = 'http://test.example.org/test1' + it.organization = new OrganizationRepresentation().with { + it.name = 'name' + it.displayName = 'display name' + it.url = 'http://test.example.org' + it + } + it + })) + + def diff = DiffBuilder.compare(Input.fromString(expected)).withTest(Input.fromString(test)).ignoreComments().ignoreWhitespace().build() + + then: + !diff.hasDifferences() + } + + def "test SPSSODescriptor configuration, single protocol"() { + when: + def expected = ''' + + + +''' + def test = openSamlObjects.marshalToXmlString(service.createDescriptorFromRepresentation(new EntityDescriptorRepresentation().with { + it.entityId = 'http://test.example.org/test1' + it.serviceProviderSsoDescriptor = new ServiceProviderSsoDescriptorRepresentation().with { + it.protocolSupportEnum = 'SAML 2' + it + } + it + })) + + def diff = DiffBuilder.compare(Input.fromString(expected)).withTest(Input.fromString(test)).ignoreComments().ignoreWhitespace().build() + + then: + !diff.hasDifferences() + } + + def "test NameID formats"() { + when: + def expected = ''' + + + nameidformat + + +''' + def test = openSamlObjects.marshalToXmlString(service.createDescriptorFromRepresentation(new EntityDescriptorRepresentation().with { + it.entityId = 'http://test.example.org/test1' + it.serviceProviderSsoDescriptor = new ServiceProviderSsoDescriptorRepresentation().with { + it.nameIdFormats = ['nameidformat'] + it + } + it + })) + + def diff = DiffBuilder.compare(Input.fromString(expected)).withTest(Input.fromString(test)).ignoreComments().ignoreWhitespace().build() + + then: + !diff.hasDifferences() + } + + def "test single contacts setting"() { + when: + def expected = ''' + + + givenName + email@example.org + + +''' + def test = openSamlObjects.marshalToXmlString(service.createDescriptorFromRepresentation(new EntityDescriptorRepresentation().with { + it.entityId = 'http://test.example.org/test1' + it.contacts = [new ContactRepresentation().with { + it.type = 'administrative' + it.name = 'givenName' + it.emailAddress = 'email@example.org' + it + }] + it + })) + + def diff = DiffBuilder.compare(Input.fromString(expected)).withTest(Input.fromString(test)).ignoreComments().ignoreWhitespace().build() + + then: + !diff.hasDifferences() + } + + def "test multiple contacts setting"() { + when: + def expected = ''' + + + givenName + email@example.org + + + support + support@example.org + + +''' + def test = openSamlObjects.marshalToXmlString(service.createDescriptorFromRepresentation(new EntityDescriptorRepresentation().with { + it.entityId = 'http://test.example.org/test1' + it.contacts = [new ContactRepresentation().with { + it.type = 'administrative' + it.name = 'givenName' + it.emailAddress = 'email@example.org' + it + }, new ContactRepresentation().with { + it.type = 'support' + it.name = 'support' + it.emailAddress = 'support@example.org' + it + }] + it + })) + + def diff = DiffBuilder.compare(Input.fromString(expected)).withTest(Input.fromString(test)).ignoreComments().ignoreWhitespace().build() + + then: + !diff.hasDifferences() + } + + def "test MDUI setting"() { + when: + def expected = ''' + + + + + displayName + description + informationUrl + privacyUrl + logoUrl + + + + +''' + def test = openSamlObjects.marshalToXmlString(service.createDescriptorFromRepresentation(new EntityDescriptorRepresentation().with { + it.entityId = 'http://test.example.org/test1' + it.mdui = new MduiRepresentation().with { + it.displayName = 'displayName' + it.description = 'description' + it.informationUrl = 'informationUrl' + it.privacyStatementUrl = 'privacyUrl' + it.logoUrl = 'logoUrl' + it.logoHeight = 10 + it.logoWidth = 11 + it + } + it + })) + + def diff = DiffBuilder.compare(Input.fromString(expected)).withTest(Input.fromString(test)).ignoreComments().checkForSimilar().ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)).build() + + then: + !diff.hasDifferences() + } + + def "test security setting, signing"() { + when: + def expected = ''' + + + + + + certificate + + + + + +''' + def test = openSamlObjects.marshalToXmlString(service.createDescriptorFromRepresentation(new EntityDescriptorRepresentation().with { + it.entityId = 'http://test.example.org/test1' + it.securityInfo = new SecurityInfoRepresentation().with { + it.x509CertificateAvailable = true + it.x509Certificates = [new SecurityInfoRepresentation.X509CertificateRepresentation().with { + it.type = 'signing' + it.value = 'certificate' + it + }] + it + } + it + })) + + def diff = DiffBuilder.compare(Input.fromString(expected)).withTest(Input.fromString(test)).ignoreComments().ignoreWhitespace().build() + + then: + !diff.hasDifferences() + } + + def "test security setting, encryption"() { + when: + def expected = ''' + + + + + + certificate + + + + + +''' + def test = openSamlObjects.marshalToXmlString(service.createDescriptorFromRepresentation(new EntityDescriptorRepresentation().with { + it.entityId = 'http://test.example.org/test1' + it.securityInfo = new SecurityInfoRepresentation().with { + it.x509CertificateAvailable = true + it.x509Certificates = [new SecurityInfoRepresentation.X509CertificateRepresentation().with { + it.type = 'encryption' + it.value = 'certificate' + it + }] + it + } + it + })) + + def diff = DiffBuilder.compare(Input.fromString(expected)).withTest(Input.fromString(test)).ignoreComments().ignoreWhitespace().build() + + then: + !diff.hasDifferences() + } + + def "test security setting, both"() { + when: + def expected = ''' + + + + + + certificate + + + + + +''' + def test = openSamlObjects.marshalToXmlString(service.createDescriptorFromRepresentation(new EntityDescriptorRepresentation().with { + it.entityId = 'http://test.example.org/test1' + it.securityInfo = new SecurityInfoRepresentation().with { + it.x509CertificateAvailable = true + it.x509Certificates = [new SecurityInfoRepresentation.X509CertificateRepresentation().with { + it.type = 'both' + it.value = 'certificate' + it + }] + it + } + it + })) + + def diff = DiffBuilder.compare(Input.fromString(expected)).withTest(Input.fromString(test)).ignoreComments().ignoreWhitespace().build() + + then: + !diff.hasDifferences() + } + + def "test security setting, want assertions signed"() { + when: + def expected = ''' + + + + +''' + def test = openSamlObjects.marshalToXmlString(service.createDescriptorFromRepresentation(new EntityDescriptorRepresentation().with { + it.entityId = 'http://test.example.org/test1' + it.securityInfo = new SecurityInfoRepresentation().with { + it.wantAssertionsSigned = true + it + } + it + })) + + def diff = DiffBuilder.compare(Input.fromString(expected)).withTest(Input.fromString(test)).ignoreComments().ignoreWhitespace().build() + + then: + !diff.hasDifferences() + } + + def "test security setting, want requests signed"() { + when: + def expected = ''' + + + + +''' + def test = openSamlObjects.marshalToXmlString(service.createDescriptorFromRepresentation(new EntityDescriptorRepresentation().with { + it.entityId = 'http://test.example.org/test1' + it.securityInfo = new SecurityInfoRepresentation().with { + it.authenticationRequestsSigned = true + it + } + it + })) + + def diff = DiffBuilder.compare(Input.fromString(expected)).withTest(Input.fromString(test)).ignoreComments().ignoreWhitespace().build() + + then: + !diff.hasDifferences() + } + + def "test ACS configuration"() { + when: + def expected = ''' + + + + + + +''' + def test = openSamlObjects.marshalToXmlString(service.createDescriptorFromRepresentation(new EntityDescriptorRepresentation().with { + it.entityId = 'http://test.example.org/test1' + it.assertionConsumerServices = [ + new AssertionConsumerServiceRepresentation().with { + it.binding = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' + it.locationUrl = 'https://test.example.org/SAML/POST' + it + }, + new AssertionConsumerServiceRepresentation().with { + it.binding = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' + it.locationUrl = 'https://test.example.org/SAML/GET' + it.makeDefault = true + it + } + ] + it + })) + + def diff = DiffBuilder.compare(Input.fromString(expected)).withTest(Input.fromString(test)).ignoreComments().ignoreWhitespace().build() + + then: + !diff.hasDifferences() + } + + def "test logout configuration"() { + when: + def expected = ''' + + + + +''' + def test = openSamlObjects.marshalToXmlString(service.createDescriptorFromRepresentation(new EntityDescriptorRepresentation().with { + it.entityId = 'http://test.example.org/test1' + it.logoutEndpoints = [ + new LogoutEndpointRepresentation().with { + it.bindingType = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' + it.url = 'https://test.example.org/SAML2/POST' + it + } + ] + it + })) + + def diff = DiffBuilder.compare(Input.fromString(expected)).withTest(Input.fromString(test)).ignoreComments().ignoreWhitespace().build() + + then: + !diff.hasDifferences() + } + + def "SHIBUI-199"() { + when: + def input = openSamlObjects.unmarshalFromXml this.class.getResource('/metadata/SHIBUI-199.xml').bytes + def representation = service.createRepresentationFromDescriptor(input) + + then: + !input.getContactPersons().empty && input.getContactPersons()[0].givenName.name == 'New Contact_EDIT' + !representation.contacts.empty && representation.contacts[0].name == 'New Contact_EDIT' + } + + def "SHIBUI-211"() { + when: + def representation = new ObjectMapper().readValue(this.class.getResource('/json/SHIBUI-211.json').bytes, EntityDescriptorRepresentation) + def interstitial = service.createDescriptorFromRepresentation(representation) + def output = service.createRepresentationFromDescriptor(interstitial) + + then: + assert output.mdui != null + assert output.mdui.displayName == 'display name' + assert output.mdui.informationUrl == 'http://example.org' + assert output.mdui.privacyStatementUrl == 'http://example.org/privacy' + assert output.mdui.description == 'this is a description' + assert output.mdui.logoUrl == 'http://example.org/logo.png' + assert output.mdui.logoHeight == 100 + assert output.mdui.logoWidth == 100 + } + + def "SHIBUI-219-1"() { + when: + def representation = new ObjectMapper().readValue(this.class.getResource('/json/SHIBUI-219-1.json').bytes, EntityDescriptorRepresentation) + def output = service.createRepresentationFromDescriptor(service.createDescriptorFromRepresentation(representation)) + + then: + assert output.serviceProviderSsoDescriptor.protocolSupportEnum == 'SAML 2' + } + + def "SHIBUI-219-2"() { + when: + def representation = new ObjectMapper().readValue(this.class.getResource('/json/SHIBUI-219-2.json').bytes, EntityDescriptorRepresentation) + def output = service.createRepresentationFromDescriptor(service.createDescriptorFromRepresentation(representation)) + + then: + assert output.securityInfo?.authenticationRequestsSigned == true + } + + def "SHIBUI-219-3"() { + when: + def representation = new ObjectMapper().readValue(this.class.getResource('/json/SHIBUI-219-3.json').bytes, EntityDescriptorRepresentation) + def output = service.createRepresentationFromDescriptor(service.createDescriptorFromRepresentation(representation)) + + then: + assert output.assertionConsumerServices.size() > 0 + assert output.assertionConsumerServices[0].binding == 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' + assert output.assertionConsumerServices[0].locationUrl == 'test' + assert output.assertionConsumerServices[0].makeDefault + } + + def "SHIBUI-187"() { + when: + def representation = new ObjectMapper().readValue(this.class.getResource('/json/SHIBUI-187.json').bytes, EntityDescriptorRepresentation) + def output = service.createRepresentationFromDescriptor(service.createDescriptorFromRepresentation(representation)) + + then: + assert output.assertionConsumerServices[0].binding == 'urn:oasis:names:tc:SAML:1.0:profiles:browser-post' + } + + def "payload mapping round trip: JSON->FrontendModel->BackendModel->FrontendModel->JSON"() { + when: + EntityDescriptorRepresentation inputRepresentation = jacksonTester.readObject('/json/SHIBUI-219-3.json') + def actualOutputRepresentation = service.createRepresentationFromDescriptor(service.createDescriptorFromRepresentation(inputRepresentation)) + def actualOutputJson = jacksonTester.write(actualOutputRepresentation) + + then: + // TODO: finish + // Assertions.assertThat(actualOutputJson).isEqualToJson('/json/SHIBUI-219-3.json') + assert true + } + + def "SHIBUI-223"() { + when: + def representation = new ObjectMapper().readValue(this.class.getResource('/json/SHIBUI-223.json').bytes, EntityDescriptorRepresentation) + def descriptor = service.createDescriptorFromRepresentation(representation) + def output = service.createRepresentationFromDescriptor(descriptor) + + then: + assert output.securityInfo.x509Certificates.size() == 1 + assert output.securityInfo.x509Certificates[0].type == 'both' + + assert descriptor.getSPSSODescriptor('').getKeyDescriptors().size() == 1 + assert descriptor.getSPSSODescriptor('').getKeyDescriptors()[0].getUse() == null + } +} diff --git a/backend/src/test/resources/entity-descriptor-json-schema.json b/backend/src/test/resources/entity-descriptor-json-schema.json new file mode 100644 index 000000000..cb9e3a46c --- /dev/null +++ b/backend/src/test/resources/entity-descriptor-json-schema.json @@ -0,0 +1,272 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Entity Descriptor Representation", + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "serviceProviderName": { + "type": "string" + }, + "entityId": { + "type": "string" + }, + "organization": { + "$ref": "#/definitions/OrganizationRepresentation" + }, + "contacts": { + "type": "array", + "items": { + "$ref": "#/definitions/ContactRepresentation" + } + }, + "mdui": { + "$ref": "#/definitions/MduiRepresentation" + }, + "serviceProviderSsoDescriptor": { + "$ref": "#/definitions/ServiceProviderSsoDescriptorRepresentation" + }, + "logoutEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/LogoutEndpointRepresentation" + } + }, + "securityInfo": { + "$ref": "#/definitions/SecurityInfoRepresentation" + }, + "assertionConsumerServices": { + "type": "array", + "items": { + "$ref": "#/definitions/AssertionConsumerServiceRepresentation" + } + }, + "serviceEnabled": { + "type": "boolean" + }, + "createdDate": { + "type": "string" + }, + "modifiedDate": { + "type": "string" + }, + "relyingPartyOverrides": { + "$ref": "#/definitions/RelyingPartyOverridesRepresentation" + }, + "attributeRelease": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "serviceProviderName", + "entityId", + "serviceEnabled" + ], + "definitions": { + "OrganizationRepresentation": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "ContactRepresentation": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string" + }, + "name": { + "type": "string" + }, + "emailAddress": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "MduiRepresentation": { + "type": "object", + "additionalProperties": false, + "properties": { + "displayName": { + "type": "string" + }, + "informationUrl": { + "type": "string" + }, + "privacyStatementUrl": { + "type": "string" + }, + "description": { + "type": "string" + }, + "logoUrl": { + "type": "string" + }, + "logoHeight": { + "type": "integer" + }, + "logoWidth": { + "type": "integer" + } + }, + "required": [ + "logoHeight", + "logoWidth" + ] + }, + "ServiceProviderSsoDescriptorRepresentation": { + "type": "object", + "additionalProperties": false, + "properties": { + "protocolSupportEnum": { + "type": "string" + }, + "nameIdFormats": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "LogoutEndpointRepresentation": { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { + "type": "string" + }, + "bindingType": { + "type": "string" + } + } + }, + "SecurityInfoRepresentation": { + "type": "object", + "additionalProperties": false, + "properties": { + "x509CertificateAvailable": { + "type": "boolean" + }, + "authenticationRequestsSigned": { + "type": "boolean" + }, + "wantAssertionsSigned": { + "type": "boolean" + }, + "x509Certificates": { + "type": "array", + "items": { + "$ref": "#/definitions/X509CertificateRepresentation" + } + } + }, + "required": [ + "x509CertificateAvailable", + "authenticationRequestsSigned", + "wantAssertionsSigned" + ] + }, + "X509CertificateRepresentation": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "AssertionConsumerServiceRepresentation": { + "type": "object", + "additionalProperties": false, + "properties": { + "locationUrl": { + "type": "string" + }, + "binding": { + "type": "string" + }, + "makeDefault": { + "type": "boolean" + } + }, + "required": [ + "makeDefault" + ] + }, + "RelyingPartyOverridesRepresentation": { + "type": "object", + "additionalProperties": false, + "properties": { + "signAssertion": { + "type": "boolean" + }, + "dontSignResponse": { + "type": "boolean" + }, + "turnOffEncryption": { + "type": "boolean" + }, + "useSha": { + "type": "boolean" + }, + "ignoreAuthenticationMethod": { + "type": "boolean" + }, + "omitNotBefore": { + "type": "boolean" + }, + "responderId": { + "type": "string" + }, + "nameIdFormats": { + "type": "array", + "items": { + "type": "string" + } + }, + "authenticationMethods": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "signAssertion", + "dontSignResponse", + "turnOffEncryption", + "useSha", + "ignoreAuthenticationMethod", + "omitNotBefore" + ] + } + } +} \ No newline at end of file diff --git a/backend/src/test/resources/json/SHIBUI-187.json b/backend/src/test/resources/json/SHIBUI-187.json new file mode 100644 index 000000000..05415592f --- /dev/null +++ b/backend/src/test/resources/json/SHIBUI-187.json @@ -0,0 +1,44 @@ +{ + "id": "", + "entityId": "test", + "serviceProviderName": "test", + "organization": { + "name": null, + "displayName": null, + "url": null + }, + "contacts": [], + "mdui": { + "displayName": null, + "informationUrl": null, + "privacyStatementUrl": null, + "description": null, + "logoUrl": null, + "logoHeight": null, + "logoWidth": null + }, + "securityInfo": { + "x509CertificateAvailable": false, + "authenticationRequestsSigned": false, + "wantAssertionsSigned": false, + "x509Certificates": [] + }, + "assertionConsumerServices": [ + { + "binding": "urn:oasis:names:tc:SAML:1.0:profiles:browser-post", + "locationUrl": "test", + "makeDefault": false + } + ], + "serviceProviderSsoDescriptor": { + "protocolSupportEnum": null, + "nameIdFormats": [] + }, + "logoutEndpoints": [], + "serviceEnabled": false, + "relyingPartyOverrides": { + "nameIdFormats": [], + "authenticationMethods": [] + }, + "attributeRelease": [] +} \ No newline at end of file diff --git a/backend/src/test/resources/json/SHIBUI-211.json b/backend/src/test/resources/json/SHIBUI-211.json new file mode 100644 index 000000000..1bb678714 --- /dev/null +++ b/backend/src/test/resources/json/SHIBUI-211.json @@ -0,0 +1,38 @@ +{ + "id": "", + "entityId": "SHIBUI-211", + "serviceProviderName": "SHIBUI-211", + "organization": { + "name": null, + "displayName": null, + "url": null + }, + "contacts": [], + "mdui": { + "displayName": "display name", + "informationUrl": "http://example.org", + "privacyStatementUrl": "http://example.org/privacy", + "description": "this is a description", + "logoUrl": "http://example.org/logo.png", + "logoHeight": 100, + "logoWidth": 100 + }, + "securityInfo": { + "x509CertificateAvailable": false, + "authenticationRequestsSigned": false, + "wantAssertionsSigned": false, + "x509Certificates": [] + }, + "assertionConsumerServices": [], + "serviceProviderSsoDescriptor": { + "protocolSupportEnum": null, + "nameIdFormats": [] + }, + "logoutEndpoints": [], + "serviceEnabled": false, + "relyingPartyOverrides": { + "nameIdFormats": [], + "authenticationMethods": [] + }, + "attributeRelease": [] +} \ No newline at end of file diff --git a/backend/src/test/resources/json/SHIBUI-219-1.json b/backend/src/test/resources/json/SHIBUI-219-1.json new file mode 100644 index 000000000..4f1851975 --- /dev/null +++ b/backend/src/test/resources/json/SHIBUI-219-1.json @@ -0,0 +1,38 @@ +{ + "id": "", + "entityId": "test", + "serviceProviderName": "test", + "organization": { + "name": null, + "displayName": null, + "url": null + }, + "contacts": [], + "mdui": { + "displayName": null, + "informationUrl": null, + "privacyStatementUrl": null, + "description": null, + "logoUrl": null, + "logoHeight": null, + "logoWidth": null + }, + "securityInfo": { + "x509CertificateAvailable": false, + "authenticationRequestsSigned": false, + "wantAssertionsSigned": false, + "x509Certificates": [] + }, + "assertionConsumerServices": [], + "serviceProviderSsoDescriptor": { + "protocolSupportEnum": "SAML 2", + "nameIdFormats": [] + }, + "logoutEndpoints": [], + "serviceEnabled": false, + "relyingPartyOverrides": { + "nameIdFormats": [], + "authenticationMethods": [] + }, + "attributeRelease": [] +} \ No newline at end of file diff --git a/backend/src/test/resources/json/SHIBUI-219-2.json b/backend/src/test/resources/json/SHIBUI-219-2.json new file mode 100644 index 000000000..31c2a0d6d --- /dev/null +++ b/backend/src/test/resources/json/SHIBUI-219-2.json @@ -0,0 +1,38 @@ +{ + "id": "", + "entityId": "test2", + "serviceProviderName": "test2", + "organization": { + "name": null, + "displayName": null, + "url": null + }, + "contacts": [], + "mdui": { + "displayName": null, + "informationUrl": null, + "privacyStatementUrl": null, + "description": null, + "logoUrl": null, + "logoHeight": null, + "logoWidth": null + }, + "securityInfo": { + "x509CertificateAvailable": false, + "authenticationRequestsSigned": true, + "wantAssertionsSigned": false, + "x509Certificates": [] + }, + "assertionConsumerServices": [], + "serviceProviderSsoDescriptor": { + "protocolSupportEnum": null, + "nameIdFormats": [] + }, + "logoutEndpoints": [], + "serviceEnabled": false, + "relyingPartyOverrides": { + "nameIdFormats": [], + "authenticationMethods": [] + }, + "attributeRelease": [] +} \ No newline at end of file diff --git a/backend/src/test/resources/json/SHIBUI-219-3.json b/backend/src/test/resources/json/SHIBUI-219-3.json new file mode 100644 index 000000000..551cf3718 --- /dev/null +++ b/backend/src/test/resources/json/SHIBUI-219-3.json @@ -0,0 +1,44 @@ +{ + "id": "", + "entityId": "test", + "serviceProviderName": "test", + "organization": { + "name": null, + "displayName": null, + "url": null + }, + "contacts": [], + "mdui": { + "displayName": null, + "informationUrl": null, + "privacyStatementUrl": null, + "description": null, + "logoUrl": null, + "logoHeight": null, + "logoWidth": null + }, + "securityInfo": { + "x509CertificateAvailable": false, + "authenticationRequestsSigned": false, + "wantAssertionsSigned": false, + "x509Certificates": [] + }, + "assertionConsumerServices": [ + { + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", + "locationUrl": "test", + "makeDefault": true + } + ], + "serviceProviderSsoDescriptor": { + "protocolSupportEnum": null, + "nameIdFormats": [] + }, + "logoutEndpoints": [], + "serviceEnabled": false, + "relyingPartyOverrides": { + "nameIdFormats": [], + "authenticationMethods": [] + }, + "attributeRelease": [] +} \ No newline at end of file diff --git a/backend/src/test/resources/json/SHIBUI-223.json b/backend/src/test/resources/json/SHIBUI-223.json new file mode 100644 index 000000000..50a0c1334 --- /dev/null +++ b/backend/src/test/resources/json/SHIBUI-223.json @@ -0,0 +1,44 @@ +{ + "id": "", + "entityId": "test", + "serviceProviderName": "test", + "organization": { + "name": null, + "displayName": null, + "url": null + }, + "contacts": [], + "mdui": { + "displayName": null, + "informationUrl": null, + "privacyStatementUrl": null, + "description": null, + "logoUrl": null, + "logoHeight": null, + "logoWidth": null + }, + "securityInfo": { + "x509CertificateAvailable": true, + "authenticationRequestsSigned": false, + "wantAssertionsSigned": false, + "x509Certificates": [ + { + "name": "test cert", + "type": "both", + "value": "testcert" + } + ] + }, + "assertionConsumerServices": [], + "serviceProviderSsoDescriptor": { + "protocolSupportEnum": null, + "nameIdFormats": [] + }, + "logoutEndpoints": [], + "serviceEnabled": false, + "relyingPartyOverrides": { + "nameIdFormats": [], + "authenticationMethods": [] + }, + "attributeRelease": [] +} \ No newline at end of file diff --git a/backend/src/test/resources/metadata/SHIBUI-199.xml b/backend/src/test/resources/metadata/SHIBUI-199.xml new file mode 100644 index 000000000..0d8ddfaf1 --- /dev/null +++ b/backend/src/test/resources/metadata/SHIBUI-199.xml @@ -0,0 +1,156 @@ + + + + + + + shibboleth.SecurityConfiguration.SHA1 + + + + + false + + + + + ResponderID_EDIT + + + + + urn:oasis:names:tc:SAML:2.0:nameid-format:transient_EDIT + + + NameIDFormatToSend_EDIT + + + _EDIT + + + + + Authentication Methods to Use_EDIT + + + _EDIT + + + + + eduPersonPrincipalName + + + givenName + + + eduPersonAffiliation + + + eduPersonPrimaryAffiliation + + + eduPersonAssurance + + + employeeNumber + + + + + + + + + + Certificate Both_EDIT + + + + + + + + + Certificate Both_EDIT + + + + + + + + + CERT ENCRYPTION_EDIT + + + + + + + + + CERT SIGNING_EDIT + + + + + + + + + _EDIT + + + + + + + + + _EDIT + + + + + + + + + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + + + _EDIT + + + + + + + + Organization Name_EDIT + + + Organization Display Name _EDIT + + + Organization URL _EDIT + + + + + New Contact_EDIT + + + newcontact@email_EDIT.com + + + + + _EDIT + + + _EDIT@email.com + + + \ No newline at end of file diff --git a/backend/src/test/resources/metadata/metadata.xml b/backend/src/test/resources/metadata/metadata.xml new file mode 100644 index 000000000..74109bfa5 --- /dev/null +++ b/backend/src/test/resources/metadata/metadata.xml @@ -0,0 +1,25 @@ + + + + + + internal + + + givenName + employeeNumber + + + + + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified + + + \ No newline at end of file diff --git a/backend/src/test/resources/shibui.json b/backend/src/test/resources/shibui.json new file mode 100644 index 000000000..891597332 --- /dev/null +++ b/backend/src/test/resources/shibui.json @@ -0,0 +1,67 @@ +{ + "id": "", + "entityId": "test", + "serviceProviderName": "test", + "organization": { + "name": "org name", + "displayName": "org display name", + "url": "http://org.ed" + }, + "contacts": [ + { + "type": "support", + "name": "test support name", + "emailAddress": "test@here.ed" + }, + { + "type": "administrative", + "name": "test admin", + "emailAddress": "testadmin@here.ed" + } + ], + "mdui": { + "displayName": null, + "informationUrl": null, + "privacyStatementUrl": null, + "description": null, + "logoUrl": null, + "logoHeight": null, + "logoWidth": null + }, + "securityInfo": { + "x509CertificateAvailable": false, + "authenticationRequestsSigned": false, + "wantAssertionsSigned": false, + "x509Certificates": [] + }, + "assertionConsumerServices": [ + { + "binding": "HTTP-REDIRECT", + "locationUrl": "https://test.ed", + "makeDefault": true + }, + { + "binding": "HTTP-POST", + "locationUrl": "https://test.ed/POST", + "makeDefault": false + } + ], + "serviceProviderSsoDescriptor": { + "protocolSupportEnum": "SAML 2", + "nameIdFormats": [ + "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + ] + }, + "logoutEndpoints": [ + { + "url": "http://logout.ed", + "bindingType": "HTTP-POST" + } + ], + "serviceEnabled": false, + "relyingPartyOverrides": { + "nameIdFormats": [], + "authenticationMethods": [] + }, + "attributeRelease": {} +} \ No newline at end of file diff --git a/backend/src/test/scripts/marshall-test.groovy b/backend/src/test/scripts/marshall-test.groovy new file mode 100644 index 000000000..43db265b3 --- /dev/null +++ b/backend/src/test/scripts/marshall-test.groovy @@ -0,0 +1,61 @@ +import edu.internet2.tier.shibboleth.admin.ui.domain.XSAny +import edu.internet2.tier.shibboleth.admin.ui.domain.XSBoolean +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute +import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeValue +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesBuilder +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptorBuilder +import edu.internet2.tier.shibboleth.admin.ui.domain.ExtensionsBuilder +import edu.internet2.tier.shibboleth.admin.ui.domain.SPSSODescriptorBuilder +import org.opensaml.core.xml.schema.XSString + +println "start" + +def openSAMLObjects = new OpenSamlObjects().with { + it.init() + return it +} + +def ed = new EntityDescriptorBuilder().buildObject().with { + id = 1 + entityID = "testme" + roleDescriptors.add SPSSODescriptorBuilder.newInstance().buildObject().with { + id = 3 + wantAssertionsSigned = false + authnRequestsSigned = false + addSupportedProtocol('urn:oasis:names:tc:SAML:2.0:protocol') + extensions = ExtensionsBuilder.newInstance().buildObject().with { + unknownXMLObjects.add(EntityAttributesBuilder.newInstance().buildObject().with { + id = 9 + it.attributes.add(openSAMLObjects.builderFactory.getBuilder(Attribute.DEFAULT_ELEMENT_NAME).buildObject().with { + name = "http://scaldingspoon.org/realm" + id = 10 + attributeValues.add(openSAMLObjects.builderFactory.getBuilder(XSString.TYPE_NAME).buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME).with { + value = "this is a test" + it + }) + it + }) + + it.attributes.add(openSAMLObjects.builderFactory.getBuilder(Attribute.DEFAULT_ELEMENT_NAME).buildObject().with { + name = "http://shibboleth.net/ns/profiles/disallowedFeatures" + id = 34 + + attributeValues.add(openSAMLObjects.builderFactory.getBuilder(XSAny.TYPE_NAME).buildObject(AttributeValue.DEFAULT_ELEMENT_NAME).with { + textContent = "this is an arbitrary String" + it + }) + it + }) + it + }) + it + } + return it + } + return it +} + +println openSAMLObjects.marshalToXmlString(ed) + +println "end" \ No newline at end of file diff --git a/backend/src/test/scripts/umarshall-test.groovy b/backend/src/test/scripts/umarshall-test.groovy new file mode 100644 index 000000000..54b0b8547 --- /dev/null +++ b/backend/src/test/scripts/umarshall-test.groovy @@ -0,0 +1,15 @@ +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import org.opensaml.saml.saml2.core.AttributeValue + +println "start" + +def openSAMLObjects = new OpenSamlObjects().with { + it.init() + return it +} + +def file = new File('../resources/metadata/metadata.xml') + +def here = openSAMLObjects.unmarshalFromXml(file.getBytes()) + +println "end" \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..0aff375c2 --- /dev/null +++ b/build.gradle @@ -0,0 +1,3 @@ +task wrapper(type: Wrapper) { + gradleVersion = 4.1 +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..8e15f7661 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,9 @@ +version=1.0.0-SNAPSHOT + +shibboleth.version=3.4.0-SNAPSHOT +opensaml.version=3.4.0-SNAPSHOT +xmltooling.version=1.4.7-SNAPSHOT + +spring-boot.version=1.5.6.RELEASE + +hibernate.version=5.2.11.Final \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..ca78035ef Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..c188b6cc2 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Sep 12 16:59:56 CDT 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 000000000..27309d923 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..832fdb607 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/run-app-es.sh b/run-app-es.sh new file mode 100755 index 000000000..99c47d1a1 --- /dev/null +++ b/run-app-es.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +./gradlew -Dshibui.logout-url=/dashboard clean bootRun npm_run_start --parallel -Pnpm-args="-- --i18nFile=./src/locale/es.xlf --i18nFormat=xlf --locale=es --aot" \ No newline at end of file diff --git a/run-app.sh b/run-app.sh new file mode 100755 index 000000000..d9f4083d4 --- /dev/null +++ b/run-app.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +./gradlew -Dshibui.logout-url=/dashboard clean bootRun npm_run_start --parallel \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 000000000..adb85a37b --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include 'backend', 'ui' \ No newline at end of file diff --git a/ui/.angular-cli.json b/ui/.angular-cli.json new file mode 100644 index 000000000..568f9b788 --- /dev/null +++ b/ui/.angular-cli.json @@ -0,0 +1,69 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "project": { + "name": "ui" + }, + "apps": [ + { + "root": "src", + "outDir": "dist", + "assets": [ + "assets", + "favicon.ico" + ], + "index": "index.html", + "main": "main.ts", + "polyfills": "polyfills.ts", + "test": "test.ts", + "tsconfig": "tsconfig.app.json", + "testTsconfig": "tsconfig.spec.json", + "prefix": "app", + "styles": [ + "styles.scss" + ], + "scripts": [], + "environmentSource": "environments/environment.ts", + "environments": { + "dev": "environments/environment.ts", + "prod": "environments/environment.prod.ts" + } + } + ], + "e2e": { + "protractor": { + "config": "./protractor.conf.js" + } + }, + "lint": [ + { + "project": "src/tsconfig.app.json", + "exclude": "**/node_modules/**" + }, + { + "project": "src/tsconfig.spec.json", + "exclude": "**/node_modules/**" + }, + { + "project": "e2e/tsconfig.e2e.json", + "exclude": "**/node_modules/**" + } + ], + "test": { + "karma": { + "config": "./karma.conf.js" + }, + "codeCoverage": { + "exclude": [ + "./src/testing/**/*" + ] + } + }, + "defaults": { + "styleExt": "scss", + "component": { + } + }, + "warnings": { + "typescriptMismatch": false + } +} diff --git a/ui/.editorconfig b/ui/.editorconfig new file mode 100644 index 000000000..9b7352176 --- /dev/null +++ b/ui/.editorconfig @@ -0,0 +1,13 @@ +# Editor configuration, see http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/ui/README.md b/ui/README.md new file mode 100644 index 000000000..e3fd36108 --- /dev/null +++ b/ui/README.md @@ -0,0 +1,28 @@ +# Ui + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.3.1. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). +Before running the tests make sure you are serving the app via `ng serve`. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). diff --git a/ui/build.gradle b/ui/build.gradle new file mode 100644 index 000000000..985fa8b6b --- /dev/null +++ b/ui/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'base' + id 'com.moowork.node' version '1.2.0' +} + +node { + version = '8.9.1' + npmVersion = '5.5.1' + download = true +} + +npm_run_build { + inputs.dir 'src' + outputs.dir 'dist' + if (project.hasProperty('npm-args')) { + args = project.'npm-args'.tokenize() + } +} + +npm_run_buildProd { + inputs.dir 'src' + outputs.dir 'dist' +} + +npm_run_start { + if (project.hasProperty('npm-args')) { + args = project.'npm-args'.tokenize() + } +} + +clean.doFirst { + file('node_modules').deleteDir() + file('dist').deleteDir() +} diff --git a/ui/e2e/app.e2e-spec.ts b/ui/e2e/app.e2e-spec.ts new file mode 100644 index 000000000..0721bdae1 --- /dev/null +++ b/ui/e2e/app.e2e-spec.ts @@ -0,0 +1,14 @@ +import { AppPage } from './app.po'; + +describe('ui App', () => { + let page: AppPage; + + beforeEach(() => { + page = new AppPage(); + }); + + it('should display welcome message', () => { + page.navigateTo(); + expect(page.getParagraphText()).toEqual('Welcome to app!'); + }); +}); diff --git a/ui/e2e/app.po.ts b/ui/e2e/app.po.ts new file mode 100644 index 000000000..82ea75ba5 --- /dev/null +++ b/ui/e2e/app.po.ts @@ -0,0 +1,11 @@ +import { browser, by, element } from 'protractor'; + +export class AppPage { + navigateTo() { + return browser.get('/'); + } + + getParagraphText() { + return element(by.css('app-root h1')).getText(); + } +} diff --git a/ui/e2e/tsconfig.e2e.json b/ui/e2e/tsconfig.e2e.json new file mode 100644 index 000000000..1d9e5edf0 --- /dev/null +++ b/ui/e2e/tsconfig.e2e.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/e2e", + "baseUrl": "./", + "module": "commonjs", + "target": "es5", + "types": [ + "jasmine", + "jasminewd2", + "node" + ] + } +} diff --git a/ui/karma.conf.js b/ui/karma.conf.js new file mode 100644 index 000000000..6ae361b0c --- /dev/null +++ b/ui/karma.conf.js @@ -0,0 +1,58 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html +const path = require('path'); + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular/cli'], + plugins: [ + require('karma-jasmine'), + require('karma-phantomjs-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('karma-spec-reporter'), + require('@angular/cli/plugins/karma') + ], + client: { + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + coverageIstanbulReporter: { + reports: ['html', 'lcovonly', 'text-summary'], + fixWebpackSourcePaths: true, + skipFilesWithNoCoverage: false, + thresholds: { + emitWarning: false, + global: { + statements: 80, + branches: 80, + functions: 80, + lines: 80 + }, + each: { + statements: 30, + branches: 30, + functions: 30, + lines: 30, + overrides: {} + } + } + }, + angularCli: { + environment: 'dev' + }, + reporters: ['spec', 'coverage-istanbul'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: false, + browsers: ['PhantomJS_custom'], + customLaunchers: { + 'PhantomJS_custom': { + base: 'PhantomJS', + flags: ['--disk-cache=false'] + } + }, + singleRun: true + }); +}; diff --git a/ui/package-lock.json b/ui/package-lock.json new file mode 100644 index 000000000..ec91ff3a2 --- /dev/null +++ b/ui/package-lock.json @@ -0,0 +1,10807 @@ +{ + "name": "ui", + "version": "0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@angular-devkit/build-optimizer": { + "version": "0.0.35", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.0.35.tgz", + "integrity": "sha512-7JxZZAYFSCc0tP6+NrRn3b2Cd1b9d+a3+OfwVNyNsNd2unelqUMko2hm0KLbC8BXcXt/OILg1E/ZgLAXSS47nw==", + "dev": true, + "requires": { + "loader-utils": "1.1.0", + "source-map": "0.5.7", + "typescript": "2.6.2", + "webpack-sources": "1.1.0" + }, + "dependencies": { + "typescript": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.2.tgz", + "integrity": "sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q=", + "dev": true + } + } + }, + "@angular-devkit/core": { + "version": "0.0.22", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-0.0.22.tgz", + "integrity": "sha512-zxrNtTiv60liye/GGeRMnnGgLgAWoqlMTfPLMW0D1qJ4bbrPHtme010mpxS3QL4edcDtQseyXSFCnEkuo2MrRw==", + "dev": true, + "requires": { + "source-map": "0.5.7" + } + }, + "@angular-devkit/schematics": { + "version": "0.0.41", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-0.0.41.tgz", + "integrity": "sha512-eSXyRLM7g9NvNUwDd71iPjHEL0Zutg9PcLUSCrwFXR3Z8S6iStO2FpZACNmz5/Y7ksWLy5/1wjLuDJCHS4X/ig==", + "dev": true, + "requires": { + "@angular-devkit/core": "0.0.22", + "@ngtools/json-schema": "1.1.0", + "@schematics/schematics": "0.0.10", + "minimist": "1.2.0", + "rxjs": "5.5.5" + } + }, + "@angular/animations": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-5.0.5.tgz", + "integrity": "sha1-igCJAKec2eyw7nQVNOxDXM6VOaU=", + "requires": { + "tslib": "1.8.1" + } + }, + "@angular/cli": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-1.5.0.tgz", + "integrity": "sha512-nCXvqNCdi+8aOU2v6EABZsMg5bB7iM+wfaoWKnu9M5fOW2Rm+7/3Y1gDQKyFkgXCzXdy3J/xpfmwT0gjmjlvIA==", + "dev": true, + "requires": { + "@angular-devkit/build-optimizer": "0.0.35", + "@angular-devkit/schematics": "0.0.41", + "@ngtools/json-schema": "1.1.0", + "@ngtools/webpack": "1.8.0", + "@schematics/angular": "0.1.10", + "autoprefixer": "6.7.7", + "chalk": "2.2.2", + "circular-dependency-plugin": "3.0.0", + "common-tags": "1.5.1", + "copy-webpack-plugin": "4.2.3", + "core-object": "3.1.5", + "css-loader": "0.28.7", + "cssnano": "3.10.0", + "denodeify": "1.2.1", + "ember-cli-string-utils": "1.1.0", + "exports-loader": "0.6.4", + "extract-text-webpack-plugin": "3.0.0", + "file-loader": "1.1.5", + "fs-extra": "4.0.3", + "glob": "7.1.2", + "html-webpack-plugin": "2.30.1", + "istanbul-instrumenter-loader": "2.0.0", + "karma-source-map-support": "1.2.0", + "less": "2.7.3", + "less-loader": "4.0.5", + "license-webpack-plugin": "1.1.1", + "lodash": "4.17.4", + "memory-fs": "0.4.1", + "node-modules-path": "1.0.1", + "node-sass": "4.7.2", + "nopt": "4.0.1", + "opn": "5.1.0", + "portfinder": "1.0.13", + "postcss-custom-properties": "6.2.0", + "postcss-loader": "1.3.3", + "postcss-url": "5.1.2", + "raw-loader": "0.5.1", + "resolve": "1.5.0", + "rxjs": "5.5.5", + "sass-loader": "6.0.6", + "semver": "5.4.1", + "silent-error": "1.1.0", + "source-map-loader": "0.2.3", + "source-map-support": "0.4.18", + "style-loader": "0.13.2", + "stylus": "0.54.5", + "stylus-loader": "3.0.1", + "uglifyjs-webpack-plugin": "1.0.0", + "url-loader": "0.6.2", + "webpack": "3.8.1", + "webpack-concat-plugin": "1.4.0", + "webpack-dev-middleware": "1.12.2", + "webpack-dev-server": "2.9.7", + "webpack-merge": "4.1.1", + "webpack-sources": "1.1.0", + "webpack-subresource-integrity": "1.0.3", + "zone.js": "0.8.18" + }, + "dependencies": { + "@ngtools/webpack": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-1.8.0.tgz", + "integrity": "sha512-QefALj8VUakHMI/Z/7RjyQR4UpAAfCXeoHqqD9+7Td3CZkuryyGQILqOSAg3d+cP+64iCwIb2jSKC+YAIy722Q==", + "dev": true, + "requires": { + "chalk": "2.2.2", + "enhanced-resolve": "3.4.1", + "loader-utils": "1.1.0", + "magic-string": "0.22.4", + "semver": "5.4.1", + "source-map": "0.5.7", + "tree-kill": "1.2.0" + } + }, + "postcss-loader": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-1.3.3.tgz", + "integrity": "sha1-piHqH6KQYqg5cqRvVEhncTAZFus=", + "dev": true, + "requires": { + "loader-utils": "1.1.0", + "object-assign": "4.1.1", + "postcss": "5.2.18", + "postcss-load-config": "1.2.0" + } + }, + "postcss-url": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-url/-/postcss-url-5.1.2.tgz", + "integrity": "sha1-mLMWW+jVkkccsMqt3iwNH4MvEz4=", + "dev": true, + "requires": { + "directory-encoder": "0.7.2", + "js-base64": "2.4.0", + "mime": "1.6.0", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "path-is-absolute": "1.0.1", + "postcss": "5.2.18" + } + } + } + }, + "@angular/common": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-5.0.5.tgz", + "integrity": "sha1-oFMDIL7vp7NEbHtcaxDX7aQrWZg=", + "requires": { + "tslib": "1.8.1" + } + }, + "@angular/compiler": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-5.0.5.tgz", + "integrity": "sha1-EoMpvTopYRYAFIDFbsuV8CA6qwc=", + "requires": { + "tslib": "1.8.1" + } + }, + "@angular/compiler-cli": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-5.0.5.tgz", + "integrity": "sha1-TCXObwoE6snlYAe+Njn4YXKpx+o=", + "dev": true, + "requires": { + "chokidar": "1.7.0", + "minimist": "1.2.0", + "reflect-metadata": "0.1.10", + "tsickle": "0.24.1" + } + }, + "@angular/core": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-5.0.5.tgz", + "integrity": "sha1-nwMq/0z6zODjNikzhGb5O7pMvsQ=", + "requires": { + "tslib": "1.8.1" + } + }, + "@angular/forms": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-5.0.5.tgz", + "integrity": "sha1-PpEL/jRhjEgr8hqGWfqaGy28Hzo=", + "requires": { + "tslib": "1.8.1" + } + }, + "@angular/http": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@angular/http/-/http-5.0.5.tgz", + "integrity": "sha1-cBz3qNkX7WAnR7sCxf7zUptI8qk=", + "requires": { + "tslib": "1.8.1" + } + }, + "@angular/language-service": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-5.0.5.tgz", + "integrity": "sha1-UBm96pOqijxFqD61IQZJFt6jDm8=", + "dev": true + }, + "@angular/platform-browser": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-5.0.5.tgz", + "integrity": "sha1-cQOchbK8Xj9EBRUf/R4glsTzc6E=", + "requires": { + "tslib": "1.8.1" + } + }, + "@angular/platform-browser-dynamic": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-5.0.5.tgz", + "integrity": "sha1-16x0xf/UyaEzupCvFgQCJm6SNR0=", + "requires": { + "tslib": "1.8.1" + } + }, + "@angular/router": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-5.0.5.tgz", + "integrity": "sha1-m/ZEgLJk/YzfqWpqGb+8JXJ0Ulo=", + "requires": { + "tslib": "1.8.1" + } + }, + "@ng-bootstrap/ng-bootstrap": { + "version": "1.0.0-beta.7", + "resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-1.0.0-beta.7.tgz", + "integrity": "sha1-WLyB9hACj0BSZSnOQEg6lQKBY7A=" + }, + "@ngrx/effects": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-4.1.1.tgz", + "integrity": "sha1-y3WLhSeWSyWOpBlR9ZqhROPvn64=" + }, + "@ngrx/router-store": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@ngrx/router-store/-/router-store-4.1.1.tgz", + "integrity": "sha1-F/rHwPX/3e+LdemnTtLLCQdPO8o=" + }, + "@ngrx/store": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-4.1.1.tgz", + "integrity": "sha1-aA403yd16IUnVO13f/rJW9gbfeA=" + }, + "@ngtools/json-schema": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@ngtools/json-schema/-/json-schema-1.1.0.tgz", + "integrity": "sha1-w6DFRNYjkqzCgTpCyKDcb1j4aSI=", + "dev": true + }, + "@schematics/angular": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-0.1.10.tgz", + "integrity": "sha512-ykq4FL0WTygkpvIcGDxnxHHT2uvJMWseDeAujmfyZpzdT9X22GOTURNo3LjvOIhhVUpMVZvnAYqjV46KqB702g==", + "dev": true, + "requires": { + "@angular-devkit/core": "0.0.22" + } + }, + "@schematics/schematics": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@schematics/schematics/-/schematics-0.0.10.tgz", + "integrity": "sha512-9vr9W1X6oRp42pbiGRIk3L+T6SoFtHlAGrzbh6rbFQDNXT4UCHarqDigow+DEL6PR2ptXZO9WeLcad4it7zNyA==", + "dev": true + }, + "@types/file-saver": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-1.3.0.tgz", + "integrity": "sha512-fC12hKtEzVkrV/ZRcrmqvpHG/TMYDZtgpAmgMUA4F7KneDaQeFMwmPz8AfygKKJMqsdTi8bL+E+fciaaMLxUhg==", + "dev": true + }, + "@types/jasmine": { + "version": "2.5.54", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.5.54.tgz", + "integrity": "sha512-B9YofFbUljs19g5gBKUYeLIulsh31U5AK70F41BImQRHEZQGm4GcN922UvnYwkduMqbC/NH+9fruWa/zrqvHIg==", + "dev": true + }, + "@types/jasminewd2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/jasminewd2/-/jasminewd2-2.0.3.tgz", + "integrity": "sha512-hYDVmQZT5VA2kigd4H4bv7vl/OhlympwREUemqBdOqtrYTo5Ytm12a5W5/nGgGYdanGVxj0x/VhZ7J3hOg/YKg==", + "dev": true, + "requires": { + "@types/jasmine": "2.5.54" + } + }, + "@types/node": { + "version": "6.0.92", + "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.92.tgz", + "integrity": "sha512-awEYSSTn7dauwVCYSx2CJaPTu0Z1Ht2oR1b2AD3CYao6ZRb+opb6EL43fzmD7eMFgMHzTBWSUzlWSD+S8xN0Nw==", + "dev": true + }, + "@types/q": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz", + "integrity": "sha1-vShOV8hPEyXacCur/IKlMoGQwMU=", + "dev": true + }, + "@types/selenium-webdriver": { + "version": "2.53.43", + "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-2.53.43.tgz", + "integrity": "sha512-UBYHWph6P3tutkbXpW6XYg9ZPbTKjw/YC2hGG1/GEvWwTbvezBUv3h+mmUFw79T3RFPnmedpiXdOBbXX+4l0jg==", + "dev": true + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "accepts": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", + "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", + "dev": true, + "requires": { + "mime-types": "2.1.17", + "negotiator": "0.6.1" + } + }, + "acorn": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.2.1.tgz", + "integrity": "sha512-jG0u7c4Ly+3QkkW18V+NRDN+4bWHdln30NL1ZL2AvFZZmQe/BfopYCtghCKKVBUSetZ4QKcyA0pY6/4Gw8Pv8w==", + "dev": true + }, + "acorn-dynamic-import": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz", + "integrity": "sha1-x1K9IQvvZ5UBtsbLf8hPj0cVjMQ=", + "dev": true, + "requires": { + "acorn": "4.0.13" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=", + "dev": true + } + } + }, + "adm-zip": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.7.tgz", + "integrity": "sha1-hgbCy/HEJs6MjsABdER/1Jtur8E=", + "dev": true + }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=", + "dev": true + }, + "agent-base": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-2.1.1.tgz", + "integrity": "sha1-1t4Q1a9hMtW9aSQn1G/FOFOQlMc=", + "dev": true, + "requires": { + "extend": "3.0.1", + "semver": "5.0.3" + }, + "dependencies": { + "semver": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.0.3.tgz", + "integrity": "sha1-d0Zt5YnNXTyV8TiqeLxWmjy10no=", + "dev": true + } + } + }, + "ajv": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.1.tgz", + "integrity": "sha1-s4u4h22ehr7plJVqBOch6IskjrI=", + "dev": true, + "requires": { + "co": "4.6.0", + "fast-deep-equal": "1.0.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" + } + }, + "ajv-keywords": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", + "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", + "dev": true + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "requires": { + "kind-of": "3.2.2", + "longest": "1.0.1", + "repeat-string": "1.6.1" + } + }, + "alphanum-sort": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", + "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", + "dev": true + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "ansi-html": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", + "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "dev": true, + "requires": { + "micromatch": "2.3.11", + "normalize-path": "2.1.1" + } + }, + "app-root-path": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.0.1.tgz", + "integrity": "sha1-zWLc+OT9WkF+/GZNLlsQZTxlG0Y=", + "dev": true + }, + "append-transform": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-0.4.0.tgz", + "integrity": "sha1-126/jKlNJ24keja61EpLdKthGZE=", + "dev": true, + "requires": { + "default-require-extensions": "1.0.0" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", + "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", + "dev": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.3.3" + } + }, + "argparse": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", + "dev": true, + "requires": { + "sprintf-js": "1.0.3" + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "1.1.0" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-flatten": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.1.tgz", + "integrity": "sha1-Qmu52oQJDBg42BLIFQryCoMx4pY=", + "dev": true + }, + "array-includes": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", + "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", + "dev": true, + "requires": { + "define-properties": "1.1.2", + "es-abstract": "1.10.0" + } + }, + "array-slice": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", + "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=", + "dev": true + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "1.0.3" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "arraybuffer.slice": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz", + "integrity": "sha1-8zshWfBTKj8xB6JywMz70a0peco=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", + "dev": true, + "optional": true + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "dev": true + }, + "asn1.js": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.9.2.tgz", + "integrity": "sha512-b/OsSjvWEo8Pi8H0zsDd2P6Uqo2TK2pH8gNLSJtNLM2Db0v2QaAZ0pBQJXVjAn4gBuugeVDr7s63ZogpUIwWDg==", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "inherits": "2.0.3", + "minimalistic-assert": "1.0.0" + } + }, + "assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", + "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "dev": true, + "requires": { + "util": "0.10.3" + } + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", + "dev": true + }, + "async": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", + "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", + "dev": true, + "requires": { + "lodash": "4.17.4" + } + }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", + "dev": true + }, + "async-foreach": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", + "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", + "dev": true, + "optional": true + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "autoprefixer": { + "version": "6.7.7", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.7.7.tgz", + "integrity": "sha1-Hb0cg1ZY41zj+ZhAmdsAWFx4IBQ=", + "dev": true, + "requires": { + "browserslist": "1.7.7", + "caniuse-db": "1.0.30000782", + "normalize-range": "0.1.2", + "num2fraction": "1.2.2", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", + "dev": true + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", + "dev": true + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "babel-generator": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.0.tgz", + "integrity": "sha1-rBriAHC3n248odMmlhMFN3TyDcU=", + "dev": true, + "requires": { + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "detect-indent": "4.0.0", + "jsesc": "1.3.0", + "lodash": "4.17.4", + "source-map": "0.5.7", + "trim-right": "1.0.1" + }, + "dependencies": { + "jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", + "dev": true + } + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "dev": true, + "requires": { + "core-js": "2.5.2", + "regenerator-runtime": "0.11.1" + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "lodash": "4.17.4" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "debug": "2.6.9", + "globals": "9.18.0", + "invariant": "2.2.2", + "lodash": "4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "esutils": "2.0.2", + "lodash": "4.17.4", + "to-fast-properties": "1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true + }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", + "dev": true + }, + "base64-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz", + "integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==", + "dev": true + }, + "base64id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", + "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=", + "dev": true + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "dev": true, + "requires": { + "callsite": "1.0.0" + } + }, + "bfj-node4": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bfj-node4/-/bfj-node4-5.2.1.tgz", + "integrity": "sha512-w+OTPD/R0AvDVR/sy/uVUVeoCpEgUoYj9/1P2zB6mR1yx7F/ADzLX4nlvZ/91WWzGgdZnuLxWP/J89D7ZDt0DA==", + "dev": true, + "requires": { + "bluebird": "3.5.1", + "check-types": "7.3.0", + "tryer": "1.0.0" + } + }, + "big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "dev": true + }, + "binary-extensions": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", + "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", + "dev": true + }, + "blob": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz", + "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=", + "dev": true + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "dev": true, + "optional": true, + "requires": { + "inherits": "2.0.3" + } + }, + "blocking-proxy": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/blocking-proxy/-/blocking-proxy-0.0.5.tgz", + "integrity": "sha1-RikF4Nz76pcPQao3Ij3anAexkSs=", + "dev": true, + "requires": { + "minimist": "1.2.0" + } + }, + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==", + "dev": true + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "dev": true + }, + "body-parser": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", + "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "dev": true, + "requires": { + "bytes": "3.0.0", + "content-type": "1.0.4", + "debug": "2.6.9", + "depd": "1.1.1", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "on-finished": "2.3.0", + "qs": "6.5.1", + "raw-body": "2.3.2", + "type-is": "1.6.15" + }, + "dependencies": { + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", + "dev": true + } + } + }, + "bonjour": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", + "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", + "dev": true, + "requires": { + "array-flatten": "2.1.1", + "deep-equal": "1.0.1", + "dns-equal": "1.0.0", + "dns-txt": "2.0.2", + "multicast-dns": "6.2.1", + "multicast-dns-service-types": "1.1.0" + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "bootstrap": { + "version": "4.0.0-beta.2", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.0.0-beta.2.tgz", + "integrity": "sha512-DzGtdTlKbrMoGMpz0LigKSqJ+MgtFKxA791PU/q062OlRG0HybNZcTLH7rpDAmLS66Y3esN9yzKHLLbqa5UR3w==" + }, + "brace-expansion": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browserify-aes": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.1.1.tgz", + "integrity": "sha512-UGnTYAnB2a3YuYKIRy1/4FB2HdM866E0qC46JXvVTYKlBlZlnvfpSfY6OKfXZAkv70eJ2a1SqzpAo5CRhZGDFg==", + "dev": true, + "requires": { + "buffer-xor": "1.0.3", + "cipher-base": "1.0.4", + "create-hash": "1.1.3", + "evp_bytestokey": "1.0.3", + "inherits": "2.0.3", + "safe-buffer": "5.1.1" + } + }, + "browserify-cipher": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.0.tgz", + "integrity": "sha1-mYgkSHS/XtTijalWZtzWasj8Njo=", + "dev": true, + "requires": { + "browserify-aes": "1.1.1", + "browserify-des": "1.0.0", + "evp_bytestokey": "1.0.3" + } + }, + "browserify-des": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.0.tgz", + "integrity": "sha1-2qJ3cXRwki7S/hhZQRihdUOXId0=", + "dev": true, + "requires": { + "cipher-base": "1.0.4", + "des.js": "1.0.0", + "inherits": "2.0.3" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "randombytes": "2.0.5" + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "browserify-rsa": "4.0.1", + "create-hash": "1.1.3", + "create-hmac": "1.1.6", + "elliptic": "6.4.0", + "inherits": "2.0.3", + "parse-asn1": "5.1.0" + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "1.0.6" + } + }, + "browserslist": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", + "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", + "dev": true, + "requires": { + "caniuse-db": "1.0.30000782", + "electron-to-chromium": "1.3.28" + } + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "dev": true, + "requires": { + "base64-js": "1.2.1", + "ieee754": "1.1.8", + "isarray": "1.0.0" + } + }, + "buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", + "dev": true + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "dev": true + }, + "cacache": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-10.0.1.tgz", + "integrity": "sha512-dRHYcs9LvG9cHgdPzjiI+/eS7e1xRhULrcyOx04RZQsszNJXU2SL9CyG60yLnge282Qq5nwTv+ieK2fH+WPZmA==", + "dev": true, + "requires": { + "bluebird": "3.5.1", + "chownr": "1.0.1", + "glob": "7.1.2", + "graceful-fs": "4.1.11", + "lru-cache": "4.1.1", + "mississippi": "1.3.0", + "mkdirp": "0.5.1", + "move-concurrently": "1.0.1", + "promise-inflight": "1.0.1", + "rimraf": "2.6.2", + "ssri": "5.0.0", + "unique-filename": "1.1.0", + "y18n": "3.2.1" + } + }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", + "dev": true + }, + "camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", + "dev": true, + "requires": { + "no-case": "2.3.2", + "upper-case": "1.1.3" + } + }, + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "2.1.1", + "map-obj": "1.0.1" + } + }, + "caniuse-api": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-1.6.1.tgz", + "integrity": "sha1-tTTnxzTE+B7F++isoq0kNUuWLGw=", + "dev": true, + "requires": { + "browserslist": "1.7.7", + "caniuse-db": "1.0.30000782", + "lodash.memoize": "4.1.2", + "lodash.uniq": "4.5.0" + } + }, + "caniuse-db": { + "version": "1.0.30000782", + "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000782.tgz", + "integrity": "sha1-2IFbzhV4w1Cs7REyUHMBIF4Pq1M=", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "requires": { + "align-text": "0.1.4", + "lazy-cache": "1.0.4" + }, + "dependencies": { + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true + } + } + }, + "chalk": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.2.2.tgz", + "integrity": "sha512-LvixLAQ4MYhbf7hgL4o5PeK32gJKvVzDRiSNIApDofQvyhl8adgG2lJVXn4+ekQoK7HL9RF8lqxwerpe0x2pCw==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=", + "dev": true + }, + "check-types": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/check-types/-/check-types-7.3.0.tgz", + "integrity": "sha1-Ro9XGkQ1wkJI9f0MsOjYfDw0Hn0=", + "dev": true + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "dev": true, + "requires": { + "anymatch": "1.3.2", + "async-each": "1.0.1", + "fsevents": "1.1.3", + "glob-parent": "2.0.0", + "inherits": "2.0.3", + "is-binary-path": "1.0.1", + "is-glob": "2.0.1", + "path-is-absolute": "1.0.1", + "readdirp": "2.1.0" + }, + "dependencies": { + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + } + } + }, + "chownr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", + "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=", + "dev": true + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.1" + } + }, + "circular-dependency-plugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-3.0.0.tgz", + "integrity": "sha1-m2hpLjWw41EJmNAWS2rlARvqV2A=", + "dev": true + }, + "clap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz", + "integrity": "sha512-4CoL/A3hf90V3VIEjeuhSvlGFEHKzOz+Wfc2IVZc+FaUgU0ZQafJTP49fvnULipOPcAfqhyI2duwQyns6xqjYA==", + "dev": true, + "requires": { + "chalk": "1.1.3" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "clean-css": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.9.tgz", + "integrity": "sha1-Nc7ornaHpJuYA09w3gDE7dOCYwE=", + "dev": true, + "requires": { + "source-map": "0.5.7" + } + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wrap-ansi": "2.1.0" + } + }, + "clone": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.3.tgz", + "integrity": "sha1-KY1+IjFmD0DAA8LtMUDezz9TCF8=", + "dev": true + }, + "clone-deep": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.3.0.tgz", + "integrity": "sha1-NIxhrpzb4O3+BT2R/0zFIdeQ7eg=", + "dev": true, + "requires": { + "for-own": "1.0.0", + "is-plain-object": "2.0.4", + "kind-of": "3.2.2", + "shallow-clone": "0.1.2" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "coa": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/coa/-/coa-1.0.4.tgz", + "integrity": "sha1-qe8VNmDWqGqL3sAomlxoTSF0Mv0=", + "dev": true, + "requires": { + "q": "1.5.1" + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "codelyzer": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-3.1.2.tgz", + "integrity": "sha1-n/HwQfubXuXb60W6hm368EmDrwQ=", + "dev": true, + "requires": { + "app-root-path": "2.0.1", + "css-selector-tokenizer": "0.7.0", + "cssauron": "1.4.0", + "semver-dsl": "1.0.1", + "source-map": "0.5.7", + "sprintf-js": "1.0.3" + } + }, + "color": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/color/-/color-0.11.4.tgz", + "integrity": "sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q=", + "dev": true, + "requires": { + "clone": "1.0.3", + "color-convert": "1.9.1", + "color-string": "0.3.0" + } + }, + "color-convert": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", + "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "color-string": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz", + "integrity": "sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE=", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "colormin": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colormin/-/colormin-1.1.2.tgz", + "integrity": "sha1-6i90IKcrlogaOKrlnsEkpvcpgTM=", + "dev": true, + "requires": { + "color": "0.11.4", + "css-color-names": "0.0.4", + "has": "1.0.1" + } + }, + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true + }, + "combine-lists": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/combine-lists/-/combine-lists-1.0.1.tgz", + "integrity": "sha1-RYwH4J4NkA/Ci3Cj/sLazR0st/Y=", + "dev": true, + "requires": { + "lodash": "4.17.4" + } + }, + "combined-stream": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "commander": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.12.2.tgz", + "integrity": "sha512-BFnaq5ZOGcDN7FlrtBT4xxkgIToalIIxwjxLWVJ8bGTpe1LroqMiqQXdA7ygc7CRvaYS+9zfPGFnJqFSayx+AA==", + "dev": true + }, + "common-tags": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.5.1.tgz", + "integrity": "sha512-NrUYGY5TApAk9KB+IZXkR3GR4tA3g26HDsoiGt4kCMHZ727gOGkC+UNfq0Z22jE15bLkc/6RV5Jw1RBW6Usg6A==", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=", + "dev": true + }, + "component-emitter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz", + "integrity": "sha1-KWWU8nU9qmOZbSrwjRWpURbJrsM=", + "dev": true + }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=", + "dev": true + }, + "compressible": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.12.tgz", + "integrity": "sha1-xZpcmdt2dn6YdlAOJx72OzSTvWY=", + "dev": true, + "requires": { + "mime-db": "1.30.0" + } + }, + "compression": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.1.tgz", + "integrity": "sha1-7/JgPvwuIs+G810uuTWJ+YdTc9s=", + "dev": true, + "requires": { + "accepts": "1.3.4", + "bytes": "3.0.0", + "compressible": "2.0.12", + "debug": "2.6.9", + "on-headers": "1.0.1", + "safe-buffer": "5.1.1", + "vary": "1.1.2" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.3", + "typedarray": "0.0.6" + } + }, + "connect": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.5.tgz", + "integrity": "sha1-+43ee6B2OHfQ7J352sC0tA5yx9o=", + "dev": true, + "requires": { + "debug": "2.6.9", + "finalhandler": "1.0.6", + "parseurl": "1.3.2", + "utils-merge": "1.0.1" + }, + "dependencies": { + "finalhandler": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.0.6.tgz", + "integrity": "sha1-AHrqM9Gk0+QgF/YkhIrVjSEvgU8=", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "1.0.1", + "escape-html": "1.0.3", + "on-finished": "2.3.0", + "parseurl": "1.3.2", + "statuses": "1.3.1", + "unpipe": "1.0.0" + } + } + } + }, + "connect-history-api-fallback": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz", + "integrity": "sha1-sGhzk0vF40T+9hGhlqb6rgruAVo=", + "dev": true + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "requires": { + "date-now": "0.1.4" + } + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=", + "dev": true + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, + "convert-source-map": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", + "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=", + "dev": true + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "dev": true, + "requires": { + "aproba": "1.2.0", + "fs-write-stream-atomic": "1.0.10", + "iferr": "0.1.5", + "mkdirp": "0.5.1", + "rimraf": "2.6.2", + "run-queue": "1.0.3" + } + }, + "copy-webpack-plugin": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-4.2.3.tgz", + "integrity": "sha512-cL/Wl3Y1QmmKThl/mWeGB+HH3YH+25tn8nhqEGsZda4Yn7GqGnDZ+TbeKJ7A6zvrxyNhhuviYAxn/tCyyAqh8Q==", + "dev": true, + "requires": { + "bluebird": "3.5.1", + "glob": "7.1.2", + "is-glob": "4.0.0", + "loader-utils": "0.2.17", + "lodash": "4.17.4", + "minimatch": "3.0.4" + }, + "dependencies": { + "loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "dev": true, + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1", + "object-assign": "4.1.1" + } + } + } + }, + "core-js": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.2.tgz", + "integrity": "sha1-vEZIZW59ydyA19PHu8Fy2W50TmM=" + }, + "core-object": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/core-object/-/core-object-3.1.5.tgz", + "integrity": "sha512-sA2/4+/PZ/KV6CKgjrVrrUVBKCkdDO02CUlQ0YKTQoYUwPYNOtOAcWlbYhd5v/1JqYaA6oZ4sDlOU4ppVw6Wbg==", + "dev": true, + "requires": { + "chalk": "2.2.2" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cosmiconfig": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-2.2.2.tgz", + "integrity": "sha512-GiNXLwAFPYHy25XmTPpafYvn3CLAkJ8FLsscq78MQd1Kh0OU6Yzhn4eV2MVF4G9WEQZoWEGltatdR+ntGPMl5A==", + "dev": true, + "requires": { + "is-directory": "0.3.1", + "js-yaml": "3.7.0", + "minimist": "1.2.0", + "object-assign": "4.1.1", + "os-homedir": "1.0.2", + "parse-json": "2.2.0", + "require-from-string": "1.2.1" + } + }, + "create-ecdh": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.0.tgz", + "integrity": "sha1-iIxyNZbN92EvZJgjPuvXo1MBc30=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "elliptic": "6.4.0" + } + }, + "create-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", + "integrity": "sha1-YGBCrIuSYnUPSDyt2rD1gZFy2P0=", + "dev": true, + "requires": { + "cipher-base": "1.0.4", + "inherits": "2.0.3", + "ripemd160": "2.0.1", + "sha.js": "2.4.9" + } + }, + "create-hmac": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.6.tgz", + "integrity": "sha1-rLniIaThe9sHbpBlfEK5PjcmzwY=", + "dev": true, + "requires": { + "cipher-base": "1.0.4", + "create-hash": "1.1.3", + "inherits": "2.0.3", + "ripemd160": "2.0.1", + "safe-buffer": "5.1.1", + "sha.js": "2.4.9" + } + }, + "cross-spawn": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", + "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", + "dev": true, + "optional": true, + "requires": { + "lru-cache": "4.1.1", + "which": "1.3.0" + } + }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=", + "dev": true + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "dev": true, + "requires": { + "boom": "2.10.1" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "requires": { + "browserify-cipher": "1.0.0", + "browserify-sign": "4.0.4", + "create-ecdh": "4.0.0", + "create-hash": "1.1.3", + "create-hmac": "1.1.6", + "diffie-hellman": "5.0.2", + "inherits": "2.0.3", + "pbkdf2": "3.0.14", + "public-encrypt": "4.0.0", + "randombytes": "2.0.5", + "randomfill": "1.0.3" + } + }, + "css-color-names": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", + "dev": true + }, + "css-loader": { + "version": "0.28.7", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-0.28.7.tgz", + "integrity": "sha512-GxMpax8a/VgcfRrVy0gXD6yLd5ePYbXX/5zGgTVYp4wXtJklS8Z2VaUArJgc//f6/Dzil7BaJObdSv8eKKCPgg==", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "css-selector-tokenizer": "0.7.0", + "cssnano": "3.10.0", + "icss-utils": "2.1.0", + "loader-utils": "1.1.0", + "lodash.camelcase": "4.3.0", + "object-assign": "4.1.1", + "postcss": "5.2.18", + "postcss-modules-extract-imports": "1.1.0", + "postcss-modules-local-by-default": "1.2.0", + "postcss-modules-scope": "1.1.0", + "postcss-modules-values": "1.3.0", + "postcss-value-parser": "3.3.0", + "source-list-map": "2.0.0" + } + }, + "css-parse": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-1.7.0.tgz", + "integrity": "sha1-Mh9s9zeCpv91ERE5D8BeLGV9jJs=", + "dev": true + }, + "css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", + "dev": true, + "requires": { + "boolbase": "1.0.0", + "css-what": "2.1.0", + "domutils": "1.5.1", + "nth-check": "1.0.1" + } + }, + "css-selector-tokenizer": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz", + "integrity": "sha1-5piEdK6MlTR3v15+/s/OzNnPTIY=", + "dev": true, + "requires": { + "cssesc": "0.1.0", + "fastparse": "1.1.1", + "regexpu-core": "1.0.0" + } + }, + "css-what": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz", + "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=", + "dev": true + }, + "cssauron": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cssauron/-/cssauron-1.4.0.tgz", + "integrity": "sha1-pmAt/34EqDBtwNuaVR6S6LVmKtg=", + "dev": true, + "requires": { + "through": "2.3.8" + } + }, + "cssesc": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz", + "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=", + "dev": true + }, + "cssnano": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-3.10.0.tgz", + "integrity": "sha1-Tzj2zqK5sX+gFJDyPx3GjqZcHDg=", + "dev": true, + "requires": { + "autoprefixer": "6.7.7", + "decamelize": "1.2.0", + "defined": "1.0.0", + "has": "1.0.1", + "object-assign": "4.1.1", + "postcss": "5.2.18", + "postcss-calc": "5.3.1", + "postcss-colormin": "2.2.2", + "postcss-convert-values": "2.6.1", + "postcss-discard-comments": "2.0.4", + "postcss-discard-duplicates": "2.1.0", + "postcss-discard-empty": "2.1.0", + "postcss-discard-overridden": "0.1.1", + "postcss-discard-unused": "2.2.3", + "postcss-filter-plugins": "2.0.2", + "postcss-merge-idents": "2.1.7", + "postcss-merge-longhand": "2.0.2", + "postcss-merge-rules": "2.1.2", + "postcss-minify-font-values": "1.0.5", + "postcss-minify-gradients": "1.0.5", + "postcss-minify-params": "1.2.2", + "postcss-minify-selectors": "2.1.1", + "postcss-normalize-charset": "1.1.1", + "postcss-normalize-url": "3.0.8", + "postcss-ordered-values": "2.2.3", + "postcss-reduce-idents": "2.4.0", + "postcss-reduce-initial": "1.0.1", + "postcss-reduce-transforms": "1.0.4", + "postcss-svgo": "2.1.6", + "postcss-unique-selectors": "2.0.2", + "postcss-value-parser": "3.3.0", + "postcss-zindex": "2.2.0" + } + }, + "csso": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/csso/-/csso-2.3.2.tgz", + "integrity": "sha1-3dUsWHAz9J6Utx/FVWnyUuj/X4U=", + "dev": true, + "requires": { + "clap": "1.2.3", + "source-map": "0.5.7" + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "1.0.2" + } + }, + "custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=", + "dev": true + }, + "cyclist": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", + "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=", + "dev": true + }, + "d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "dev": true, + "requires": { + "es5-ext": "0.10.37" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", + "dev": true + }, + "default-require-extensions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-1.0.0.tgz", + "integrity": "sha1-836hXT4T/9m0N9M+GnW1+5eHTLg=", + "dev": true, + "requires": { + "strip-bom": "2.0.0" + } + }, + "define-properties": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", + "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", + "dev": true, + "requires": { + "foreach": "2.0.5", + "object-keys": "1.0.11" + } + }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", + "dev": true + }, + "del": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz", + "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=", + "dev": true, + "requires": { + "globby": "6.1.0", + "is-path-cwd": "1.0.0", + "is-path-in-cwd": "1.0.0", + "p-map": "1.2.0", + "pify": "3.0.0", + "rimraf": "2.6.2" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, + "denodeify": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz", + "integrity": "sha1-OjYof1A05pnnV3kBBSwubJQlFjE=", + "dev": true + }, + "depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=", + "dev": true + }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "minimalistic-assert": "1.0.0" + } + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "detect-node": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.3.tgz", + "integrity": "sha1-ogM8CcyOFY03dI+951B4Mr1s4Sc=", + "dev": true + }, + "di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=", + "dev": true + }, + "diff": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.4.0.tgz", + "integrity": "sha512-QpVuMTEoJMF7cKzi6bvWhRulU1fZqZnvyVQgNhPaxxuTYwyjn/j1v9falseQ/uXWwPnO56RBfwtg4h/EQXmucA==", + "dev": true + }, + "diffie-hellman": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz", + "integrity": "sha1-tYNXOScM/ias9jIJn97SoH8gnl4=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "miller-rabin": "4.0.1", + "randombytes": "2.0.5" + } + }, + "directory-encoder": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/directory-encoder/-/directory-encoder-0.7.2.tgz", + "integrity": "sha1-WbTiqk8lQi9sY7UntGL14tDdLFg=", + "dev": true, + "requires": { + "fs-extra": "0.23.1", + "handlebars": "1.3.0", + "img-stats": "0.5.2" + }, + "dependencies": { + "async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=", + "dev": true, + "optional": true + }, + "fs-extra": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.23.1.tgz", + "integrity": "sha1-ZhHbpq3yq43Jxp+rN83fiBgVfj0=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "2.4.0", + "path-is-absolute": "1.0.1", + "rimraf": "2.6.2" + } + }, + "handlebars": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-1.3.0.tgz", + "integrity": "sha1-npsTCpPjiUkTItl1zz7BgYw3zjQ=", + "dev": true, + "requires": { + "optimist": "0.3.7", + "uglify-js": "2.3.6" + } + }, + "jsonfile": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + } + }, + "optimist": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", + "integrity": "sha1-yQlBrVnkJzMokjB00s8ufLxuwNk=", + "dev": true, + "requires": { + "wordwrap": "0.0.2" + } + }, + "source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "dev": true, + "optional": true, + "requires": { + "amdefine": "1.0.1" + } + }, + "uglify-js": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.3.6.tgz", + "integrity": "sha1-+gmEdwtCi3qbKoBY9GNV0U/vIRo=", + "dev": true, + "optional": true, + "requires": { + "async": "0.2.10", + "optimist": "0.3.7", + "source-map": "0.1.43" + } + } + } + }, + "dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", + "dev": true + }, + "dns-packet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.2.2.tgz", + "integrity": "sha512-kN+DjfGF7dJGUL7nWRktL9Z18t1rWP3aQlyZdY8XlpvU3Nc6GeFTQApftcjtWKxAZfiggZSGrCEoszNgvnpwDg==", + "dev": true, + "requires": { + "ip": "1.1.5", + "safe-buffer": "5.1.1" + } + }, + "dns-txt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", + "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", + "dev": true, + "requires": { + "buffer-indexof": "1.1.1" + } + }, + "dom-converter": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.1.4.tgz", + "integrity": "sha1-pF71cnuJDJv/5tfIduexnLDhfzs=", + "dev": true, + "requires": { + "utila": "0.3.3" + }, + "dependencies": { + "utila": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz", + "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=", + "dev": true + } + } + }, + "dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=", + "dev": true, + "requires": { + "custom-event": "1.0.1", + "ent": "2.2.0", + "extend": "3.0.1", + "void-elements": "2.0.1" + } + }, + "dom-serializer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", + "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", + "dev": true, + "requires": { + "domelementtype": "1.1.3", + "entities": "1.1.1" + }, + "dependencies": { + "domelementtype": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", + "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=", + "dev": true + } + } + }, + "domain-browser": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz", + "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=", + "dev": true + }, + "domelementtype": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", + "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=", + "dev": true + }, + "domhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.1.0.tgz", + "integrity": "sha1-0mRvXlf2w7qxHPbLBdPArPdBJZQ=", + "dev": true, + "requires": { + "domelementtype": "1.3.0" + } + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "dev": true, + "requires": { + "dom-serializer": "0.1.0", + "domelementtype": "1.3.0" + } + }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", + "dev": true + }, + "duplexify": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.1.tgz", + "integrity": "sha512-j5goxHTwVED1Fpe5hh3q9R93Kip0Bg2KVAt4f8CEYM3UEwYcPSvWbXaUQOzdX/HtiNomipv+gU7ASQPDbV7pGQ==", + "dev": true, + "requires": { + "end-of-stream": "1.4.0", + "inherits": "2.0.3", + "readable-stream": "2.3.3", + "stream-shift": "1.0.0" + } + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "ejs": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.5.7.tgz", + "integrity": "sha1-zIcsFoiArjxxiXYv1f/ACJbJUYo=", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.28", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.28.tgz", + "integrity": "sha1-jdTmRYCGZE6fnwoc8y4qH53/2e4=", + "dev": true + }, + "elliptic": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", + "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "brorand": "1.1.0", + "hash.js": "1.1.3", + "hmac-drbg": "1.0.1", + "inherits": "2.0.3", + "minimalistic-assert": "1.0.0", + "minimalistic-crypto-utils": "1.0.1" + } + }, + "ember-cli-string-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ember-cli-string-utils/-/ember-cli-string-utils-1.1.0.tgz", + "integrity": "sha1-ObZ3/CgF9VFzc1N2/O8njqpEUqE=", + "dev": true + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true + }, + "encodeurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", + "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=", + "dev": true + }, + "end-of-stream": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.0.tgz", + "integrity": "sha1-epDYM+/abPpurA9JSduw+tOmMgY=", + "dev": true, + "requires": { + "once": "1.4.0" + } + }, + "engine.io": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-1.8.3.tgz", + "integrity": "sha1-jef5eJXSDTm4X4ju7nd7K9QrE9Q=", + "dev": true, + "requires": { + "accepts": "1.3.3", + "base64id": "1.0.0", + "cookie": "0.3.1", + "debug": "2.3.3", + "engine.io-parser": "1.3.2", + "ws": "1.1.2" + }, + "dependencies": { + "accepts": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz", + "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=", + "dev": true, + "requires": { + "mime-types": "2.1.17", + "negotiator": "0.6.1" + } + }, + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + } + } + }, + "engine.io-client": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-1.8.3.tgz", + "integrity": "sha1-F5jtk0USRkU9TG9jXXogH+lA1as=", + "dev": true, + "requires": { + "component-emitter": "1.2.1", + "component-inherit": "0.0.3", + "debug": "2.3.3", + "engine.io-parser": "1.3.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parsejson": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "1.1.2", + "xmlhttprequest-ssl": "1.5.3", + "yeast": "0.1.2" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + } + } + }, + "engine.io-parser": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-1.3.2.tgz", + "integrity": "sha1-k3sHnwAH0Ik+xW1GyyILjLQ1Igo=", + "dev": true, + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "0.0.6", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.4", + "has-binary": "0.1.7", + "wtf-8": "1.0.0" + } + }, + "enhanced-resolve": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz", + "integrity": "sha1-BCHjOf1xQZs9oT0Smzl5BAIwR24=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "memory-fs": "0.4.1", + "object-assign": "4.1.1", + "tapable": "0.2.8" + } + }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", + "dev": true + }, + "entities": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", + "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=", + "dev": true + }, + "errno": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.5.tgz", + "integrity": "sha512-tv2H+e3KBnMmNRuoVG24uorOj3XfYo+/nJJd07PUISRr0kaMKQKL5kyD+6ANXk1ZIIsvbORsjvHnCfC4KIc7uQ==", + "dev": true, + "requires": { + "prr": "1.0.1" + } + }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, + "es-abstract": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.10.0.tgz", + "integrity": "sha512-/uh/DhdqIOSkAWifU+8nG78vlQxdLckUdI/sPgy0VhuXi2qJ7T8czBmqIYtLQVpCIFYafChnsRsB5pyb1JdmCQ==", + "dev": true, + "requires": { + "es-to-primitive": "1.1.1", + "function-bind": "1.1.1", + "has": "1.0.1", + "is-callable": "1.1.3", + "is-regex": "1.0.4" + } + }, + "es-to-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", + "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", + "dev": true, + "requires": { + "is-callable": "1.1.3", + "is-date-object": "1.0.1", + "is-symbol": "1.0.1" + } + }, + "es5-ext": { + "version": "0.10.37", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.37.tgz", + "integrity": "sha1-DudB0Ui4AGm6J9AgOTdWryV978M=", + "dev": true, + "requires": { + "es6-iterator": "2.0.3", + "es6-symbol": "3.1.1" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.37", + "es6-symbol": "3.1.1" + } + }, + "es6-map": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", + "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.37", + "es6-iterator": "2.0.3", + "es6-set": "0.1.5", + "es6-symbol": "3.1.1", + "event-emitter": "0.3.5" + } + }, + "es6-promise": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.1.1.tgz", + "integrity": "sha512-OaU1hHjgJf+b0NzsxCg7NdIYERD6Hy/PEmFLTjw+b65scuisG3Kt4QoTvJ66BBkPZ581gr0kpoVzKnxniM8nng==", + "dev": true + }, + "es6-set": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", + "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.37", + "es6-iterator": "2.0.3", + "es6-symbol": "3.1.1", + "event-emitter": "0.3.5" + } + }, + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.37" + } + }, + "es6-weak-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", + "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.37", + "es6-iterator": "2.0.3", + "es6-symbol": "3.1.1" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escope": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", + "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", + "dev": true, + "requires": { + "es6-map": "0.1.5", + "es6-weak-map": "2.0.2", + "esrecurse": "4.2.0", + "estraverse": "4.2.0" + } + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "esrecurse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.0.tgz", + "integrity": "sha1-+pVo2Y04I/mkHZHpAtyrnqblsWM=", + "dev": true, + "requires": { + "estraverse": "4.2.0", + "object-assign": "4.1.1" + } + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.37" + } + }, + "eventemitter3": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz", + "integrity": "sha1-HIaZHYFq0eUEdQ5zh0Ik7PO+xQg=", + "dev": true + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "dev": true + }, + "eventsource": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-0.1.6.tgz", + "integrity": "sha1-Cs7ehJ7X3RzMMsgRuxG5RNTykjI=", + "dev": true, + "requires": { + "original": "1.0.0" + } + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "requires": { + "md5.js": "1.3.4", + "safe-buffer": "5.1.1" + } + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, + "requires": { + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "4.1.1", + "shebang-command": "1.2.0", + "which": "1.3.0" + } + } + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "expand-braces": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/expand-braces/-/expand-braces-0.1.2.tgz", + "integrity": "sha1-SIsdHSRRyz06axks/AMPRMWFX+o=", + "dev": true, + "requires": { + "array-slice": "0.2.3", + "array-unique": "0.2.1", + "braces": "0.1.5" + }, + "dependencies": { + "braces": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-0.1.5.tgz", + "integrity": "sha1-wIVxEIUpHYt1/ddOqw+FlygHEeY=", + "dev": true, + "requires": { + "expand-range": "0.1.1" + } + }, + "expand-range": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-0.1.1.tgz", + "integrity": "sha1-TLjtoJk8pW+k9B/ELzy7TMrf8EQ=", + "dev": true, + "requires": { + "is-number": "0.1.1", + "repeat-string": "0.2.2" + } + }, + "is-number": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-0.1.1.tgz", + "integrity": "sha1-aaevEWlj1HIG7JvZtIoUIW8eOAY=", + "dev": true + }, + "repeat-string": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-0.2.2.tgz", + "integrity": "sha1-x6jTI2BoNiBZp+RlH8aITosftK4=", + "dev": true + } + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true, + "requires": { + "fill-range": "2.2.3" + } + }, + "exports-loader": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/exports-loader/-/exports-loader-0.6.4.tgz", + "integrity": "sha1-1w/GEhl1s1/BKDDPUnVL4nQPyIY=", + "dev": true, + "requires": { + "loader-utils": "1.1.0", + "source-map": "0.5.7" + } + }, + "express": { + "version": "4.16.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.2.tgz", + "integrity": "sha1-41xt/i1kt9ygpc1PIXgb4ymeB2w=", + "dev": true, + "requires": { + "accepts": "1.3.4", + "array-flatten": "1.1.1", + "body-parser": "1.18.2", + "content-disposition": "0.5.2", + "content-type": "1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "1.1.1", + "encodeurl": "1.0.1", + "escape-html": "1.0.3", + "etag": "1.8.1", + "finalhandler": "1.1.0", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "1.1.2", + "on-finished": "2.3.0", + "parseurl": "1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "2.0.2", + "qs": "6.5.1", + "range-parser": "1.2.0", + "safe-buffer": "5.1.1", + "send": "0.16.1", + "serve-static": "1.13.1", + "setprototypeof": "1.1.0", + "statuses": "1.3.1", + "type-is": "1.6.15", + "utils-merge": "1.0.1", + "vary": "1.1.2" + }, + "dependencies": { + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", + "dev": true + } + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + }, + "dependencies": { + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + } + } + }, + "extract-text-webpack-plugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.0.tgz", + "integrity": "sha1-kMqnkHvESfM1AF46x1MrQbAN5hI=", + "dev": true, + "requires": { + "async": "2.6.0", + "loader-utils": "1.1.0", + "schema-utils": "0.3.0", + "webpack-sources": "1.1.0" + } + }, + "extract-zip": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.6.tgz", + "integrity": "sha1-EpDt6NINCHK0Kf0/NRyhKOxe+Fw=", + "dev": true, + "requires": { + "concat-stream": "1.6.0", + "debug": "2.6.9", + "mkdirp": "0.5.0", + "yauzl": "2.4.1" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz", + "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", + "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, + "fastparse": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.1.tgz", + "integrity": "sha1-0eJkOzipTXWDtHkGDmxK/8lAcfg=", + "dev": true + }, + "faye-websocket": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", + "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", + "dev": true, + "requires": { + "websocket-driver": "0.7.0" + } + }, + "fd-slicer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "dev": true, + "requires": { + "pend": "1.2.0" + } + }, + "file-loader": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-1.1.5.tgz", + "integrity": "sha512-RzGHDatcVNpGISTvCpfUfOGpYuSR7HSsSg87ki+wF6rw1Hm0RALPTiAdsxAq1UwLf0RRhbe22/eHK6nhXspiOQ==", + "dev": true, + "requires": { + "loader-utils": "1.1.0", + "schema-utils": "0.3.0" + } + }, + "file-saver": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-1.3.3.tgz", + "integrity": "sha1-zdTETTqiZOrC9o7BZbx5HDSvEjI=" + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true + }, + "fileset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", + "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=", + "dev": true, + "requires": { + "glob": "7.1.2", + "minimatch": "3.0.4" + } + }, + "filesize": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.0.tgz", + "integrity": "sha512-g5OWtoZWcPI56js1DFhIEqyG9tnu/7sG3foHwgS9KGYFMfsYguI3E+PRVCmtmE96VajQIEMRU2OhN+ME589Gdw==", + "dev": true + }, + "fill-range": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", + "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "dev": true, + "requires": { + "is-number": "2.1.0", + "isobject": "2.1.0", + "randomatic": "1.1.7", + "repeat-element": "1.1.2", + "repeat-string": "1.6.1" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "finalhandler": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", + "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "1.0.1", + "escape-html": "1.0.3", + "on-finished": "2.3.0", + "parseurl": "1.3.2", + "statuses": "1.3.1", + "unpipe": "1.0.0" + } + }, + "find-cache-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz", + "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=", + "dev": true, + "requires": { + "commondir": "1.0.1", + "make-dir": "1.1.0", + "pkg-dir": "2.0.0" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + }, + "flatten": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz", + "integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=", + "dev": true + }, + "flush-write-stream": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.2.tgz", + "integrity": "sha1-yBuQ2HRnZvGmCaRoCZRsRd2K5Bc=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.3" + } + }, + "font-awesome": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz", + "integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM=" + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "dev": true, + "requires": { + "for-in": "1.0.2" + } + }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "dev": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.17" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "dev": true + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.3" + } + }, + "fs-access": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz", + "integrity": "sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o=", + "dev": true, + "requires": { + "null-check": "1.0.0" + } + }, + "fs-extra": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", + "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "4.0.0", + "universalify": "0.1.1" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "iferr": "0.1.5", + "imurmurhash": "0.1.4", + "readable-stream": "2.3.3" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz", + "integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==", + "dev": true, + "optional": true, + "requires": { + "nan": "2.8.0", + "node-pre-gyp": "0.6.39" + }, + "dependencies": { + "abbrev": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz", + "integrity": "sha1-0FVMIlZjbi9W58LlrRg/hZQo2B8=", + "dev": true, + "optional": true + }, + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true, + "optional": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "aproba": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.1.1.tgz", + "integrity": "sha1-ldNgDwdxCqDpKYxyatXs8urLq6s=", + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", + "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", + "dev": true, + "optional": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.2.9" + } + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "dev": true, + "optional": true + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", + "dev": true, + "optional": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true, + "optional": true + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", + "dev": true, + "optional": true + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", + "dev": true, + "optional": true + }, + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz", + "integrity": "sha1-Pv/DxQ4ABTH7cg6v+A8K6O8jz1k=", + "dev": true, + "requires": { + "balanced-match": "0.4.2", + "concat-map": "0.0.1" + } + }, + "buffer-shims": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", + "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true, + "optional": true + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "combined-stream": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "dev": true, + "requires": { + "boom": "2.10.1" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", + "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=", + "dev": true, + "optional": true + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.2.tgz", + "integrity": "sha1-ca1dIEvxempsqPRQxhRUBm70YeE=", + "dev": true, + "optional": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true, + "optional": true + }, + "extsprintf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", + "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true, + "optional": true + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "dev": true, + "optional": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.15" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fstream": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.1" + } + }, + "fstream-ignore": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", + "integrity": "sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=", + "dev": true, + "optional": true, + "requires": { + "fstream": "1.0.11", + "inherits": "2.0.3", + "minimatch": "3.0.4" + } + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "optional": true, + "requires": { + "aproba": "1.1.1", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=", + "dev": true, + "optional": true + }, + "har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", + "dev": true, + "optional": true, + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true, + "optional": true + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "dev": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", + "dev": true + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.0", + "sshpk": "1.13.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ini": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", + "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=", + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true, + "optional": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true, + "optional": true + }, + "jodid25519": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz", + "integrity": "sha1-BtSRIlUJNBlHfUJWM2BuDpB4KWc=", + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true, + "optional": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "optional": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true, + "optional": true + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true, + "optional": true + }, + "jsprim": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz", + "integrity": "sha1-o7h+QCmNjDgFUtjMdiigu5WiKRg=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.0.2", + "json-schema": "0.2.3", + "verror": "1.3.6" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "mime-db": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz", + "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=", + "dev": true + }, + "mime-types": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", + "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=", + "dev": true, + "requires": { + "mime-db": "1.27.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true, + "optional": true + }, + "node-pre-gyp": { + "version": "0.6.39", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz", + "integrity": "sha512-OsJV74qxnvz/AMGgcfZoDaeDXKD3oY3QVIbBmwszTFkRisTSXbMQyn4UWzUMOtA5SVhrBZOTp0wcoSBgfMfMmQ==", + "dev": true, + "optional": true, + "requires": { + "detect-libc": "1.0.2", + "hawk": "3.1.3", + "mkdirp": "0.5.1", + "nopt": "4.0.1", + "npmlog": "4.1.0", + "rc": "1.2.1", + "request": "2.81.0", + "rimraf": "2.6.1", + "semver": "5.3.0", + "tar": "2.2.1", + "tar-pack": "3.4.0" + } + }, + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "dev": true, + "optional": true, + "requires": { + "abbrev": "1.1.0", + "osenv": "0.1.4" + } + }, + "npmlog": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.0.tgz", + "integrity": "sha512-ocolIkZYZt8UveuiDS0yAkkIjid1o7lPG8cYm05yNYzBn8ykQtaiPMEGp8fY9tKdDgm8okpdKzkvu1y9hUYugA==", + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz", + "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=", + "dev": true, + "optional": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=", + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true, + "optional": true + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=", + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.1.tgz", + "integrity": "sha1-LgPo5C7kULjLPc5lvhv4l04d/ZU=", + "dev": true, + "optional": true, + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.4", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.9.tgz", + "integrity": "sha1-z3jsb0ptHrQ9JkiMrJfwQudLf8g=", + "dev": true, + "requires": { + "buffer-shims": "1.0.0", + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "1.0.1", + "util-deprecate": "1.0.2" + } + }, + "request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "dev": true, + "optional": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.15", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.0.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.2", + "tunnel-agent": "0.6.0", + "uuid": "3.0.1" + } + }, + "rimraf": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", + "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=", + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", + "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c=", + "dev": true + }, + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true, + "optional": true + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "sshpk": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.0.tgz", + "integrity": "sha1-/yo+T9BEl1Vf7Zezmg/YL6+zozw=", + "dev": true, + "optional": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jodid25519": "1.0.2", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.1.tgz", + "integrity": "sha1-YuIA8DmVWmgQ2N8KM//A8BNmLZg=", + "dev": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", + "dev": true, + "optional": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true, + "optional": true + }, + "tar": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "dev": true, + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "tar-pack": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.0.tgz", + "integrity": "sha1-I74tf2cagzk3bL2wuP4/3r8xeYQ=", + "dev": true, + "optional": true, + "requires": { + "debug": "2.6.8", + "fstream": "1.0.11", + "fstream-ignore": "1.0.5", + "once": "1.4.0", + "readable-stream": "2.2.9", + "rimraf": "2.6.1", + "tar": "2.2.1", + "uid-number": "0.0.6" + } + }, + "tough-cookie": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", + "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=", + "dev": true, + "optional": true, + "requires": { + "punycode": "1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, + "optional": true + }, + "uid-number": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", + "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=", + "dev": true, + "optional": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz", + "integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE=", + "dev": true, + "optional": true + }, + "verror": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", + "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=", + "dev": true, + "optional": true, + "requires": { + "extsprintf": "1.0.2" + } + }, + "wide-align": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", + "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", + "dev": true, + "optional": true, + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } + }, + "fstream": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.2" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "requires": { + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "gaze": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.2.tgz", + "integrity": "sha1-hHIkZ3rbiHDWeSV+0ziP22HkAQU=", + "dev": true, + "optional": true, + "requires": { + "globule": "1.2.0" + } + }, + "generate-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", + "dev": true, + "optional": true + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true, + "optional": true, + "requires": { + "is-property": "1.0.2" + } + }, + "get-caller-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", + "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=", + "dev": true + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "requires": { + "glob-parent": "2.0.0", + "is-glob": "2.0.1" + }, + "dependencies": { + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + } + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "2.0.1" + }, + "dependencies": { + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + } + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "requires": { + "array-union": "1.0.2", + "glob": "7.1.2", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "globule": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.0.tgz", + "integrity": "sha1-HcScaCLdnoovoAuiopUAboZkvQk=", + "dev": true, + "optional": true, + "requires": { + "glob": "7.1.2", + "lodash": "4.17.4", + "minimatch": "3.0.4" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "gzip-size": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-4.1.0.tgz", + "integrity": "sha1-iuCWJX6r59acRb4rZ8RIEk/7UXw=", + "dev": true, + "requires": { + "duplexer": "0.1.1", + "pify": "3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "handle-thing": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-1.2.5.tgz", + "integrity": "sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ=", + "dev": true + }, + "handlebars": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", + "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", + "dev": true, + "requires": { + "async": "1.5.2", + "optimist": "0.6.1", + "source-map": "0.4.4", + "uglify-js": "2.8.29" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true, + "optional": true + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "optional": true, + "requires": { + "center-align": "0.1.3", + "right-align": "0.1.3", + "wordwrap": "0.0.2" + } + }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dev": true, + "optional": true, + "requires": { + "source-map": "0.5.7", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "optional": true + } + } + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "optional": true, + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + } + } + } + }, + "har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=", + "dev": true + }, + "har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", + "dev": true, + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + }, + "dependencies": { + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + } + } + }, + "has": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", + "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", + "dev": true, + "requires": { + "function-bind": "1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-binary": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.7.tgz", + "integrity": "sha1-aOYesWIQyVRaClzOBqhzkS/h5ow=", + "dev": true, + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=", + "dev": true + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, + "hash-base": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", + "integrity": "sha1-ZuodhW206KVHDK32/OI65SRO8uE=", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "hash.js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", + "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", + "dev": true, + "requires": { + "inherits": "2.0.3", + "minimalistic-assert": "1.0.0" + } + }, + "hasha": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-2.2.0.tgz", + "integrity": "sha1-eNfL/B5tZjA/55g3NlmEUXsvbuE=", + "dev": true, + "requires": { + "is-stream": "1.1.0", + "pinkie-promise": "2.0.1" + } + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "dev": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "1.1.3", + "minimalistic-assert": "1.0.0", + "minimalistic-crypto-utils": "1.0.1" + } + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", + "dev": true + }, + "homedir-polyfill": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", + "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", + "dev": true, + "requires": { + "parse-passwd": "1.0.0" + } + }, + "hosted-git-info": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", + "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==", + "dev": true + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "obuf": "1.1.1", + "readable-stream": "2.3.3", + "wbuf": "1.7.2" + } + }, + "html-comment-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.1.tgz", + "integrity": "sha1-ZouTd26q5V696POtRkswekljYl4=", + "dev": true + }, + "html-entities": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", + "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=", + "dev": true + }, + "html-minifier": { + "version": "3.5.7", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.7.tgz", + "integrity": "sha512-GISXn6oKDo7+gVpKOgZJTbHMCUI2TSGfpg/8jgencWhWJsvEmsvp3M8emX7QocsXsYznWloLib3OeSfeyb/ewg==", + "dev": true, + "requires": { + "camel-case": "3.0.0", + "clean-css": "4.1.9", + "commander": "2.12.2", + "he": "1.1.1", + "ncname": "1.0.0", + "param-case": "2.1.1", + "relateurl": "0.2.7", + "uglify-js": "3.2.2" + } + }, + "html-webpack-plugin": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-2.30.1.tgz", + "integrity": "sha1-f5xCG36pHsRg9WUn1430hO51N9U=", + "dev": true, + "requires": { + "bluebird": "3.5.1", + "html-minifier": "3.5.7", + "loader-utils": "0.2.17", + "lodash": "4.17.4", + "pretty-error": "2.1.1", + "toposort": "1.0.6" + }, + "dependencies": { + "loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "dev": true, + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1", + "object-assign": "4.1.1" + } + } + } + }, + "htmlparser2": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.3.0.tgz", + "integrity": "sha1-zHDQWln2VC5D8OaFyYLhTJJKnv4=", + "dev": true, + "requires": { + "domelementtype": "1.3.0", + "domhandler": "2.1.0", + "domutils": "1.1.6", + "readable-stream": "1.0.34" + }, + "dependencies": { + "domutils": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.1.6.tgz", + "integrity": "sha1-vdw94Jm5ou+sxRxiPyj0FuzFdIU=", + "dev": true, + "requires": { + "domelementtype": "1.3.0" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", + "dev": true + }, + "http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "dev": true, + "requires": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": "1.3.1" + }, + "dependencies": { + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=", + "dev": true + } + } + }, + "http-parser-js": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.9.tgz", + "integrity": "sha1-6hoE+2St/wJC6ZdPKX3Uw8rSceE=", + "dev": true + }, + "http-proxy": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.16.2.tgz", + "integrity": "sha1-Bt/ykpUr9k2+hHH6nfcwZtTzd0I=", + "dev": true, + "requires": { + "eventemitter3": "1.2.0", + "requires-port": "1.0.0" + } + }, + "http-proxy-middleware": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz", + "integrity": "sha1-ZC6ISIUdZvCdTxJJEoRtuutBuDM=", + "dev": true, + "requires": { + "http-proxy": "1.16.2", + "is-glob": "3.1.0", + "lodash": "4.17.4", + "micromatch": "2.3.11" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "2.1.1" + } + } + } + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "dev": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "https-proxy-agent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-1.0.0.tgz", + "integrity": "sha1-NffabEjOTdv6JkiRrFk+5f+GceY=", + "dev": true, + "requires": { + "agent-base": "2.1.1", + "debug": "2.6.9", + "extend": "3.0.1" + } + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", + "dev": true + }, + "icss-replace-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", + "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=", + "dev": true + }, + "icss-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-2.1.0.tgz", + "integrity": "sha1-g/Cg7DeL8yRheLbCrZE28TWxyWI=", + "dev": true, + "requires": { + "postcss": "6.0.14" + }, + "dependencies": { + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "postcss": { + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.14.tgz", + "integrity": "sha512-NJ1z0f+1offCgadPhz+DvGm5Mkci+mmV5BqD13S992o0Xk9eElxUfPPF+t2ksH5R/17gz4xVK8KWocUQ5o3Rog==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "source-map": "0.6.1", + "supports-color": "4.5.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "ieee754": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", + "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=", + "dev": true + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "dev": true + }, + "image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", + "dev": true, + "optional": true + }, + "img-stats": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/img-stats/-/img-stats-0.5.2.tgz", + "integrity": "sha1-wgNJbELy2esuWrgjL6dWurMsnis=", + "dev": true, + "requires": { + "xmldom": "0.1.27" + } + }, + "import-local": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-0.1.1.tgz", + "integrity": "sha1-sReVcqrNwRxqkQCftDDbyrX2aKg=", + "dev": true, + "requires": { + "pkg-dir": "2.0.0", + "resolve-cwd": "2.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "in-publish": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", + "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=", + "dev": true, + "optional": true + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, + "internal-ip": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-1.2.0.tgz", + "integrity": "sha1-rp+/k7mEh4eF1QqN4bNWlWBYz1w=", + "dev": true, + "requires": { + "meow": "3.7.0" + } + }, + "interpret": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", + "dev": true + }, + "invariant": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz", + "integrity": "sha1-nh9WrArNtr8wMwbzOL47IErmA2A=", + "dev": true, + "requires": { + "loose-envify": "1.3.1" + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true + }, + "ipaddr.js": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.5.2.tgz", + "integrity": "sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A=", + "dev": true + }, + "is-absolute-url": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", + "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "1.11.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-callable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", + "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=", + "dev": true + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true, + "requires": { + "is-primitive": "2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", + "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "dev": true, + "requires": { + "is-extglob": "2.1.1" + } + }, + "is-my-json-valid": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz", + "integrity": "sha512-ochPsqWS1WXj8ZnMIV0vnNXooaMhp7cyL4FMSIPKTtnV0Ha/T19G2b9kkhcNsabV9bxYkze7/aLZJb/bYuFduQ==", + "dev": true, + "optional": true, + "requires": { + "generate-function": "2.0.0", + "generate-object-property": "1.2.0", + "jsonpointer": "4.0.1", + "xtend": "4.0.1" + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz", + "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=", + "dev": true, + "requires": { + "is-path-inside": "1.0.1" + } + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "1.0.2" + } + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "3.0.1" + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true, + "optional": true + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, + "requires": { + "has": "1.0.1" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-svg": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-2.1.0.tgz", + "integrity": "sha1-z2EJDaDZ77yrhyLeum8DIgjbsOk=", + "dev": true, + "requires": { + "html-comment-regex": "1.1.1" + } + }, + "is-symbol": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", + "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isbinaryfile": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.2.tgz", + "integrity": "sha1-Sj6XTsDLqQBNP8bN5yCeppNopiE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul-api": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.2.1.tgz", + "integrity": "sha512-oFCwXvd65amgaPCzqrR+a2XjanS1MvpXN6l/MlMUTv6uiA1NOgGX+I0uyq8Lg3GDxsxPsaP1049krz3hIJ5+KA==", + "dev": true, + "requires": { + "async": "2.6.0", + "fileset": "2.0.3", + "istanbul-lib-coverage": "1.1.1", + "istanbul-lib-hook": "1.1.0", + "istanbul-lib-instrument": "1.9.1", + "istanbul-lib-report": "1.1.2", + "istanbul-lib-source-maps": "1.2.2", + "istanbul-reports": "1.1.3", + "js-yaml": "3.7.0", + "mkdirp": "0.5.1", + "once": "1.4.0" + } + }, + "istanbul-instrumenter-loader": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/istanbul-instrumenter-loader/-/istanbul-instrumenter-loader-2.0.0.tgz", + "integrity": "sha1-5UkpAKsLuoNe+oAkywC+mz7qJwA=", + "dev": true, + "requires": { + "convert-source-map": "1.5.1", + "istanbul-lib-instrument": "1.9.1", + "loader-utils": "0.2.17", + "object-assign": "4.1.1" + }, + "dependencies": { + "loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "dev": true, + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1", + "object-assign": "4.1.1" + } + } + } + }, + "istanbul-lib-coverage": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz", + "integrity": "sha512-0+1vDkmzxqJIn5rcoEqapSB4DmPxE31EtI2dF2aCkV5esN9EWHxZ0dwgDClivMXJqE7zaYQxq30hj5L0nlTN5Q==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.1.0.tgz", + "integrity": "sha512-U3qEgwVDUerZ0bt8cfl3dSP3S6opBoOtk3ROO5f2EfBr/SRiD9FQqzwaZBqFORu8W7O0EXpai+k7kxHK13beRg==", + "dev": true, + "requires": { + "append-transform": "0.4.0" + } + }, + "istanbul-lib-instrument": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.9.1.tgz", + "integrity": "sha512-RQmXeQ7sphar7k7O1wTNzVczF9igKpaeGQAG9qR2L+BS4DCJNTI9nytRmIVYevwO0bbq+2CXvJmYDuz0gMrywA==", + "dev": true, + "requires": { + "babel-generator": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "istanbul-lib-coverage": "1.1.1", + "semver": "5.4.1" + } + }, + "istanbul-lib-report": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.2.tgz", + "integrity": "sha512-UTv4VGx+HZivJQwAo1wnRwe1KTvFpfi/NYwN7DcsrdzMXwpRT/Yb6r4SBPoHWj4VuQPakR32g4PUUeyKkdDkBA==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "1.1.1", + "mkdirp": "0.5.1", + "path-parse": "1.0.5", + "supports-color": "3.2.3" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.2.tgz", + "integrity": "sha512-8BfdqSfEdtip7/wo1RnrvLpHVEd8zMZEDmOFEnpC6dg0vXflHt9nvoAyQUzig2uMSXfF2OBEYBV3CVjIL9JvaQ==", + "dev": true, + "requires": { + "debug": "3.1.0", + "istanbul-lib-coverage": "1.1.1", + "mkdirp": "0.5.1", + "rimraf": "2.6.2", + "source-map": "0.5.7" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "istanbul-reports": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.1.3.tgz", + "integrity": "sha512-ZEelkHh8hrZNI5xDaKwPMFwDsUf5wIEI2bXAFGp1e6deR2mnEKBPhLJEgr4ZBt8Gi6Mj38E/C8kcy9XLggVO2Q==", + "dev": true, + "requires": { + "handlebars": "4.0.11" + } + }, + "jasmine": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-2.8.0.tgz", + "integrity": "sha1-awicChFXax8W3xG4AUbZHU6Lij4=", + "dev": true, + "requires": { + "exit": "0.1.2", + "glob": "7.1.2", + "jasmine-core": "2.8.0" + }, + "dependencies": { + "jasmine-core": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.8.0.tgz", + "integrity": "sha1-vMl5rh+f0FcB5F5S5l06XWPxok4=", + "dev": true + } + } + }, + "jasmine-core": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.6.4.tgz", + "integrity": "sha1-3skmzQqfoof7bbXHVfpIfnTOysU=", + "dev": true + }, + "jasmine-marbles": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/jasmine-marbles/-/jasmine-marbles-0.1.0.tgz", + "integrity": "sha1-yezcZOILbPVbSaECAaW+M5B9rcw=", + "dev": true, + "requires": { + "lodash.isequal": "4.5.0" + } + }, + "jasmine-spec-reporter": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-4.1.1.tgz", + "integrity": "sha1-Wm1Yq11hvqcwn7wnkjlRF1axtYg=", + "dev": true, + "requires": { + "colors": "1.1.2" + } + }, + "jasminewd2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/jasminewd2/-/jasminewd2-2.2.0.tgz", + "integrity": "sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4=", + "dev": true + }, + "js-base64": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.0.tgz", + "integrity": "sha512-Wehd+7Pf9tFvGb+ydPm9TjYjV8X1YHOVyG8QyELZxEMqOhemVwGRmoG8iQ/soqI3n8v4xn59zaLxiCJiaaRzKA==", + "dev": true + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.7.0.tgz", + "integrity": "sha1-XJZ93YN6m/3KXy3oQlOr6KHAO4A=", + "dev": true, + "requires": { + "argparse": "1.0.9", + "esprima": "2.7.3" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, + "optional": true + }, + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + }, + "json-loader": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz", + "integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json3": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "dev": true + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + } + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true, + "optional": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "karma": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/karma/-/karma-1.7.1.tgz", + "integrity": "sha512-k5pBjHDhmkdaUccnC7gE3mBzZjcxyxYsYVaqiL2G5AqlfLyBO5nw2VdNK+O16cveEPd/gIOWULH7gkiYYwVNHg==", + "dev": true, + "requires": { + "bluebird": "3.5.1", + "body-parser": "1.18.2", + "chokidar": "1.7.0", + "colors": "1.1.2", + "combine-lists": "1.0.1", + "connect": "3.6.5", + "core-js": "2.5.2", + "di": "0.0.1", + "dom-serialize": "2.2.1", + "expand-braces": "0.1.2", + "glob": "7.1.2", + "graceful-fs": "4.1.11", + "http-proxy": "1.16.2", + "isbinaryfile": "3.0.2", + "lodash": "3.10.1", + "log4js": "0.6.38", + "mime": "1.6.0", + "minimatch": "3.0.4", + "optimist": "0.6.1", + "qjobs": "1.1.5", + "range-parser": "1.2.0", + "rimraf": "2.6.2", + "safe-buffer": "5.1.1", + "socket.io": "1.7.3", + "source-map": "0.5.7", + "tmp": "0.0.31", + "useragent": "2.2.1" + }, + "dependencies": { + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + } + } + }, + "karma-chrome-launcher": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-2.1.1.tgz", + "integrity": "sha1-IWh5xorATY1RQOmWGboEtZr9Rs8=", + "dev": true, + "requires": { + "fs-access": "1.0.1", + "which": "1.3.0" + } + }, + "karma-cli": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/karma-cli/-/karma-cli-1.0.1.tgz", + "integrity": "sha1-rmw8WKMTodALRRZMRVubhs4X+WA=", + "dev": true, + "requires": { + "resolve": "1.5.0" + } + }, + "karma-coverage-istanbul-reporter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-1.3.0.tgz", + "integrity": "sha1-0ULNnFVzHJ42Pvc3To7xoxvr+ts=", + "dev": true, + "requires": { + "istanbul-api": "1.2.1", + "minimatch": "3.0.4" + } + }, + "karma-jasmine": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-1.1.1.tgz", + "integrity": "sha1-b+hA51oRYAydkehLM8RY4cRqNSk=", + "dev": true + }, + "karma-jasmine-html-reporter": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-0.2.2.tgz", + "integrity": "sha1-SKjl7xiAdhfuK14zwRlMNbQ5Ukw=", + "dev": true, + "requires": { + "karma-jasmine": "1.1.1" + } + }, + "karma-phantomjs-launcher": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/karma-phantomjs-launcher/-/karma-phantomjs-launcher-1.0.4.tgz", + "integrity": "sha1-0jyjSAG9qYY60xjju0vUBisTrNI=", + "dev": true, + "requires": { + "lodash": "4.17.4", + "phantomjs-prebuilt": "2.1.16" + } + }, + "karma-source-map-support": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.2.0.tgz", + "integrity": "sha1-G/gee7SwiWJ6s1LsQXnhF8QGpUA=", + "dev": true, + "requires": { + "source-map-support": "0.4.18" + } + }, + "karma-spec-reporter": { + "version": "0.0.31", + "resolved": "https://registry.npmjs.org/karma-spec-reporter/-/karma-spec-reporter-0.0.31.tgz", + "integrity": "sha1-SDDccUihVcfXoYbmMjOaDYD63sM=", + "dev": true, + "requires": { + "colors": "1.1.2" + } + }, + "kew": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz", + "integrity": "sha1-edk9LTM2PW/dKXCzNdkUGtWR15s=", + "dev": true + }, + "killable": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.0.tgz", + "integrity": "sha1-2ouEvUfeU5WHj5XWTQLyRJ/gXms=", + "dev": true + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + }, + "klaw": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", + "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + } + }, + "lazy-cache": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", + "integrity": "sha1-f+3fLctu23fRHvHRF6tf/fCrG2U=", + "dev": true + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "requires": { + "invert-kv": "1.0.0" + } + }, + "less": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/less/-/less-2.7.3.tgz", + "integrity": "sha512-KPdIJKWcEAb02TuJtaLrhue0krtRLoRoo7x6BNJIBelO00t/CCdJQUnHW5V34OnHMWzIktSalJxRO+FvytQlCQ==", + "dev": true, + "requires": { + "errno": "0.1.5", + "graceful-fs": "4.1.11", + "image-size": "0.5.5", + "mime": "1.6.0", + "mkdirp": "0.5.1", + "promise": "7.3.1", + "request": "2.81.0", + "source-map": "0.5.7" + } + }, + "less-loader": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-4.0.5.tgz", + "integrity": "sha1-rhVadAbKxqzSk9eFWH/P8PR4xN0=", + "dev": true, + "requires": { + "clone": "2.1.1", + "loader-utils": "1.1.0", + "pify": "2.3.0" + }, + "dependencies": { + "clone": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", + "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=", + "dev": true + } + } + }, + "license-webpack-plugin": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-1.1.1.tgz", + "integrity": "sha512-TjKOyiC0exqd4Idy/4M8/DETR22dXBZks387DuS5LbslxHiMRXGx/Q2F/j9IUtvEoH5uFvt72vRgk/G6f8j3Dg==", + "dev": true, + "requires": { + "ejs": "2.5.7" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + } + }, + "loader-runner": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.0.tgz", + "integrity": "sha1-9IKuqC1UPgeSFwDVpG7yb9rGuKI=", + "dev": true + }, + "loader-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "dev": true, + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "2.0.0", + "path-exists": "3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", + "dev": true + }, + "lodash.assign": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", + "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=", + "dev": true, + "optional": true + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", + "dev": true + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "dev": true + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", + "dev": true + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "dev": true + }, + "lodash.mergewith": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz", + "integrity": "sha1-FQzwoWeR9ZA7iJHqsVRgknS96lU=", + "dev": true, + "optional": true + }, + "lodash.tail": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.tail/-/lodash.tail-4.1.1.tgz", + "integrity": "sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=", + "dev": true + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "dev": true + }, + "log4js": { + "version": "0.6.38", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-0.6.38.tgz", + "integrity": "sha1-LElBFmldb7JUgJQ9P8hy5mKlIv0=", + "dev": true, + "requires": { + "readable-stream": "1.0.34", + "semver": "4.3.6" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "semver": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", + "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", + "dev": true + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "loglevel": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.0.tgz", + "integrity": "sha1-rgyqVhERSYxboTcj1vtjHSQAOTQ=", + "dev": true + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true + }, + "loose-envify": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "dev": true, + "requires": { + "js-tokens": "3.0.2" + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "0.4.1", + "signal-exit": "3.0.2" + } + }, + "lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=", + "dev": true + }, + "lru-cache": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", + "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", + "dev": true, + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "macaddress": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/macaddress/-/macaddress-0.2.8.tgz", + "integrity": "sha1-WQTcU3w57G2+/q6QIycTX6hRHxI=", + "dev": true + }, + "magic-string": { + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.4.tgz", + "integrity": "sha512-kxBL06p6iO2qPBHsqGK2b3cRwiRGpnmSuVWNhwHcMX7qJOUr1HvricYP1LZOCdkQBUp0jiWg2d6WJwR3vYgByw==", + "dev": true, + "requires": { + "vlq": "0.2.3" + } + }, + "make-dir": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.1.0.tgz", + "integrity": "sha512-0Pkui4wLJ7rxvmfUvs87skoEaxmu0hCUApF8nonzpl7q//FWp9zu8W61Scz4sd/kUiqDxvUhtoam2efDyiBzcA==", + "dev": true, + "requires": { + "pify": "3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "make-error": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.0.tgz", + "integrity": "sha1-Uq06M5zPEM5itAQLcI/nByRLi5Y=", + "dev": true + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "math-expression-evaluator": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz", + "integrity": "sha1-3oGf282E3M2PrlnGrreWFbnSZqw=", + "dev": true + }, + "md5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", + "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=", + "dev": true, + "requires": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "1.1.6" + } + }, + "md5.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", + "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "dev": true, + "requires": { + "hash-base": "3.0.4", + "inherits": "2.0.3" + }, + "dependencies": { + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.1" + } + } + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "mem": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "dev": true, + "requires": { + "mimic-fn": "1.1.0" + } + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "0.1.5", + "readable-stream": "2.3.3" + } + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "2.1.0", + "decamelize": "1.2.0", + "loud-rejection": "1.6.0", + "map-obj": "1.0.1", + "minimist": "1.2.0", + "normalize-package-data": "2.4.0", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "redent": "1.0.0", + "trim-newlines": "1.0.0" + } + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.4" + }, + "dependencies": { + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + } + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "brorand": "1.1.0" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", + "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=", + "dev": true + }, + "mime-types": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "dev": true, + "requires": { + "mime-db": "1.30.0" + } + }, + "mimic-fn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.1.0.tgz", + "integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg=", + "dev": true + }, + "minimalistic-assert": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz", + "integrity": "sha1-cCvi3aazf0g2vLP121ZkG2Sh09M=", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "mississippi": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-1.3.0.tgz", + "integrity": "sha1-0gFYPrEjJ+PFwWQqQEqcrPlONPU=", + "dev": true, + "requires": { + "concat-stream": "1.6.0", + "duplexify": "3.5.1", + "end-of-stream": "1.4.0", + "flush-write-stream": "1.0.2", + "from2": "2.3.0", + "parallel-transform": "1.1.0", + "pump": "1.0.3", + "pumpify": "1.3.5", + "stream-each": "1.2.2", + "through2": "2.0.3" + } + }, + "mixin-object": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", + "integrity": "sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=", + "dev": true, + "requires": { + "for-in": "0.1.8", + "is-extendable": "0.1.1" + }, + "dependencies": { + "for-in": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", + "integrity": "sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=", + "dev": true + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "dev": true, + "requires": { + "aproba": "1.2.0", + "copy-concurrently": "1.0.5", + "fs-write-stream-atomic": "1.0.10", + "mkdirp": "0.5.1", + "rimraf": "2.6.2", + "run-queue": "1.0.3" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "multicast-dns": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.1.tgz", + "integrity": "sha512-uV3/ckdsffHx9IrGQrx613mturMdMqQ06WTq+C09NsStJ9iNG6RcUWgPKs1Rfjy+idZT6tfQoXEusGNnEZhT3w==", + "dev": true, + "requires": { + "dns-packet": "1.2.2", + "thunky": "0.1.0" + } + }, + "multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", + "dev": true + }, + "nan": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", + "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=", + "dev": true, + "optional": true + }, + "ncname": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ncname/-/ncname-1.0.0.tgz", + "integrity": "sha1-W1etGLHKCShk72Kwse2BlPODtxw=", + "dev": true, + "requires": { + "xml-char-classes": "1.0.0" + } + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", + "dev": true + }, + "no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "dev": true, + "requires": { + "lower-case": "1.1.4" + } + }, + "node-forge": { + "version": "0.6.33", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.6.33.tgz", + "integrity": "sha1-RjgRh59XPUUVWtap9D3ClujoXrw=", + "dev": true + }, + "node-gyp": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.6.2.tgz", + "integrity": "sha1-m/vlRWIoYoSDjnUOrAUpWFP6HGA=", + "dev": true, + "optional": true, + "requires": { + "fstream": "1.0.11", + "glob": "7.1.2", + "graceful-fs": "4.1.11", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "nopt": "3.0.6", + "npmlog": "4.1.2", + "osenv": "0.1.4", + "request": "2.81.0", + "rimraf": "2.6.2", + "semver": "5.3.0", + "tar": "2.2.1", + "which": "1.3.0" + }, + "dependencies": { + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "optional": true, + "requires": { + "abbrev": "1.1.1" + } + }, + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true, + "optional": true + } + } + }, + "node-libs-browser": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz", + "integrity": "sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg==", + "dev": true, + "requires": { + "assert": "1.4.1", + "browserify-zlib": "0.2.0", + "buffer": "4.9.1", + "console-browserify": "1.1.0", + "constants-browserify": "1.0.0", + "crypto-browserify": "3.12.0", + "domain-browser": "1.1.7", + "events": "1.1.1", + "https-browserify": "1.0.0", + "os-browserify": "0.3.0", + "path-browserify": "0.0.0", + "process": "0.11.10", + "punycode": "1.4.1", + "querystring-es3": "0.2.1", + "readable-stream": "2.3.3", + "stream-browserify": "2.0.1", + "stream-http": "2.7.2", + "string_decoder": "1.0.3", + "timers-browserify": "2.0.4", + "tty-browserify": "0.0.0", + "url": "0.11.0", + "util": "0.10.3", + "vm-browserify": "0.0.4" + } + }, + "node-modules-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/node-modules-path/-/node-modules-path-1.0.1.tgz", + "integrity": "sha1-QAlrCM560OoUaAhjr0ScfHWl0cg=", + "dev": true + }, + "node-sass": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.7.2.tgz", + "integrity": "sha512-CaV+wLqZ7//Jdom5aUFCpGNoECd7BbNhjuwdsX/LkXBrHl8eb1Wjw4HvWqcFvhr5KuNgAk8i/myf/MQ1YYeroA==", + "dev": true, + "optional": true, + "requires": { + "async-foreach": "0.1.3", + "chalk": "1.1.3", + "cross-spawn": "3.0.1", + "gaze": "1.1.2", + "get-stdin": "4.0.1", + "glob": "7.1.2", + "in-publish": "2.0.0", + "lodash.assign": "4.2.0", + "lodash.clonedeep": "4.5.0", + "lodash.mergewith": "4.6.0", + "meow": "3.7.0", + "mkdirp": "0.5.1", + "nan": "2.8.0", + "node-gyp": "3.6.2", + "npmlog": "4.1.2", + "request": "2.79.0", + "sass-graph": "2.2.4", + "stdout-stream": "1.4.0", + "true-case-path": "1.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "caseless": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", + "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", + "dev": true, + "optional": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "har-validator": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", + "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", + "dev": true, + "optional": true, + "requires": { + "chalk": "1.1.3", + "commander": "2.12.2", + "is-my-json-valid": "2.16.1", + "pinkie-promise": "2.0.1" + } + }, + "qs": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz", + "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=", + "dev": true, + "optional": true + }, + "request": { + "version": "2.79.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz", + "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=", + "dev": true, + "optional": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.11.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "2.0.6", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.17", + "oauth-sign": "0.8.2", + "qs": "6.3.2", + "stringstream": "0.0.5", + "tough-cookie": "2.3.3", + "tunnel-agent": "0.4.3", + "uuid": "3.1.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "tunnel-agent": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=", + "dev": true, + "optional": true + } + } + }, + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "dev": true, + "requires": { + "abbrev": "1.1.1", + "osenv": "0.1.4" + } + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "dev": true, + "requires": { + "hosted-git-info": "2.5.0", + "is-builtin-module": "1.0.0", + "semver": "5.4.1", + "validate-npm-package-license": "3.0.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "1.1.0" + } + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true + }, + "normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", + "dev": true, + "requires": { + "object-assign": "4.1.1", + "prepend-http": "1.0.4", + "query-string": "4.3.4", + "sort-keys": "1.1.2" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "2.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "nth-check": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", + "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", + "dev": true, + "requires": { + "boolbase": "1.0.0" + } + }, + "null-check": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/null-check/-/null-check-1.0.0.tgz", + "integrity": "sha1-l33/1xdgErnsMNKjnbXPcqBDnt0=", + "dev": true + }, + "num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", + "dev": true + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=", + "dev": true + }, + "object-keys": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", + "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=", + "dev": true + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true, + "requires": { + "for-own": "0.1.5", + "is-extendable": "0.1.1" + }, + "dependencies": { + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "requires": { + "for-in": "1.0.2" + } + } + } + }, + "obuf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.1.tgz", + "integrity": "sha1-EEEktsYCxnlogaBCVB0220OlJk4=", + "dev": true + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", + "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "opener": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.4.3.tgz", + "integrity": "sha1-XG2ixdflgx6P+jlklQ+NZnSskLg=", + "dev": true + }, + "opn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.1.0.tgz", + "integrity": "sha512-iPNl7SyM8L30Rm1sjGdLLheyHVw5YXVfi3SKWJzBI7efxRwHojfRFjwE/OLM6qp9xJYMgab8WicTU1cPoY+Hpg==", + "dev": true, + "requires": { + "is-wsl": "1.1.0" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "0.0.10", + "wordwrap": "0.0.2" + }, + "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", + "dev": true + } + } + }, + "options": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", + "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=", + "dev": true + }, + "original": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/original/-/original-1.0.0.tgz", + "integrity": "sha1-kUf5P6FpbQS+YeAb1QuurKZWvTs=", + "dev": true, + "requires": { + "url-parse": "1.0.5" + }, + "dependencies": { + "url-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.0.5.tgz", + "integrity": "sha1-CFSGBCKv3P7+tsllxmLUgAFpkns=", + "dev": true, + "requires": { + "querystringify": "0.0.4", + "requires-port": "1.0.0" + } + } + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "dev": true, + "requires": { + "lcid": "1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "osenv": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz", + "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=", + "dev": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.1.0.tgz", + "integrity": "sha1-sH/y2aXYi+yAYDWJWiurZqJ5iLw=", + "dev": true + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "1.1.0" + } + }, + "p-map": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", + "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", + "dev": true + }, + "pako": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", + "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==", + "dev": true + }, + "parallel-transform": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz", + "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", + "dev": true, + "requires": { + "cyclist": "0.2.2", + "inherits": "2.0.3", + "readable-stream": "2.3.3" + } + }, + "param-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", + "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", + "dev": true, + "requires": { + "no-case": "2.3.2" + } + }, + "parse-asn1": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.0.tgz", + "integrity": "sha1-N8T5t+06tlx0gXtfJICTf7+XxxI=", + "dev": true, + "requires": { + "asn1.js": "4.9.2", + "browserify-aes": "1.1.1", + "create-hash": "1.1.3", + "evp_bytestokey": "1.0.3", + "pbkdf2": "3.0.14" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "requires": { + "glob-base": "0.3.0", + "is-dotfile": "1.0.3", + "is-extglob": "1.0.0", + "is-glob": "2.0.1" + }, + "dependencies": { + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + } + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "1.3.1" + } + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "dev": true + }, + "parsejson": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.3.tgz", + "integrity": "sha1-q343WfIJ7OmUN5c/fQ8fZK4OZKs=", + "dev": true, + "requires": { + "better-assert": "1.0.2" + } + }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "dev": true, + "requires": { + "better-assert": "1.0.2" + } + }, + "parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "dev": true, + "requires": { + "better-assert": "1.0.2" + } + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=", + "dev": true + }, + "path": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", + "integrity": "sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8=", + "dev": true, + "requires": { + "process": "0.11.10", + "util": "0.10.3" + } + }, + "path-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", + "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", + "dev": true + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "2.0.1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "pbkdf2": { + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.14.tgz", + "integrity": "sha512-gjsZW9O34fm0R7PaLHRJmLLVfSoesxztjPjE9o6R+qtVJij90ltg1joIovN9GKrRW3t1PzhDDG3UMEMFfZ+1wA==", + "dev": true, + "requires": { + "create-hash": "1.1.3", + "create-hmac": "1.1.6", + "ripemd160": "2.0.1", + "safe-buffer": "5.1.1", + "sha.js": "2.4.9" + } + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=", + "dev": true + }, + "phantomjs-prebuilt": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz", + "integrity": "sha1-79ISpKOWbTZHaE6ouniFSb4q7+8=", + "dev": true, + "requires": { + "es6-promise": "4.1.1", + "extract-zip": "1.6.6", + "fs-extra": "1.0.0", + "hasha": "2.2.0", + "kew": "0.7.0", + "progress": "1.1.8", + "request": "2.81.0", + "request-progress": "2.0.1", + "which": "1.3.0" + }, + "dependencies": { + "fs-extra": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz", + "integrity": "sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "2.4.0", + "klaw": "1.3.1" + } + }, + "jsonfile": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + } + } + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "2.1.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + } + } + }, + "portfinder": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.13.tgz", + "integrity": "sha1-uzLs2HwnEErm7kS1o8y/Drsa7ek=", + "dev": true, + "requires": { + "async": "1.5.2", + "debug": "2.6.9", + "mkdirp": "0.5.1" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + } + } + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "dev": true, + "requires": { + "chalk": "1.1.3", + "js-base64": "2.4.0", + "source-map": "0.5.7", + "supports-color": "3.2.3" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-calc": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-5.3.1.tgz", + "integrity": "sha1-d7rnypKK2FcW4v2kLyYb98HWW14=", + "dev": true, + "requires": { + "postcss": "5.2.18", + "postcss-message-helpers": "2.0.0", + "reduce-css-calc": "1.3.0" + } + }, + "postcss-colormin": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-2.2.2.tgz", + "integrity": "sha1-ZjFBfV8OkJo9fsJrJMio0eT5bks=", + "dev": true, + "requires": { + "colormin": "1.1.2", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-convert-values": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz", + "integrity": "sha1-u9hZPFwf0uPRwyK7kl3K6Nrk1i0=", + "dev": true, + "requires": { + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-custom-properties": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-6.2.0.tgz", + "integrity": "sha512-eNR2h9T9ciKMoQEORrPjH33XeN/nuvVuxArOKmHtsFbGbNss631tgTrKou3/pmjAZbA4QQkhLIkPQkIk3WW+8w==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "postcss": "6.0.14" + }, + "dependencies": { + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "postcss": { + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.14.tgz", + "integrity": "sha512-NJ1z0f+1offCgadPhz+DvGm5Mkci+mmV5BqD13S992o0Xk9eElxUfPPF+t2ksH5R/17gz4xVK8KWocUQ5o3Rog==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "source-map": "0.6.1", + "supports-color": "4.5.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-discard-comments": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz", + "integrity": "sha1-vv6J+v1bPazlzM5Rt2uBUUvgDj0=", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-discard-duplicates": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-2.1.0.tgz", + "integrity": "sha1-uavye4isGIFYpesSq8riAmO5GTI=", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-discard-empty": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz", + "integrity": "sha1-0rS9nVztXr2Nyt52QMfXzX9PkrU=", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-discard-overridden": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz", + "integrity": "sha1-ix6vVU9ob7KIzYdMVWZ7CqNmjVg=", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-discard-unused": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz", + "integrity": "sha1-vOMLLMWR/8Y0Mitfs0ZLbZNPRDM=", + "dev": true, + "requires": { + "postcss": "5.2.18", + "uniqs": "2.0.0" + } + }, + "postcss-filter-plugins": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/postcss-filter-plugins/-/postcss-filter-plugins-2.0.2.tgz", + "integrity": "sha1-bYWGJTTXNaxCDkqFgG4fXUKG2Ew=", + "dev": true, + "requires": { + "postcss": "5.2.18", + "uniqid": "4.1.1" + } + }, + "postcss-load-config": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-1.2.0.tgz", + "integrity": "sha1-U56a/J3chiASHr+djDZz4M5Q0oo=", + "dev": true, + "requires": { + "cosmiconfig": "2.2.2", + "object-assign": "4.1.1", + "postcss-load-options": "1.2.0", + "postcss-load-plugins": "2.3.0" + } + }, + "postcss-load-options": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-load-options/-/postcss-load-options-1.2.0.tgz", + "integrity": "sha1-sJixVZ3awt8EvAuzdfmaXP4rbYw=", + "dev": true, + "requires": { + "cosmiconfig": "2.2.2", + "object-assign": "4.1.1" + } + }, + "postcss-load-plugins": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz", + "integrity": "sha1-dFdoEWWZrKLwCfrUJrABdQSdjZI=", + "dev": true, + "requires": { + "cosmiconfig": "2.2.2", + "object-assign": "4.1.1" + } + }, + "postcss-merge-idents": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz", + "integrity": "sha1-TFUwMTwI4dWzu/PSu8dH4njuonA=", + "dev": true, + "requires": { + "has": "1.0.1", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-merge-longhand": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-2.0.2.tgz", + "integrity": "sha1-I9kM0Sewp3mUkVMyc5A0oaTz1lg=", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-merge-rules": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-2.1.2.tgz", + "integrity": "sha1-0d9d+qexrMO+VT8OnhDofGG19yE=", + "dev": true, + "requires": { + "browserslist": "1.7.7", + "caniuse-api": "1.6.1", + "postcss": "5.2.18", + "postcss-selector-parser": "2.2.3", + "vendors": "1.0.1" + } + }, + "postcss-message-helpers": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz", + "integrity": "sha1-pPL0+rbk/gAvCu0ABHjN9S+bpg4=", + "dev": true + }, + "postcss-minify-font-values": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz", + "integrity": "sha1-S1jttWZB66fIR0qzUmyv17vey2k=", + "dev": true, + "requires": { + "object-assign": "4.1.1", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-minify-gradients": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz", + "integrity": "sha1-Xb2hE3NwP4PPtKPqOIHY11/15uE=", + "dev": true, + "requires": { + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-minify-params": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz", + "integrity": "sha1-rSzgcTc7lDs9kwo/pZo1jCjW8fM=", + "dev": true, + "requires": { + "alphanum-sort": "1.0.2", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0", + "uniqs": "2.0.0" + } + }, + "postcss-minify-selectors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz", + "integrity": "sha1-ssapjAByz5G5MtGkllCBFDEXNb8=", + "dev": true, + "requires": { + "alphanum-sort": "1.0.2", + "has": "1.0.1", + "postcss": "5.2.18", + "postcss-selector-parser": "2.2.3" + } + }, + "postcss-modules-extract-imports": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.1.0.tgz", + "integrity": "sha1-thTJcgvmgW6u41+zpfqh26agXds=", + "dev": true, + "requires": { + "postcss": "6.0.14" + }, + "dependencies": { + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "postcss": { + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.14.tgz", + "integrity": "sha512-NJ1z0f+1offCgadPhz+DvGm5Mkci+mmV5BqD13S992o0Xk9eElxUfPPF+t2ksH5R/17gz4xVK8KWocUQ5o3Rog==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "source-map": "0.6.1", + "supports-color": "4.5.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-modules-local-by-default": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz", + "integrity": "sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk=", + "dev": true, + "requires": { + "css-selector-tokenizer": "0.7.0", + "postcss": "6.0.14" + }, + "dependencies": { + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "postcss": { + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.14.tgz", + "integrity": "sha512-NJ1z0f+1offCgadPhz+DvGm5Mkci+mmV5BqD13S992o0Xk9eElxUfPPF+t2ksH5R/17gz4xVK8KWocUQ5o3Rog==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "source-map": "0.6.1", + "supports-color": "4.5.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-modules-scope": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz", + "integrity": "sha1-1upkmUx5+XtipytCb75gVqGUu5A=", + "dev": true, + "requires": { + "css-selector-tokenizer": "0.7.0", + "postcss": "6.0.14" + }, + "dependencies": { + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "postcss": { + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.14.tgz", + "integrity": "sha512-NJ1z0f+1offCgadPhz+DvGm5Mkci+mmV5BqD13S992o0Xk9eElxUfPPF+t2ksH5R/17gz4xVK8KWocUQ5o3Rog==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "source-map": "0.6.1", + "supports-color": "4.5.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-modules-values": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz", + "integrity": "sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA=", + "dev": true, + "requires": { + "icss-replace-symbols": "1.1.0", + "postcss": "6.0.14" + }, + "dependencies": { + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "postcss": { + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.14.tgz", + "integrity": "sha512-NJ1z0f+1offCgadPhz+DvGm5Mkci+mmV5BqD13S992o0Xk9eElxUfPPF+t2ksH5R/17gz4xVK8KWocUQ5o3Rog==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "source-map": "0.6.1", + "supports-color": "4.5.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-normalize-charset": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz", + "integrity": "sha1-757nEhLX/nWceO0WL2HtYrXLk/E=", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-normalize-url": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz", + "integrity": "sha1-EI90s/L82viRov+j6kWSJ5/HgiI=", + "dev": true, + "requires": { + "is-absolute-url": "2.1.0", + "normalize-url": "1.9.1", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-ordered-values": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz", + "integrity": "sha1-7sbCpntsQSqNsgQud/6NpD+VwR0=", + "dev": true, + "requires": { + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-reduce-idents": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz", + "integrity": "sha1-wsbSDMlYKE9qv75j92Cb9AkFmtM=", + "dev": true, + "requires": { + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-reduce-initial": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz", + "integrity": "sha1-aPgGlfBF0IJjqHmtJA343WT2ROo=", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-reduce-transforms": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz", + "integrity": "sha1-/3b02CEkN7McKYpC0uFEQCV3GuE=", + "dev": true, + "requires": { + "has": "1.0.1", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-selector-parser": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz", + "integrity": "sha1-+UN3iGBsPJrO4W/+jYsWKX8nu5A=", + "dev": true, + "requires": { + "flatten": "1.0.2", + "indexes-of": "1.0.1", + "uniq": "1.0.1" + } + }, + "postcss-svgo": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-2.1.6.tgz", + "integrity": "sha1-tt8YqmE7Zm4TPwittSGcJoSsEI0=", + "dev": true, + "requires": { + "is-svg": "2.1.0", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0", + "svgo": "0.7.2" + } + }, + "postcss-unique-selectors": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz", + "integrity": "sha1-mB1X0p3csz57Hf4f1DuGSfkzyh0=", + "dev": true, + "requires": { + "alphanum-sort": "1.0.2", + "postcss": "5.2.18", + "uniqs": "2.0.0" + } + }, + "postcss-value-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz", + "integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=", + "dev": true + }, + "postcss-zindex": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-2.2.0.tgz", + "integrity": "sha1-0hCd3AVbka9n/EyzsCWUZjnSryI=", + "dev": true, + "requires": { + "has": "1.0.1", + "postcss": "5.2.18", + "uniqs": "2.0.0" + } + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true + }, + "pretty-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz", + "integrity": "sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=", + "dev": true, + "requires": { + "renderkid": "2.0.1", + "utila": "0.4.0" + } + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", + "dev": true + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dev": true, + "optional": true, + "requires": { + "asap": "2.0.6" + } + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "protractor": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/protractor/-/protractor-5.1.2.tgz", + "integrity": "sha1-myIXQXCaTGLVzVPGqt1UpxE36V8=", + "dev": true, + "requires": { + "@types/node": "6.0.92", + "@types/q": "0.0.32", + "@types/selenium-webdriver": "2.53.43", + "blocking-proxy": "0.0.5", + "chalk": "1.1.3", + "glob": "7.1.2", + "jasmine": "2.8.0", + "jasminewd2": "2.2.0", + "optimist": "0.6.1", + "q": "1.4.1", + "saucelabs": "1.3.0", + "selenium-webdriver": "3.0.1", + "source-map-support": "0.4.18", + "webdriver-js-extender": "1.0.0", + "webdriver-manager": "12.0.6" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "dev": true, + "requires": { + "globby": "5.0.0", + "is-path-cwd": "1.0.0", + "is-path-in-cwd": "1.0.0", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "rimraf": "2.6.2" + } + }, + "globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "dev": true, + "requires": { + "array-union": "1.0.2", + "arrify": "1.0.1", + "glob": "7.1.2", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "q": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz", + "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "webdriver-manager": { + "version": "12.0.6", + "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.0.6.tgz", + "integrity": "sha1-PfGkgZdwELTL+MnYXHpXeCjA5ws=", + "dev": true, + "requires": { + "adm-zip": "0.4.7", + "chalk": "1.1.3", + "del": "2.2.2", + "glob": "7.1.2", + "ini": "1.3.5", + "minimist": "1.2.0", + "q": "1.4.1", + "request": "2.81.0", + "rimraf": "2.6.2", + "semver": "5.4.1", + "xml2js": "0.4.19" + } + } + } + }, + "proxy-addr": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz", + "integrity": "sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew=", + "dev": true, + "requires": { + "forwarded": "0.1.2", + "ipaddr.js": "1.5.2" + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "public-encrypt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.0.tgz", + "integrity": "sha1-OfaZ86RlYN1eusvKaTyvfGXBjMY=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "browserify-rsa": "4.0.1", + "create-hash": "1.1.3", + "parse-asn1": "5.1.0", + "randombytes": "2.0.5" + } + }, + "pump": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", + "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==", + "dev": true, + "requires": { + "end-of-stream": "1.4.0", + "once": "1.4.0" + } + }, + "pumpify": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.3.5.tgz", + "integrity": "sha1-G2ccYZlAq8rqwK0OOjwWS+dgmTs=", + "dev": true, + "requires": { + "duplexify": "3.5.1", + "inherits": "2.0.3", + "pump": "1.0.3" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + }, + "qjobs": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.1.5.tgz", + "integrity": "sha1-ZZ3p8s+NzCehSBJ28gU3cnI4LnM=", + "dev": true + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=", + "dev": true + }, + "query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "dev": true, + "requires": { + "object-assign": "4.1.1", + "strict-uri-encode": "1.1.0" + } + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "querystringify": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-0.0.4.tgz", + "integrity": "sha1-DPf4T5Rj/wrlHExLFC2VvjdyTZw=", + "dev": true + }, + "randomatic": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", + "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "dev": true, + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "randombytes": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.5.tgz", + "integrity": "sha512-8T7Zn1AhMsQ/HI1SjcCfT/t4ii3eAqco3yOcSzS4mozsOz69lHLsoMXmF9nZgnFanYscnSlUSgs8uZyKzpE6kg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "randomfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.3.tgz", + "integrity": "sha512-YL6GrhrWoic0Eq8rXVbMptH7dAxCs0J+mh5Y0euNekPPYaxEmdVGim6GdoxoRzKW2yJoU8tueifS7mYxvcFDEQ==", + "dev": true, + "requires": { + "randombytes": "2.0.5", + "safe-buffer": "5.1.1" + } + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=", + "dev": true + }, + "raw-body": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", + "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "dev": true, + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "unpipe": "1.0.0" + } + }, + "raw-loader": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-0.5.1.tgz", + "integrity": "sha1-DD0L6u2KAclm2Xh793goElKpeao=", + "dev": true + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + } + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "minimatch": "3.0.4", + "readable-stream": "2.3.3", + "set-immediate-shim": "1.0.1" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "2.1.0", + "strip-indent": "1.0.1" + } + }, + "reduce-css-calc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz", + "integrity": "sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=", + "dev": true, + "requires": { + "balanced-match": "0.4.2", + "math-expression-evaluator": "1.2.17", + "reduce-function-call": "1.0.2" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + } + } + }, + "reduce-function-call": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.2.tgz", + "integrity": "sha1-WiAL+S4ON3UXUv5FsKszD9S2vpk=", + "dev": true, + "requires": { + "balanced-match": "0.4.2" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + } + } + }, + "reflect-metadata": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.10.tgz", + "integrity": "sha1-tPg3BEFqytiZiMmxVjXUfgO5NEo=", + "dev": true + }, + "regenerate": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.3.tgz", + "integrity": "sha512-jVpo1GadrDAK59t/0jRx5VxYWQEDkkEKi6+HjE3joFVLfDOh9Xrdh0dF1eSq+BI/SwvTQ44gSscJ8N5zYL61sg==", + "dev": true + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "dev": true, + "requires": { + "is-equal-shallow": "0.1.3" + } + }, + "regexpu-core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", + "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", + "dev": true, + "requires": { + "regenerate": "1.3.3", + "regjsgen": "0.2.0", + "regjsparser": "0.1.5" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", + "dev": true + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "dev": true, + "requires": { + "jsesc": "0.5.0" + } + }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", + "dev": true + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "renderkid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.1.tgz", + "integrity": "sha1-iYyr/Ivt5Le5ETWj/9Mj5YwNsxk=", + "dev": true, + "requires": { + "css-select": "1.2.0", + "dom-converter": "0.1.4", + "htmlparser2": "3.3.0", + "strip-ansi": "3.0.1", + "utila": "0.3.3" + }, + "dependencies": { + "utila": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz", + "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=", + "dev": true + } + } + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "1.0.2" + } + }, + "request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "dev": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.17", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.3", + "tunnel-agent": "0.6.0", + "uuid": "3.1.0" + } + }, + "request-progress": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-2.0.1.tgz", + "integrity": "sha1-XTa7V5YcZzqlt4jbyBQf3yO0Tgg=", + "dev": true, + "requires": { + "throttleit": "1.0.0" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-from-string": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.1.tgz", + "integrity": "sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg=", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, + "resolve": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz", + "integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==", + "dev": true, + "requires": { + "path-parse": "1.0.5" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "3.0.0" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "requires": { + "align-text": "0.1.4" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, + "ripemd160": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", + "integrity": "sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=", + "dev": true, + "requires": { + "hash-base": "2.0.2", + "inherits": "2.0.3" + } + }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "dev": true, + "requires": { + "aproba": "1.2.0" + } + }, + "rxjs": { + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.5.tgz", + "integrity": "sha512-D/MfQnPMBk8P8gfwGxvCkuaWBcG58W7dUMT//URPoYzIbDEKT0GezdirkK5whMgKFBATfCoTpxO8bJQGJ04W5A==", + "requires": { + "symbol-observable": "1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "dev": true + }, + "sass-graph": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", + "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", + "dev": true, + "optional": true, + "requires": { + "glob": "7.1.2", + "lodash": "4.17.4", + "scss-tokenizer": "0.2.3", + "yargs": "7.1.0" + } + }, + "sass-loader": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-6.0.6.tgz", + "integrity": "sha512-c3/Zc+iW+qqDip6kXPYLEgsAu2lf4xz0EZDplB7EmSUMda12U1sGJPetH55B/j9eu0bTtKzKlNPWWyYC7wFNyQ==", + "dev": true, + "requires": { + "async": "2.6.0", + "clone-deep": "0.3.0", + "loader-utils": "1.1.0", + "lodash.tail": "4.1.1", + "pify": "3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "saucelabs": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/saucelabs/-/saucelabs-1.3.0.tgz", + "integrity": "sha1-0kDoAJ33+ocwbsRXimm6O1xCT+4=", + "dev": true, + "requires": { + "https-proxy-agent": "1.0.0" + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "schema-utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz", + "integrity": "sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8=", + "dev": true, + "requires": { + "ajv": "5.5.1" + } + }, + "scss-tokenizer": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", + "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", + "dev": true, + "optional": true, + "requires": { + "js-base64": "2.4.0", + "source-map": "0.4.4" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "optional": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", + "dev": true + }, + "selenium-webdriver": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-3.0.1.tgz", + "integrity": "sha1-ot6l2kqX9mcuiefKcnbO+jZRR6c=", + "dev": true, + "requires": { + "adm-zip": "0.4.7", + "rimraf": "2.6.2", + "tmp": "0.0.30", + "xml2js": "0.4.19" + }, + "dependencies": { + "tmp": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.30.tgz", + "integrity": "sha1-ckGdSovn1s51FI/YsyTlk6cRwu0=", + "dev": true, + "requires": { + "os-tmpdir": "1.0.2" + } + } + } + }, + "selfsigned": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.1.tgz", + "integrity": "sha1-v4y3uDJWxFUeMTR8YxF3jbme7FI=", + "dev": true, + "requires": { + "node-forge": "0.6.33" + } + }, + "semver": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", + "dev": true + }, + "semver-dsl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/semver-dsl/-/semver-dsl-1.0.1.tgz", + "integrity": "sha1-02eN5VVeimH2Ke7QJTZq5fJzQKA=", + "dev": true, + "requires": { + "semver": "5.4.1" + } + }, + "send": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.1.tgz", + "integrity": "sha512-ElCLJdJIKPk6ux/Hocwhk7NFHpI3pVm/IZOYWqUmoxcgeyM+MpxHHKhb8QmlJDX1pU6WrgaHBkVNm73Sv7uc2A==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "1.1.1", + "destroy": "1.0.4", + "encodeurl": "1.0.1", + "escape-html": "1.0.3", + "etag": "1.8.1", + "fresh": "0.5.2", + "http-errors": "1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "2.3.0", + "range-parser": "1.2.0", + "statuses": "1.3.1" + }, + "dependencies": { + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", + "dev": true + } + } + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "dev": true, + "requires": { + "accepts": "1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "1.0.3", + "http-errors": "1.6.2", + "mime-types": "2.1.17", + "parseurl": "1.3.2" + } + }, + "serve-static": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz", + "integrity": "sha512-hSMUZrsPa/I09VYFJwa627JJkNs0NrfL1Uzuup+GqHfToR2KcsXFymXSV90hoyw3M+msjFuQly+YzIH/q0MGlQ==", + "dev": true, + "requires": { + "encodeurl": "1.0.1", + "escape-html": "1.0.3", + "parseurl": "1.3.2", + "send": "0.16.1" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "dev": true + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "sha.js": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.9.tgz", + "integrity": "sha512-G8zektVqbiPHrylgew9Zg1VRB1L/DtXNUVAM6q4QLy8NE3qtHlFXTf8VLL4k1Yl6c7NMjtZUTdXV+X44nFaT6A==", + "dev": true, + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.1" + } + }, + "shallow-clone": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz", + "integrity": "sha1-WQnodLp3EG1zrEFM/sH/yofZcGA=", + "dev": true, + "requires": { + "is-extendable": "0.1.1", + "kind-of": "2.0.1", + "lazy-cache": "0.2.7", + "mixin-object": "2.0.1" + }, + "dependencies": { + "kind-of": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", + "integrity": "sha1-AY7HpM5+OobLkUG+UZ0kyPqpgbU=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "silent-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/silent-error/-/silent-error-1.1.0.tgz", + "integrity": "sha1-IglwbxyFCp8dENDYQJGLRvJuG8k=", + "dev": true, + "requires": { + "debug": "2.6.9" + } + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "socket.io": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.7.3.tgz", + "integrity": "sha1-uK+cq6AJSeVo42nxMn6pvp6iRhs=", + "dev": true, + "requires": { + "debug": "2.3.3", + "engine.io": "1.8.3", + "has-binary": "0.1.7", + "object-assign": "4.1.0", + "socket.io-adapter": "0.5.0", + "socket.io-client": "1.7.3", + "socket.io-parser": "2.3.1" + }, + "dependencies": { + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + }, + "object-assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz", + "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=", + "dev": true + } + } + }, + "socket.io-adapter": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-0.5.0.tgz", + "integrity": "sha1-y21LuL7IHhB4uZZ3+c7QBGBmu4s=", + "dev": true, + "requires": { + "debug": "2.3.3", + "socket.io-parser": "2.3.1" + }, + "dependencies": { + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + } + } + }, + "socket.io-client": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-1.7.3.tgz", + "integrity": "sha1-sw6GqhDV7zVGYBwJzeR2Xjgdo3c=", + "dev": true, + "requires": { + "backo2": "1.0.2", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "2.3.3", + "engine.io-client": "1.8.3", + "has-binary": "0.1.7", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseuri": "0.0.5", + "socket.io-parser": "2.3.1", + "to-array": "0.1.4" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + } + } + }, + "socket.io-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.3.1.tgz", + "integrity": "sha1-3VMgJRA85Clpcya+/WQAX8/ltKA=", + "dev": true, + "requires": { + "component-emitter": "1.1.2", + "debug": "2.2.0", + "isarray": "0.0.1", + "json3": "3.3.2" + }, + "dependencies": { + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + } + } + }, + "sockjs": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.18.tgz", + "integrity": "sha1-2bKJMWyn33dZXvKZ4HXw+TfrQgc=", + "dev": true, + "requires": { + "faye-websocket": "0.10.0", + "uuid": "2.0.3" + }, + "dependencies": { + "uuid": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", + "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=", + "dev": true + } + } + }, + "sockjs-client": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.1.4.tgz", + "integrity": "sha1-W6vjhrd15M8U51IJEUUmVAFsixI=", + "dev": true, + "requires": { + "debug": "2.6.9", + "eventsource": "0.1.6", + "faye-websocket": "0.11.1", + "inherits": "2.0.3", + "json3": "3.3.2", + "url-parse": "1.2.0" + }, + "dependencies": { + "faye-websocket": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.1.tgz", + "integrity": "sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg=", + "dev": true, + "requires": { + "websocket-driver": "0.7.0" + } + } + } + }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "dev": true, + "requires": { + "is-plain-obj": "1.1.0" + } + }, + "source-list-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz", + "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-loader": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-0.2.3.tgz", + "integrity": "sha512-MYbFX9DYxmTQFfy2v8FC1XZwpwHKYxg3SK8Wb7VPBKuhDjz8gi9re2819MsG4p49HDyiOSUKlmZ+nQBArW5CGw==", + "dev": true, + "requires": { + "async": "2.6.0", + "loader-utils": "0.2.17", + "source-map": "0.6.1" + }, + "dependencies": { + "loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "dev": true, + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1", + "object-assign": "4.1.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "dev": true, + "requires": { + "source-map": "0.5.7" + } + }, + "spdx-correct": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", + "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", + "dev": true, + "requires": { + "spdx-license-ids": "1.2.2" + } + }, + "spdx-expression-parse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", + "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=", + "dev": true + }, + "spdx-license-ids": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", + "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=", + "dev": true + }, + "spdy": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-3.4.7.tgz", + "integrity": "sha1-Qv9B7OXMD5mjpsKKq7c/XDsDrLw=", + "dev": true, + "requires": { + "debug": "2.6.9", + "handle-thing": "1.2.5", + "http-deceiver": "1.2.7", + "safe-buffer": "5.1.1", + "select-hose": "2.0.0", + "spdy-transport": "2.0.20" + } + }, + "spdy-transport": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-2.0.20.tgz", + "integrity": "sha1-c15yBUxIayNU/onnAiVgBKOazk0=", + "dev": true, + "requires": { + "debug": "2.6.9", + "detect-node": "2.0.3", + "hpack.js": "2.1.6", + "obuf": "1.1.1", + "readable-stream": "2.3.3", + "safe-buffer": "5.1.1", + "wbuf": "1.7.2" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "dev": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "ssri": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-5.0.0.tgz", + "integrity": "sha512-728D4yoQcQm1ooZvSbywLkV1RjfITZXh0oWrhM/lnsx3nAHx7LsRGJWB/YyvoceAYRq98xqbstiN4JBv1/wNHg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=", + "dev": true + }, + "stdout-stream": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz", + "integrity": "sha1-osfIWH5U2UJ+qe2zrD8s1SLfN4s=", + "dev": true, + "optional": true, + "requires": { + "readable-stream": "2.3.3" + } + }, + "stream-browserify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", + "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.3" + } + }, + "stream-each": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.2.tgz", + "integrity": "sha512-mc1dbFhGBxvTM3bIWmAAINbqiuAk9TATcfIQC8P+/+HJefgaiTlMn2dHvkX8qlI12KeYKSQ1Ua9RrIqrn1VPoA==", + "dev": true, + "requires": { + "end-of-stream": "1.4.0", + "stream-shift": "1.0.0" + } + }, + "stream-http": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.7.2.tgz", + "integrity": "sha512-c0yTD2rbQzXtSsFSVhtpvY/vS6u066PcXOX9kBB3mSO76RiUQzL340uJkGBWnlBg4/HZzqiUXtaVA7wcRcJgEw==", + "dev": true, + "requires": { + "builtin-status-codes": "3.0.0", + "inherits": "2.0.3", + "readable-stream": "2.3.3", + "to-arraybuffer": "1.0.1", + "xtend": "4.0.1" + } + }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", + "dev": true + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "dev": true + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "4.0.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "style-loader": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.13.2.tgz", + "integrity": "sha1-dFMzhM9pjHEEx5URULSXF63C87s=", + "dev": true, + "requires": { + "loader-utils": "1.1.0" + } + }, + "stylus": { + "version": "0.54.5", + "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.5.tgz", + "integrity": "sha1-QrlWCTHKcJDOhRWnmLqeaqPW3Hk=", + "dev": true, + "requires": { + "css-parse": "1.7.0", + "debug": "2.6.9", + "glob": "7.0.6", + "mkdirp": "0.5.1", + "sax": "0.5.8", + "source-map": "0.1.43" + }, + "dependencies": { + "glob": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz", + "integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "sax": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/sax/-/sax-0.5.8.tgz", + "integrity": "sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE=", + "dev": true + }, + "source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "stylus-loader": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/stylus-loader/-/stylus-loader-3.0.1.tgz", + "integrity": "sha1-d/SzT9Aw0lsmF7z1UT21sHMMQIk=", + "dev": true, + "requires": { + "loader-utils": "1.1.0", + "lodash.clonedeep": "4.5.0", + "when": "3.6.4" + } + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + }, + "svgo": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-0.7.2.tgz", + "integrity": "sha1-n1dyQTlSE1xv779Ar+ak+qiLS7U=", + "dev": true, + "requires": { + "coa": "1.0.4", + "colors": "1.1.2", + "csso": "2.3.2", + "js-yaml": "3.7.0", + "mkdirp": "0.5.1", + "sax": "1.2.4", + "whet.extend": "0.9.9" + } + }, + "symbol-observable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", + "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=" + }, + "tapable": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.8.tgz", + "integrity": "sha1-mTcqXJmb8t8WCvwNdL7U9HlIzSI=", + "dev": true + }, + "tar": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "dev": true, + "optional": true, + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "throttleit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", + "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "dev": true, + "requires": { + "readable-stream": "2.3.3", + "xtend": "4.0.1" + } + }, + "thunky": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-0.1.0.tgz", + "integrity": "sha1-vzAUaCTituZ7Dy16Ssi+smkIaE4=", + "dev": true + }, + "time-stamp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-2.0.0.tgz", + "integrity": "sha1-lcakRTDhW6jW9KPsuMOj+sRto1c=", + "dev": true + }, + "timers-browserify": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.4.tgz", + "integrity": "sha512-uZYhyU3EX8O7HQP+J9fTVYwsq90Vr68xPEFo7yrVImIxYvHgukBEgOB/SgGoorWVTzGM/3Z+wUNnboA4M8jWrg==", + "dev": true, + "requires": { + "setimmediate": "1.0.5" + } + }, + "tmp": { + "version": "0.0.31", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.31.tgz", + "integrity": "sha1-jzirlDjhcxXl29izZX6L+yd65Kc=", + "dev": true, + "requires": { + "os-tmpdir": "1.0.2" + } + }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=", + "dev": true + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true + }, + "toposort": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.6.tgz", + "integrity": "sha1-wxdI5V0hDv/AD9zcfW5o19e7nOw=", + "dev": true + }, + "tough-cookie": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", + "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", + "dev": true, + "requires": { + "punycode": "1.4.1" + } + }, + "tree-kill": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.0.tgz", + "integrity": "sha512-DlX6dR0lOIRDFxI0mjL9IYg6OTncLm/Zt+JiBhE5OlFcAR8yc9S7FFXU9so0oda47frdM/JFsk7UjNt9vscKcg==", + "dev": true + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "true-case-path": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.2.tgz", + "integrity": "sha1-fskRMJJHZsf1c74wIMNPj9/QDWI=", + "dev": true, + "optional": true, + "requires": { + "glob": "6.0.4" + }, + "dependencies": { + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "dev": true, + "optional": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + } + } + }, + "tryer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.0.tgz", + "integrity": "sha1-Antp+oIyJeVRys4+8DsR9qs3wdc=", + "dev": true + }, + "ts-node": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-3.2.2.tgz", + "integrity": "sha1-u9KOOK9Kqj6WB2xGbhsiAZfBo84=", + "dev": true, + "requires": { + "arrify": "1.0.1", + "chalk": "2.2.2", + "diff": "3.4.0", + "make-error": "1.3.0", + "minimist": "1.2.0", + "mkdirp": "0.5.1", + "source-map-support": "0.4.18", + "tsconfig": "6.0.0", + "v8flags": "3.0.1", + "yn": "2.0.0" + } + }, + "tsconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-6.0.0.tgz", + "integrity": "sha1-aw6DdgA9evGGT434+J3QBZ/80DI=", + "dev": true, + "requires": { + "strip-bom": "3.0.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } + } + }, + "tsickle": { + "version": "0.24.1", + "resolved": "https://registry.npmjs.org/tsickle/-/tsickle-0.24.1.tgz", + "integrity": "sha512-XloFQZhVhgjpQsi3u2ORNRJvuID5sflOg6HfP093IqAbhE1+fIUXznULpdDwHgG4p+v8w78KdHruQtkWUKx5AQ==", + "dev": true, + "requires": { + "minimist": "1.2.0", + "mkdirp": "0.5.1", + "source-map": "0.5.7", + "source-map-support": "0.4.18" + } + }, + "tslib": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.8.1.tgz", + "integrity": "sha1-aUavLR1lGnsYY7Ux1uWvpBqkTqw=" + }, + "tslint": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.3.2.tgz", + "integrity": "sha1-5WRZ+wlacwfxA7hAUhdPXju+9u0=", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "colors": "1.1.2", + "diff": "3.4.0", + "glob": "7.1.2", + "optimist": "0.6.1", + "resolve": "1.5.0", + "semver": "5.4.1", + "tslib": "1.8.1", + "tsutils": "2.13.0" + }, + "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", + "dev": true + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "0.0.10", + "wordwrap": "0.0.2" + } + } + } + }, + "tsutils": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.13.0.tgz", + "integrity": "sha512-FuWzNJbMsp3gcZMbI3b5DomhW4Ia41vMxjN63nKWI0t7f+I3UmHfRl0TrXJTwI2LUduDG+eR1Mksp3pvtlyCFQ==", + "dev": true, + "requires": { + "tslib": "1.8.1" + } + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, + "optional": true + }, + "type-is": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", + "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "2.1.17" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "typescript": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.5.3.tgz", + "integrity": "sha512-ptLSQs2S4QuS6/OD1eAKG+S5G8QQtrU5RT32JULdZQtM1L3WTi34Wsu48Yndzi8xsObRAB9RPt/KhA9wlpEF6w==", + "dev": true + }, + "uglify-js": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.2.2.tgz", + "integrity": "sha512-++1NO/zZIEdWf6cDIGceSJQPX31SqIpbVAHwFG5+240MtZqPG/NIPoinj8zlXQtAfMBqEt1Jyv2FiLP3n9gVhQ==", + "dev": true, + "requires": { + "commander": "2.12.2", + "source-map": "0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true, + "optional": true + }, + "uglifyjs-webpack-plugin": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.0.0.tgz", + "integrity": "sha512-23qmtiLm1X7O0XVSZ54W7XGHykPss+2lo3RYC9zSzK3DDT5W27woZpDFDKguDCnG1RIX8cDnmy5j+dtXxJCA/Q==", + "dev": true, + "requires": { + "cacache": "10.0.1", + "find-cache-dir": "1.0.0", + "schema-utils": "0.3.0", + "source-map": "0.5.7", + "uglify-es": "3.2.2", + "webpack-sources": "1.1.0", + "worker-farm": "1.5.2" + }, + "dependencies": { + "uglify-es": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.2.2.tgz", + "integrity": "sha512-l+s5VLzFwGJfS+fbqaGf/Dfwo1MF13jLOF2ekL0PytzqEqQ6cVppvHf4jquqFok+35USMpKjqkYxy6pQyUcuug==", + "dev": true, + "requires": { + "commander": "2.12.2", + "source-map": "0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + } + } + }, + "ultron": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz", + "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=", + "dev": true + }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, + "uniqid": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/uniqid/-/uniqid-4.1.1.tgz", + "integrity": "sha1-iSIN32t1GuUrX3JISGNShZa7hME=", + "dev": true, + "requires": { + "macaddress": "0.2.8" + } + }, + "uniqs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", + "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=", + "dev": true + }, + "unique-filename": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.0.tgz", + "integrity": "sha1-0F8v5AMlYIcfMOk8vnNe6iAVFPM=", + "dev": true, + "requires": { + "unique-slug": "2.0.0" + } + }, + "unique-slug": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.0.tgz", + "integrity": "sha1-22Z258fMBimHj/GWCXx4hVrp9Ks=", + "dev": true, + "requires": { + "imurmurhash": "0.1.4" + } + }, + "universalify": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", + "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "url-loader": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-0.6.2.tgz", + "integrity": "sha512-h3qf9TNn53BpuXTTcpC+UehiRrl0Cv45Yr/xWayApjw6G8Bg2dGke7rIwDQ39piciWCWrC+WiqLjOh3SUp9n0Q==", + "dev": true, + "requires": { + "loader-utils": "1.1.0", + "mime": "1.6.0", + "schema-utils": "0.3.0" + } + }, + "url-parse": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.2.0.tgz", + "integrity": "sha512-DT1XbYAfmQP65M/mE6OALxmXzZ/z1+e5zk2TcSKe/KiYbNGZxgtttzC0mR/sjopbpOXcbniq7eIKmocJnUWlEw==", + "dev": true, + "requires": { + "querystringify": "1.0.0", + "requires-port": "1.0.0" + }, + "dependencies": { + "querystringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-1.0.0.tgz", + "integrity": "sha1-YoYkIRLFtxL6ZU5SZlK/ahP/Bcs=", + "dev": true + } + } + }, + "useragent": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.2.1.tgz", + "integrity": "sha1-z1k+9PLRdYdei7ZY6pLhik/QbY4=", + "dev": true, + "requires": { + "lru-cache": "2.2.4", + "tmp": "0.0.31" + }, + "dependencies": { + "lru-cache": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.2.4.tgz", + "integrity": "sha1-bGWGGb7PFAMdDQtZSxYELOTcBj0=", + "dev": true + } + } + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true + }, + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==", + "dev": true + }, + "v8flags": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.0.1.tgz", + "integrity": "sha1-3Oj8N5wX2fLJ6e142JzgAFKxt2s=", + "dev": true, + "requires": { + "homedir-polyfill": "1.0.1" + } + }, + "validate-npm-package-license": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", + "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", + "dev": true, + "requires": { + "spdx-correct": "1.0.2", + "spdx-expression-parse": "1.0.4" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true + }, + "vendors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.1.tgz", + "integrity": "sha1-N61zyO5Bf7PVgOeFMSMH0nSEfyI=", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "vlq": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.3.tgz", + "integrity": "sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==", + "dev": true + }, + "vm-browserify": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", + "dev": true, + "requires": { + "indexof": "0.0.1" + } + }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", + "dev": true + }, + "watchpack": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.4.0.tgz", + "integrity": "sha1-ShRyvLuVK9Cpu0A2gB+VTfs5+qw=", + "dev": true, + "requires": { + "async": "2.6.0", + "chokidar": "1.7.0", + "graceful-fs": "4.1.11" + } + }, + "wbuf": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.2.tgz", + "integrity": "sha1-1pe5nx9ZUS3ydRvkJ2nBWAtYAf4=", + "dev": true, + "requires": { + "minimalistic-assert": "1.0.0" + } + }, + "webdriver-js-extender": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/webdriver-js-extender/-/webdriver-js-extender-1.0.0.tgz", + "integrity": "sha1-gcUzqeM9W/tZe05j4s2yW1R3dRU=", + "dev": true, + "requires": { + "@types/selenium-webdriver": "2.53.43", + "selenium-webdriver": "2.53.3" + }, + "dependencies": { + "adm-zip": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.4.tgz", + "integrity": "sha1-ph7VrmkFw66lizplfSUDMJEFJzY=", + "dev": true + }, + "sax": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-0.6.1.tgz", + "integrity": "sha1-VjsZx8HeiS4Jv8Ty/DDjwn8JUrk=", + "dev": true + }, + "selenium-webdriver": { + "version": "2.53.3", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-2.53.3.tgz", + "integrity": "sha1-0p/1qVff8aG0ncRXdW5OS/vc4IU=", + "dev": true, + "requires": { + "adm-zip": "0.4.4", + "rimraf": "2.6.2", + "tmp": "0.0.24", + "ws": "1.1.2", + "xml2js": "0.4.4" + } + }, + "tmp": { + "version": "0.0.24", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.24.tgz", + "integrity": "sha1-1qXhmNFKmDXMby18PZ4wJCjIzxI=", + "dev": true + }, + "xml2js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.4.tgz", + "integrity": "sha1-MREBAAMAiuGSQOuhdJe1fHKcVV0=", + "dev": true, + "requires": { + "sax": "0.6.1", + "xmlbuilder": "9.0.4" + } + } + } + }, + "webpack": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-3.8.1.tgz", + "integrity": "sha512-5ZXLWWsMqHKFr5y0N3Eo5IIisxeEeRAajNq4mELb/WELOR7srdbQk2N5XiyNy2A/AgvlR3AmeBCZJW8lHrolbw==", + "dev": true, + "requires": { + "acorn": "5.2.1", + "acorn-dynamic-import": "2.0.2", + "ajv": "5.5.1", + "ajv-keywords": "2.1.1", + "async": "2.6.0", + "enhanced-resolve": "3.4.1", + "escope": "3.6.0", + "interpret": "1.1.0", + "json-loader": "0.5.7", + "json5": "0.5.1", + "loader-runner": "2.3.0", + "loader-utils": "1.1.0", + "memory-fs": "0.4.1", + "mkdirp": "0.5.1", + "node-libs-browser": "2.1.0", + "source-map": "0.5.7", + "supports-color": "4.5.0", + "tapable": "0.2.8", + "uglifyjs-webpack-plugin": "0.4.6", + "watchpack": "1.4.0", + "webpack-sources": "1.1.0", + "yargs": "8.0.2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "requires": { + "center-align": "0.1.3", + "right-align": "0.1.3", + "wordwrap": "0.0.2" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "strip-bom": "3.0.0" + } + }, + "os-locale": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", + "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "dev": true, + "requires": { + "execa": "0.7.0", + "lcid": "1.0.0", + "mem": "1.1.0" + } + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "2.3.0" + } + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "2.0.0", + "normalize-package-data": "2.4.0", + "path-type": "2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "2.1.0", + "read-pkg": "2.0.0" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dev": true, + "requires": { + "source-map": "0.5.7", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + }, + "dependencies": { + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + } + } + } + }, + "uglifyjs-webpack-plugin": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz", + "integrity": "sha1-uVH0q7a9YX5m9j64kUmOORdj4wk=", + "dev": true, + "requires": { + "source-map": "0.5.7", + "uglify-js": "2.8.29", + "webpack-sources": "1.1.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "yargs": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz", + "integrity": "sha1-YpmpBVsc78lp/355wdkY3Osiw2A=", + "dev": true, + "requires": { + "camelcase": "4.1.0", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "os-locale": "2.1.0", + "read-pkg-up": "2.0.0", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "2.1.1", + "which-module": "2.0.0", + "y18n": "3.2.1", + "yargs-parser": "7.0.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wrap-ansi": "2.1.0" + }, + "dependencies": { + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + } + } + } + } + }, + "yargs-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz", + "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", + "dev": true, + "requires": { + "camelcase": "4.1.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + } + } + } + } + }, + "webpack-bundle-analyzer": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.11.0.tgz", + "integrity": "sha512-GGz//de6DHqKIBN75vRPUqwTUobgYCzygS+ei/Z5wRpKYEZ+HO2e8Pd6CbQewGfofjRHoCFqL10pi2lu+/fqDg==", + "dev": true, + "requires": { + "acorn": "5.5.0", + "bfj-node4": "5.2.1", + "chalk": "2.3.1", + "commander": "2.14.1", + "ejs": "2.5.7", + "express": "4.16.2", + "filesize": "3.6.0", + "gzip-size": "4.1.0", + "lodash": "4.17.4", + "mkdirp": "0.5.1", + "opener": "1.4.3", + "ws": "4.1.0" + }, + "dependencies": { + "acorn": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.0.tgz", + "integrity": "sha512-arn53F07VXmls4o4pUhSzBa4fvaagPRe7AVZ8l7NHxFWUie2DsuFSBMMNAkgzRlOhEhzAnxeKyaWVzOH4xqp/g==", + "dev": true + }, + "chalk": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", + "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "5.2.0" + } + }, + "commander": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz", + "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", + "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + }, + "ws": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-4.1.0.tgz", + "integrity": "sha512-ZGh/8kF9rrRNffkLFV4AzhvooEclrOH0xaugmqGsIfFgOE/pIz4fMc4Ef+5HSQqTEug2S9JZIWDR47duDSLfaA==", + "dev": true, + "requires": { + "async-limiter": "1.0.0", + "safe-buffer": "5.1.1" + } + } + } + }, + "webpack-concat-plugin": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/webpack-concat-plugin/-/webpack-concat-plugin-1.4.0.tgz", + "integrity": "sha512-Ym9Qm5Sw9oXJYChNJk09I/yaXDaV3UDxsa07wcCvILzIeSJTnSUZjhS4y2YkULzgE8VHOv9X04KtlJPZGwXqMg==", + "dev": true, + "requires": { + "md5": "2.2.1", + "uglify-js": "2.8.29" + }, + "dependencies": { + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "requires": { + "center-align": "0.1.3", + "right-align": "0.1.3", + "wordwrap": "0.0.2" + } + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dev": true, + "requires": { + "source-map": "0.5.7", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + } + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + } + } + } + }, + "webpack-core": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/webpack-core/-/webpack-core-0.6.9.tgz", + "integrity": "sha1-/FcViMhVjad76e+23r3Fo7FyvcI=", + "dev": true, + "requires": { + "source-list-map": "0.1.8", + "source-map": "0.4.4" + }, + "dependencies": { + "source-list-map": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-0.1.8.tgz", + "integrity": "sha1-xVCyq1Qn9rPyH1r+rYjE9Vh7IQY=", + "dev": true + }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "webpack-dev-middleware": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-1.12.2.tgz", + "integrity": "sha512-FCrqPy1yy/sN6U/SaEZcHKRXGlqU0DUaEBL45jkUYoB8foVb6wCnbIJ1HKIx+qUFTW+3JpVcCJCxZ8VATL4e+A==", + "dev": true, + "requires": { + "memory-fs": "0.4.1", + "mime": "1.6.0", + "path-is-absolute": "1.0.1", + "range-parser": "1.2.0", + "time-stamp": "2.0.0" + } + }, + "webpack-dev-server": { + "version": "2.9.7", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-2.9.7.tgz", + "integrity": "sha512-Pu7uoQFgQj5RE5wmlfkpYSzihMKxulwEuO2xCsaMnAnyRSApwoVi3B8WCm9XbigyWTHaIMzYGkB90Vr6leAeTQ==", + "dev": true, + "requires": { + "ansi-html": "0.0.7", + "array-includes": "3.0.3", + "bonjour": "3.5.0", + "chokidar": "1.7.0", + "compression": "1.7.1", + "connect-history-api-fallback": "1.5.0", + "debug": "3.1.0", + "del": "3.0.0", + "express": "4.16.2", + "html-entities": "1.2.1", + "http-proxy-middleware": "0.17.4", + "import-local": "0.1.1", + "internal-ip": "1.2.0", + "ip": "1.1.5", + "killable": "1.0.0", + "loglevel": "1.6.0", + "opn": "5.1.0", + "portfinder": "1.0.13", + "selfsigned": "1.10.1", + "serve-index": "1.9.1", + "sockjs": "0.3.18", + "sockjs-client": "1.1.4", + "spdy": "3.4.7", + "strip-ansi": "3.0.1", + "supports-color": "4.5.0", + "webpack-dev-middleware": "1.12.2", + "yargs": "6.6.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "yargs": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", + "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=", + "dev": true, + "requires": { + "camelcase": "3.0.0", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "os-locale": "1.4.0", + "read-pkg-up": "1.0.1", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "1.0.2", + "which-module": "1.0.0", + "y18n": "3.2.1", + "yargs-parser": "4.2.1" + } + }, + "yargs-parser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", + "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=", + "dev": true, + "requires": { + "camelcase": "3.0.0" + } + } + } + }, + "webpack-merge": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.1.1.tgz", + "integrity": "sha512-geQsZ86YkXOVOjvPC5yv3JSNnL6/X3Kzh935AQ/gJNEYXEfJDQFu/sdFuktS9OW2JcH/SJec8TGfRdrpHshH7A==", + "dev": true, + "requires": { + "lodash": "4.17.4" + } + }, + "webpack-sources": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.1.0.tgz", + "integrity": "sha512-aqYp18kPphgoO5c/+NaUvEeACtZjMESmDChuD3NBciVpah3XpMEU9VAAtIaB1BsfJWWTSdv8Vv1m3T0aRk2dUw==", + "dev": true, + "requires": { + "source-list-map": "2.0.0", + "source-map": "0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "webpack-subresource-integrity": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-1.0.3.tgz", + "integrity": "sha1-wGBtQAkLBwzeQovsjfNgMhbkcus=", + "dev": true, + "requires": { + "webpack-core": "0.6.9" + } + }, + "websocket-driver": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz", + "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=", + "dev": true, + "requires": { + "http-parser-js": "0.4.9", + "websocket-extensions": "0.1.3" + } + }, + "websocket-extensions": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", + "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==", + "dev": true + }, + "when": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/when/-/when-3.6.4.tgz", + "integrity": "sha1-RztRfsFZ4rhQBUl6E5g/CVQS404=", + "dev": true + }, + "whet.extend": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/whet.extend/-/whet.extend-0.9.9.tgz", + "integrity": "sha1-+HfVv2SMl+WqVC+twW1qJZucEaE=", + "dev": true + }, + "which": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", + "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "dev": true, + "requires": { + "isexe": "2.0.0" + } + }, + "which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", + "dev": true + }, + "wide-align": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", + "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", + "dev": true, + "requires": { + "string-width": "1.0.2" + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true + }, + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true + }, + "worker-farm": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.5.2.tgz", + "integrity": "sha512-XxiQ9kZN5n6mmnW+mFJ+wXjNNI/Nx4DIdaAKLX1Bn6LYBWlN/zaBhu34DQYPZ1AJobQuu67S2OfDdNSVULvXkQ==", + "dev": true, + "requires": { + "errno": "0.1.5", + "xtend": "4.0.1" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "ws": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.2.tgz", + "integrity": "sha1-iiRPoFJAHgjJiGz0SoUYnh/UBn8=", + "dev": true, + "requires": { + "options": "0.0.6", + "ultron": "1.0.2" + } + }, + "wtf-8": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wtf-8/-/wtf-8-1.0.0.tgz", + "integrity": "sha1-OS2LotDxw00e4tYw8V0O+2jhBIo=", + "dev": true + }, + "xml-char-classes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/xml-char-classes/-/xml-char-classes-1.0.0.tgz", + "integrity": "sha1-ZGV4SKIP/F31g6Qq2KJ3tFErvE0=", + "dev": true + }, + "xml-formatter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml-formatter/-/xml-formatter-1.0.1.tgz", + "integrity": "sha1-OAgz3dC86iwJht7+u6cfhDhPNT0=", + "requires": { + "xml-parser-xo": "2.1.3" + } + }, + "xml-parser-xo": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/xml-parser-xo/-/xml-parser-xo-2.1.3.tgz", + "integrity": "sha1-TqjrhW36TddcSrVLJRVY3grcHGE=", + "requires": { + "debug": "2.6.9" + } + }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "dev": true, + "requires": { + "sax": "1.2.4", + "xmlbuilder": "9.0.4" + } + }, + "xmlbuilder": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.4.tgz", + "integrity": "sha1-UZy0ymhtAFqEINNJbz8MruzKWA8=", + "dev": true + }, + "xmldom": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz", + "integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk=", + "dev": true + }, + "xmlhttprequest-ssl": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz", + "integrity": "sha1-GFqIjATspGw+QHDZn3tJ3jUomS0=", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", + "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", + "dev": true, + "optional": true, + "requires": { + "camelcase": "3.0.0", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "os-locale": "1.4.0", + "read-pkg-up": "1.0.1", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "1.0.2", + "which-module": "1.0.0", + "y18n": "3.2.1", + "yargs-parser": "5.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true, + "optional": true + } + } + }, + "yargs-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", + "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", + "dev": true, + "optional": true, + "requires": { + "camelcase": "3.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true, + "optional": true + } + } + }, + "yauzl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", + "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", + "dev": true, + "requires": { + "fd-slicer": "1.0.1" + } + }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=", + "dev": true + }, + "yn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", + "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", + "dev": true + }, + "zone.js": { + "version": "0.8.18", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.8.18.tgz", + "integrity": "sha512-knKOBQM0oea3/x9pdyDuDi7RhxDlJhOIkeixXSiTKWLgs4LpK37iBc+1HaHwzlciHUKT172CymJFKo8Xgh+44Q==" + } + } +} diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 000000000..1f0c304c4 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,66 @@ +{ + "name": "ui", + "version": "0.0.0", + "license": "MIT", + "scripts": { + "ng": "ng", + "start": "ng serve --proxy-config proxy.conf.json", + "build": "ng build", + "test": "ng test --code-coverage", + "lint": "ng lint", + "e2e": "ng e2e", + "buildProd": "ng build --prod", + "bundle-report": "webpack-bundle-analyzer dist/stats.json" + }, + "private": true, + "dependencies": { + "@angular/animations": "5.0.5", + "@angular/common": "5.0.5", + "@angular/compiler": "5.0.5", + "@angular/core": "5.0.5", + "@angular/forms": "5.0.5", + "@angular/http": "5.0.5", + "@angular/platform-browser": "5.0.5", + "@angular/platform-browser-dynamic": "5.0.5", + "@angular/router": "5.0.5", + "@ng-bootstrap/ng-bootstrap": "^1.0.0-beta.7", + "@ngrx/effects": "^4.0.5", + "@ngrx/router-store": "^4.0.4", + "@ngrx/store": "^4.0.3", + "bootstrap": "4.0.0-beta.2", + "core-js": "^2.4.1", + "file-saver": "^1.3.3", + "font-awesome": "^4.7.0", + "rxjs": "^5.5.2", + "xml-formatter": "^1.0.1", + "zone.js": "^0.8.14" + }, + "devDependencies": { + "@angular/cli": "1.5.0", + "@angular/compiler-cli": "5.0.5", + "@angular/language-service": "5.0.5", + "@types/file-saver": "^1.3.0", + "@types/jasmine": "~2.5.53", + "@types/jasminewd2": "~2.0.2", + "@types/node": "~6.0.60", + "codelyzer": "~3.1.1", + "jasmine-core": "~2.6.2", + "jasmine-marbles": "^0.1.0", + "jasmine-spec-reporter": "~4.1.0", + "karma": "~1.7.0", + "karma-chrome-launcher": "~2.1.1", + "karma-cli": "~1.0.1", + "karma-coverage-istanbul-reporter": "^1.2.1", + "karma-jasmine": "~1.1.0", + "karma-jasmine-html-reporter": "^0.2.2", + "karma-phantomjs-launcher": "^1.0.4", + "karma-spec-reporter": "0.0.31", + "path": "^0.12.7", + "phantomjs-prebuilt": "^2.1.15", + "protractor": "~5.1.2", + "ts-node": "~3.2.0", + "tslint": "~5.3.2", + "typescript": "~2.5.3", + "webpack-bundle-analyzer": "^2.11.0" + } +} diff --git a/ui/protractor.conf.js b/ui/protractor.conf.js new file mode 100644 index 000000000..cc76abe2f --- /dev/null +++ b/ui/protractor.conf.js @@ -0,0 +1,28 @@ +// Protractor configuration file, see link for more information +// https://github.com/angular/protractor/blob/master/lib/config.ts + +const { SpecReporter } = require('jasmine-spec-reporter'); + +exports.config = { + allScriptsTimeout: 11000, + specs: [ + './e2e/**/*.e2e-spec.ts' + ], + capabilities: { + 'browserName': 'chrome' + }, + directConnect: true, + baseUrl: 'http://localhost:4200/', + framework: 'jasmine', + jasmineNodeOpts: { + showColors: true, + defaultTimeoutInterval: 30000, + print: function () { } + }, + onPrepare() { + require('ts-node').register({ + project: 'e2e/tsconfig.e2e.json' + }); + jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); + } +}; diff --git a/ui/proxy.conf.json b/ui/proxy.conf.json new file mode 100644 index 000000000..72708a90b --- /dev/null +++ b/ui/proxy.conf.json @@ -0,0 +1,17 @@ +{ + "/api": { + "target": "http://localhost:8080", + "secure": false, + "logLevel": "debug" + }, + "/login": { + "target": "http://localhost:8080", + "secure": false, + "logLevel": "debug" + }, + "/logout": { + "target": "http://localhost:8080", + "secure": false, + "logLevel": "debug" + } +} diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html new file mode 100644 index 000000000..2a0ada9c8 --- /dev/null +++ b/ui/src/app/app.component.html @@ -0,0 +1,53 @@ + +

+ + + diff --git a/ui/src/app/app.component.scss b/ui/src/app/app.component.scss new file mode 100644 index 000000000..e72a7d96c --- /dev/null +++ b/ui/src/app/app.component.scss @@ -0,0 +1,4 @@ +@import '../theme/palette'; +nav.navbar { + background-color: $white; +} \ No newline at end of file diff --git a/ui/src/app/app.component.spec.ts b/ui/src/app/app.component.spec.ts new file mode 100644 index 000000000..890189a4b --- /dev/null +++ b/ui/src/app/app.component.spec.ts @@ -0,0 +1,37 @@ +import { TestBed, async } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { AppComponent } from './app.component'; + +import { User } from './core/model/user'; +import * as fromUser from './core/reducer/user.reducer'; +import { NotificationModule } from './notification/notification.module'; + +describe('AppComponent', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + StoreModule.forRoot({ + user: combineReducers(fromUser.reducer), + }), + NotificationModule + ], + declarations: [ + AppComponent + ], + }).compileComponents(); + })); + + it('should create the app', async(() => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app).toBeTruthy(); + })); + + it(`should have as title 'Shib-UI'`, async(() => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app.title).toEqual('Shib UI'); + })); +}); diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts new file mode 100644 index 000000000..33d1acba3 --- /dev/null +++ b/ui/src/app/app.component.ts @@ -0,0 +1,22 @@ +import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { Store } from '@ngrx/store'; + +import * as fromUser from './core/reducer/user.reducer'; +import { LoadProviderRequest } from './metadata-provider/action/provider.action'; +import { LoadDraftRequest } from './metadata-provider/action/draft.action'; +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'] +}) +export class AppComponent implements OnInit { + title = 'Shib UI'; + + constructor(private store: Store) { } + + ngOnInit(): void { + this.store.dispatch(new LoadProviderRequest()); + this.store.dispatch(new LoadDraftRequest()); + } +} diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts new file mode 100644 index 000000000..95a179270 --- /dev/null +++ b/ui/src/app/app.module.ts @@ -0,0 +1,66 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { StoreModule } from '@ngrx/store'; +import { EffectsModule } from '@ngrx/effects'; +import { StoreRouterConnectingModule, RouterStateSerializer } from '@ngrx/router-store'; +import { HTTP_INTERCEPTORS } from '@angular/common/http'; + +import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'; + +import { AppRoutingModule } from './routing.module'; +import { AppComponent } from './app.component'; + +import { CoreModule } from './core/core.module'; +import { MetadataProviderModule } from './metadata-provider/metadata-provider.module'; +import { reducers } from './core/reducer'; +import { CustomRouterStateSerializer } from './shared/util'; + +import { UserEffects } from './core/effect/user.effect'; +import { CachingInterceptor } from './core/service/cache.interceptor'; +import { AuthorizedInterceptor } from './core/service/authorized.interceptor'; +import { NotificationModule } from './notification/notification.module'; +import { ErrorInterceptor } from './core/service/error.interceptor'; + +@NgModule({ + declarations: [ + AppComponent + ], + imports: [ + StoreModule.forRoot({ + ...reducers + }), + EffectsModule.forRoot([ + UserEffects + ]), + BrowserModule, + AppRoutingModule, + CoreModule.forRoot(), + MetadataProviderModule.forRoot(), + StoreRouterConnectingModule, + NgbDropdownModule.forRoot(), + NgbModalModule.forRoot(), + NgbPopoverModule.forRoot(), + NgbPaginationModule.forRoot(), + NotificationModule + ], + providers: [ + { provide: RouterStateSerializer, useClass: CustomRouterStateSerializer }, + { + provide: HTTP_INTERCEPTORS, + useClass: CachingInterceptor, + multi: true + }, + { + provide: HTTP_INTERCEPTORS, + useClass: AuthorizedInterceptor, + multi: true + }, + { + provide: HTTP_INTERCEPTORS, + useClass: ErrorInterceptor, + multi: true + } + ], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/ui/src/app/core/action/user.action.ts b/ui/src/app/core/action/user.action.ts new file mode 100644 index 000000000..adf35f1a9 --- /dev/null +++ b/ui/src/app/core/action/user.action.ts @@ -0,0 +1,40 @@ +import { Action } from '@ngrx/store'; +import { User } from '../model/user'; + +export const USER_LOAD_REQUEST = '[Auth] User REQUEST'; +export const USER_LOAD_SUCCESS = '[Auth] User SUCCESS'; +export const USER_LOAD_ERROR = '[Auth] User ERROR'; +export const REDIRECT = '[Auth] User Redirect'; + +/** + * Add User to Collection Actions + */ +export class UserLoadRequestAction implements Action { + readonly type = USER_LOAD_REQUEST; + + constructor() { } +} + +export class UserLoadSuccessAction implements Action { + readonly type = USER_LOAD_SUCCESS; + + constructor(public payload: User) { } +} + +export class UserLoadErrorAction implements Action { + readonly type = USER_LOAD_ERROR; + + constructor(public payload: { message: string, type: string }) { } +} + +export class UserRedirect implements Action { + readonly type = REDIRECT; + + constructor(public payload: string) { } +} + +export type Actions = + | UserLoadRequestAction + | UserLoadSuccessAction + | UserLoadErrorAction + | UserRedirect; diff --git a/ui/src/app/core/core.module.ts b/ui/src/app/core/core.module.ts new file mode 100644 index 000000000..e5d3e3284 --- /dev/null +++ b/ui/src/app/core/core.module.ts @@ -0,0 +1,33 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; +import { HttpModule } from '@angular/http'; + +import { UserService } from './service/user.service'; +import { CanDeactivateGuard } from './service/can-deactivate.guard'; +import { FileService } from './service/file.service'; + +export const COMPONENTS = []; + +@NgModule({ + imports: [ + CommonModule, + RouterModule, + HttpModule + ], + declarations: COMPONENTS, + entryComponents: COMPONENTS, + exports: COMPONENTS, +}) +export class CoreModule { + static forRoot() { + return { + ngModule: CoreModule, + providers: [ + UserService, + FileService, + CanDeactivateGuard + ] + }; + } +} diff --git a/ui/src/app/core/effect/user.effect.ts b/ui/src/app/core/effect/user.effect.ts new file mode 100644 index 000000000..06420e5ea --- /dev/null +++ b/ui/src/app/core/effect/user.effect.ts @@ -0,0 +1,38 @@ +import 'rxjs/add/operator/switchMap'; +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/catch'; +import 'rxjs/add/operator/do'; +import { of } from 'rxjs/observable/of'; +import { Injectable } from '@angular/core'; +import { Effect, Actions } from '@ngrx/effects'; +import { Location } from '@angular/common'; + +import * as user from '../action/user.action'; +import { User } from '../model/user'; +import { UserService } from '../service/user.service'; + +@Injectable() +export class UserEffects { + + @Effect() + loadUser$ = this.actions$ + .ofType(user.USER_LOAD_REQUEST) + .switchMap(() => + this.userService + .get() + .map(u => new user.UserLoadSuccessAction({ ...u })) + .catch(error => of(new user.UserLoadErrorAction(error))) + ); + @Effect({dispatch: false}) + redirect$ = this.actions$ + .ofType(user.REDIRECT) + .map((action: user.UserRedirect) => action.payload) + .do(path => { + window.location.href = path; + }); + + constructor( + private userService: UserService, + private actions$: Actions + ) { } +} /* istanbul ignore next */ diff --git a/ui/src/app/core/model/user.ts b/ui/src/app/core/model/user.ts new file mode 100644 index 000000000..37d05212e --- /dev/null +++ b/ui/src/app/core/model/user.ts @@ -0,0 +1,8 @@ +export interface User { + id: string; + role: string; + name: { + first: string, + last: string + }; +} diff --git a/ui/src/app/core/reducer/index.ts b/ui/src/app/core/reducer/index.ts new file mode 100644 index 000000000..360b704b5 --- /dev/null +++ b/ui/src/app/core/reducer/index.ts @@ -0,0 +1,29 @@ +import { + ActionReducerMap, + createSelector, + createFeatureSelector, + ActionReducer, + MetaReducer, + combineReducers +} from '@ngrx/store'; + +import { routerReducer, RouterReducerState } from '@ngrx/router-store'; +import * as fromRouter from '@ngrx/router-store'; +import * as fromUser from './user.reducer'; +import { RouterStateUrl } from '../../shared/util'; + +export interface State { + routerReducer: fromRouter.RouterReducerState; + user: fromUser.UserState; +} + +export const reducers: ActionReducerMap = { + routerReducer: fromRouter.routerReducer, + user: fromUser.reducer +}; + +export const getState = createFeatureSelector('user'); +export const getUserState = createSelector(getState, (state: State) => state.user); +export const getUser = createSelector(getUserState, fromUser.getUser); +export const isFetching = createSelector(getUserState, fromUser.isFetching); +export const getError = createSelector(getUserState, fromUser.getError); diff --git a/ui/src/app/core/reducer/user.reducer.spec.ts b/ui/src/app/core/reducer/user.reducer.spec.ts new file mode 100644 index 000000000..6dc2bed01 --- /dev/null +++ b/ui/src/app/core/reducer/user.reducer.spec.ts @@ -0,0 +1,84 @@ +import { reducer } from './user.reducer'; +import * as fromUser from './user.reducer'; +import * as actions from '../action/user.action'; +import { User } from '../model/user'; + +describe('User Reducer', () => { + const initialState: fromUser.UserState = { + fetching: false, + user: null, + error: null + }; + + const user: User = { + id: '1', + role: 'admin', + name: { + first: 'foo', + last: 'bar' + } + }; + + describe('undefined action', () => { + it('should return the default state', () => { + const result = reducer(undefined, {} as any); + expect(result).toEqual(initialState); + }); + }); + + describe('User Load Request', () => { + it('should set fetching to true', () => { + const action = new actions.UserLoadRequestAction(); + const result = reducer(initialState, action); + expect(result).toEqual( + Object.assign({}, initialState, { fetching: true }) + ); + }); + }); + + describe('User Load Success', () => { + it('should update the user', () => { + const action = new actions.UserLoadSuccessAction(user); + const result = reducer(initialState, action); + expect(result).toEqual( + Object.assign({}, initialState, { user: user }) + ); + }); + }); + + describe('User Load Error', () => { + it('should store the error message', () => { + const error = { message: 'Failed', type: '404' }; + const action = new actions.UserLoadErrorAction(error); + const result = reducer(initialState, action); + expect(result).toEqual( + Object.assign({}, initialState, { error: error }) + ); + }); + }); + + describe('User Selectors', () => { + const state = { + user: { + id: '1', + role: 'admin', + name: { + first: 'foo', + last: 'bar' + } + }, + fetching: true, + error: { message: 'foo', type: 'bar' } + } as fromUser.UserState; + + it('should select the user', () => { + expect(fromUser.getUser(state)).toEqual(state.user); + }); + it('should select the fetching state', () => { + expect(fromUser.isFetching(state)).toEqual(state.fetching); + }); + it('should select the user', () => { + expect(fromUser.getError(state)).toEqual(state.error); + }); + }); +}); diff --git a/ui/src/app/core/reducer/user.reducer.ts b/ui/src/app/core/reducer/user.reducer.ts new file mode 100644 index 000000000..b18810b2c --- /dev/null +++ b/ui/src/app/core/reducer/user.reducer.ts @@ -0,0 +1,50 @@ +import { createSelector, createFeatureSelector } from '@ngrx/store'; +import { User } from '../model/user'; +import * as user from '../action/user.action'; +import * as fromRoot from '../../core/reducer'; + +export interface UserState { + fetching: boolean; + user: User | null; + error: { + type: string, + message: string + }; +} + +export const initialState: UserState = { + fetching: false, + user: null, + error: null +}; + +export function reducer(state = initialState, action: user.Actions): UserState { + switch (action.type) { + case user.USER_LOAD_REQUEST: { + return Object.assign({}, state, { + fetching: true + }); + } + case user.USER_LOAD_SUCCESS: { + return Object.assign({}, state, { + fetching: false, + user: action.payload + }); + } + case user.USER_LOAD_ERROR: { + return Object.assign({}, state, { + fetching: false, + user: null, + error: action.payload + }); + } + default: { + return state; + } + } +} + + +export const getUser = (state: UserState) => state.user; +export const getError = (state: UserState) => state.error; +export const isFetching = (state: UserState) => state.fetching; diff --git a/ui/src/app/core/service/authorized.interceptor.ts b/ui/src/app/core/service/authorized.interceptor.ts new file mode 100644 index 000000000..b4c8e7c59 --- /dev/null +++ b/ui/src/app/core/service/authorized.interceptor.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/observable/throw'; +import { HttpRequest, HttpResponse, HttpErrorResponse, HttpInterceptor, HttpHandler, HttpEvent } from '@angular/common/http'; +import { Store } from '@ngrx/store'; +import * as fromUser from '../reducer/user.reducer'; +import { UserRedirect } from '../action/user.action'; + +@Injectable() +export class AuthorizedInterceptor implements HttpInterceptor { + constructor(private store: Store) { } + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + return next + .handle(req) + .catch((error) => { + if (!error.url.match(req.url)) { + this.store.dispatch(new UserRedirect(error.url)); + } + return Observable.throw(error); + }); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/core/service/cache.interceptor.ts b/ui/src/app/core/service/cache.interceptor.ts new file mode 100644 index 000000000..03122055c --- /dev/null +++ b/ui/src/app/core/service/cache.interceptor.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { HttpRequest, HttpResponse, HttpInterceptor, HttpHandler, HttpEvent } from '@angular/common/http'; + +class HttpCache { + private store: {[key: string]: HttpResponse}; + constructor() { + this.store = {}; + } + private generateKey(request: HttpRequest): string { + return `${request.method}.${request.urlWithParams}`; + } + get(req: HttpRequest): HttpResponse | null { + return this.store[this.generateKey(req)]; + } + put(req: HttpRequest, resp: HttpResponse): void { + this.store[`${req.method}.${req.urlWithParams}`] = resp; + } + clear(): void { + this.store = {}; + } +} + +@Injectable() +export class CachingInterceptor implements HttpInterceptor { + private cache: HttpCache; + + constructor() { + this.cache = new HttpCache(); + } + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + if (req.method !== 'GET') { + this.cache.clear(); + return next.handle(req); + } + const cachedResponse = this.cache.get(req); + if (cachedResponse) { + return Observable.of(cachedResponse); + } + return next.handle(req).do(event => { + if (event instanceof HttpResponse) { + this.cache.put(req, event); + } + }); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/core/service/can-deactivate.guard.ts b/ui/src/app/core/service/can-deactivate.guard.ts new file mode 100644 index 000000000..cde89e448 --- /dev/null +++ b/ui/src/app/core/service/can-deactivate.guard.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; +import { + CanDeactivate, + ActivatedRouteSnapshot, + RouterStateSnapshot +} from '@angular/router'; +import { Observable } from 'rxjs/Observable'; + +export interface CanComponentDeactivate { + canDeactivate: ( + currentRoute: ActivatedRouteSnapshot, + currentState: RouterStateSnapshot, + nextState: RouterStateSnapshot + ) => Observable | Promise | boolean; +} + +@Injectable() +export class CanDeactivateGuard implements CanDeactivate { + canDeactivate( + component: CanComponentDeactivate, + currentRoute: ActivatedRouteSnapshot, + currentState: RouterStateSnapshot, + nextState: RouterStateSnapshot + ) { + return component.canDeactivate ? component.canDeactivate(currentRoute, currentState, nextState) : true; + } +} /* istanbul ignore next */ diff --git a/ui/src/app/core/service/error.interceptor.ts b/ui/src/app/core/service/error.interceptor.ts new file mode 100644 index 000000000..bec847de0 --- /dev/null +++ b/ui/src/app/core/service/error.interceptor.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/observable/throw'; +import { HttpRequest, HttpResponse, HttpErrorResponse, HttpInterceptor, HttpHandler, HttpEvent } from '@angular/common/http'; +import { Store } from '@ngrx/store'; +import * as fromNotifications from '../../notification/reducer'; +import { AddNotification } from '../../notification/action/notification.action'; +import { Notification, NotificationType } from '../../notification/model/notification'; + +@Injectable() +export class ErrorInterceptor implements HttpInterceptor { + private caches = []; + constructor(private store: Store) { } + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + return next + .handle(req) + .catch((error) => { + let msg = error.error; + if (typeof msg !== 'string' && msg.hasOwnProperty('message')) { + msg = `${msg.exception}: ${msg.message}`; + } + this.store.dispatch(new AddNotification(new Notification( + NotificationType.Danger, + msg, + 8000 + ))); + return Observable.throw(error); + }); + } +} diff --git a/ui/src/app/core/service/file.service.spec.ts b/ui/src/app/core/service/file.service.spec.ts new file mode 100644 index 000000000..518d8269f --- /dev/null +++ b/ui/src/app/core/service/file.service.spec.ts @@ -0,0 +1,33 @@ +import { TestBed, async, inject } from '@angular/core/testing'; +import { FileService } from './file.service'; + +const getFakeFile = (str: string) => { + let blob = new Blob([str], { type: 'text/html' }); + blob['lastModifiedDate'] = ''; + blob['name'] = str; + return blob; +}; + +describe('File Service', () => { + let service: FileService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [ + FileService + ] + }); + service = TestBed.get(FileService); + }); + + it('should instantiate', () => { + expect(service).toBeDefined(); + }); + + describe('readAsText method', () => { + it('should return an observable', () => { + expect(service.readAsText(getFakeFile('foo'))).toBeDefined(); + }); + }); +}); diff --git a/ui/src/app/core/service/file.service.ts b/ui/src/app/core/service/file.service.ts new file mode 100644 index 000000000..568fc9d57 --- /dev/null +++ b/ui/src/app/core/service/file.service.ts @@ -0,0 +1,26 @@ +import 'rxjs/add/observable/of'; +import { Injectable } from '@angular/core'; +import { Http } from '@angular/http'; +import { Observable } from 'rxjs/Observable'; +import { User } from '../model/user'; +import { Subject } from 'rxjs/Subject'; + +@Injectable() +export class FileService { + + getLoader(sub: Subject): any { + return (evt) => { + const reader = evt.target as FileReader; + const txt = reader.result; + sub.next(txt); + sub.complete(); + }; + } + readAsText(file: File): Observable { + let sub = new Subject(), + fileReader = new FileReader(); + fileReader.onload = this.getLoader(sub); + fileReader.readAsText(file); + return sub.asObservable(); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/core/service/user.service.spec.ts b/ui/src/app/core/service/user.service.spec.ts new file mode 100644 index 000000000..62ef7e5ad --- /dev/null +++ b/ui/src/app/core/service/user.service.spec.ts @@ -0,0 +1,21 @@ +import { TestBed, async, inject } from '@angular/core/testing'; +import { HttpModule } from '@angular/http'; +import { UserService } from './user.service'; + +describe('User Service', () => { + let service: UserService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpModule], + providers: [ + UserService + ] + }); + service = TestBed.get(UserService); + }); + + it('should instantiate', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/ui/src/app/core/service/user.service.ts b/ui/src/app/core/service/user.service.ts new file mode 100644 index 000000000..f9dcdffd4 --- /dev/null +++ b/ui/src/app/core/service/user.service.ts @@ -0,0 +1,23 @@ +import 'rxjs/add/observable/of'; +import { Injectable } from '@angular/core'; +import { Http } from '@angular/http'; +import { Observable } from 'rxjs/Observable'; +import { User } from '../model/user'; + +@Injectable() +export class UserService { + + constructor(private http: Http) { } + + get(): Observable { + const defUser = Object.assign({}, { + id: 'foo', + role: 'admin', + name: { + first: 'Ryan', + last: 'Mathis' + } + }); + return Observable.of(defUser); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/dashboard/action/dashboard.action.ts b/ui/src/app/dashboard/action/dashboard.action.ts new file mode 100644 index 000000000..d9c3d01f5 --- /dev/null +++ b/ui/src/app/dashboard/action/dashboard.action.ts @@ -0,0 +1,21 @@ +import { Action } from '@ngrx/store'; +import { MetadataProvider } from '../../metadata-provider/model/metadata-provider'; + +export const TOGGLE_PROVIDER_DISPLAY = '[Dashboard] Display Provider'; +export const PREVIEW_PROVIDER = '[Dashboard] Preview Provider'; + +export class ToggleProviderDisplay implements Action { + readonly type = TOGGLE_PROVIDER_DISPLAY; + + constructor(public payload: string) { } +} + +export class PreviewProvider implements Action { + readonly type = PREVIEW_PROVIDER; + + constructor(public payload: MetadataProvider) { } +} + +export type Actions = + | ToggleProviderDisplay + | PreviewProvider; diff --git a/ui/src/app/dashboard/action/search.action.ts b/ui/src/app/dashboard/action/search.action.ts new file mode 100644 index 000000000..afe1b590f --- /dev/null +++ b/ui/src/app/dashboard/action/search.action.ts @@ -0,0 +1,24 @@ +import { Action } from '@ngrx/store'; +import { MetadataProvider } from '../../metadata-provider/model/metadata-provider'; + +export const PROVIDER_SEARCH = '[Metadata Provider Search] Provider Search'; +export const PROVIDER_SEARCH_COMPLETE = '[Metadata Provider Search] Provider Search COMPLETE'; + +/** + * Add Provider to Collection Actions + */ +export class SearchAction implements Action { + readonly type = PROVIDER_SEARCH; + + constructor(public payload) { } +} + +export class SearchCompleteAction implements Action { + readonly type = PROVIDER_SEARCH_COMPLETE; + + constructor(public payload: MetadataProvider[]) { } +} + +export type Actions = + | SearchAction + | SearchCompleteAction; diff --git a/ui/src/app/dashboard/component/delete-dialog.component.html b/ui/src/app/dashboard/component/delete-dialog.component.html new file mode 100644 index 000000000..bd1f3394c --- /dev/null +++ b/ui/src/app/dashboard/component/delete-dialog.component.html @@ -0,0 +1,10 @@ + + + diff --git a/ui/src/app/dashboard/component/delete-dialog.component.ts b/ui/src/app/dashboard/component/delete-dialog.component.ts new file mode 100644 index 000000000..6fb44dd28 --- /dev/null +++ b/ui/src/app/dashboard/component/delete-dialog.component.ts @@ -0,0 +1,16 @@ +import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; + +import { MetadataProvider } from '../../metadata-provider/model/metadata-provider'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'; + +@Component({ + selector: 'delete-dialog', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './delete-dialog.component.html' +}) +export class DeleteDialogComponent { + constructor( + public activeModal: NgbActiveModal + ) { } +} /* istanbul ignore next */ diff --git a/ui/src/app/dashboard/component/provider-item.component.html b/ui/src/app/dashboard/component/provider-item.component.html new file mode 100644 index 000000000..52753aa4b --- /dev/null +++ b/ui/src/app/dashboard/component/provider-item.component.html @@ -0,0 +1,65 @@ +
+
+
+
+
+ +
+
+ {{ provider.serviceProviderName }} + + Incomplete Form + {{ provider.entityId }} +
+
+
+ + +
+
+
+
+
+
+
+
+
Service Provider Name:
+
{{ provider.serviceProviderName }}
+
Created Date:
+
{{ provider.createdDate | date:'medium' }}
+
+
+
+
Service Provider Entity ID:
+
{{ provider.entityId }}
+
Service Provider Status:
+
+ + Enabled + Disabled +
+
+ Incomplete Form +
+
+
+
+ +
+
+ +
+
+
diff --git a/ui/src/app/dashboard/component/provider-item.component.scss b/ui/src/app/dashboard/component/provider-item.component.scss new file mode 100644 index 000000000..8eb28bc45 --- /dev/null +++ b/ui/src/app/dashboard/component/provider-item.component.scss @@ -0,0 +1,12 @@ +@import '../../../theme/palette'; + +.card { + &:focus { + outline: -webkit-focus-ring-color auto 5px; + } + .btn-link { + &:focus { + outline: -webkit-focus-ring-color auto 5px; + } + } +} diff --git a/ui/src/app/dashboard/component/provider-item.component.spec.ts b/ui/src/app/dashboard/component/provider-item.component.spec.ts new file mode 100644 index 000000000..1fd07a8b6 --- /dev/null +++ b/ui/src/app/dashboard/component/provider-item.component.spec.ts @@ -0,0 +1,31 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { ProviderItemComponent } from './provider-item.component'; +import { MetadataProvider } from '../../metadata-provider/model/metadata-provider'; + +describe('Provider List item', () => { + let fixture: ComponentFixture; + let instance: ProviderItemComponent; + + let provider = { entityId: 'foo', serviceProviderName: 'bar' } as MetadataProvider; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [], + imports: [ + NoopAnimationsModule + ], + declarations: [ProviderItemComponent], + }); + + fixture = TestBed.createComponent(ProviderItemComponent); + instance = fixture.componentInstance; + instance.provider = provider; + }); + + it('should compile', () => { + fixture.detectChanges(); + + expect(fixture).toBeDefined(); + }); +}); diff --git a/ui/src/app/dashboard/component/provider-item.component.ts b/ui/src/app/dashboard/component/provider-item.component.ts new file mode 100644 index 000000000..bcad29156 --- /dev/null +++ b/ui/src/app/dashboard/component/provider-item.component.ts @@ -0,0 +1,21 @@ +import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; + +import { MetadataProvider } from '../../metadata-provider/model/metadata-provider'; + +@Component({ + selector: 'provider-item', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './provider-item.component.html', + styleUrls: ['./provider-item.component.scss'] +}) +export class ProviderItemComponent { + + @Input() provider: MetadataProvider; + @Input() isOpen: boolean; + @Output() select = new EventEmitter(); + @Output() toggle = new EventEmitter(); + @Output() preview = new EventEmitter(); + @Output() delete = new EventEmitter(); + +} /* istanbul ignore next */ diff --git a/ui/src/app/dashboard/component/provider-search.component.html b/ui/src/app/dashboard/component/provider-search.component.html new file mode 100644 index 000000000..d9149e8cd --- /dev/null +++ b/ui/src/app/dashboard/component/provider-search.component.html @@ -0,0 +1,24 @@ +
+
+ + + + + +
+
diff --git a/ui/src/app/dashboard/component/provider-search.component.ts b/ui/src/app/dashboard/component/provider-search.component.ts new file mode 100644 index 000000000..c02932052 --- /dev/null +++ b/ui/src/app/dashboard/component/provider-search.component.ts @@ -0,0 +1,26 @@ +import { Component, Output, Input, EventEmitter, OnChanges } from '@angular/core'; +import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms'; + +@Component({ + selector: 'provider-search', + templateUrl: './provider-search.component.html' +}) +export class ProviderSearchComponent implements OnChanges { + @Input() query = ''; + @Input() searching = false; + @Output() search = new EventEmitter(); + + searchForm: FormGroup; + + constructor(private fb: FormBuilder) { + this.searchForm = this.fb.group({ + search: [this.query] + }); + } + + ngOnChanges(): void { + this.searchForm.setValue({ + search: this.query + }); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/dashboard/container/dashboard.component.html b/ui/src/app/dashboard/container/dashboard.component.html new file mode 100644 index 000000000..96104742a --- /dev/null +++ b/ui/src/app/dashboard/container/dashboard.component.html @@ -0,0 +1,42 @@ +
+
+
+
+
+ Current Metadata Sources +
+
+ + +
+
+
+
+
    +
  • + + +
  • +
+
+ + +
+
+
+
diff --git a/ui/src/app/dashboard/container/dashboard.component.spec.ts b/ui/src/app/dashboard/container/dashboard.component.spec.ts new file mode 100644 index 000000000..3058f6e19 --- /dev/null +++ b/ui/src/app/dashboard/container/dashboard.component.spec.ts @@ -0,0 +1,147 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { Router } from '@angular/router'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap/pagination/pagination.module'; +import { NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap/modal/modal.module'; +import { DashboardComponent } from './dashboard.component'; +import * as fromDashboard from '../reducer'; +import { ProviderSearchComponent } from '../component/provider-search.component'; +import { ProviderItemComponent } from '../component/provider-item.component'; +import { DeleteDialogComponent } from '../component/delete-dialog.component'; +import { RouterStub } from '../../../testing/router.stub'; +import { NgbModalStub } from '../../../testing/modal.stub'; +import { MetadataProvider } from '../../metadata-provider/model/metadata-provider'; + + +describe('Dashboard Page', () => { + let fixture: ComponentFixture; + let store: Store; + let router: Router; + let modal: NgbModal; + let instance: DashboardComponent; + + let draft = { + entityId: 'foo', + serviceProviderName: 'bar' + } as MetadataProvider, + provider = { + ...draft, + id: '1' + } as MetadataProvider; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { provide: Router, useClass: RouterStub }, + { provide: NgbModal, useClass: NgbModalStub } + ], + imports: [ + NoopAnimationsModule, + StoreModule.forRoot({ + dashboard: combineReducers(fromDashboard.reducers), + }), + ReactiveFormsModule, + NgbPaginationModule, + NgbModalModule + ], + declarations: [ + DashboardComponent, + ProviderSearchComponent, + ProviderItemComponent, + DeleteDialogComponent + ], + }); + + fixture = TestBed.createComponent(DashboardComponent); + instance = fixture.componentInstance; + store = TestBed.get(Store); + router = TestBed.get(Router); + modal = TestBed.get(NgbModal); + + spyOn(store, 'dispatch').and.callThrough(); + }); + + it('should compile', () => { + fixture.detectChanges(); + + expect(fixture).toBeDefined(); + }); + + xdescribe('getPagedProviders method', () => {}); + + describe('changePage method', () => { + it('should update the page value', () => { + let page = 2; + instance.changePage(page); + expect(instance.page).toBe(page); + }); + + it('should update the paged providers list', () => { + let page = 2; + spyOn(instance, 'getPagedProviders'); + instance.changePage(page); + expect(instance.getPagedProviders).toHaveBeenCalled(); + }); + }); + + describe('toggleProvider method', () => { + it('should fire a redux action', () => { + instance.toggleProvider(draft); + expect(store.dispatch).toHaveBeenCalled(); + }); + }); + + describe('openPreviewDialog method', () => { + it('should fire a redux action', () => { + instance.openPreviewDialog(provider); + expect(store.dispatch).toHaveBeenCalled(); + }); + }); + + describe('search method', () => { + it('should fire a redux action', () => { + instance.search(); + expect(store.dispatch).toHaveBeenCalled(); + }); + it('should reset the page number to 1', () => { + instance.search('foo'); + expect(instance.page).toBe(1); + }); + }); + + describe('edit method', () => { + it('should route to the edit page', () => { + spyOn(router, 'navigate'); + instance.edit(provider); + expect(router.navigate).toHaveBeenCalledWith(['provider', provider.id, 'edit']); + }); + it('should route to the wizard page', () => { + spyOn(router, 'navigate'); + instance.edit(draft); + expect(router.navigate).toHaveBeenCalledWith(['provider', draft.entityId, 'wizard']); + }); + }); + + describe('deleteProvider method', () => { + it('should call the modal service', () => { + spyOn(modal, 'open').and.callFake(() => { + return { + result: Promise.resolve(true) + }; + }); + instance.deleteProvider(provider); + expect(modal.open).toHaveBeenCalled(); + }); + it('should log an error to the console on failure', () => { + spyOn(modal, 'open').and.callFake(() => { + return { + result: Promise.reject(false) + }; + }); + instance.deleteProvider(provider); + expect(modal.open).toHaveBeenCalled(); + }); + }); +}); diff --git a/ui/src/app/dashboard/container/dashboard.component.ts b/ui/src/app/dashboard/container/dashboard.component.ts new file mode 100644 index 000000000..4e4331494 --- /dev/null +++ b/ui/src/app/dashboard/container/dashboard.component.ts @@ -0,0 +1,98 @@ +import 'rxjs/add/operator/take'; +import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { Router } from '@angular/router'; +import { Observable } from 'rxjs/Observable'; +import { Store } from '@ngrx/store'; + +import { MetadataProvider } from '../../metadata-provider/model/metadata-provider'; +import * as searchActions from '../action/search.action'; +import * as providerActions from '../../metadata-provider/action/provider.action'; +import * as draftActions from '../../metadata-provider/action/draft.action'; +import * as fromProviders from '../../metadata-provider/reducer'; +import * as fromDashboard from '../reducer'; +import { ToggleProviderDisplay, PreviewProvider } from '../action/dashboard.action'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { DeleteDialogComponent } from '../component/delete-dialog.component'; + +@Component({ + selector: 'dashboard-page', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './dashboard.component.html' +}) +export class DashboardComponent implements OnInit { + searchQuery$: Observable; + providers$: Observable; + loading$: Observable; + + total$: Observable; + page = 1; + limit = 8; + limited$: Observable; + + providersOpen$: Observable<{[key: string]: boolean}>; + + constructor( + private store: Store, + private router: Router, + private modalService: NgbModal + ) { + this.providers$ = store.select(fromDashboard.getSearchResults); + this.searchQuery$ = store.select(fromDashboard.getSearchQuery); + this.loading$ = store.select(fromDashboard.getSearchLoading); + this.providersOpen$ = store.select(fromDashboard.getOpenProviders); + + this.total$ = this.providers$.map(list => list.length); + + this.limited$ = this.getPagedProviders(this.page, this.providers$); + } + + ngOnInit (): void { + this.search(); + } + + getPagedProviders(page: number, list$: Observable): Observable { + return list$.map((providers: MetadataProvider[]) => { + let maxIndex = (page * this.limit) - 1, + minIndex = ((page - 1) * this.limit); + return providers.filter((provider: MetadataProvider, index: number) => (maxIndex >= index && index >= minIndex) ); + }); + } + + changePage(index: number): void { + this.page = index; + this.limited$ = this.getPagedProviders(index, this.providers$); + } + + search(query: string = ''): void { + this.store.dispatch(new searchActions.SearchAction(query)); + this.page = 1; + } + + edit(provider: MetadataProvider): void { + let path = provider.id ? 'edit' : 'wizard', + id = provider.id ? provider.id : provider.entityId; + this.router.navigate(['provider', id, path]); + } + + toggleProvider(provider: MetadataProvider): void { + this.store.dispatch(new ToggleProviderDisplay(provider.entityId)); + } + + openPreviewDialog(provider: MetadataProvider): void { + this.store.dispatch(new PreviewProvider(provider)); + } + + deleteProvider(provider: MetadataProvider): void { + this.modalService + .open(DeleteDialogComponent) + .result + .then( + success => { + this.store.dispatch(new draftActions.RemoveDraftRequest(provider)); + }, + err => { + console.log('Cancelled'); + } + ); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/dashboard/dashboard.module.ts b/ui/src/app/dashboard/dashboard.module.ts new file mode 100644 index 000000000..fc47ab9d0 --- /dev/null +++ b/ui/src/app/dashboard/dashboard.module.ts @@ -0,0 +1,41 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { CommonModule } from '@angular/common'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap/pagination/pagination.module'; +import { StoreModule } from '@ngrx/store'; +import { EffectsModule } from '@ngrx/effects'; + +import { DashboardComponent } from './container/dashboard.component'; +import { ProviderItemComponent } from './component/provider-item.component'; +import { ProviderSearchComponent } from './component/provider-search.component'; +import { reducers } from './reducer'; +import { DashboardEffects } from './effect/dashboard.effect'; +import { SearchEffects } from './effect/search.effects'; +import { DeleteDialogComponent } from './component/delete-dialog.component'; +import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap/modal/modal.module'; + +@NgModule({ + declarations: [ + DashboardComponent, + ProviderItemComponent, + ProviderSearchComponent, + DeleteDialogComponent + ], + entryComponents: [ + DeleteDialogComponent + ], + imports: [ + RouterModule.forChild([ + { path: '', component: DashboardComponent } + ]), + StoreModule.forFeature('dashboard', reducers), + EffectsModule.forFeature([DashboardEffects, SearchEffects]), + CommonModule, + ReactiveFormsModule, + NgbPaginationModule, + NgbModalModule + ], + providers: [] +}) +export class DashboardModule { } diff --git a/ui/src/app/dashboard/effect/dashboard.effect.ts b/ui/src/app/dashboard/effect/dashboard.effect.ts new file mode 100644 index 000000000..d201f8e01 --- /dev/null +++ b/ui/src/app/dashboard/effect/dashboard.effect.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { Effect, Actions } from '@ngrx/effects'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import * as dashboardActions from '../action/dashboard.action'; +import { MetadataProvider } from '../../metadata-provider/model/metadata-provider'; +import { PreviewProviderDialogComponent } from '../../metadata-provider/component/preview-provider-dialog.component'; + +@Injectable() +export class DashboardEffects { + + @Effect({ dispatch: false }) + previewProviderXml$ = this.actions$ + .ofType(dashboardActions.PREVIEW_PROVIDER) + .map(action => action.payload) + .switchMap(provider => { + let modal = this.modalService.open(PreviewProviderDialogComponent, { + size: 'lg', + windowClass: 'modal-xl' + }); + modal.componentInstance.provider = provider; + return modal.result.then( + result => result, + err => err + ); + }); + + constructor( + private actions$: Actions, + private modalService: NgbModal + ) { } +} /* istanbul ignore next */ diff --git a/ui/src/app/dashboard/effect/search.effects.ts b/ui/src/app/dashboard/effect/search.effects.ts new file mode 100644 index 000000000..8b6770ac5 --- /dev/null +++ b/ui/src/app/dashboard/effect/search.effects.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { Effect, Actions } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; + +import * as providerSearch from '../action/search.action'; +import * as fromProviders from '../../metadata-provider/reducer'; +import { MetadataProvider } from '../../metadata-provider/model/metadata-provider'; +import { EntityDescriptorService } from '../../metadata-provider/service/entity-descriptor.service'; + +@Injectable() +export class SearchEffects { + + @Effect() + search$ = this.actions$ + .ofType(providerSearch.PROVIDER_SEARCH) + .map(action => action.payload) + .switchMap(query => + this.store.select(fromProviders.getAllProviders) + .map(descriptors => { + return descriptors.filter( + p => p.entityId.toLocaleLowerCase().match(query.toLocaleLowerCase()) || + p.serviceProviderName.toLocaleLowerCase().match(query.toLocaleLowerCase())); + }) + .map(providers => new providerSearch.SearchCompleteAction(providers)) + ); + + constructor( + private descriptorService: EntityDescriptorService, + private actions$: Actions, + private store: Store + ) { } +} /* istanbul ignore next */ diff --git a/ui/src/app/dashboard/reducer/dashboard.reducer.spec.ts b/ui/src/app/dashboard/reducer/dashboard.reducer.spec.ts new file mode 100644 index 000000000..26ad5e24d --- /dev/null +++ b/ui/src/app/dashboard/reducer/dashboard.reducer.spec.ts @@ -0,0 +1,30 @@ +import { reducer } from './dashboard.reducer'; +import * as fromDashboard from './dashboard.reducer'; +import { ToggleProviderDisplay } from '../action/dashboard.action'; + +describe('Dashboard Reducer', () => { + const initialState: fromDashboard.State = { + providersOpen: {} + }; + + describe('undefined action', () => { + it('should return the default state', () => { + const result = reducer(undefined, {} as any); + + expect(result).toEqual(initialState); + }); + }); + + describe('Toggle Provider Display', () => { + it('should toggle the selected providers open state', () => { + const id = 'foo'; + const action = new ToggleProviderDisplay(id); + + const result = reducer(initialState, action); + + expect(result).toEqual( + Object.assign({}, initialState, { providersOpen: { foo: true } }) + ); + }); + }); +}); diff --git a/ui/src/app/dashboard/reducer/dashboard.reducer.ts b/ui/src/app/dashboard/reducer/dashboard.reducer.ts new file mode 100644 index 000000000..d77423a50 --- /dev/null +++ b/ui/src/app/dashboard/reducer/dashboard.reducer.ts @@ -0,0 +1,32 @@ +import { createSelector, createFeatureSelector } from '@ngrx/store'; +import { MetadataProvider } from '../../metadata-provider/model/metadata-provider'; +import * as dashboard from '../action/dashboard.action'; +import * as provider from '../../metadata-provider/action/provider.action'; +import * as fromRoot from '../../core/reducer'; + +export interface State { + providersOpen: {[key: string]: boolean}; +} + +export const initialState: State = { + providersOpen: {} +}; + +export function reducer(state = initialState, action: dashboard.Actions): State { + switch (action.type) { + case dashboard.TOGGLE_PROVIDER_DISPLAY: { + return Object.assign({}, state, { + providersOpen: { + ...state.providersOpen, + ...{[action.payload]: !state.providersOpen[action.payload]} + } + }); + } + default: { + // console.log(state); + return state; + } + } +} + +export const providersOpen = (state: State) => state.providersOpen; diff --git a/ui/src/app/dashboard/reducer/index.ts b/ui/src/app/dashboard/reducer/index.ts new file mode 100644 index 000000000..48437443f --- /dev/null +++ b/ui/src/app/dashboard/reducer/index.ts @@ -0,0 +1,40 @@ +import { createSelector, createFeatureSelector } from '@ngrx/store'; +import * as fromRoot from '../../core/reducer'; +import * as fromDashboard from './dashboard.reducer'; +import * as fromSearch from './search.reducer'; +import * as fromProviders from '../../metadata-provider/reducer'; + +export interface DashboardState { + dashboard: fromDashboard.State; + search: fromSearch.SearchState; +} + +export interface State extends fromRoot.State { + 'dashboard': DashboardState; +} + +export const reducers = { + dashboard: fromDashboard.reducer, + search: fromSearch.reducer +}; + +export const getFeatureState = createFeatureSelector('dashboard'); +export const getDashboardState = createSelector(getFeatureState, (state: DashboardState) => state.dashboard); +export const getOpenProviders = createSelector(getDashboardState, (dashboard: fromDashboard.State) => dashboard.providersOpen); + +export const getSearchState = createSelector(getFeatureState, + (state: DashboardState) => state.search +); + +export const getSearchResults = createSelector( + getSearchState, + fromSearch.getEntities +); +export const getSearchQuery = createSelector( + getSearchState, + fromSearch.getQuery +); +export const getSearchLoading = createSelector( + getSearchState, + fromSearch.getLoading +); diff --git a/ui/src/app/dashboard/reducer/search.reducer.ts b/ui/src/app/dashboard/reducer/search.reducer.ts new file mode 100644 index 000000000..df10345b3 --- /dev/null +++ b/ui/src/app/dashboard/reducer/search.reducer.ts @@ -0,0 +1,46 @@ +import * as searchActions from '../action/search.action'; +import { MetadataProvider } from '../../metadata-provider/model/metadata-provider'; + +export interface SearchState { + entities: MetadataProvider[]; + loading: boolean; + query: string; +} + +const initialState: SearchState = { + entities: [], + loading: false, + query: '', +}; + +export function reducer(state = initialState, action: searchActions.Actions): SearchState { + switch (action.type) { + case searchActions.PROVIDER_SEARCH: { + const query = action.payload; + + return { + ...state, + query, + loading: true, + }; + } + + case searchActions.PROVIDER_SEARCH_COMPLETE: { + return { + entities: action.payload, + loading: false, + query: state.query, + }; + } + + default: { + return state; + } + } +} + +export const getEntities = (state: SearchState) => state.entities; + +export const getQuery = (state: SearchState) => state.query; + +export const getLoading = (state: SearchState) => state.loading; diff --git a/ui/src/app/edit-provider/action/editor.action.ts b/ui/src/app/edit-provider/action/editor.action.ts new file mode 100644 index 000000000..112ce865b --- /dev/null +++ b/ui/src/app/edit-provider/action/editor.action.ts @@ -0,0 +1,53 @@ +import { Action } from '@ngrx/store'; +import { MetadataProvider } from '../../metadata-provider/model/metadata-provider'; + +export const UPDATE_STATUS = '[Editor] Update Status'; +export const UPDATE_SAVED = '[Editor] Update Saved'; +export const UPDATE_CHANGES = '[Editor] Update Changes'; +export const CANCEL_CHANGES = '[Editor] Cancel Changes'; +export const SAVE_CHANGES = '[Editor] Save Changes'; +export const RESET_CHANGES = '[Editor] Reset Changes'; + +export class UpdateStatus implements Action { + readonly type = UPDATE_STATUS; + + constructor(public payload: { [key: string]: string }) { } +} + +export class UpdateSaved implements Action { + readonly type = UPDATE_SAVED; + + constructor(public payload: boolean) { } +} + +export class UpdateChanges implements Action { + readonly type = UPDATE_CHANGES; + + constructor(public payload: MetadataProvider) { } +} + +export class CancelChanges implements Action { + readonly type = CANCEL_CHANGES; + + constructor(public payload: boolean = true) { } +} + +export class SaveChanges implements Action { + readonly type = SAVE_CHANGES; + + constructor(public payload: MetadataProvider) { } +} + +export class ResetChanges implements Action { + readonly type = RESET_CHANGES; + + constructor() { } +} + +export type Actions = + | UpdateStatus + | UpdateSaved + | UpdateChanges + | CancelChanges + | SaveChanges + | ResetChanges; diff --git a/ui/src/app/edit-provider/component/unsaved-dialog.component.html b/ui/src/app/edit-provider/component/unsaved-dialog.component.html new file mode 100644 index 000000000..d93c2226b --- /dev/null +++ b/ui/src/app/edit-provider/component/unsaved-dialog.component.html @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/ui/src/app/edit-provider/component/unsaved-dialog.component.ts b/ui/src/app/edit-provider/component/unsaved-dialog.component.ts new file mode 100644 index 000000000..82d87fdc8 --- /dev/null +++ b/ui/src/app/edit-provider/component/unsaved-dialog.component.ts @@ -0,0 +1,36 @@ +import { Component, Input, OnChanges } from '@angular/core'; + +import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { Store, Action } from '@ngrx/store'; +import { Subject } from 'rxjs/Subject'; + +import { MetadataProvider } from '../../metadata-provider/model/metadata-provider'; +import * as fromEditor from '../reducer'; +import { UpdateDraftRequest } from '../../metadata-provider/action/draft.action'; +import { EntityDescriptorService } from '../../metadata-provider/service/entity-descriptor.service'; +import { SaveChanges, CancelChanges } from '../action/editor.action'; + +@Component({ + selector: 'unsaved-dialog', + templateUrl: './unsaved-dialog.component.html' +}) +export class UnsavedDialogComponent { + @Input() message; + @Input() action: Action; + + readonly subject: Subject = new Subject(); + + constructor( + public activeModal: NgbActiveModal, + private store: Store + ) { } + + close(): void { + this.store.dispatch(this.action); + this.activeModal.close(); + } + + dismiss(): void { + this.activeModal.dismiss(); + } +} diff --git a/ui/src/app/edit-provider/component/wizard-nav.component.html b/ui/src/app/edit-provider/component/wizard-nav.component.html new file mode 100644 index 000000000..079448ed4 --- /dev/null +++ b/ui/src/app/edit-provider/component/wizard-nav.component.html @@ -0,0 +1,41 @@ + diff --git a/ui/src/app/edit-provider/component/wizard-nav.component.spec.ts b/ui/src/app/edit-provider/component/wizard-nav.component.spec.ts new file mode 100644 index 000000000..ec147a173 --- /dev/null +++ b/ui/src/app/edit-provider/component/wizard-nav.component.spec.ts @@ -0,0 +1,78 @@ +import { Component, ViewChild } from '@angular/core'; +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { WizardNavComponent } from './wizard-nav.component'; +import * as fromEditor from '../reducer'; +import { ProviderEditorFormModule } from '../../metadata-provider/component/index'; + +@Component({ + template: + ` + ` +}) +class TestHostComponent { + config: any = { + index: 1 + }; + + @ViewChild(WizardNavComponent) + public navUnderTest: WizardNavComponent; + + configure(opts: any): void { + this.config = Object.assign({}, this.config, opts); + } + + onNext(): void {} + onPrevious(): void {} + onSave(): void {} +} + +describe('Wizard Nav Component', () => { + let fixture: ComponentFixture; + let store: Store; + let instance: TestHostComponent; + let wizard: WizardNavComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [], + imports: [ + NoopAnimationsModule, + StoreModule.forRoot({ + 'edit-provider': combineReducers(fromEditor.reducers), + }), + ReactiveFormsModule, + ProviderEditorFormModule + ], + declarations: [TestHostComponent, WizardNavComponent], + }).compileComponents(); + }); + + beforeEach(() => { + store = TestBed.get(Store); + spyOn(store, 'dispatch').and.callThrough(); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + wizard = instance.navUnderTest; + instance.configure({ index: 1 }); + fixture.detectChanges(); + }); + + it('should compile', () => { + expect(fixture).toBeDefined(); + }); + + describe('ngOnChanges lifecycle event', () => { + it('should set currentPage when an index is provided', () => { + instance.configure({index: 2}); + fixture.detectChanges(); + expect(wizard.currentPage).toBeDefined(); + }); + }); +}); diff --git a/ui/src/app/edit-provider/component/wizard-nav.component.ts b/ui/src/app/edit-provider/component/wizard-nav.component.ts new file mode 100644 index 000000000..9e717c0c4 --- /dev/null +++ b/ui/src/app/edit-provider/component/wizard-nav.component.ts @@ -0,0 +1,47 @@ +import { Component, Input, Output, EventEmitter, OnChanges } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { Store } from '@ngrx/store'; + +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/switchMap'; + +import * as fromProviders from '../../metadata-provider/reducer'; +import * as fromEditor from '../reducer'; +import { WIZARD as WizardDef, EditorFlowDefinition } from '../editor-definition.const'; + +@Component({ + selector: 'wizard-nav', + templateUrl: './wizard-nav.component.html' +}) +export class WizardNavComponent implements OnChanges { + @Input() index: number; + @Output() onNext = new EventEmitter(); + @Output() onPrevious = new EventEmitter(); + @Output() onSave = new EventEmitter(); + + currentPage: any = {}; + previousPage: any = {}; + nextPage: any = {}; + isLastPage = false; + isFirstPage = false; + wizardIsValid$: Observable; + wizardIsInvalid$: Observable; + + wizard: EditorFlowDefinition[] = WizardDef; + + constructor( + private store: Store + ) {} + + ngOnChanges(): void { + this.currentPage = WizardDef.find(r => r.index === this.index); + this.previousPage = WizardDef.find(r => r.index === this.index - 1); + this.nextPage = WizardDef.find(r => r.index === this.index + 1); + this.isLastPage = WizardDef[WizardDef.length - 1].index === this.index; + this.isFirstPage = WizardDef[0].index === this.index; + this.wizard = WizardDef; + + this.wizardIsValid$ = this.store.select(fromEditor.getEditorIsValid); + this.wizardIsInvalid$ = this.wizardIsValid$.map(valid => !valid); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/edit-provider/container/draft.component.html b/ui/src/app/edit-provider/container/draft.component.html new file mode 100644 index 000000000..90c6b6463 --- /dev/null +++ b/ui/src/app/edit-provider/container/draft.component.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/src/app/edit-provider/container/draft.component.ts b/ui/src/app/edit-provider/container/draft.component.ts new file mode 100644 index 000000000..7a0b9e0fe --- /dev/null +++ b/ui/src/app/edit-provider/container/draft.component.ts @@ -0,0 +1,34 @@ +import { Component, Output, Input, EventEmitter, OnDestroy, ChangeDetectionStrategy } from '@angular/core'; +import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs/Observable'; +import { Subscription } from 'rxjs/Subscription'; +import { Store } from '@ngrx/store'; +import { NgbPopoverConfig } from '@ng-bootstrap/ng-bootstrap'; + +import { MetadataProvider } from '../../metadata-provider/model/metadata-provider'; +import { SelectDraft } from '../../metadata-provider/action/draft.action'; +import * as fromProviders from '../../metadata-provider/reducer'; +@Component({ + selector: 'provider-page', + templateUrl: './provider.component.html', + styleUrls: ['./provider.component.scss'], + providers: [NgbPopoverConfig] +}) +export class DraftComponent implements OnDestroy { + actionsSubscription: Subscription; + + constructor( + store: Store, + route: ActivatedRoute + ) { + this.actionsSubscription = route.params + .distinctUntilChanged() + .map(params => new SelectDraft(params.entityId)) + .subscribe(store); + } + + ngOnDestroy() { + this.actionsSubscription.unsubscribe(); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/edit-provider/container/editor.component.html b/ui/src/app/edit-provider/container/editor.component.html new file mode 100644 index 000000000..10ed6feaf --- /dev/null +++ b/ui/src/app/edit-provider/container/editor.component.html @@ -0,0 +1,82 @@ +
+
+
+
+
+ Edit Metadata Source - {{ (provider$ | async).serviceProviderName }} +
+
+
+
+
+
+ +
+
+ + +
+
+
+
+
+ +
+
+
+ + + + + + + + +
+
+
+
+
+
diff --git a/ui/src/app/edit-provider/container/editor.component.ts b/ui/src/app/edit-provider/container/editor.component.ts new file mode 100644 index 000000000..201051d21 --- /dev/null +++ b/ui/src/app/edit-provider/container/editor.component.ts @@ -0,0 +1,164 @@ +import { + Component, + ViewChild, + AfterViewInit, + OnInit, + OnDestroy, + EventEmitter +} from '@angular/core'; +import { + ActivatedRoute, + Router, + CanDeactivate, + ActivatedRouteSnapshot, + RouterStateSnapshot +} from '@angular/router'; +import { FormBuilder, FormGroup, FormControl, Validators, NgModel } from '@angular/forms'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; +import { Store } from '@ngrx/store'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap/modal/modal'; + +import 'rxjs/add/operator/skip'; +import 'rxjs/add/operator/combineLatest'; + +import { MetadataProvider } from '../../metadata-provider/model/metadata-provider'; +import * as fromProviders from '../../metadata-provider/reducer'; +import { UpdateProviderRequest } from '../../metadata-provider/action/provider.action'; +import * as fromEditor from '../reducer'; + +import { ProviderStatusEmitter, ProviderValueEmitter } from '../../metadata-provider/service/provider-change-emitter.service'; +import { UpdateStatus, UpdateChanges, CancelChanges } from '../action/editor.action'; +import { EDITOR as EditorDef, EditorFlowDefinition } from '../editor-definition.const'; +import { UnsavedDialogComponent } from '../component/unsaved-dialog.component'; + +@Component({ + selector: 'editor-page', + templateUrl: './editor.component.html' +}) +export class EditorComponent implements OnInit, OnDestroy { + + private ngUnsubscribe: Subject = new Subject(); + + unsavedMessage = `You haven't saved your changes!`; + modified = false; + open = false; + + changes$: Observable; + changes: MetadataProvider; + updates: MetadataProvider; + latest: MetadataProvider; + latest$: Observable; + + provider$: Observable; + providerName$: Observable; + provider: MetadataProvider; + editorIndex$: Observable; + editor$: Observable; + editor: EditorFlowDefinition[]; + currentPage$: Observable; + saving: boolean; + + wizardIsValid$: Observable; + wizardIsInvalid$: Observable; + + constructor( + private store: Store, + private route: ActivatedRoute, + private router: Router, + private statusEmitter: ProviderStatusEmitter, + private valueEmitter: ProviderValueEmitter, + private modalService: NgbModal + ) { + this.provider$ = this.store.select(fromProviders.getSelectedProvider); + this.changes$ = this.store.select(fromEditor.getEditorChanges); + + this.latest$ = this.provider$ + .combineLatest(this.changes$, (base, changes) => Object.assign({}, base, changes)); + + this.providerName$ = this.provider$.map(p => p.serviceProviderName); + this.changes$ = this.store.select(fromEditor.getEditorChanges); + this.editorIndex$ = this.route.params.map(params => Number(params.index)); + this.currentPage$ = this.editorIndex$.map(index => EditorDef.find(r => r.index === index)); + this.editor = EditorDef; + this.store.select(fromEditor.getEditorIsSaving).takeUntil(this.ngUnsubscribe).subscribe(saving => this.saving = saving); + + this.wizardIsValid$ = this.store.select(fromEditor.getEditorIsValid); + this.wizardIsInvalid$ = this.wizardIsValid$.map(valid => !valid); + } + + save(): void { + this.store.dispatch(new UpdateProviderRequest({ + ...this.latest, + ...this.changes, + ...this.updates + })); + } + + cancel(): void { + this.store.dispatch(new CancelChanges()); + } + + go(event, index: number): void { + event.stopPropagation(); + event.preventDefault(); + this.store.dispatch(new UpdateChanges(this.updates)); + this.router.navigate(['../', index], { relativeTo: this.route }); + this.open = false; + } + + ngOnInit(): void { + this.provider$ + .takeUntil(this.ngUnsubscribe) + .subscribe(provider => this.provider = provider); + this.changes$ + .takeUntil(this.ngUnsubscribe) + .subscribe(changes => this.changes = changes); + + this.valueEmitter + .changeEmitted$ + .takeUntil(this.ngUnsubscribe) + .subscribe(updates => { + this.updates = updates; + }); + + this.statusEmitter + .changeEmitted$ + .takeUntil(this.ngUnsubscribe) + .combineLatest(this.currentPage$, (status: string, page: any) => { + return { [page.path]: status }; + }) + .subscribe(status => { + this.store.dispatch(new UpdateStatus(status)); + }); + + this.latest$ + .takeUntil(this.ngUnsubscribe) + .skipWhile(() => this.saving) + .subscribe(latest => this.latest = latest); + } + + ngOnDestroy(): void { + this.ngUnsubscribe.next(); + this.ngUnsubscribe.complete(); + } + + canDeactivate( + currentRoute: ActivatedRouteSnapshot, + currentState: RouterStateSnapshot, + nextState: RouterStateSnapshot + ): Observable { + if (nextState.url.match('edit')) { return Observable.of(true); } + if (Object.keys({ ...this.changes }).length > 0) { + let modal = this.modalService.open(UnsavedDialogComponent); + modal.componentInstance.provider = this.latest; + modal.componentInstance.message = 'editor'; + modal.componentInstance.action = new CancelChanges(); + modal.result.then( + () => this.router.navigate([nextState.url]), + () => console.warn('denied') + ); + } + return this.store.select(fromEditor.getEditorIsSaved); + } +} diff --git a/ui/src/app/edit-provider/container/provider.component.html b/ui/src/app/edit-provider/container/provider.component.html new file mode 100644 index 000000000..90c6b6463 --- /dev/null +++ b/ui/src/app/edit-provider/container/provider.component.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/src/app/edit-provider/container/provider.component.scss b/ui/src/app/edit-provider/container/provider.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/edit-provider/container/provider.component.spec.ts b/ui/src/app/edit-provider/container/provider.component.spec.ts new file mode 100644 index 000000000..0e4e64df6 --- /dev/null +++ b/ui/src/app/edit-provider/container/provider.component.spec.ts @@ -0,0 +1,48 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { APP_BASE_HREF } from '@angular/common'; +import { RouterModule, ActivatedRoute } from '@angular/router'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { ProviderComponent } from './provider.component'; +import * as fromProviders from '../../metadata-provider/reducer'; +import { ActivatedRouteStub } from '../../../testing/activated-route.stub'; +import { routes } from '../editor.module'; + +describe('Provider Select (Editor) Page', () => { + let fixture: ComponentFixture; + let store: Store; + let instance: ProviderComponent; + let activatedRoute: ActivatedRouteStub = new ActivatedRouteStub(); + activatedRoute.testParamMap = { id: 'foo' }; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: APP_BASE_HREF, useValue: '/' } + ], + imports: [ + NoopAnimationsModule, + StoreModule.forRoot({ + providers: combineReducers(fromProviders.reducers), + }), + ReactiveFormsModule, + RouterModule.forRoot([]) + ], + declarations: [ProviderComponent], + }); + + fixture = TestBed.createComponent(ProviderComponent); + instance = fixture.componentInstance; + store = TestBed.get(Store); + + spyOn(store, 'dispatch').and.callThrough(); + }); + + it('should compile', () => { + fixture.detectChanges(); + + expect(fixture).toBeDefined(); + }); +}); diff --git a/ui/src/app/edit-provider/container/provider.component.ts b/ui/src/app/edit-provider/container/provider.component.ts new file mode 100644 index 000000000..e2145ed55 --- /dev/null +++ b/ui/src/app/edit-provider/container/provider.component.ts @@ -0,0 +1,34 @@ +import { Component, Output, Input, EventEmitter, OnDestroy, ChangeDetectionStrategy } from '@angular/core'; +import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs/Observable'; +import { Subscription } from 'rxjs/Subscription'; +import { Store } from '@ngrx/store'; +import { NgbPopoverConfig } from '@ng-bootstrap/ng-bootstrap'; + +import { MetadataProvider } from '../../metadata-provider/model/metadata-provider'; +import { SelectProvider } from '../../metadata-provider/action/provider.action'; +import * as fromProviders from '../../metadata-provider/reducer'; +@Component({ + selector: 'provider-page', + templateUrl: './provider.component.html', + styleUrls: ['./provider.component.scss'], + providers: [NgbPopoverConfig] +}) +export class ProviderComponent implements OnDestroy { + actionsSubscription: Subscription; + + constructor( + store: Store, + route: ActivatedRoute + ) { + this.actionsSubscription = route.params + .distinctUntilChanged() + .map(params => new SelectProvider(params.id)) + .subscribe(store); + } + + ngOnDestroy() { + this.actionsSubscription.unsubscribe(); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/edit-provider/container/wizard.component.html b/ui/src/app/edit-provider/container/wizard.component.html new file mode 100644 index 000000000..3cc781ccc --- /dev/null +++ b/ui/src/app/edit-provider/container/wizard.component.html @@ -0,0 +1,36 @@ +
+
+
+
+
+ + + Add a new metadata provider + – {{ (currentPage$ | async).label }} ({{ (provider$ | async).serviceProviderName }}) + + (Step {{ wizardIndex$ | async }} of {{ wizard.length + 1 }}) + + +
+
+
+
+ +
+
+ + + + + + + + + +
+
+
+
diff --git a/ui/src/app/edit-provider/container/wizard.component.scss b/ui/src/app/edit-provider/container/wizard.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/edit-provider/container/wizard.component.ts b/ui/src/app/edit-provider/container/wizard.component.ts new file mode 100644 index 000000000..716b2e848 --- /dev/null +++ b/ui/src/app/edit-provider/container/wizard.component.ts @@ -0,0 +1,159 @@ +import { + Component, + ViewChild, + AfterViewInit, + OnInit, + OnDestroy, + EventEmitter +} from '@angular/core'; +import { + ActivatedRoute, + Router, + CanDeactivate, + ActivatedRouteSnapshot, + RouterStateSnapshot +} from '@angular/router'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; +import { Store } from '@ngrx/store'; + +import 'rxjs/add/operator/skip'; +import 'rxjs/add/operator/combineLatest'; +import 'rxjs/add/operator/withLatestFrom'; +import 'rxjs/add/operator/takeLast'; +import 'rxjs/add/operator/skipWhile'; + +import { MetadataProvider } from '../../metadata-provider/model/metadata-provider'; +import * as fromProviders from '../../metadata-provider/reducer'; +import * as draftActions from '../../metadata-provider/action/draft.action'; +import { AddProviderRequest, RemoveProviderRequest } from '../../metadata-provider/action/provider.action'; +import * as fromEditor from '../reducer'; + +import { ProviderStatusEmitter, ProviderValueEmitter } from '../../metadata-provider/service/provider-change-emitter.service'; +import { UpdateStatus, UpdateChanges, SaveChanges } from '../action/editor.action'; +import { WIZARD as WizardDef, EditorFlowDefinition } from '../editor-definition.const'; +import { CanComponentDeactivate } from '../../core/service/can-deactivate.guard'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap/modal/modal'; +import { UnsavedDialogComponent } from '../component/unsaved-dialog.component'; + +@Component({ + selector: 'wizard-page', + templateUrl: './wizard.component.html', + styleUrls: ['./wizard.component.scss'] +}) +export class WizardComponent implements OnInit, OnDestroy, CanComponentDeactivate { + + private ngUnsubscribe: Subject = new Subject(); + + provider$: Observable; + provider: MetadataProvider; + providerName$: Observable; + changes$: Observable; + changes: MetadataProvider; + latest: MetadataProvider; + + wizardIndex$: Observable; + currentPage$: Observable; + wizard$: Observable; + wizard: EditorFlowDefinition[]; + saved$: Observable; + saving: boolean; + + constructor( + private store: Store, + private route: ActivatedRoute, + private router: Router, + private statusEmitter: ProviderStatusEmitter, + private valueEmitter: ProviderValueEmitter, + private modalService: NgbModal + ) { + this.provider$ = this.store.select(fromProviders.getSelectedDraft); + this.providerName$ = this.provider$.map(p => p.serviceProviderName); + this.changes$ = this.store.select(fromEditor.getEditorChanges); + + this.wizardIndex$ = this.route.params.map(params => Number(params.index)); + this.currentPage$ = this.wizardIndex$.map(index => WizardDef.find(r => r.index === index)); + this.wizard = WizardDef; + + this.saved$ = this.store.select(fromEditor.getEditorIsSaved); + + this.store.select(fromEditor.getEditorIsSaving).takeUntil(this.ngUnsubscribe).subscribe(saving => this.saving = saving); + } + + save(): void { + this.store.dispatch(new AddProviderRequest(this.latest)); + } + + next(index: number): void { + this.go(index); + } + + previous(index: number): void { + this.go(index); + } + + go(index: number): void { + this.store.dispatch(new draftActions.UpdateDraftRequest(this.latest)); + this.router.navigate(['../', index], { relativeTo: this.route }); + } + + ngOnInit(): void { + this.subscribe(); + } + + subscribe(): void { + this.provider$ + .takeUntil(this.ngUnsubscribe) + .skipWhile(() => this.saving) + .subscribe(provider => this.provider = provider); + this.changes$ + .takeUntil(this.ngUnsubscribe) + .skipWhile(() => this.saving) + .subscribe(changes => this.changes = changes); + this.changes$ + .takeUntil(this.ngUnsubscribe) + .skipWhile(() => this.saving) + .combineLatest(this.provider$, (changes, base) => Object.assign({}, base, changes)) + .subscribe(latest => this.latest = latest); + + this.valueEmitter + .changeEmitted$ + .takeUntil(this.ngUnsubscribe) + .skipWhile(() => this.saving) + .subscribe(changes => { + this.store.dispatch(new UpdateChanges(changes)); + }); + this.statusEmitter + .changeEmitted$ + .takeUntil(this.ngUnsubscribe) + .skipWhile(() => this.saving) + .combineLatest(this.currentPage$, (status: string, page: any) => { + return { [page.path]: status }; + }) + .subscribe(status => { + this.store.dispatch(new UpdateStatus(status)); + }); + } + + ngOnDestroy(): void { + this.ngUnsubscribe.next(); + this.ngUnsubscribe.complete(); + } + + canDeactivate( + currentRoute: ActivatedRouteSnapshot, + currentState: RouterStateSnapshot, + nextState: RouterStateSnapshot + ): Observable { + if (nextState.url.match('wizard')) { return Observable.of(true); } + if (Object.keys(this.changes).length > 0) { + let modal = this.modalService.open(UnsavedDialogComponent); + modal.componentInstance.action = new SaveChanges(this.latest); + modal.result.then( + () => this.router.navigate([nextState.url]), + () => console.warn('denied') + ); + } + return this.store.select(fromEditor.getEditorIsSaved); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/edit-provider/editor-definition.const.ts b/ui/src/app/edit-provider/editor-definition.const.ts new file mode 100644 index 000000000..f78590f36 --- /dev/null +++ b/ui/src/app/edit-provider/editor-definition.const.ts @@ -0,0 +1,38 @@ +import { AdvancedInfoFormComponent } from '../metadata-provider/component/forms/advanced-info-form.component'; +import { OrganizationInfoFormComponent } from '../metadata-provider/component/forms/organization-info-form.component'; +import { MetadataUiFormComponent } from '../metadata-provider/component/forms/metadata-ui-form.component'; +import { KeyInfoFormComponent } from '../metadata-provider/component/forms/key-info-form.component'; +import { LogoutFormComponent } from '../metadata-provider/component/forms/logout-form.component'; +import { AssertionFormComponent } from '../metadata-provider/component/forms/assertion-form.component'; +import { DescriptorInfoFormComponent } from '../metadata-provider/component/forms/descriptor-info-form.component'; +import { RelyingPartyFormComponent } from '../metadata-provider/component/forms/relying-party-form.component'; +import { AttributeReleaseFormComponent } from '../metadata-provider/component/forms/attribute-release-form.component'; +import { FinishFormComponent } from '../metadata-provider/component/forms/finish-form.component'; + +export interface EditorFlowDefinition { + index: number; + path: string; + label: string; + component: any; +} + +export const COMMON: EditorFlowDefinition[] = [ + { index: 3, path: 'metadata-ui', label: 'User Interface / MDUI Information', component: MetadataUiFormComponent }, + { index: 4, path: 'descriptor-info', label: 'SP SSO Descriptor Information', component: DescriptorInfoFormComponent }, + { index: 5, path: 'logout-endpoints', label: 'Logout Endpoints', component: LogoutFormComponent }, + { index: 6, path: 'key-info', label: 'Security Information', component: KeyInfoFormComponent }, + { index: 7, path: 'assertion', label: 'Assertion Consumer Service', component: AssertionFormComponent }, + { index: 8, path: 'relying-party', label: 'Relying Party Overrides', component: RelyingPartyFormComponent }, + { index: 9, path: 'attribute', label: 'Attribute Release', component: AttributeReleaseFormComponent } +]; + +export const EDITOR: EditorFlowDefinition[] = [ + { index: 2, path: 'sp-org-info', label: 'SP/Organization Information', component: AdvancedInfoFormComponent }, + ...COMMON +]; + +export const WIZARD: EditorFlowDefinition[] = [ + { index: 2, path: 'org-info', label: 'Organization Information', component: OrganizationInfoFormComponent }, + ...COMMON, + { index: 10, path: 'finish', label: 'Finished!', component: FinishFormComponent } +]; diff --git a/ui/src/app/edit-provider/editor.module.ts b/ui/src/app/edit-provider/editor.module.ts new file mode 100644 index 000000000..582592d84 --- /dev/null +++ b/ui/src/app/edit-provider/editor.module.ts @@ -0,0 +1,78 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { CommonModule } from '@angular/common'; +import { ReactiveFormsModule, FormsModule } from '@angular/forms'; +import { StoreModule } from '@ngrx/store'; +import { EffectsModule } from '@ngrx/effects'; + +import { ProviderComponent } from './container/provider.component'; +import { DraftComponent } from './container/draft.component'; +import { WizardComponent } from './container/wizard.component'; +import { EditorComponent } from './container/editor.component'; +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; + +import { RootProviderModule } from '../metadata-provider/metadata-provider.module'; +import { ProviderEditorFormModule } from '../metadata-provider/component'; +import { reducers } from './reducer'; + +import { UnsavedDialogComponent } from './component/unsaved-dialog.component'; +import { CanDeactivateGuard } from '../core/service/can-deactivate.guard'; +import { WizardNavComponent } from './component/wizard-nav.component'; +import { WizardEffects } from './effect/wizard.effect'; +import { EditorEffects } from './effect/editor.effect'; + +export const routes: Routes = [ + { + path: ':id', + component: ProviderComponent, + canActivate: [], + children: [ + { path: 'edit', redirectTo: 'edit/2' }, + { + path: 'edit/:index', + component: EditorComponent, + canDeactivate: [CanDeactivateGuard] + } + ] + }, + { + path: ':entityId', + component: DraftComponent, + canActivate: [], + children: [ + { path: 'wizard', redirectTo: 'wizard/2' }, + { + path: 'wizard/:index', + component: WizardComponent, + canDeactivate: [CanDeactivateGuard] + } + ] + } +]; + +@NgModule({ + declarations: [ + ProviderComponent, + EditorComponent, + WizardComponent, + UnsavedDialogComponent, + WizardNavComponent, + DraftComponent + ], + entryComponents: [ + UnsavedDialogComponent + ], + imports: [ + CommonModule, + RouterModule, + ReactiveFormsModule, + RootProviderModule, + ProviderEditorFormModule, + NgbDropdownModule, + StoreModule.forFeature('edit-provider', reducers), + EffectsModule.forFeature([WizardEffects, EditorEffects]), + RouterModule.forChild(routes) + ], + providers: [] +}) +export class EditorModule { } diff --git a/ui/src/app/edit-provider/effect/editor.effect.ts b/ui/src/app/edit-provider/effect/editor.effect.ts new file mode 100644 index 000000000..3f6630774 --- /dev/null +++ b/ui/src/app/edit-provider/effect/editor.effect.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@angular/core'; +import { Effect, Actions } from '@ngrx/effects'; + +import * as editor from '../action/editor.action'; +import * as provider from '../../metadata-provider/action/provider.action'; +import { MetadataProvider } from '../../metadata-provider/model/metadata-provider'; +import { EntityDescriptorService } from '../../metadata-provider/service/entity-descriptor.service'; +import { Router } from '@angular/router'; + +@Injectable() +export class EditorEffects { + + @Effect({dispatch: false}) + cancelChanges$ = this.actions$ + .ofType(editor.CANCEL_CHANGES) + .map(action => action.payload) + .switchMap(() => this.router.navigate(['/dashboard'])); + @Effect() + updateProviderSuccessRedirect$ = this.actions$ + .ofType(provider.UPDATE_PROVIDER_SUCCESS) + .map(action => action.payload) + .map(p => new editor.ResetChanges()); + + constructor( + private actions$: Actions, + private router: Router + ) { } +} diff --git a/ui/src/app/edit-provider/effect/wizard.effect.ts b/ui/src/app/edit-provider/effect/wizard.effect.ts new file mode 100644 index 000000000..aacba412c --- /dev/null +++ b/ui/src/app/edit-provider/effect/wizard.effect.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { Effect, Actions } from '@ngrx/effects'; + +import * as editorActions from '../action/editor.action'; +import * as draft from '../../metadata-provider/action/draft.action'; +import * as provider from '../../metadata-provider/action/provider.action'; +import { MetadataProvider } from '../../metadata-provider/model/metadata-provider'; +import { EntityDraftService } from '../../metadata-provider/service/entity-draft.service'; + +@Injectable() +export class WizardEffects { + + @Effect({ dispatch: false }) + updateProvider$ = this.actions$ + .ofType(editorActions.SAVE_CHANGES) + .map(action => action.payload) + .switchMap(provider => { + return this.draftService.update(provider); + }); + @Effect() + addProviderSuccessDiscard$ = this.actions$ + .ofType(provider.ADD_PROVIDER_SUCCESS) + .map(action => action.payload) + .map(provider => { + return new editorActions.ResetChanges(); + }); + + constructor( + private actions$: Actions, + private draftService: EntityDraftService + ) { } +} diff --git a/ui/src/app/edit-provider/reducer/editor.reducer.spec.ts b/ui/src/app/edit-provider/reducer/editor.reducer.spec.ts new file mode 100644 index 000000000..673cad397 --- /dev/null +++ b/ui/src/app/edit-provider/reducer/editor.reducer.spec.ts @@ -0,0 +1,169 @@ +import { reducer } from './editor.reducer'; +import * as fromEditor from './editor.reducer'; +import * as actions from '../action/editor.action'; +import * as providerActions from '../../metadata-provider/action/provider.action'; +import { MetadataProvider } from '../../metadata-provider/model/metadata-provider'; +import { EntityDescriptor } from '../../metadata-provider/model/entity-descriptor'; + +describe('Editor Reducer', () => { + const initialState: fromEditor.EditorState = { + saving: false, + formStatus: {}, + changes: {} as MetadataProvider + }; + + const changes = { + entityId: 'foo', + serviceProviderName: 'bar' + } as MetadataProvider; + + describe('undefined action', () => { + it('should return the default state', () => { + const result = reducer(undefined, {} as any); + expect(result).toEqual(initialState); + }); + }); + + describe('Editor Add Provider', () => { + it('should update the status when a provider is saved', () => { + const action = new providerActions.AddProviderRequest(changes); + const result = reducer(initialState, action); + expect(result).toEqual( + Object.assign({}, initialState, { + saving: true + }) + ); + }); + + it('should update the status on success', () => { + const action = new providerActions.AddProviderSuccess(changes); + const result = reducer({...initialState, changes: {...changes, organization: {name: 'foo'}}}, action); + expect(result).toEqual( + Object.assign({}, initialState, { + saving: false, + changes: initialState.changes + }) + ); + }); + + it('should update the status on success', () => { + const action = new providerActions.AddProviderFail(changes); + const result = reducer(initialState, action); + expect(result).toEqual( + Object.assign({}, initialState, { + saving: false + }) + ); + }); + }); + + describe('Editor Update Status', () => { + it('should update the status of the provided form', () => { + const status = { organization: 'VALID' }; + const action = new actions.UpdateStatus(status); + const result = reducer(initialState, action); + expect(result).toEqual( + Object.assign({}, initialState, { + formStatus: status + }) + ); + }); + }); + + describe('Editor Update Changes', () => { + it('should add changes of the provided form', () => { + const action = new actions.UpdateChanges(changes); + const result = reducer(initialState, action); + expect(result).toEqual( + Object.assign({}, initialState, { + changes: changes + }) + ); + }); + }); + + describe('Editor Reset', () => { + it('should remove changes', () => { + const action = new actions.ResetChanges(); + const result = reducer(initialState, action); + expect(result).toEqual( + Object.assign({}, initialState, { + changes: initialState.changes + }) + ); + }); + }); + + describe('Editor Save', () => { + it('should remove changes', () => { + const action = new actions.SaveChanges(changes); + const result = reducer(initialState, action); + expect(result).toEqual( + Object.assign({}, initialState, { + changes: initialState.changes + }) + ); + }); + }); + + describe('Editor Cancel', () => { + it('should remove changes', () => { + const action = new actions.CancelChanges(); + const result = reducer(initialState, action); + expect(result).toEqual( + Object.assign({}, initialState, { + changes: initialState.changes + }) + ); + }); + }); + + describe('Selectors', () => { + it('should aggregate the status', () => { + expect(fromEditor.isEditorValid({ + saving: false, + changes: {} as MetadataProvider, + formStatus: { + organization: 'INVALID', + foo: 'VALID' + } + })).toBe(false); + }); + + it('should calculate a saved status based on changes', () => { + expect(fromEditor.isEditorSaved({ + saving: false, + changes: {} as MetadataProvider, + formStatus: {} + })).toBe(true); + + expect(fromEditor.isEditorSaved({ + saving: false, + changes: {organization: {}, entityId: 'bar'} as MetadataProvider, + formStatus: {} + })).toBe(false); + }); + + it('should return current changes', () => { + expect(fromEditor.getChanges({ + saving: false, + changes: {} as MetadataProvider, + formStatus: {} + })).toEqual({} as MetadataProvider); + }); + + it('should return `saving` status', () => { + expect(fromEditor.isEditorSaving({ + saving: false, + changes: {} as MetadataProvider, + formStatus: {} + })).toBe(false); + + expect(fromEditor.isEditorSaving({ + saving: true, + changes: {} as MetadataProvider, + formStatus: {} + })).toBe(true); + }); + }); +}); diff --git a/ui/src/app/edit-provider/reducer/editor.reducer.ts b/ui/src/app/edit-provider/reducer/editor.reducer.ts new file mode 100644 index 000000000..a6ee48c11 --- /dev/null +++ b/ui/src/app/edit-provider/reducer/editor.reducer.ts @@ -0,0 +1,70 @@ +import { createSelector, createFeatureSelector } from '@ngrx/store'; +import { MetadataProvider } from '../../metadata-provider/model/metadata-provider'; +import * as editor from '../action/editor.action'; +import * as provider from '../../metadata-provider/action/provider.action'; +import * as fromRoot from '../../core/reducer'; + +export interface EditorState { + saving: boolean; + formStatus: { [key: string]: string }; + changes: MetadataProvider; +} + +export const initialState: EditorState = { + saving: false, + formStatus: {}, + changes: {} as MetadataProvider +}; + +export function reducer(state = initialState, action: editor.Actions | provider.Actions): EditorState { + switch (action.type) { + case provider.ADD_PROVIDER: { + return { + ...state, + saving: true, + }; + } + case provider.ADD_PROVIDER_FAIL: { + return { + ...state, + saving: false + }; + } + case provider.ADD_PROVIDER_SUCCESS: { + return { + ...state, + changes: { ...initialState.changes }, + saving: false + }; + } + case editor.UPDATE_STATUS: { + return Object.assign({}, state, { + formStatus: { ...state.formStatus, ...action.payload } + }); + } + case editor.UPDATE_CHANGES: { + return Object.assign({}, state, { + changes: { ...state.changes, ...action.payload } + }); + } + case editor.CANCEL_CHANGES: + case editor.SAVE_CHANGES: + case editor.RESET_CHANGES: + return Object.assign({}, state, { + changes: { ...initialState.changes } + }); + default: { + return state; + } + } +} + +export const isEditorValid = (state: EditorState) => + !Object + .keys(state.formStatus) + .some(key => { + return state.formStatus[key] === ('INVALID'); + }); +export const isEditorSaved = (state: EditorState) => !Object.keys(state.changes).length; +export const getChanges = (state: EditorState) => state.changes; +export const isEditorSaving = (state: EditorState) => state.saving; diff --git a/ui/src/app/edit-provider/reducer/index.ts b/ui/src/app/edit-provider/reducer/index.ts new file mode 100644 index 000000000..0a381f14e --- /dev/null +++ b/ui/src/app/edit-provider/reducer/index.ts @@ -0,0 +1,22 @@ +import { createSelector, createFeatureSelector } from '@ngrx/store'; +import * as fromRoot from '../../core/reducer'; +import * as fromEditor from './editor.reducer'; + +export interface EditProviderState { + editor: fromEditor.EditorState; +} + +export const reducers = { + editor: fromEditor.reducer +}; + +export interface State extends fromRoot.State { + 'edit-provider': EditProviderState; +} + +export const getEditProviderState = createFeatureSelector('edit-provider'); +export const getEditorState = createSelector(getEditProviderState, (state: EditProviderState) => state.editor); +export const getEditorIsValid = createSelector(getEditorState, fromEditor.isEditorValid); +export const getEditorIsSaved = createSelector(getEditorState, fromEditor.isEditorSaved); +export const getEditorChanges = createSelector(getEditorState, fromEditor.getChanges); +export const getEditorIsSaving = createSelector(getEditorState, fromEditor.isEditorSaving); diff --git a/ui/src/app/metadata-provider/action/draft.action.ts b/ui/src/app/metadata-provider/action/draft.action.ts new file mode 100644 index 000000000..ede97cd2c --- /dev/null +++ b/ui/src/app/metadata-provider/action/draft.action.ts @@ -0,0 +1,119 @@ +import { Action } from '@ngrx/store'; +import { MetadataProvider } from '../model/metadata-provider'; + +export const FIND = '[Metadata Draft] Find'; +export const SELECT = '[Metadata Draft] Select'; + +export const UPDATE_DRAFT_REQUEST = '[Metadata Draft] Update Request'; +export const UPDATE_DRAFT_SUCCESS = '[Metadata Draft] Update Success'; +export const UPDATE_DRAFT_FAIL = '[Metadata Draft] Update Fail'; + +export const LOAD_DRAFT_REQUEST = '[Metadata Draft Collection] Draft REQUEST'; +export const LOAD_DRAFT_SUCCESS = '[Metadata Draft Collection] Draft SUCCESS'; +export const LOAD_DRAFT_ERROR = '[Metadata Draft Collection] Draft ERROR'; +export const ADD_DRAFT = '[Metadata Draft Collection] Add Draft'; +export const ADD_DRAFT_SUCCESS = '[Metadata Draft Collection] Add Draft Success'; +export const ADD_DRAFT_FAIL = '[Metadata Draft Collection] Add Draft Fail'; +export const REMOVE_DRAFT = '[Metadata Draft Collection] Remove Draft'; +export const REMOVE_DRAFT_SUCCESS = '[Metadata Draft Collection] Remove Draft Success'; +export const REMOVE_DRAFT_FAIL = '[Metadata Draft Collection] Remove Draft Fail'; + +export class FindDraft implements Action { + readonly type = FIND; + + constructor(public payload: string) { } +} + +export class SelectDraft implements Action { + readonly type = SELECT; + + constructor(public payload: string) { } +} + +export class UpdateDraftRequest implements Action { + readonly type = UPDATE_DRAFT_REQUEST; + + constructor(public payload: MetadataProvider) { } +} + +export class UpdateDraftSuccess implements Action { + readonly type = UPDATE_DRAFT_SUCCESS; + + constructor(public payload: MetadataProvider) { } +} + +export class UpdateDraftFail implements Action { + readonly type = UPDATE_DRAFT_FAIL; + + constructor(public payload: MetadataProvider) { } +} + +export class AddDraftRequest implements Action { + readonly type = ADD_DRAFT; + + constructor(public payload: MetadataProvider) { } +} + +export class AddDraftSuccess implements Action { + readonly type = ADD_DRAFT_SUCCESS; + + constructor(public payload: MetadataProvider) { } +} + +export class AddDraftFail implements Action { + readonly type = ADD_DRAFT_FAIL; + + constructor(public payload: any) { } +} + +export class RemoveDraftRequest implements Action { + readonly type = REMOVE_DRAFT; + + constructor(public payload: MetadataProvider) { } +} + +export class RemoveDraftSuccess implements Action { + readonly type = REMOVE_DRAFT_SUCCESS; + + constructor(public payload: MetadataProvider) { } +} + +export class RemoveDraftFail implements Action { + readonly type = REMOVE_DRAFT_FAIL; + + constructor(public payload: MetadataProvider) { } +} + +export class LoadDraftRequest implements Action { + readonly type = LOAD_DRAFT_REQUEST; + + constructor() { } +} + +export class LoadDraftSuccess implements Action { + readonly type = LOAD_DRAFT_SUCCESS; + + constructor(public payload: MetadataProvider[]) { } +} + +export class LoadDraftError implements Action { + readonly type = LOAD_DRAFT_ERROR; + + constructor(public payload: any) { } +} + +export type Actions = + | LoadDraftRequest + | LoadDraftSuccess + | LoadDraftError + | AddDraftRequest + | AddDraftSuccess + | AddDraftFail + | RemoveDraftRequest + | RemoveDraftSuccess + | RemoveDraftFail + | FindDraft + | SelectDraft + | UpdateDraftRequest + | UpdateDraftSuccess + | UpdateDraftFail; diff --git a/ui/src/app/metadata-provider/action/provider.action.ts b/ui/src/app/metadata-provider/action/provider.action.ts new file mode 100644 index 000000000..5c8720080 --- /dev/null +++ b/ui/src/app/metadata-provider/action/provider.action.ts @@ -0,0 +1,144 @@ +import { Action } from '@ngrx/store'; +import { MetadataProvider } from '../model/metadata-provider'; + +export const FIND = '[Metadata Provider] Find'; +export const SELECT = '[Metadata Provider] Select'; +export const SELECT_SUCCESS = '[Metadata Provider] Select Success'; + +export const UPDATE_PROVIDER_REQUEST = '[Metadata Provider] Update Request'; +export const UPDATE_PROVIDER_SUCCESS = '[Metadata Provider] Update Success'; +export const UPDATE_PROVIDER_FAIL = '[Metadata Provider] Update Fail'; + +export const LOAD_PROVIDER_REQUEST = '[Metadata Provider Collection] Provider REQUEST'; +export const LOAD_PROVIDER_SUCCESS = '[Metadata Provider Collection] Provider SUCCESS'; +export const LOAD_PROVIDER_ERROR = '[Metadata Provider Collection] Provider ERROR'; +export const ADD_PROVIDER = '[Metadata Provider Collection] Add Provider'; +export const ADD_PROVIDER_SUCCESS = '[Metadata Provider Collection] Add Provider Success'; +export const ADD_PROVIDER_FAIL = '[Metadata Provider Collection] Add Provider Fail'; +export const REMOVE_PROVIDER = '[Metadata Provider Collection] Remove Provider'; +export const REMOVE_PROVIDER_SUCCESS = '[Metadata Provider Collection] Remove Provider Success'; +export const REMOVE_PROVIDER_FAIL = '[Metadata Provider Collection] Remove Provider Fail'; + +export const UPLOAD_PROVIDER_REQUEST = '[Metadata Provider Collection] Upload Provider Request'; +export const CREATE_PROVIDER_FROM_URL_REQUEST = '[Metadata Provider Collection] Create Provider From URL Request'; + +export class FindProvider implements Action { + readonly type = FIND; + + constructor(public payload: string) { } +} + +export class SelectProvider implements Action { + readonly type = SELECT; + + constructor(public payload: string) { } +} + +export class SelectProviderSuccess implements Action { + readonly type = SELECT_SUCCESS; + + constructor(public payload: MetadataProvider) { } +} + +export class LoadProviderRequest implements Action { + readonly type = LOAD_PROVIDER_REQUEST; + + constructor() { } +} + +export class LoadProviderSuccess implements Action { + readonly type = LOAD_PROVIDER_SUCCESS; + + constructor(public payload: MetadataProvider[]) { } +} + +export class LoadProviderError implements Action { + readonly type = LOAD_PROVIDER_ERROR; + + constructor(public payload: any) { } +} + +export class UpdateProviderRequest implements Action { + readonly type = UPDATE_PROVIDER_REQUEST; + + constructor(public payload: MetadataProvider) { } +} + +export class UpdateProviderSuccess implements Action { + readonly type = UPDATE_PROVIDER_SUCCESS; + + constructor(public payload: MetadataProvider) { } +} + +export class UpdateProviderFail implements Action { + readonly type = UPDATE_PROVIDER_FAIL; + + constructor(public err: any) { } +} + +export class AddProviderRequest implements Action { + readonly type = ADD_PROVIDER; + + constructor(public payload: MetadataProvider) { } +} + +export class AddProviderSuccess implements Action { + readonly type = ADD_PROVIDER_SUCCESS; + + constructor(public payload: MetadataProvider) { } +} + +export class AddProviderFail implements Action { + readonly type = ADD_PROVIDER_FAIL; + + constructor(public payload: any) { } +} + +export class RemoveProviderRequest implements Action { + readonly type = REMOVE_PROVIDER; + + constructor(public payload: MetadataProvider) { } +} + +export class RemoveProviderSuccess implements Action { + readonly type = REMOVE_PROVIDER_SUCCESS; + + constructor(public payload: MetadataProvider) { } +} + +export class RemoveProviderFail implements Action { + readonly type = REMOVE_PROVIDER_FAIL; + + constructor(public payload: MetadataProvider) { } +} + +export class UploadProviderRequest implements Action { + readonly type = UPLOAD_PROVIDER_REQUEST; + + constructor(public payload: { name: string, body: string }) { } +} + +export class CreateProviderFromUrlRequest implements Action { + readonly type = CREATE_PROVIDER_FROM_URL_REQUEST; + + constructor(public payload: { name: string, url: string }) { } +} + +export type Actions = + | LoadProviderRequest + | LoadProviderSuccess + | LoadProviderError + | AddProviderRequest + | AddProviderSuccess + | AddProviderFail + | RemoveProviderRequest + | RemoveProviderSuccess + | RemoveProviderFail + | FindProvider + | SelectProvider + | SelectProviderSuccess + | UpdateProviderRequest + | UpdateProviderSuccess + | UpdateProviderFail + | UploadProviderRequest + | CreateProviderFromUrlRequest; diff --git a/ui/src/app/metadata-provider/component/forms/advanced-info-form.component.html b/ui/src/app/metadata-provider/component/forms/advanced-info-form.component.html new file mode 100644 index 000000000..feea39477 --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/advanced-info-form.component.html @@ -0,0 +1,236 @@ +
+
+
+
+
+ + + Service Provider Name (Dashboard Display Only) popover + + +
+ +
+
+
+ + + Service Provider Entity ID popover + + +
+ +
+
+ +
+
+
+
+ + + Organization Name popover + + +
+ +
+
+
+ + + Organization Display Name popover + + +
+ +
+
+
+ + + Organization URL popover + + +
+ +
+
+

* These three fields must all be entered if any single field has a value.

+
+
+

+ Contact Information: + +

+
+ +
+
+
+
diff --git a/ui/src/app/metadata-provider/component/forms/advanced-info-form.component.spec.ts b/ui/src/app/metadata-provider/component/forms/advanced-info-form.component.spec.ts new file mode 100644 index 000000000..77b11cfff --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/advanced-info-form.component.spec.ts @@ -0,0 +1,82 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { ProviderValueEmitter, ProviderStatusEmitter } from '../../service/provider-change-emitter.service'; +import * as fromProviders from '../../reducer'; +import { AdvancedInfoFormComponent } from './advanced-info-form.component'; +import { NgbPopoverModule, NgbPopoverConfig } from '@ng-bootstrap/ng-bootstrap/popover/popover.module'; +import * as stubs from '../../../../testing/provider.stub'; + +describe('Advanced Info Form Component', () => { + let fixture: ComponentFixture; + let instance: AdvancedInfoFormComponent; + let store: Store; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + ProviderValueEmitter, + ProviderStatusEmitter, + NgbPopoverConfig + ], + imports: [ + NoopAnimationsModule, + ReactiveFormsModule, + StoreModule.forRoot({ + 'providers': combineReducers(fromProviders.reducers), + }), + NgbPopoverModule + ], + declarations: [ + AdvancedInfoFormComponent + ], + }); + store = TestBed.get(Store); + spyOn(store, 'dispatch').and.callThrough(); + + fixture = TestBed.createComponent(AdvancedInfoFormComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should compile', () => { + expect(fixture).toBeDefined(); + }); + + describe('ngOnChanges method', () => { + it('should set properties on the provider', () => { + instance.provider = stubs.provider; + fixture.detectChanges(); + instance.ngOnChanges(); + expect(instance.provider.organization).toEqual({}); + expect(instance.provider.contacts).toEqual([]); + }); + }); + + describe('removeContact method', () => { + it('should remove the contact at the given index', () => { + instance.provider = { + ...stubs.provider, + contacts: [stubs.contact] + }; + fixture.detectChanges(); + instance.ngOnChanges(); + instance.removeContact(0); + expect(instance.contacts.length).toBe(0); + }); + }); + + describe('addContact method', () => { + it('should remove the contact at the given index', () => { + instance.provider = { + ...stubs.provider, + contacts: [stubs.contact] + }; + fixture.detectChanges(); + instance.ngOnChanges(); + instance.addContact(); + expect(instance.contacts.length).toBe(2); + }); + }); +}); diff --git a/ui/src/app/metadata-provider/component/forms/advanced-info-form.component.ts b/ui/src/app/metadata-provider/component/forms/advanced-info-form.component.ts new file mode 100644 index 000000000..dc6a16a87 --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/advanced-info-form.component.ts @@ -0,0 +1,128 @@ +import { Component, Input, OnInit, OnChanges, OnDestroy, ChangeDetectionStrategy, EventEmitter } from '@angular/core'; +import { FormBuilder, FormGroup, FormControl, FormArray, Validators } from '@angular/forms'; +import { Observable } from 'rxjs/Observable'; +import { Subscription } from 'rxjs/Subscription'; +import { Store } from '@ngrx/store'; + +import 'rxjs/add/operator/throttleTime'; +import 'rxjs/add/operator/takeUntil'; +import 'rxjs/add/operator/startWith'; + +import * as fromProviders from '../../reducer'; +import { ProviderStatusEmitter, ProviderValueEmitter } from '../../service/provider-change-emitter.service'; +import { MetadataProvider, Organization, Contact } from '../../model/metadata-provider'; +import { ProviderFormFragmentComponent } from './provider-form-fragment.component'; +import { EntityValidators } from '../../service/entity-validators.service'; +import * as patterns from '../../../shared/regex'; + +@Component({ + selector: 'adv-info-form', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './advanced-info-form.component.html' +}) +export class AdvancedInfoFormComponent extends ProviderFormFragmentComponent implements OnInit, OnChanges, OnDestroy { + @Input() provider: MetadataProvider; + + contactTypes: string[] = [ + 'support', + 'technical', + 'administrative', + 'other' + ]; + + form: FormGroup; + + hasValue$: Observable; + totalValue$: Observable; + ids$: Observable = Observable.of([]); + + private validationSubscription: Subscription; + + constructor( + protected fb: FormBuilder, + protected statusEmitter: ProviderStatusEmitter, + protected valueEmitter: ProviderValueEmitter, + protected store: Store + ) { + super(fb, statusEmitter, valueEmitter); + + this.ids$ = this.store + .select(fromProviders.getAllEntityIds) + .takeUntil(this.ngUnsubscribe) + .combineLatest(this.store.select(fromProviders.getSelectedProvider), (ids: string[], provider: MetadataProvider) => { + return ids.filter(id => provider.entityId !== id); + }); + } + + createForm(): void { + let orgEmitter$ = this.valueEmitter.changeEmitted$ + .takeUntil(this.ngUnsubscribe) + .switchMap(changes => Observable.of(changes.organization)); + this.form = this.fb.group({ + entityId: ['', Validators.required], + serviceProviderName: ['', Validators.required], + serviceEnabled: [false], + organization: this.fb.group({ + name: [''], + displayName: [''], + url: [''] + }, { asyncValidator: EntityValidators.createOrgValidator() }), + contacts: this.fb.array([]) + }); + } + + ngOnInit(): void { + super.ngOnInit(); + + this.hasValue$ = this.form + .get('organization') + .valueChanges + .startWith(this.form.get('organization').value) + .map(values => Object.keys(values).reduce((coll, key) => coll + (values[key] || ''), '')) + .map(value => !!value); + + this.form + .get('entityId') + .setAsyncValidators( + EntityValidators.createUniqueIdValidator(this.ids$) + ); + } + + ngOnChanges(): void { + this.provider.organization = this.provider.organization || {}; + this.provider.contacts = this.provider.contacts || []; + this.form.reset({ + serviceProviderName: this.provider.serviceProviderName, + serviceEnabled: this.provider.serviceEnabled, + entityId: this.provider.entityId, + organization: this.provider.organization + }); + this.setContacts(this.provider.contacts); + } + + get contacts(): FormArray { + return this.form.get('contacts') as FormArray; + } + + setContacts(contacts: Contact[] = []): void { + let fgs = contacts.map(contact => this.getContact(contact)), + list = this.fb.array(fgs); + this.form.setControl('contacts', list); + } + + addContact(): void { + this.contacts.push(this.getContact()); + } + + getContact(contact: Contact = {} as Contact): FormGroup { + return this.fb.group({ + type: [contact.type || null, Validators.required], + name: [contact.name || null, Validators.required], + emailAddress: [contact.emailAddress || null, [Validators.required, Validators.email]] + }); + } + + removeContact(index: number): void { + this.contacts.removeAt(index); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/metadata-provider/component/forms/assertion-form.component.html b/ui/src/app/metadata-provider/component/forms/assertion-form.component.html new file mode 100644 index 000000000..a8f5b8e4d --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/assertion-form.component.html @@ -0,0 +1,104 @@ +
+
+
+

+ Assertion Consumer Service Endpoints: + +

+
+ +
+
+
+
diff --git a/ui/src/app/metadata-provider/component/forms/assertion-form.component.spec.ts b/ui/src/app/metadata-provider/component/forms/assertion-form.component.spec.ts new file mode 100644 index 000000000..58ac9c818 --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/assertion-form.component.spec.ts @@ -0,0 +1,81 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { ProviderValueEmitter, ProviderStatusEmitter } from '../../service/provider-change-emitter.service'; +import * as fromProviders from '../../reducer'; +import { NgbPopoverModule, NgbPopoverConfig } from '@ng-bootstrap/ng-bootstrap/popover/popover.module'; +import { AssertionFormComponent } from './assertion-form.component'; +import * as stubs from '../../../../testing/provider.stub'; + +describe('Assertion Form Component', () => { + let fixture: ComponentFixture; + let instance: AssertionFormComponent; + let store: Store; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + ProviderValueEmitter, + ProviderStatusEmitter, + NgbPopoverConfig + ], + imports: [ + NoopAnimationsModule, + ReactiveFormsModule, + StoreModule.forRoot({ + 'providers': combineReducers(fromProviders.reducers), + }), + NgbPopoverModule + ], + declarations: [ + AssertionFormComponent + ], + }); + store = TestBed.get(Store); + spyOn(store, 'dispatch').and.callThrough(); + + fixture = TestBed.createComponent(AssertionFormComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should compile', () => { + expect(fixture).toBeDefined(); + }); + + describe('ngOnChanges method', () => { + it('should set properties on the provider', () => { + instance.provider = stubs.provider; + fixture.detectChanges(); + instance.ngOnChanges(); + expect(instance.provider.assertionConsumerServices).toEqual([]); + }); + }); + + describe('removeEndpoint method', () => { + it('should remove the endpoint at the given index', () => { + instance.provider = { + ...stubs.provider, + assertionConsumerServices: [stubs.endpoint] + }; + fixture.detectChanges(); + instance.ngOnChanges(); + instance.removeEndpoint(0); + expect(instance.assertionConsumerServices.length).toBe(0); + }); + }); + + describe('addEndpoint method', () => { + it('should remove the endpoint at the given index', () => { + instance.provider = { + ...stubs.provider, + assertionConsumerServices: [stubs.endpoint] + }; + fixture.detectChanges(); + instance.ngOnChanges(); + instance.addEndpoint(); + expect(instance.assertionConsumerServices.length).toBe(2); + }); + }); +}); diff --git a/ui/src/app/metadata-provider/component/forms/assertion-form.component.ts b/ui/src/app/metadata-provider/component/forms/assertion-form.component.ts new file mode 100644 index 000000000..32922c084 --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/assertion-form.component.ts @@ -0,0 +1,75 @@ +import { Component, Output, Input, EventEmitter, OnInit, OnChanges, OnDestroy, ChangeDetectionStrategy } from '@angular/core'; +import { FormBuilder, FormGroup, FormControl, FormArray, AbstractControl, Validators } from '@angular/forms'; +import { Observable } from 'rxjs/Observable'; + +import { ProviderFormFragmentComponent } from './provider-form-fragment.component'; + +import { ProviderStatusEmitter, ProviderValueEmitter } from '../../service/provider-change-emitter.service'; +import { MetadataProvider, SsoService } from '../../model/metadata-provider'; +import * as patterns from '../../../shared/regex'; + +@Component({ + selector: 'assertion-form', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './assertion-form.component.html' +}) +export class AssertionFormComponent extends ProviderFormFragmentComponent implements OnInit, OnChanges, OnDestroy { + @Input() provider: MetadataProvider; + + form: FormGroup; + + bindingTypes: string[] = [ + 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', + 'urn:oasis:names:tc:SAML:1.0:profiles:browser-post' + ]; + + constructor( + protected fb: FormBuilder, + protected statusEmitter: ProviderStatusEmitter, + protected valueEmitter: ProviderValueEmitter + ) { + super(fb, statusEmitter, valueEmitter); + } + + createForm(): void { + this.form = this.fb.group({ + assertionConsumerServices: this.fb.array([]) + }); + } + + get assertionConsumerServices(): FormArray { + return this.form.get('assertionConsumerServices') as FormArray; + } + + setEndpoints(endpoints: SsoService[] = []): void { + let fgs = endpoints.map(ep => this.fb.group(ep)), + list = this.fb.array(fgs); + this.form.setControl('assertionConsumerServices', list); + } + + addEndpoint(): void { + this.assertionConsumerServices.push(this.fb.group({ + binding: null, + locationUrl: [''], + makeDefault: false + })); + } + + removeEndpoint(index: number): void { + this.assertionConsumerServices.removeAt(index); + } + + markAsDefault(endpoint: AbstractControl): void { + this.assertionConsumerServices.controls.forEach(element => { + element.patchValue({ + makeDefault: (endpoint === element) ? !endpoint.get('makeDefault').value : false + }); + }); + this.assertionConsumerServices.updateValueAndValidity(); + } + + ngOnChanges(): void { + this.provider.assertionConsumerServices = this.provider.assertionConsumerServices || []; + this.setEndpoints(this.provider.assertionConsumerServices); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/metadata-provider/component/forms/attribute-release-form.component.html b/ui/src/app/metadata-provider/component/forms/attribute-release-form.component.html new file mode 100644 index 000000000..1adb77d67 --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/attribute-release-form.component.html @@ -0,0 +1,35 @@ +
+
+
+ + + + + + + + + + + + + +
Attribute NameYes
{{ attr.label }} +
+ +
+
+
+
+
diff --git a/ui/src/app/metadata-provider/component/forms/attribute-release-form.component.spec.ts b/ui/src/app/metadata-provider/component/forms/attribute-release-form.component.spec.ts new file mode 100644 index 000000000..cb106c4b8 --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/attribute-release-form.component.spec.ts @@ -0,0 +1,74 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { ProviderValueEmitter, ProviderStatusEmitter } from '../../service/provider-change-emitter.service'; +import * as fromProviders from '../../reducer'; +import { NgbPopoverModule, NgbPopoverConfig } from '@ng-bootstrap/ng-bootstrap/popover/popover.module'; +import { AttributeReleaseFormComponent } from './attribute-release-form.component'; +import { ListValuesService } from '../../service/list-values.service'; +import { EntityDescriptor } from '../../model/entity-descriptor'; +import * as stubs from '../../../../testing/provider.stub'; + +describe('Attribute Release Form Component', () => { + let fixture: ComponentFixture; + let instance: AttributeReleaseFormComponent; + let store: Store; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + ProviderValueEmitter, + ProviderStatusEmitter, + NgbPopoverConfig, + ListValuesService + ], + imports: [ + NoopAnimationsModule, + ReactiveFormsModule, + StoreModule.forRoot({ + 'providers': combineReducers(fromProviders.reducers), + }), + NgbPopoverModule + ], + declarations: [ + AttributeReleaseFormComponent + ], + }); + store = TestBed.get(Store); + spyOn(store, 'dispatch').and.callThrough(); + + fixture = TestBed.createComponent(AttributeReleaseFormComponent); + instance = fixture.componentInstance; + instance.provider = { + ...stubs.provider, + attributeRelease: [] + }; + fixture.detectChanges(); + }); + + it('should compile', () => { + expect(fixture).toBeDefined(); + }); + + describe('ngOnChanges method', () => { + it('should set properties on the provider', () => { + spyOn(instance, 'setAttributes'); + instance.ngOnChanges(); + expect(instance.provider.attributeRelease).toEqual([]); + expect(instance.setAttributes).toHaveBeenCalled(); + }); + }); + + describe('onCheck method', () => { + it('should add the attribute to the list if checked', () => { + instance.onCheck({ target: { checked: true } }, 'foo'); + expect(instance.attributeRelease.length).toBe(1); + }); + it('should remove the attribute if not checked', () => { + spyOn(instance.attributeRelease, 'removeAt').and.callThrough(); + instance.onCheck({ target: { checked: false } }, 'foo'); + expect(instance.attributeRelease.removeAt).toHaveBeenCalled(); + }); + }); +}); diff --git a/ui/src/app/metadata-provider/component/forms/attribute-release-form.component.ts b/ui/src/app/metadata-provider/component/forms/attribute-release-form.component.ts new file mode 100644 index 000000000..d1e80f9af --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/attribute-release-form.component.ts @@ -0,0 +1,71 @@ +import { Component, Output, Input, EventEmitter, OnInit, OnChanges, OnDestroy, ChangeDetectionStrategy } from '@angular/core'; +import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms'; +import { Observable } from 'rxjs/Observable'; +import { ProviderFormFragmentComponent } from './provider-form-fragment.component'; +import { ProviderStatusEmitter, ProviderValueEmitter } from '../../service/provider-change-emitter.service'; +import { MetadataProvider, Organization, Contact } from '../../model/metadata-provider'; +import { ListValuesService } from '../../service/list-values.service'; +import { FormArray } from '@angular/forms/src/model'; + +@Component({ + selector: 'attribute-release-form', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './attribute-release-form.component.html' +}) +export class AttributeReleaseFormComponent extends ProviderFormFragmentComponent implements OnInit, OnChanges, OnDestroy { + @Input() provider: MetadataProvider; + + form: FormGroup; + attributesToRelease: any[]; + listOfAttributes$: Observable<{ key: string, label: string }[]>; + + constructor( + protected fb: FormBuilder, + protected statusEmitter: ProviderStatusEmitter, + protected valueEmitter: ProviderValueEmitter, + protected listService: ListValuesService + ) { + super(fb, statusEmitter, valueEmitter); + } + + createForm(): void { + this.form = this.fb.group({ + attributeRelease: this.fb.array([]) + }); + } + + ngOnInit(): void { + super.ngOnInit(); + this.listOfAttributes$ = this.listService.attributesToRelease; + } + + ngOnChanges(): void { + this.provider.attributeRelease = this.provider.attributeRelease || []; + this.setAttributes(this.provider.attributeRelease); + } + + get attributeRelease(): FormArray { + return this.form.get('attributeRelease') as FormArray; + } + + isChecked(attr): boolean { + return this.provider.attributeRelease.indexOf(attr) > -1; + } + + setAttributes(list: string[] = []): void { + let attrs = list.map(attr => this.fb.control(attr)); + const fbarray = this.fb.array(attrs); + this.form.setControl('attributeRelease', fbarray); + } + + onCheck($event, attr: string): void { + const checked = $event.target.checked; + if (checked) { + this.attributeRelease.push(this.fb.control(attr)); + } else { + const index = this.attributeRelease.controls.findIndex(control => control.value === attr); + this.attributeRelease.removeAt(index); + } + this.attributeRelease.updateValueAndValidity(); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/metadata-provider/component/forms/descriptor-info-form.component.html b/ui/src/app/metadata-provider/component/forms/descriptor-info-form.component.html new file mode 100644 index 000000000..10d6ead8f --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/descriptor-info-form.component.html @@ -0,0 +1,64 @@ +
+
+
+
+
+ + + Protocol Support Enumeration popover + + +
+ +
+
+
+ + NameID Format  + + + + Add NameID Format Popover + + +
+
+
+
+
+ + + +
+
+ +
+
+
+
+
+
diff --git a/ui/src/app/metadata-provider/component/forms/descriptor-info-form.component.spec.ts b/ui/src/app/metadata-provider/component/forms/descriptor-info-form.component.spec.ts new file mode 100644 index 000000000..0069d7bca --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/descriptor-info-form.component.spec.ts @@ -0,0 +1,108 @@ +import { Component, ViewChild } from '@angular/core'; +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { ProviderValueEmitter, ProviderStatusEmitter } from '../../service/provider-change-emitter.service'; +import * as fromProviders from '../../reducer'; +import { NgbPopoverModule, NgbPopoverConfig } from '@ng-bootstrap/ng-bootstrap/popover/popover.module'; +import { ListValuesService } from '../../service/list-values.service'; +import { EntityDescriptor } from '../../model/entity-descriptor'; +import { DescriptorInfoFormComponent } from './descriptor-info-form.component'; +import { AutoCompleteComponent } from '../../../widget/autocomplete/autocomplete.component'; + +import * as stubs from '../../../../testing/provider.stub'; + +@Component({ + template: `` +}) +class TestHostComponent { + provider = new EntityDescriptor({ + ...stubs.provider, + serviceProviderSsoDescriptor: { + protocolSupportEnum: 'foo', + nameIdFormats: [] + } + }); + + @ViewChild(DescriptorInfoFormComponent) + public formUnderTest: DescriptorInfoFormComponent; + + changeProvider(opts: any): void { + this.provider = Object.assign({}, this.provider, opts); + } + + addFormat(value: string): void { + this.provider.serviceProviderSsoDescriptor.nameIdFormats.push(value); + } +} + +describe('Descriptor Info Form Component', () => { + let fixture: ComponentFixture; + let instance: TestHostComponent; + let store: Store; + let form: DescriptorInfoFormComponent; + let fb: FormBuilder; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + ProviderValueEmitter, + ProviderStatusEmitter, + NgbPopoverConfig, + ListValuesService + ], + imports: [ + NoopAnimationsModule, + ReactiveFormsModule, + StoreModule.forRoot({ + 'providers': combineReducers(fromProviders.reducers), + }), + NgbPopoverModule + ], + declarations: [ + DescriptorInfoFormComponent, + AutoCompleteComponent, + TestHostComponent + ], + }); + store = TestBed.get(Store); + spyOn(store, 'dispatch').and.callThrough(); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + form = instance.formUnderTest; + fb = TestBed.get(FormBuilder); + fixture.detectChanges(); + }); + + it('should compile', () => { + expect(fixture).toBeDefined(); + }); + + describe('removeFormat method', () => { + it('should remove the nameid format at the given index', () => { + instance.addFormat('foo'); + fixture.detectChanges(); + form.removeFormat(0); + fixture.detectChanges(); + expect(form.nameIdFormats.length).toBe(0); + }); + }); + + describe('addFormat method', () => { + it('should add a new nameid format', () => { + form.addFormat(); + fixture.detectChanges(); + expect(form.nameIdFormats.length).toBe(1); + }); + }); + + describe('getRequiredControl method', () => { + it('should create a form control with the required validator attached', () => { + spyOn(fb, 'control').and.callThrough(); + form.getRequiredControl('foo'); + expect(fb.control).toHaveBeenCalledWith('foo', Validators.required); + }); + }); +}); diff --git a/ui/src/app/metadata-provider/component/forms/descriptor-info-form.component.ts b/ui/src/app/metadata-provider/component/forms/descriptor-info-form.component.ts new file mode 100644 index 000000000..5aaf1423c --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/descriptor-info-form.component.ts @@ -0,0 +1,74 @@ +import { Component, Output, Input, EventEmitter, OnInit, OnChanges, OnDestroy, ChangeDetectionStrategy } from '@angular/core'; +import { FormBuilder, FormGroup, FormControl, FormArray, Validators } from '@angular/forms'; +import { Observable } from 'rxjs/Observable'; + +import { ProviderFormFragmentComponent } from './provider-form-fragment.component'; + +import { ProviderStatusEmitter, ProviderValueEmitter } from '../../service/provider-change-emitter.service'; +import { MetadataProvider, Organization, Contact } from '../../model/metadata-provider'; +import { ListValuesService } from '../../service/list-values.service'; + +@Component({ + selector: 'descriptor-info-form', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './descriptor-info-form.component.html' +}) +export class DescriptorInfoFormComponent extends ProviderFormFragmentComponent implements OnInit, OnChanges, OnDestroy { + @Input() provider: MetadataProvider; + + form: FormGroup; + + nameIdFormatOptions: Observable = this.listValues.nameIdFormats; + + enumOptions: string[] = [ + 'SAML 2', + 'SAML 1.1' + ]; + + constructor( + protected fb: FormBuilder, + protected statusEmitter: ProviderStatusEmitter, + protected valueEmitter: ProviderValueEmitter, + private listValues: ListValuesService + ) { + super(fb, statusEmitter, valueEmitter); + } + + createForm(): void { + this.form = this.fb.group({ + serviceProviderSsoDescriptor: this.fb.group({ + protocolSupportEnum: null, + nameIdFormats: this.fb.array([]) + }) + }); + } + + ngOnChanges(): void { + let descriptor = this.provider.serviceProviderSsoDescriptor; + this.form.reset({ + serviceProviderSsoDescriptor: descriptor || {} + }); + this.setNameIdFormats(descriptor ? descriptor.nameIdFormats : []); + } + + get nameIdFormats(): FormArray { + return this.form.get('serviceProviderSsoDescriptor.nameIdFormats') as FormArray; + } + + getRequiredControl = (name: string): FormControl => this.fb.control(name, Validators.required); + + setNameIdFormats(nameIdFormats: string[]): void { + let fcs = nameIdFormats.map(this.getRequiredControl), + list = this.fb.array(fcs), + group = this.form.get('serviceProviderSsoDescriptor') as FormGroup; + group.setControl('nameIdFormats', list); + } + + addFormat(text: string = ''): void { + this.nameIdFormats.push(this.fb.control(text, Validators.required)); + } + + removeFormat(index: number): void { + this.nameIdFormats.removeAt(index); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/metadata-provider/component/forms/finish-form.component.html b/ui/src/app/metadata-provider/component/forms/finish-form.component.html new file mode 100644 index 000000000..99b5a5a6f --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/finish-form.component.html @@ -0,0 +1,264 @@ +
+
+
+
+
+ + Enable this service upon saving popover + +
+
+
+

+ 1 + Name and Entity ID +

+
+
Service Provider Name (Dashboard Display Only)
+
{{ provider.serviceProviderName }}
+
Service Provider Entity ID
+
{{ provider.entityId }}
+
Enable this service?
+
{{ provider.serviceEnabled ? 'Yes' : 'No' }}
+
+
+
+ + 2 + Organization Information + +
+
Organization Name
+
{{ provider.organization.name }}
+
Organization Display Name
+
{{ provider.organization.displayName }}
+
Organization URL
+
{{ provider.organization.url }}
+
Contact Information
+
+

+ + + + + + + + + + + + + + + +
Given NameEmail AddressContact Type
{{ contact.name }}{{ contact.emailAddress }}{{ contact.type }}
+
+
+
+
+ + 3 + User Interface / MDUI Information + +
+
Display Name
+
{{ provider.mdui.displayName }}
+
Information URL
+
{{ provider.mdui.informationUrl }}
+
Description
+
{{ provider.mdui.description }}
+
Privacy Statement URL
+
{{ provider.mdui.privacyStatementUrl }}
+
Logo URL
+
{{ provider.mdui.logoUrl }}
+
Logo Width
+
{{ provider.mdui.logoWidth }}
+
Logo Height
+
{{ provider.mdui.logoHeight }}
+
+
+
+ + 4 + SP SSO Descriptor Information + +
+
Protocol Support Enumeration
+
{{ provider.serviceProviderSsoDescriptor.protocolSupportEnum }}
+
NameID Format
+
+

+
    +
  • + {{ format }} +
  • +
+
+
+
+
+ + 5 + Logout Endpoints + +

+ + + + + + + + + + + + + +
Logout EndpointBinding Type
{{ endpoint.url }}{{ endpoint.bindingType }}
+
+
+
+
+ + 6 + Security Information + +
+
Is there a X509 Certificate?
+
{{ provider.securityInfo.x509CertificateAvailable ? 'Yes' : 'No' }}
+
Authentication Requests Signed?
+
{{ provider.securityInfo.authenticationRequestsSigned ? 'Yes' : 'No' }}
+
Want Assertions Signed?
+
{{ provider.securityInfo.wantAssertionsSigned ? 'Yes' : 'No' }}
+
X509 Certificates
+
+

+ + + + + + + + + + + + + + + + +
X509 Certificates
Certificate Name (Display Only)TypeCertificate
{{ cert.name }}{{ cert.type }}{{ cert.value | slice:0:9 }}…
+
+
+
+
+ + 7 + Assertion Consumer Services + +
+
Assertion Consumer Service Endpoints
+
+

+ + + + + + + + + + + + + + + +
#Assertion Consumer Service LocationAssertion Consumer Service Binding
{{ i + 1 }}{{ service.locationUrl }}{{ service.binding }}
+
+
+
+
+ + 8 + Relying Party Overrides + +
+
Sign the Assertion?
+
{{ provider.relyingPartyOverrides.signAssertion ? 'True' : 'False' }}
+
Don't Sign the Response?
+
{{ provider.relyingPartyOverrides.dontSignResponse ? 'True' : 'False' }}
+
Turn off Encryption of Response?
+
{{ provider.relyingPartyOverrides.turnOffEncryption ? 'True' : 'False' }}
+
Use SHA1 Signing Algorithm?
+
{{ provider.relyingPartyOverrides.useSha ? 'True' : 'False' }}
+
NameID Format to Send
+
+

+
    +
  • + {{ format }} +
  • +
+
+
Default Authentication Method(s)
+
+

+
    +
  1. + {{ method }} +
  2. +
+
+
Ignore any SP-Requested Authentication Method?
+
{{ provider.relyingPartyOverrides.ignoreAuthenticationMethod ? 'True' : 'False' }}
+
Omit Not Before Condition?
+
{{ provider.relyingPartyOverrides.omitNotBefore ? 'True' : 'False' }}
+
ResponderID
+
{{ provider.relyingPartyOverrides.responderId }}
+
+
+
+ + 9 + Attribute Release + + + + + + + + + + + + + + + + +
Attribute NameTrueFalse
{{ attr.label }} + + + +
+
+
+
+
diff --git a/ui/src/app/metadata-provider/component/forms/finish-form.component.scss b/ui/src/app/metadata-provider/component/forms/finish-form.component.scss new file mode 100644 index 000000000..0f9488141 --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/finish-form.component.scss @@ -0,0 +1,9 @@ +@import '../../../../theme/_palette'; + +.value:empty { + &::after { + content: "\2014"; + color: #868e96; + text-align: center; + } +} \ No newline at end of file diff --git a/ui/src/app/metadata-provider/component/forms/finish-form.component.spec.ts b/ui/src/app/metadata-provider/component/forms/finish-form.component.spec.ts new file mode 100644 index 000000000..b007e6d8a --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/finish-form.component.spec.ts @@ -0,0 +1,55 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { ActivatedRoute, Router } from '@angular/router'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { ProviderValueEmitter, ProviderStatusEmitter } from '../../service/provider-change-emitter.service'; +import * as fromProviders from '../../reducer'; +import { NgbPopoverModule, NgbPopoverConfig } from '@ng-bootstrap/ng-bootstrap/popover/popover.module'; +import { ListValuesService } from '../../service/list-values.service'; +import { EntityDescriptor } from '../../model/entity-descriptor'; +import { FinishFormComponent } from './finish-form.component'; +import { RouterStub, RouterLinkStubDirective } from '../../../../testing/router.stub'; +import { ActivatedRouteStub } from '../../../../testing/activated-route.stub'; + +describe('Finished Form Component', () => { + let fixture: ComponentFixture; + let instance: FinishFormComponent; + let store: Store; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + ProviderValueEmitter, + ProviderStatusEmitter, + NgbPopoverConfig, + ListValuesService, + { provide: Router, useClass: RouterStub }, + { provide: ActivatedRoute, useClass: ActivatedRouteStub } + ], + imports: [ + NoopAnimationsModule, + ReactiveFormsModule, + StoreModule.forRoot({ + 'providers': combineReducers(fromProviders.reducers), + }), + NgbPopoverModule + ], + declarations: [ + FinishFormComponent, + RouterLinkStubDirective + ], + }); + store = TestBed.get(Store); + spyOn(store, 'dispatch').and.callThrough(); + + fixture = TestBed.createComponent(FinishFormComponent); + instance = fixture.componentInstance; + instance.provider = new EntityDescriptor({ entityId: 'foo', serviceProviderName: 'bar' }); + fixture.detectChanges(); + }); + + it('should compile', () => { + expect(fixture).toBeDefined(); + }); +}); diff --git a/ui/src/app/metadata-provider/component/forms/finish-form.component.ts b/ui/src/app/metadata-provider/component/forms/finish-form.component.ts new file mode 100644 index 000000000..26790b677 --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/finish-form.component.ts @@ -0,0 +1,42 @@ +import { Component, Output, Input, EventEmitter, OnInit, OnChanges, OnDestroy, ChangeDetectionStrategy } from '@angular/core'; +import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms'; +import { Observable } from 'rxjs/Observable'; +import { ProviderFormFragmentComponent } from './provider-form-fragment.component'; +import { ProviderStatusEmitter, ProviderValueEmitter } from '../../service/provider-change-emitter.service'; +import { MetadataProvider, Organization, Contact } from '../../model/metadata-provider'; +import { ListValuesService } from '../../service/list-values.service'; + +@Component({ + selector: 'finish-form', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './finish-form.component.html', + styleUrls: ['./finish-form.component.scss'] +}) +export class FinishFormComponent extends ProviderFormFragmentComponent implements OnInit, OnChanges, OnDestroy { + @Input() provider: MetadataProvider; + + form: FormGroup; + attributesToRelease$: Observable; + + constructor( + protected fb: FormBuilder, + protected statusEmitter: ProviderStatusEmitter, + protected valueEmitter: ProviderValueEmitter, + private listValues: ListValuesService + ) { + super(fb, statusEmitter, valueEmitter); + this.attributesToRelease$ = listValues.attributesToRelease; + } + + createForm(): void { + this.form = this.fb.group({ + serviceEnabled: [true] + }); + } + + ngOnChanges(): void { + this.form.reset({ + serviceEnabled: !this.provider ? false : this.provider.serviceEnabled !== false ? true : false + }); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/metadata-provider/component/forms/key-info-form.component.html b/ui/src/app/metadata-provider/component/forms/key-info-form.component.html new file mode 100644 index 000000000..3b93b9f6b --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/key-info-form.component.html @@ -0,0 +1,187 @@ +
+
+
+
+
+
+   + + Is there a X509 Certificate popover + + +
+
+ +
+
+ +
+
+
+
+
+
+   + + Authentication Requests Signed popover + + + +
+
+ +
+
+ +
+
+
+
+
+
+   + + Want Assertions Signed + + +
+
+ +
+
+ +
+
+
+
+
+

+ X509 Certificates: + +

+
+ +
+
+
+
diff --git a/ui/src/app/metadata-provider/component/forms/key-info-form.component.spec.ts b/ui/src/app/metadata-provider/component/forms/key-info-form.component.spec.ts new file mode 100644 index 000000000..204084a40 --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/key-info-form.component.spec.ts @@ -0,0 +1,120 @@ +import { Component, ViewChild } from '@angular/core'; +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { ProviderValueEmitter, ProviderStatusEmitter } from '../../service/provider-change-emitter.service'; +import * as fromProviders from '../../reducer'; +import { NgbPopoverModule, NgbPopoverConfig } from '@ng-bootstrap/ng-bootstrap/popover/popover.module'; +import { ListValuesService } from '../../service/list-values.service'; +import { EntityDescriptor } from '../../model/entity-descriptor'; +import { KeyInfoFormComponent } from './key-info-form.component'; +import { InputDefaultsDirective } from '../../directive/input-defaults.directive'; + +import * as stubs from '../../../../testing/provider.stub'; + +@Component({ + template: `` +}) +class TestHostComponent { + provider = new EntityDescriptor({ + ...stubs.provider, + securityInfo: { + ...stubs.secInfo, + x509Certificates: [] + } + }); + + @ViewChild(KeyInfoFormComponent) + public formUnderTest: KeyInfoFormComponent; + + changeProvider(opts: any): void { + this.provider = Object.assign({}, this.provider, opts); + } +} + +describe('Security (Key) Info Form Component', () => { + let fixture: ComponentFixture; + let instance: TestHostComponent; + let store: Store; + let form: KeyInfoFormComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + ProviderValueEmitter, + ProviderStatusEmitter, + NgbPopoverConfig, + ListValuesService + ], + imports: [ + NoopAnimationsModule, + ReactiveFormsModule, + StoreModule.forRoot({ + 'providers': combineReducers(fromProviders.reducers), + }), + NgbPopoverModule + ], + declarations: [ + KeyInfoFormComponent, + TestHostComponent, + InputDefaultsDirective + ], + }).compileComponents(); + store = TestBed.get(Store); + spyOn(store, 'dispatch').and.callThrough(); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + form = instance.formUnderTest; + fixture.detectChanges(); + }); + + it('should compile', () => { + expect(fixture).toBeDefined(); + }); + + describe('removeCert method', () => { + it('should remove the certificate at the given index', () => { + instance.changeProvider({ + securityInfo: { + ...stubs.secInfo, + x509CertificateAvailable: true, + x509Certificates: [stubs.certificate] + } + }); + fixture.detectChanges(); + form.removeCert(0); + fixture.detectChanges(); + expect(form.x509Certificates.length).toBe(0); + }); + }); + + describe('addCert method', () => { + it('should remove the certificate at the given index', () => { + instance.changeProvider({ + securityInfo: { + ...stubs.secInfo, + x509CertificateAvailable: true + } + }); + fixture.detectChanges(); + form.addCert(); + fixture.detectChanges(); + expect(form.x509Certificates.length).toBe(1); + }); + }); + + describe('ngOnInit method', () => { + it('should remove certificates if there are none available', () => { + instance.changeProvider({ + securityInfo: { + ...stubs.secInfo, + x509Certificates: [stubs.certificate] + } + }); + fixture.detectChanges(); + expect(form.x509Certificates.length).toBe(0); + }); + }); +}); diff --git a/ui/src/app/metadata-provider/component/forms/key-info-form.component.ts b/ui/src/app/metadata-provider/component/forms/key-info-form.component.ts new file mode 100644 index 000000000..a3e3376d6 --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/key-info-form.component.ts @@ -0,0 +1,105 @@ +import { Component, Output, Input, EventEmitter, OnInit, OnChanges, OnDestroy, ChangeDetectionStrategy } from '@angular/core'; +import { FormBuilder, FormGroup, FormControl, FormArray, Validators } from '@angular/forms'; +import { Observable } from 'rxjs/Observable'; + +import { ProviderFormFragmentComponent } from './provider-form-fragment.component'; + +import { ProviderStatusEmitter, ProviderValueEmitter } from '../../service/provider-change-emitter.service'; +import { MetadataProvider, Certificate } from '../../model/metadata-provider'; + +@Component({ + selector: 'key-info-form', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './key-info-form.component.html' +}) +export class KeyInfoFormComponent extends ProviderFormFragmentComponent implements OnInit, OnChanges, OnDestroy { + @Input() provider: MetadataProvider; + + hasCert$: Observable; + + form: FormGroup; + + types: string[] = [ + 'signing', + 'encryption', + 'both' + ]; + + constructor( + protected fb: FormBuilder, + protected statusEmitter: ProviderStatusEmitter, + protected valueEmitter: ProviderValueEmitter + ) { + super(fb, statusEmitter, valueEmitter); + } + + createForm(): void { + this.form = this.fb.group({ + securityInfo: this.fb.group({ + x509CertificateAvailable: [false], + authenticationRequestsSigned: [false], + wantAssertionsSigned: [false], + x509Certificates: this.fb.array([]) + }) + }); + } + + get x509Certificates(): FormArray { + return this.form.get('securityInfo.x509Certificates') as FormArray; + } + + setCertificates(certs: Certificate[] = []): void { + let fgs = certs.map(ep => this.fb.group(ep)), + list = this.fb.array(fgs, Validators.minLength(1)), + group = this.form.get('securityInfo') as FormGroup; + group.setControl('x509Certificates', list); + } + + addCert(): void { + this.x509Certificates.push(this.fb.group({ + name: ['', Validators.required], + type: ['both', Validators.required], + value: ['', Validators.required] + })); + } + + removeCert(index: number): void { + this.x509Certificates.removeAt(index); + } + + ngOnInit(): void { + super.ngOnInit(); + this.hasCert$ = this.form.valueChanges + .distinctUntilChanged() + .map(values => values.securityInfo.x509CertificateAvailable); + + this.hasCert$ + .takeUntil(this.ngUnsubscribe) + .distinctUntilChanged() + .subscribe(hasCert => { + if (hasCert && !this.x509Certificates.length) { + this.addCert(); + this.x509Certificates.setValidators(Validators.minLength(1)); + this.x509Certificates.updateValueAndValidity(); + } + if (!hasCert) { + while (this.x509Certificates.controls.length !== 0) { + this.removeCert(0); + } + } + }); + } + + ngOnChanges(): void { + this.form.reset({ + securityInfo: this.provider.securityInfo || { + x509CertificateAvailable: false, + authenticationRequestsSigned: false, + wantAssertionsSigned: false + } + }); + if (this.provider.securityInfo && this.provider.securityInfo.x509CertificateAvailable) { + this.setCertificates(this.provider.securityInfo.x509Certificates); + } + } +} /* istanbul ignore next */ diff --git a/ui/src/app/metadata-provider/component/forms/logout-form.component.html b/ui/src/app/metadata-provider/component/forms/logout-form.component.html new file mode 100644 index 000000000..8a0fbaf14 --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/logout-form.component.html @@ -0,0 +1,69 @@ +
+
+
+

+ Logout Endpoints: + +

+
+ +
+
+
+
diff --git a/ui/src/app/metadata-provider/component/forms/logout-form.component.spec.ts b/ui/src/app/metadata-provider/component/forms/logout-form.component.spec.ts new file mode 100644 index 000000000..1993f4e62 --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/logout-form.component.spec.ts @@ -0,0 +1,49 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { ProviderValueEmitter, ProviderStatusEmitter } from '../../service/provider-change-emitter.service'; +import * as fromProviders from '../../reducer'; +import { NgbPopoverModule, NgbPopoverConfig } from '@ng-bootstrap/ng-bootstrap/popover/popover.module'; +import { ListValuesService } from '../../service/list-values.service'; +import { EntityDescriptor } from '../../model/entity-descriptor'; +import { LogoutFormComponent } from './logout-form.component'; + +describe('Logout Endpoints Form Component', () => { + let fixture: ComponentFixture; + let instance: LogoutFormComponent; + let store: Store; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + ProviderValueEmitter, + ProviderStatusEmitter, + NgbPopoverConfig, + ListValuesService + ], + imports: [ + NoopAnimationsModule, + ReactiveFormsModule, + StoreModule.forRoot({ + 'providers': combineReducers(fromProviders.reducers), + }), + NgbPopoverModule + ], + declarations: [ + LogoutFormComponent + ], + }); + store = TestBed.get(Store); + spyOn(store, 'dispatch').and.callThrough(); + + fixture = TestBed.createComponent(LogoutFormComponent); + instance = fixture.componentInstance; + instance.provider = new EntityDescriptor({ entityId: 'foo', serviceProviderName: 'bar' }); + fixture.detectChanges(); + }); + + it('should compile', () => { + expect(fixture).toBeDefined(); + }); +}); diff --git a/ui/src/app/metadata-provider/component/forms/logout-form.component.ts b/ui/src/app/metadata-provider/component/forms/logout-form.component.ts new file mode 100644 index 000000000..d0ebd9f2e --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/logout-form.component.ts @@ -0,0 +1,65 @@ +import { Component, Output, Input, EventEmitter, OnInit, OnChanges, OnDestroy, ChangeDetectionStrategy } from '@angular/core'; +import { FormBuilder, FormGroup, FormControl, FormArray, Validators } from '@angular/forms'; +import { Observable } from 'rxjs/Observable'; + +import { ProviderFormFragmentComponent } from './provider-form-fragment.component'; + +import { ProviderStatusEmitter, ProviderValueEmitter } from '../../service/provider-change-emitter.service'; +import { MetadataProvider, LogoutEndpoint } from '../../model/metadata-provider'; +import * as patterns from '../../../shared/regex'; + +@Component({ + selector: 'logout-form', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './logout-form.component.html' +}) +export class LogoutFormComponent extends ProviderFormFragmentComponent implements OnInit, OnChanges, OnDestroy { + @Input() provider: MetadataProvider; + + form: FormGroup; + + bindingTypes: string[] = [ + 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', + 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' + ]; + + constructor( + protected fb: FormBuilder, + protected statusEmitter: ProviderStatusEmitter, + protected valueEmitter: ProviderValueEmitter + ) { + super(fb, statusEmitter, valueEmitter); + } + + createForm(): void { + this.form = this.fb.group({ + logoutEndpoints: this.fb.array([]) + }); + } + + get logoutEndpoints(): FormArray { + return this.form.get('logoutEndpoints') as FormArray; + } + + setEndpoints(endpoints: LogoutEndpoint[] = []): void { + let fgs = endpoints.map(ep => this.fb.group(ep)), + list = this.fb.array(fgs); + this.form.setControl('logoutEndpoints', list); + } + + addEndpoint(): void { + this.logoutEndpoints.push(this.fb.group({ + url: ['', Validators.required], + bindingType: [null, Validators.required] + })); + } + + removeEndpoint(index: number): void { + this.logoutEndpoints.removeAt(index); + } + + ngOnChanges(): void { + this.provider.logoutEndpoints = this.provider.logoutEndpoints || []; + this.setEndpoints(this.provider.logoutEndpoints); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/metadata-provider/component/forms/metadata-ui-form.component.html b/ui/src/app/metadata-provider/component/forms/metadata-ui-form.component.html new file mode 100644 index 000000000..e15f97672 --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/metadata-ui-form.component.html @@ -0,0 +1,137 @@ +
+
+
+
+
+ + + Typically, the IdP Display Name field will be presented on IdP discovery service interfaces. + + +
+ +
+
+
+ + + The IdP Information URL is a link to a comprehensive information page about the IdP. This page should expand on the content of the IdP Description field. + + +
+ + Must be a valid URL +
+
+
+ + + The IdP Description is a brief description of the IdP service. On a well-designed discovery interface, the IdP Description will be presented to the user in addition to the IdP Display Name, and so the IdP Description helps disambiguate duplicate or similar IdP Display Names. + + +
+ + + + {{ form.get('mdui.description').value ? form.get('mdui.description').value.length : '0' }} + + / + {{ descriptionMaxLength }} + +
+
+
+
+
+ + + The IdP Privacy Statement URL is a link to the IdP's Privacy Statement. The content of the Privacy Statement should be targeted at end users. + + +
+ + Must be a valid URL +
+
+
+ + + The IdP Logo URL in metadata points to an image file on a remote server. A discovery service, for example, may rely on a visual cue (i.e., a logo) instead of or in addition to the IdP Display Name. + + +
+ + Must be a valid URL +
+
+
+
+
+ + + The logo should have a minimum width of 100 pixels + + +
+ + Must be an integer equal to or greater than 0 +
+
+
+
+
+ + + The logo should have a minimum height of 75 pixels and a maximum height of 150 pixels (or the application will scale it proportionally) + + +
+ + Must be an integer equal to or greater than 0 +
+
+
+
+
+
diff --git a/ui/src/app/metadata-provider/component/forms/metadata-ui-form.component.spec.ts b/ui/src/app/metadata-provider/component/forms/metadata-ui-form.component.spec.ts new file mode 100644 index 000000000..8d5774b44 --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/metadata-ui-form.component.spec.ts @@ -0,0 +1,49 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { ProviderValueEmitter, ProviderStatusEmitter } from '../../service/provider-change-emitter.service'; +import * as fromProviders from '../../reducer'; +import { NgbPopoverModule, NgbPopoverConfig } from '@ng-bootstrap/ng-bootstrap/popover/popover.module'; +import { ListValuesService } from '../../service/list-values.service'; +import { EntityDescriptor } from '../../model/entity-descriptor'; +import { MetadataUiFormComponent } from './metadata-ui-form.component'; + +describe('Metadata UI Form Component', () => { + let fixture: ComponentFixture; + let instance: MetadataUiFormComponent; + let store: Store; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + ProviderValueEmitter, + ProviderStatusEmitter, + NgbPopoverConfig, + ListValuesService + ], + imports: [ + NoopAnimationsModule, + ReactiveFormsModule, + StoreModule.forRoot({ + 'providers': combineReducers(fromProviders.reducers), + }), + NgbPopoverModule + ], + declarations: [ + MetadataUiFormComponent + ], + }); + store = TestBed.get(Store); + spyOn(store, 'dispatch').and.callThrough(); + + fixture = TestBed.createComponent(MetadataUiFormComponent); + instance = fixture.componentInstance; + instance.provider = new EntityDescriptor({ entityId: 'foo', serviceProviderName: 'bar' }); + fixture.detectChanges(); + }); + + it('should compile', () => { + expect(fixture).toBeDefined(); + }); +}); diff --git a/ui/src/app/metadata-provider/component/forms/metadata-ui-form.component.ts b/ui/src/app/metadata-provider/component/forms/metadata-ui-form.component.ts new file mode 100644 index 000000000..582085b3e --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/metadata-ui-form.component.ts @@ -0,0 +1,46 @@ +import { Component, Output, Input, EventEmitter, OnInit, OnChanges, OnDestroy, ChangeDetectionStrategy } from '@angular/core'; +import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms'; +import { Observable } from 'rxjs/Observable'; +import { ProviderFormFragmentComponent } from './provider-form-fragment.component'; +import { ProviderStatusEmitter, ProviderValueEmitter } from '../../service/provider-change-emitter.service'; +import { MetadataProvider, Organization, Contact } from '../../model/metadata-provider'; +import * as patterns from '../../../shared/regex'; + +@Component({ + selector: 'metadata-ui-form', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './metadata-ui-form.component.html' +}) +export class MetadataUiFormComponent extends ProviderFormFragmentComponent implements OnInit, OnChanges, OnDestroy { + @Input() provider: MetadataProvider; + + descriptionMaxLength = this.defaultMaxLength; + + constructor( + protected fb: FormBuilder, + protected statusEmitter: ProviderStatusEmitter, + protected valueEmitter: ProviderValueEmitter + ) { + super(fb, statusEmitter, valueEmitter); + } + + createForm(): void { + this.form = this.fb.group({ + mdui: this.fb.group({ + displayName: '', + informationUrl: [''], + privacyStatementUrl: [''], + description: '', + logoUrl: [''], + logoHeight: [0, [Validators.min(0), Validators.pattern(patterns.INTEGER_REGEX)]], + logoWidth: [0, [Validators.min(0), Validators.pattern(patterns.INTEGER_REGEX)]] + }) + }); + } + + ngOnChanges(): void { + this.form.reset({ + mdui: this.provider.mdui || {} + }); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/metadata-provider/component/forms/organization-info-form.component.html b/ui/src/app/metadata-provider/component/forms/organization-info-form.component.html new file mode 100644 index 000000000..c3b9f3cd7 --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/organization-info-form.component.html @@ -0,0 +1,155 @@ +
+
+
+
+
+ + + Organization Name Popover + + +
+ +
+
+
+ + + Organization Display Name Popover + + +
+ +
+
+
+ + + Organization Url Popover + + +
+ + Must be a valid URL +
+

* These three fields must all be entered if any single field has a value.

+
+
+

+ Contact Information: + +

+
+ +
+
+
+
diff --git a/ui/src/app/metadata-provider/component/forms/organization-info-form.component.spec.ts b/ui/src/app/metadata-provider/component/forms/organization-info-form.component.spec.ts new file mode 100644 index 000000000..bb32c9dbb --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/organization-info-form.component.spec.ts @@ -0,0 +1,85 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { ProviderValueEmitter, ProviderStatusEmitter } from '../../service/provider-change-emitter.service'; +import * as fromProviders from '../../reducer'; +import { NgbPopoverModule, NgbPopoverConfig } from '@ng-bootstrap/ng-bootstrap/popover/popover.module'; +import { ListValuesService } from '../../service/list-values.service'; +import { EntityDescriptor } from '../../model/entity-descriptor'; +import { OrganizationInfoFormComponent } from './organization-info-form.component'; +import * as stubs from '../../../../testing/provider.stub'; + +describe('Organization Info Form Component', () => { + let fixture: ComponentFixture; + let instance: OrganizationInfoFormComponent; + let store: Store; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + ProviderValueEmitter, + ProviderStatusEmitter, + NgbPopoverConfig, + ListValuesService + ], + imports: [ + NoopAnimationsModule, + ReactiveFormsModule, + StoreModule.forRoot({ + 'providers': combineReducers(fromProviders.reducers), + }), + NgbPopoverModule + ], + declarations: [ + OrganizationInfoFormComponent + ], + }); + store = TestBed.get(Store); + spyOn(store, 'dispatch').and.callThrough(); + + fixture = TestBed.createComponent(OrganizationInfoFormComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should compile', () => { + expect(fixture).toBeDefined(); + }); + + describe('ngOnChanges method', () => { + it('should set properties on the provider', () => { + instance.provider = stubs.provider; + fixture.detectChanges(); + instance.ngOnChanges(); + expect(instance.provider.organization).toEqual({}); + expect(instance.provider.contacts).toEqual([]); + }); + }); + + describe('removeContact method', () => { + it('should remove the contact at the given index', () => { + instance.provider = { + ...stubs.provider, + contacts: [stubs.contact] + }; + fixture.detectChanges(); + instance.ngOnChanges(); + instance.removeContact(0); + expect(instance.contacts.length).toBe(0); + }); + }); + + describe('addContact method', () => { + it('should remove the contact at the given index', () => { + instance.provider = { + ...stubs.provider, + contacts: [stubs.contact] + }; + fixture.detectChanges(); + instance.ngOnChanges(); + instance.addContact(); + expect(instance.contacts.length).toBe(2); + }); + }); +}); diff --git a/ui/src/app/metadata-provider/component/forms/organization-info-form.component.ts b/ui/src/app/metadata-provider/component/forms/organization-info-form.component.ts new file mode 100644 index 000000000..956bd81c1 --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/organization-info-form.component.ts @@ -0,0 +1,98 @@ +import { Component, Input, OnInit, OnChanges, OnDestroy, ChangeDetectionStrategy, EventEmitter } from '@angular/core'; +import { FormBuilder, FormGroup, FormControl, FormArray, Validators } from '@angular/forms'; +import { Observable } from 'rxjs/Observable'; +import { Subscription } from 'rxjs/Subscription'; +import 'rxjs/add/operator/startWith'; + +import { ProviderStatusEmitter, ProviderValueEmitter } from '../../service/provider-change-emitter.service'; +import { MetadataProvider, Organization, Contact } from '../../model/metadata-provider'; +import { ProviderFormFragmentComponent } from './provider-form-fragment.component'; +import { EntityValidators } from '../../service/entity-validators.service'; +import * as patterns from '../../../shared/regex'; + +@Component({ + selector: 'org-info-form', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './organization-info-form.component.html' +}) +export class OrganizationInfoFormComponent extends ProviderFormFragmentComponent implements OnInit, OnChanges, OnDestroy { + @Input() provider: MetadataProvider; + + contactTypes: string[] = [ + 'support', + 'technical', + 'administrative', + 'other' + ]; + + form: FormGroup; + + hasValue$: Observable; + totalValue$: Observable; + + private validationSubscription: Subscription; + + constructor( + protected fb: FormBuilder, + protected statusEmitter: ProviderStatusEmitter, + protected valueEmitter: ProviderValueEmitter + ) { + super(fb, statusEmitter, valueEmitter); + } + + createForm(): void { + this.form = this.fb.group({ + organization: this.fb.group({ + name: [''], + displayName: [''], + url: [''] + }, { asyncValidator: EntityValidators.createOrgValidator() }), + contacts: this.fb.array([]) + }); + } + + ngOnInit(): void { + super.ngOnInit(); + this.hasValue$ = this.form + .get('organization') + .valueChanges + .startWith(this.form.get('organization').value) + .map(values => Object.keys(values).reduce((coll, key) => coll + (values[key] || ''), '')) + .map(value => !!value); + } + + ngOnChanges(): void { + this.provider.organization = this.provider.organization || {}; + this.provider.contacts = this.provider.contacts || []; + this.form.reset({ + organization: this.provider.organization + }); + this.setContacts(this.provider.contacts); + } + + get contacts(): FormArray { + return this.form.get('contacts') as FormArray; + } + + setContacts(contacts: Contact[] = []): void { + let fgs = contacts.map(contact => this.getContact(contact)), + list = this.fb.array(fgs); + this.form.setControl('contacts', list); + } + + addContact(): void { + this.contacts.push(this.getContact()); + } + + getContact(contact: Contact = {} as Contact): FormGroup { + return this.fb.group({ + type: [contact.type || null, Validators.required], + name: [contact.name || null, Validators.required], + emailAddress: [contact.emailAddress || null, [Validators.required, Validators.email]] + }); + } + + removeContact(index: number): void { + this.contacts.removeAt(index); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/metadata-provider/component/forms/provider-form-fragment.component.spec.ts b/ui/src/app/metadata-provider/component/forms/provider-form-fragment.component.spec.ts new file mode 100644 index 000000000..bdc19a905 --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/provider-form-fragment.component.spec.ts @@ -0,0 +1,35 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { ProviderFormFragmentComponent } from './provider-form-fragment.component'; +import { ProviderValueEmitter, ProviderStatusEmitter } from '../../service/provider-change-emitter.service'; + + +describe('Provider Form Fragment Component', () => { + let fixture: ComponentFixture; + let instance: ProviderFormFragmentComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + ProviderValueEmitter, + ProviderStatusEmitter + ], + imports: [ + NoopAnimationsModule, + ReactiveFormsModule + ], + declarations: [ + ProviderFormFragmentComponent + ], + }); + + fixture = TestBed.createComponent(ProviderFormFragmentComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should compile', () => { + expect(fixture).toBeDefined(); + }); +}); diff --git a/ui/src/app/metadata-provider/component/forms/provider-form-fragment.component.ts b/ui/src/app/metadata-provider/component/forms/provider-form-fragment.component.ts new file mode 100644 index 000000000..97f335770 --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/provider-form-fragment.component.ts @@ -0,0 +1,65 @@ +import { Component, Input, Output, OnInit, OnDestroy, AfterViewInit, + ChangeDetectionStrategy, EventEmitter, ElementRef, ViewChildren } from '@angular/core'; +import { FormBuilder, FormGroup, FormControl, FormArray, FormControlName, Validators } from '@angular/forms'; +import { Observable } from 'rxjs/Observable'; +import { Subscription } from 'rxjs/Subscription'; +import { Subject } from 'rxjs/Subject'; + +import 'rxjs/add/operator/takeUntil'; +import 'rxjs/add/operator/startWith'; + +import { ProviderStatusEmitter, ProviderValueEmitter } from '../../service/provider-change-emitter.service'; +import { MetadataProvider, Organization, Contact } from '../../model/metadata-provider'; +import * as fromProviders from '../../reducer'; + +import * as constants from '../../../shared/constant'; + +@Component({ + selector: 'provider-form-fragment', + changeDetection: ChangeDetectionStrategy.OnPush, + template: `Foo` +}) +export class ProviderFormFragmentComponent implements OnInit, OnDestroy { + @Input() provider: MetadataProvider; + @ViewChildren(FormControlName, { read: ElementRef }) formInputElements: ElementRef[]; + + protected ngUnsubscribe: Subject = new Subject(); + protected valueEmitSubscription: Subscription; + protected statusEmitSubscription: Subscription; + + form: FormGroup; + provider$: Observable; + + defaultMaxLength = constants.DEFAULT_FIELD_MAX_LENGTH; + + constructor( + protected fb: FormBuilder, + protected statusEmitter: ProviderStatusEmitter, + protected valueEmitter: ProviderValueEmitter + ) { + this.createForm(); + } + + createForm(): void { + this.form = this.fb.group({}); + } + + ngOnInit(): void { + this.valueEmitSubscription = this.form + .valueChanges + .takeUntil(this.ngUnsubscribe) + .startWith(this.form.value) + .subscribe(changes => this.valueEmitter.emit(changes)); + + this.statusEmitSubscription = this.form + .statusChanges + .takeUntil(this.ngUnsubscribe) + .startWith(this.form.status) + .subscribe(status => this.statusEmitter.emit(status)); + } + + ngOnDestroy(): void { + this.ngUnsubscribe.next(); + this.ngUnsubscribe.complete(); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/metadata-provider/component/forms/relying-party-form.component.html b/ui/src/app/metadata-provider/component/forms/relying-party-form.component.html new file mode 100644 index 000000000..7bca52396 --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/relying-party-form.component.html @@ -0,0 +1,171 @@ +
+
+
+
+
+ + Sign Assertion popover + +
+
+
+
+ + Don't Sign Response popover + +
+
+
+
+ + Turn Off Encryption of Response popover + +
+
+
+
+ + Use SHA1 Signing Algorithm popover + +
+
+
+
+ + NameID Format to Send + + + + Add NameId Format popover + + +
+
+
+ + + +
+ +
+
+
+
+
+
+ + Authentication Methods to Use + + + + Authentication Methods to Use popover + + +
+
+
+ + + +
+ +
+
+
+
+
+
+ + Ignore any SP-Requested Authentication Method popover + +
+
+
+
+ + Omit Not Before Condition popover + +
+
+
+
+ + + ResponderId popover + + +
+ +
+
+
+
diff --git a/ui/src/app/metadata-provider/component/forms/relying-party-form.component.spec.ts b/ui/src/app/metadata-provider/component/forms/relying-party-form.component.spec.ts new file mode 100644 index 000000000..35f54f6bb --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/relying-party-form.component.spec.ts @@ -0,0 +1,126 @@ +import { Component, ViewChild } from '@angular/core'; +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { ProviderValueEmitter, ProviderStatusEmitter } from '../../service/provider-change-emitter.service'; +import * as fromProviders from '../../reducer'; +import { NgbPopoverModule, NgbPopoverConfig } from '@ng-bootstrap/ng-bootstrap/popover/popover.module'; +import { ListValuesService } from '../../service/list-values.service'; +import { EntityDescriptor } from '../../model/entity-descriptor'; +import { RelyingPartyFormComponent } from './relying-party-form.component'; +import { AutoCompleteComponent } from '../../../widget/autocomplete/autocomplete.component'; + +import * as stubs from '../../../../testing/provider.stub'; + +@Component({ + template: `` +}) +class TestHostComponent { + provider = new EntityDescriptor({ + ...stubs.provider, + relyingPartyOverrides: { + nameIdFormats: [], + authenticationMethods: [] + } + }); + + @ViewChild(RelyingPartyFormComponent) + public formUnderTest: RelyingPartyFormComponent; + + changeProvider(opts: any): void { + this.provider = Object.assign({}, this.provider, opts); + } + + addString(collection: 'nameIdFormats' | 'authenticationMethods', value: string): void { + this.provider.relyingPartyOverrides[collection].push(value); + } +} + +describe('Relying Party Form Component', () => { + let fixture: ComponentFixture; + let instance: TestHostComponent; + let store: Store; + let form: RelyingPartyFormComponent; + let fb: FormBuilder; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + ProviderValueEmitter, + ProviderStatusEmitter, + NgbPopoverConfig, + ListValuesService + ], + imports: [ + NoopAnimationsModule, + ReactiveFormsModule, + StoreModule.forRoot({ + 'providers': combineReducers(fromProviders.reducers), + }), + NgbPopoverModule + ], + declarations: [ + RelyingPartyFormComponent, + AutoCompleteComponent, + TestHostComponent + ], + }); + store = TestBed.get(Store); + spyOn(store, 'dispatch').and.callThrough(); + + fixture = TestBed.createComponent(TestHostComponent); + fb = TestBed.get(FormBuilder); + instance = fixture.componentInstance; + form = instance.formUnderTest; + fixture.detectChanges(); + }); + + it('should compile', () => { + expect(fixture).toBeDefined(); + }); + + describe('removeFormat method', () => { + it('should remove the nameid format at the given index', () => { + instance.addString('nameIdFormats', 'foo'); + fixture.detectChanges(); + form.removeFormat(0); + fixture.detectChanges(); + expect(form.nameIdFormats.length).toBe(0); + }); + }); + + describe('addFormat method', () => { + it('should add a new nameid format', () => { + form.addFormat(); + fixture.detectChanges(); + expect(form.nameIdFormats.length).toBe(1); + }); + }); + + describe('removeAuthenticationMethod method', () => { + it('should remove the auth method at the given index', () => { + instance.addString('authenticationMethods', 'foo'); + fixture.detectChanges(); + form.removeAuthenticationMethod(0); + fixture.detectChanges(); + expect(form.authenticationMethods.length).toBe(0); + }); + }); + + describe('addAuthenticationMethod method', () => { + it('should add a new auth method', () => { + form.addAuthenticationMethod(); + fixture.detectChanges(); + expect(form.authenticationMethods.length).toBe(1); + }); + }); + + describe('getRequiredControl method', () => { + it('should create a form control with the required validator attached', () => { + spyOn(fb, 'control').and.callThrough(); + form.getRequiredControl('foo'); + expect(fb.control).toHaveBeenCalledWith('foo', Validators.required); + }); + }); +}); diff --git a/ui/src/app/metadata-provider/component/forms/relying-party-form.component.ts b/ui/src/app/metadata-provider/component/forms/relying-party-form.component.ts new file mode 100644 index 000000000..39ff08778 --- /dev/null +++ b/ui/src/app/metadata-provider/component/forms/relying-party-form.component.ts @@ -0,0 +1,98 @@ +import { Component, Output, Input, EventEmitter, OnInit, OnChanges, OnDestroy, ChangeDetectionStrategy } from '@angular/core'; +import { FormBuilder, FormGroup, FormControl, FormArray, Validators } from '@angular/forms'; +import { Observable } from 'rxjs/Observable'; + +import { ProviderFormFragmentComponent } from './provider-form-fragment.component'; + +import { ProviderStatusEmitter, ProviderValueEmitter } from '../../service/provider-change-emitter.service'; +import { ListValuesService } from '../../service/list-values.service'; +import { MetadataProvider, Organization, Contact } from '../../model/metadata-provider'; + +import { URL_REGEX } from '../../../shared/regex'; + +@Component({ + selector: 'relying-party-form', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './relying-party-form.component.html' +}) +export class RelyingPartyFormComponent extends ProviderFormFragmentComponent implements OnInit, OnChanges, OnDestroy { + @Input() provider: MetadataProvider; + + form: FormGroup; + nameIdFormatOptions = this.listValues.nameIdFormats; + authenticationMethodOptions = this.listValues.authenticationMethods; + + constructor( + protected fb: FormBuilder, + protected statusEmitter: ProviderStatusEmitter, + protected valueEmitter: ProviderValueEmitter, + private listValues: ListValuesService + ) { + super(fb, statusEmitter, valueEmitter); + } + + createForm(): void { + this.form = this.fb.group({ + relyingPartyOverrides: this.fb.group({ + signAssertion: false, + dontSignResponse: false, + turnOffEncryption: false, + useSha: false, + ignoreAuthenticationMethod: false, + omitNotBefore: false, + responderId: '', + nameIdFormats: this.fb.array([]), + authenticationMethods: this.fb.array([]) + }) + }); + } + + getRequiredControl = (value: string): FormControl => this.fb.control(value, Validators.required); + + setNameIdFormats(nameIdFormats: string[] = []): void { + let fcs = nameIdFormats.map(this.getRequiredControl), + list = this.fb.array(fcs), + group = this.form.get('relyingPartyOverrides') as FormGroup; + group.setControl('nameIdFormats', list); + } + + setAuthenticationMethods(methods: string[] = []): void { + let fcs = methods.map(this.getRequiredControl), + list = this.fb.array(fcs), + group = this.form.get('relyingPartyOverrides') as FormGroup; + group.setControl('authenticationMethods', list); + } + + get nameIdFormats(): FormArray { + return this.form.get('relyingPartyOverrides.nameIdFormats') as FormArray; + } + + get authenticationMethods(): FormArray { + return this.form.get('relyingPartyOverrides.authenticationMethods') as FormArray; + } + + addFormat(text: string = ''): void { + this.nameIdFormats.push(this.fb.control(text, Validators.required)); + } + + addAuthenticationMethod(text: string = ''): void { + this.authenticationMethods.push(this.fb.control(text, Validators.required)); + } + + removeFormat(index: number): void { + this.nameIdFormats.removeAt(index); + } + + removeAuthenticationMethod(index: number): void { + this.authenticationMethods.removeAt(index); + } + + ngOnChanges(): void { + let overrides = this.provider.relyingPartyOverrides || {nameIdFormats: [], authenticationMethods: []}; + this.form.reset({ + relyingPartyOverrides: overrides + }); + this.setNameIdFormats(overrides.nameIdFormats); + this.setAuthenticationMethods(overrides.authenticationMethods); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/metadata-provider/component/i18n-text.component.html b/ui/src/app/metadata-provider/component/i18n-text.component.html new file mode 100644 index 000000000..70c01043b --- /dev/null +++ b/ui/src/app/metadata-provider/component/i18n-text.component.html @@ -0,0 +1,18 @@ + + + {{ key }} + User Interface / MDUI Information + SP SSO Descriptor Information + Logout Endpoints + Security Information + Assertion Consumer Service + Relying Party Overrides + Attribute Release + SP/Organization Information + Organization Information + Finished! + + Signing + Encryption + Both + \ No newline at end of file diff --git a/ui/src/app/metadata-provider/component/i18n-text.component.ts b/ui/src/app/metadata-provider/component/i18n-text.component.ts new file mode 100644 index 000000000..b75f1c9eb --- /dev/null +++ b/ui/src/app/metadata-provider/component/i18n-text.component.ts @@ -0,0 +1,11 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'i18n-text', + templateUrl: './i18n-text.component.html' +}) +export class I18nTextComponent { + @Input() key: string; + + constructor() { } +} diff --git a/ui/src/app/metadata-provider/component/index.ts b/ui/src/app/metadata-provider/component/index.ts new file mode 100644 index 000000000..fc840918a --- /dev/null +++ b/ui/src/app/metadata-provider/component/index.ts @@ -0,0 +1,65 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; +import { NgbPopoverModule, NgbPopoverConfig } from '@ng-bootstrap/ng-bootstrap/popover/popover.module'; +import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap/modal/modal.module'; + +import { AdvancedInfoFormComponent } from './forms/advanced-info-form.component'; +import { OrganizationInfoFormComponent } from './forms/organization-info-form.component'; +import { MetadataUiFormComponent } from './forms/metadata-ui-form.component'; +import { KeyInfoFormComponent } from './forms/key-info-form.component'; +import { AssertionFormComponent } from './forms/assertion-form.component'; +import { DescriptorInfoFormComponent } from './forms/descriptor-info-form.component'; +import { RelyingPartyFormComponent } from './forms/relying-party-form.component'; +import { AttributeReleaseFormComponent } from './forms/attribute-release-form.component'; +import { LogoutFormComponent } from './forms/logout-form.component'; +import { FinishFormComponent } from './forms/finish-form.component'; +import { ProviderFormFragmentComponent } from './forms/provider-form-fragment.component'; + +import { InfoLabelDirective } from '../directive/info-label.directive'; +import { InputDefaultsDirective } from '../directive/input-defaults.directive'; +import { I18nTextComponent } from './i18n-text.component'; + +import { ValidationClassDirective } from '../../widget/validation/validation-class.directive'; +import { AutoCompleteComponent } from '../../widget/autocomplete/autocomplete.component'; + + + +export const COMPONENTS = [ + AdvancedInfoFormComponent, + OrganizationInfoFormComponent, + MetadataUiFormComponent, + KeyInfoFormComponent, + AssertionFormComponent, + LogoutFormComponent, + DescriptorInfoFormComponent, + RelyingPartyFormComponent, + AttributeReleaseFormComponent, + FinishFormComponent, + ProviderFormFragmentComponent, + AutoCompleteComponent +]; + +export const declarations = [ + ...COMPONENTS, + InfoLabelDirective, + InputDefaultsDirective, + ValidationClassDirective, + I18nTextComponent +]; + +@NgModule({ + declarations: declarations, + entryComponents: COMPONENTS, + exports: declarations, + imports: [ + CommonModule, + ReactiveFormsModule, + RouterModule, + NgbPopoverModule, + NgbModalModule + ], + providers: [] +}) +export class ProviderEditorFormModule {} diff --git a/ui/src/app/metadata-provider/component/preview-provider-dialog.component.html b/ui/src/app/metadata-provider/component/preview-provider-dialog.component.html new file mode 100644 index 000000000..8b4645ac2 --- /dev/null +++ b/ui/src/app/metadata-provider/component/preview-provider-dialog.component.html @@ -0,0 +1,16 @@ + + + \ No newline at end of file diff --git a/ui/src/app/metadata-provider/component/preview-provider-dialog.component.ts b/ui/src/app/metadata-provider/component/preview-provider-dialog.component.ts new file mode 100644 index 000000000..cc37c6fbb --- /dev/null +++ b/ui/src/app/metadata-provider/component/preview-provider-dialog.component.ts @@ -0,0 +1,40 @@ +import { Component, Input, OnInit, OnDestroy } from '@angular/core'; +import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { Observable } from 'rxjs/Observable'; +import { MetadataProvider } from '../../metadata-provider/model/metadata-provider'; +import { EntityDescriptorService } from '../service/entity-descriptor.service'; +import * as FileSaver from 'file-saver'; +import { Subscription } from 'rxjs/Subscription'; +import XmlFormatter from 'xml-formatter'; + +@Component({ + selector: 'preview-provider-xml', + templateUrl: './preview-provider-dialog.component.html' +}) +export class PreviewProviderDialogComponent implements OnInit, OnDestroy { + @Input() provider: MetadataProvider; + + sub: Subscription; + xml: string; + + constructor( + public activeModal: NgbActiveModal, + private entityService: EntityDescriptorService + ) { } + + preview(xml): void { + const blob = new Blob([XmlFormatter(xml)], { type: 'text/xml;charset=utf-8' }); + FileSaver.saveAs(blob, `${ this.provider.serviceProviderName }.xml`); + } + + ngOnInit(): void { + let xml$ = this.entityService.preview(this.provider); + this.sub = xml$.subscribe(xml => this.xml = xml); + } + + ngOnDestroy(): void { + if (this.sub) { + this.sub.unsubscribe(); + } + } +} /* istanbul ignore next */ diff --git a/ui/src/app/metadata-provider/container/blank-provider.component.html b/ui/src/app/metadata-provider/container/blank-provider.component.html new file mode 100644 index 000000000..0d6ab77c2 --- /dev/null +++ b/ui/src/app/metadata-provider/container/blank-provider.component.html @@ -0,0 +1,55 @@ +
+ +
+
+ + + + + Service Provider Name is required + + +
+
+ + + + + Entity ID is required + + + + Entity ID must be unique + +
+ +
+
diff --git a/ui/src/app/metadata-provider/container/blank-provider.component.spec.ts b/ui/src/app/metadata-provider/container/blank-provider.component.spec.ts new file mode 100644 index 000000000..c2b64fe26 --- /dev/null +++ b/ui/src/app/metadata-provider/container/blank-provider.component.spec.ts @@ -0,0 +1,43 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { NewProviderComponent } from './new-provider.component'; +import * as fromProviders from '../reducer'; +import { BlankProviderComponent } from './blank-provider.component'; +import { UploadProviderComponent } from './upload-provider.component'; + +describe('Blank Provider Page', () => { + let fixture: ComponentFixture; + let store: Store; + let instance: BlankProviderComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + StoreModule.forRoot({ + user: combineReducers(fromProviders.reducers), + }), + ReactiveFormsModule, + ], + declarations: [ + NewProviderComponent, + BlankProviderComponent, + UploadProviderComponent + ], + }); + + fixture = TestBed.createComponent(BlankProviderComponent); + instance = fixture.componentInstance; + store = TestBed.get(Store); + + spyOn(store, 'dispatch').and.callThrough(); + }); + + it('should compile', () => { + fixture.detectChanges(); + + expect(fixture).toBeDefined(); + }); +}); diff --git a/ui/src/app/metadata-provider/container/blank-provider.component.ts b/ui/src/app/metadata-provider/container/blank-provider.component.ts new file mode 100644 index 000000000..95871aafd --- /dev/null +++ b/ui/src/app/metadata-provider/container/blank-provider.component.ts @@ -0,0 +1,54 @@ +import { + Component, + OnInit, + Output, + EventEmitter +} from '@angular/core'; +import { FormBuilder, FormGroup, FormControl, FormControlName, Validators, AbstractControl } from '@angular/forms'; +import 'rxjs/add/observable/merge'; +import 'rxjs/add/operator/debounceTime'; +import 'rxjs/add/observable/fromEvent'; +import 'rxjs/add/operator/distinctUntilChanged'; +import 'rxjs/add/operator/take'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; +import { Store } from '@ngrx/store'; + +import { MetadataProvider } from '../model/metadata-provider'; +import { EntityDescriptor } from '../model/entity-descriptor'; +import { AddDraftRequest } from '../action/draft.action'; +import { AddProviderRequest, UploadProviderRequest } from '../action/provider.action'; +import * as fromProviders from '../reducer'; +import { EntityValidators } from '../service/entity-validators.service'; + +@Component({ + selector: 'blank-provider-form', + templateUrl: './blank-provider.component.html' +}) +export class BlankProviderComponent implements OnInit { + @Output() save: EventEmitter = new EventEmitter(); + + providerForm: FormGroup; + ids$: Observable; + + constructor( + private store: Store, + private fb: FormBuilder + ) { + this.ids$ = this.store.select(fromProviders.getAllEntityIds); + } + + ngOnInit(): void { + this.providerForm = this.fb.group({ + serviceProviderName: ['', Validators.required], + entityId: ['', Validators.required, EntityValidators.createUniqueIdValidator(this.ids$)] + }); + } + + next(): void { + this.save.emit({ + entityId: this.providerForm.get('entityId').value, + serviceProviderName: this.providerForm.get('serviceProviderName').value + }); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/metadata-provider/container/new-provider.component.html b/ui/src/app/metadata-provider/container/new-provider.component.html new file mode 100644 index 000000000..040564cef --- /dev/null +++ b/ui/src/app/metadata-provider/container/new-provider.component.html @@ -0,0 +1,41 @@ +
+
+
+
+
+ Add a new metadata provider +
+
+
+
+

How are you adding the metadata information?

+
+
+
+ +
+
+  or  +
+
+ +
+
+
+
+
+ + +
+
+
+
+
diff --git a/ui/src/app/metadata-provider/container/new-provider.component.spec.ts b/ui/src/app/metadata-provider/container/new-provider.component.spec.ts new file mode 100644 index 000000000..fe4de8e56 --- /dev/null +++ b/ui/src/app/metadata-provider/container/new-provider.component.spec.ts @@ -0,0 +1,43 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { NewProviderComponent } from './new-provider.component'; +import * as fromProviders from '../reducer'; +import { BlankProviderComponent } from './blank-provider.component'; +import { UploadProviderComponent } from './upload-provider.component'; + +describe('New Provider Page', () => { + let fixture: ComponentFixture; + let store: Store; + let instance: NewProviderComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + StoreModule.forRoot({ + user: combineReducers(fromProviders.reducers), + }), + ReactiveFormsModule, + ], + declarations: [ + NewProviderComponent, + BlankProviderComponent, + UploadProviderComponent + ], + }); + + fixture = TestBed.createComponent(NewProviderComponent); + instance = fixture.componentInstance; + store = TestBed.get(Store); + + spyOn(store, 'dispatch').and.callThrough(); + }); + + it('should compile', () => { + fixture.detectChanges(); + + expect(fixture).toBeDefined(); + }); +}); diff --git a/ui/src/app/metadata-provider/container/new-provider.component.ts b/ui/src/app/metadata-provider/container/new-provider.component.ts new file mode 100644 index 000000000..c29d98d12 --- /dev/null +++ b/ui/src/app/metadata-provider/container/new-provider.component.ts @@ -0,0 +1,62 @@ +import { + Component, + OnChanges, + OnInit, + OnDestroy, + ElementRef, + ViewChildren +} from '@angular/core'; +import { FormBuilder, FormGroup, FormControl, FormControlName, Validators, AbstractControl } from '@angular/forms'; +import 'rxjs/add/observable/merge'; +import 'rxjs/add/operator/debounceTime'; +import 'rxjs/add/observable/fromEvent'; +import 'rxjs/add/operator/distinctUntilChanged'; +import 'rxjs/add/operator/take'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; +import { Store } from '@ngrx/store'; + +import { MetadataProvider } from '../model/metadata-provider'; +import { EntityDescriptor } from '../model/entity-descriptor'; +import { AddDraftRequest } from '../action/draft.action'; +import { AddProviderRequest, UploadProviderRequest, CreateProviderFromUrlRequest } from '../action/provider.action'; +import * as fromProviders from '../reducer'; +import { EntityValidators } from '../service/entity-validators.service'; + +@Component({ + selector: 'new-provider-page', + templateUrl: './new-provider.component.html' +}) +export class NewProviderComponent implements OnInit { + private ngUnsubscribe: Subject = new Subject(); + + readonly UPLOAD = Symbol('UPLOAD_FORM'); + readonly BLANK = Symbol('BLANK_FORM'); + + type: Symbol = this.BLANK; + + constructor( + private store: Store + ) { } + + ngOnInit(): void { + this.toggle(this.type); + } + + toggle(type: Symbol): void { + this.type = type; + } + + upload(uploadFile: { name: string, body: string }): void { + this.store.dispatch(new UploadProviderRequest(uploadFile)); + } + + createFromUrl(data: { name: string, url: string }): void { + this.store.dispatch(new CreateProviderFromUrlRequest(data)); + } + + next(provider: { entityId: string, serviceProviderName: string }): void { + const val: MetadataProvider = new EntityDescriptor(provider); + this.store.dispatch(new AddDraftRequest(val)); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/metadata-provider/container/upload-provider.component.html b/ui/src/app/metadata-provider/container/upload-provider.component.html new file mode 100644 index 000000000..bdea449a0 --- /dev/null +++ b/ui/src/app/metadata-provider/container/upload-provider.component.html @@ -0,0 +1,59 @@ +
+ +
+
+ + + + + Service Provider Name is required + + +
+
+ + +
+
+ — + OR + — +
+
+ + +
+ +
+
diff --git a/ui/src/app/metadata-provider/container/upload-provider.component.spec.ts b/ui/src/app/metadata-provider/container/upload-provider.component.spec.ts new file mode 100644 index 000000000..7a5f48eae --- /dev/null +++ b/ui/src/app/metadata-provider/container/upload-provider.component.spec.ts @@ -0,0 +1,134 @@ +import { Component, ViewChild } from '@angular/core'; +import { TestBed, ComponentFixture, async } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { UploadProviderComponent } from './upload-provider.component'; +import { FileService } from '../../core/service/file.service'; +import { FileServiceStub } from '../../../testing/file.service.stub'; +import { Observable } from 'rxjs/Observable'; + +@Component({ + template: `` +}) +class TestHostComponent { + @ViewChild(UploadProviderComponent) + public formUnderTest: UploadProviderComponent; + + upload(event: Event): void {} + createFromUrl(event: Event): void {} +} + +const getFakeFile = (str: string) => { + let blob = new Blob([str], { type: 'text/html' }); + blob['lastModifiedDate'] = ''; + blob['name'] = str; + return blob; +}; + +describe('Upload Provider Page', () => { + let fixture: ComponentFixture; + let instance: TestHostComponent; + let form: UploadProviderComponent; + let fileService: FileServiceStub; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { provide: FileService, useClass: FileServiceStub } + ], + imports: [ + ReactiveFormsModule, + ], + declarations: [ + UploadProviderComponent, + TestHostComponent + ], + }); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + form = instance.formUnderTest; + fileService = TestBed.get(FileService); + fixture.detectChanges(); + }); + + it('should compile', () => { + fixture.detectChanges(); + expect(form).toBeDefined(); + }); + + describe('save method', () => { + it('should call the upload method with the selected file when one is defined', () => { + fixture.detectChanges(); + form.file = getFakeFile('foo'); + spyOn(form, 'saveFromFile'); + form.save(); + expect(form.saveFromFile).toHaveBeenCalled(); + }); + it('should call the fromUrl method when there is no file selected', () => { + fixture.detectChanges(); + spyOn(form, 'saveFromFile'); + spyOn(form, 'saveFromUrl'); + form.save(); + expect(form.saveFromUrl).toHaveBeenCalled(); + expect(form.saveFromFile).not.toHaveBeenCalled(); + }); + }); + + describe('saveFromFile method', () => { + it('should retrieve the file text from a service and call the provided upload emitter', async((done) => { + fixture.detectChanges(); + spyOn(fileService, 'readAsText').and.callFake(() => Observable.of('foo')); + form.providerForm.setValue({ serviceProviderName: 'foo', file: '', url: '' }); + form.saveFromFile(getFakeFile('foo'), 'foo'); + form.upload.subscribe(v => { + expect(v).toEqual({ + name: 'foo', + body: 'foo' + }); + done(); + }); + })); + }); + + describe('saveFromUrl method', () => { + it('should retrieve the file text from a service and call the provided upload emitter', async((done) => { + fixture.detectChanges(); + form.saveFromUrl({ serviceProviderName: 'foo', url: 'foo.bar' }); + form.fromUrl.subscribe(v => { + expect(v).toEqual({ + name: 'foo', + url: 'foo.bar' + }); + done(); + }); + })); + }); + + describe('fileChange method', () => { + it('should set the reactive form value based on the provided event', async((done) => { + let evt = { + target: { + files: [{name: 'foo'}] + } + }; + form.fileChange(evt); + fixture.detectChanges(); + expect(form.file).toBeDefined(); + expect(form.providerForm.get('file').value).toBe('foo'); + })); + + it('should do nothing if no file is selected', async((done) => { + let evt = { + target: { + files: [] + } + }; + form.fileChange(evt); + fixture.detectChanges(); + expect(form.file).not.toBeDefined(); + expect(form.providerForm.get('file').value).toBe(''); + })); + }); +}); diff --git a/ui/src/app/metadata-provider/container/upload-provider.component.ts b/ui/src/app/metadata-provider/container/upload-provider.component.ts new file mode 100644 index 000000000..d3d6ce7d0 --- /dev/null +++ b/ui/src/app/metadata-provider/container/upload-provider.component.ts @@ -0,0 +1,99 @@ +import { Component, OnInit, OnDestroy, Output, EventEmitter } from '@angular/core'; +import { FormBuilder, FormGroup, FormControl, FormControlName, Validators, AbstractControl } from '@angular/forms'; +import 'rxjs/add/observable/merge'; +import 'rxjs/add/operator/debounceTime'; +import 'rxjs/add/observable/fromEvent'; +import 'rxjs/add/operator/distinctUntilChanged'; +import 'rxjs/add/operator/take'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; +import { MetadataProvider } from '../model/metadata-provider'; +import { EntityDescriptor } from '../model/entity-descriptor'; +import { EntityValidators } from '../service/entity-validators.service'; +import { FileService } from '../../core/service/file.service'; + +@Component({ + selector: 'upload-provider-form', + templateUrl: './upload-provider.component.html' +}) +export class UploadProviderComponent implements OnInit, OnDestroy { + private ngUnsubscribe: Subject = new Subject(); + + @Output() upload: EventEmitter = new EventEmitter(); + @Output() fromUrl: EventEmitter = new EventEmitter(); + + providerForm: FormGroup; + formValue: any; + + file: File; + + constructor( + private fb: FormBuilder, + private fileService: FileService + ) {} + + ngOnInit(): void { + this.providerForm = this.fb.group({ + serviceProviderName: ['', Validators.required], + file: [''], + url: [''] + }); + + this.providerForm.valueChanges.subscribe(changes => { + this.formValue = changes; + }); + + this.providerForm + .get('file') + .valueChanges + .takeUntil(this.ngUnsubscribe) + .debounceTime(100) + .subscribe(changes => { + let url = this.providerForm.get('url'); + url[changes ? 'disable' : 'enable'](); + if (!!url.value) { + url.setValue(null); + } + }); + } + + save(): void { + let file = this.file; + if (file) { + this.saveFromFile(file, this.providerForm.get('serviceProviderName').value); + } else { + this.saveFromUrl(this.providerForm.value); + } + } + + saveFromFile(file: File, name: string): void { + this.fileService.readAsText(file).subscribe(txt => { + this.upload.emit({ + name: name, + body: txt + }); + }); + } + + saveFromUrl(values: {serviceProviderName: string, url: string}): void { + this.fromUrl.emit({ + name: values.serviceProviderName, + url: values.url + }); + } + + fileChange($event): void { + let fileList = $event.target.files, + file = fileList[0]; + this.file = file; + if (file) { + this.providerForm.get('file').setValue(file.name); + this.providerForm.updateValueAndValidity(); + } + } + + ngOnDestroy(): void { + this.ngUnsubscribe.next(); + this.ngUnsubscribe.complete(); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/metadata-provider/directive/info-label.directive.spec.ts b/ui/src/app/metadata-provider/directive/info-label.directive.spec.ts new file mode 100644 index 000000000..32b3e3cb8 --- /dev/null +++ b/ui/src/app/metadata-provider/directive/info-label.directive.spec.ts @@ -0,0 +1,60 @@ +import { Component, DebugElement, ElementRef } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { NgbPopover, NgbPopoverModule, NgbPopoverConfig } from '@ng-bootstrap/ng-bootstrap'; + +import { InfoLabelDirective } from './info-label.directive'; +import * as utility from '../../../testing/utility'; + + +@Component({ + template: `` +}) +class TestComponent { + tooltipName = 'Foobar!'; +} + +describe('Info Label Directive', () => { + let component: TestComponent; + let fixture: ComponentFixture; + let element: DebugElement; + let config: NgbPopover; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [NgbPopover, NgbPopoverConfig], + imports: [ + NgbPopoverModule + ], + declarations: [ + InfoLabelDirective, + TestComponent + ], + }); + + fixture = TestBed.createComponent(TestComponent); + element = fixture.debugElement.query(By.css('i.info-icon')); + config = element.injector.get(NgbPopover); + fixture.detectChanges(); + }); + + describe('functions', () => { + it('should compile', () => { + expect(element).toBeDefined(); + }); + it('should call the config open method when spacebar is pressed', () => { + spyOn(config, 'open'); + utility.dispatchKeyboardEvent(element.nativeElement, 'keydown', 'space'); + fixture.detectChanges(); + expect(config.open).toHaveBeenCalled(); + }); + it('should call the config close method when spacebar is released', () => { + spyOn(config, 'close'); + utility.dispatchKeyboardEvent(element.nativeElement, 'keyup', 'space'); + fixture.detectChanges(); + expect(config.close).toHaveBeenCalled(); + }); + }); +}); diff --git a/ui/src/app/metadata-provider/directive/info-label.directive.ts b/ui/src/app/metadata-provider/directive/info-label.directive.ts new file mode 100644 index 000000000..30fca48f4 --- /dev/null +++ b/ui/src/app/metadata-provider/directive/info-label.directive.ts @@ -0,0 +1,25 @@ +import { Directive, ElementRef, Input, HostListener } from '@angular/core'; +import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'; + +/* tslint:disable:directive-selector */ +@Directive({ + selector: '.info-icon', + providers: [NgbPopover] +}) +export class InfoLabelDirective { + @HostListener('keydown.space') onSpaceDown() { + this.config.open(); + } + @HostListener('keyup.space') onSpaceUp() { + this.config.close(); + } + constructor( + private config: NgbPopover, + private element: ElementRef + ) { + config.triggers = 'mouseenter:mouseleave'; + config.placement = ['top']; + config.container = 'body'; + element.nativeElement.setAttribute('tabindex', 0); + } +} diff --git a/ui/src/app/metadata-provider/directive/input-defaults.directive.spec.ts b/ui/src/app/metadata-provider/directive/input-defaults.directive.spec.ts new file mode 100644 index 000000000..172c41a3f --- /dev/null +++ b/ui/src/app/metadata-provider/directive/input-defaults.directive.spec.ts @@ -0,0 +1,60 @@ +import { Component, DebugElement } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ReactiveFormsModule, FormsModule, NgControl } from '@angular/forms'; +import { Router } from '@angular/router'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterStub } from '../../../testing/router.stub'; +import { NgbModalStub } from '../../../testing/modal.stub'; +import { InputDefaultsDirective } from './input-defaults.directive'; + +import * as constants from '../../shared/constant'; + +@Component({ + template: `` +}) +class TestComponent { + isDisabled = false; + set disableDefaults(isDisabled: boolean) { + this.isDisabled = isDisabled; + } + get disableDefaults(): boolean { + return this.isDisabled; + } +} + +describe('Input Defaults Directive', () => { + let component: TestComponent; + let fixture: ComponentFixture; + let inputEl: DebugElement; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [], + imports: [ + FormsModule, + ReactiveFormsModule + ], + declarations: [ + InputDefaultsDirective, + TestComponent + ], + }); + + fixture = TestBed.createComponent(TestComponent); + inputEl = fixture.debugElement.query(By.css('input')); + }); + + describe('attributes', () => { + it('should add a maxlength attribute based on the constants file', () => { + fixture.detectChanges(); + expect(parseInt(inputEl.attributes.maxlength, 10)).toEqual(constants.DEFAULT_FIELD_MAX_LENGTH); + }); + + it('should be null if disableDefaults is set', () => { + fixture.componentInstance.disableDefaults = true; + fixture.detectChanges(); + expect(inputEl.attributes.maxlength).toBeNull(); + }); + }); +}); diff --git a/ui/src/app/metadata-provider/directive/input-defaults.directive.ts b/ui/src/app/metadata-provider/directive/input-defaults.directive.ts new file mode 100644 index 000000000..409d02ea8 --- /dev/null +++ b/ui/src/app/metadata-provider/directive/input-defaults.directive.ts @@ -0,0 +1,16 @@ +import { Directive, Self, HostBinding, Input } from '@angular/core'; +import * as constants from '../../shared/constant'; + +@Directive({ + selector: 'input[type="text"].form-control,textarea.form-control' +}) +export class InputDefaultsDirective { + public constructor() { } + + @Input() disableDefaults = false; + + @HostBinding('attr.maxlength') + get maxlength() { + return this.disableDefaults ? null : constants.DEFAULT_FIELD_MAX_LENGTH; + } +} diff --git a/ui/src/app/metadata-provider/effect/draft.effects.ts b/ui/src/app/metadata-provider/effect/draft.effects.ts new file mode 100644 index 000000000..993e35ad0 --- /dev/null +++ b/ui/src/app/metadata-provider/effect/draft.effects.ts @@ -0,0 +1,92 @@ +import { Injectable } from '@angular/core'; +import { Effect, Actions } from '@ngrx/effects'; +import { Action } from '@ngrx/store'; +import { Observable } from 'rxjs/Observable'; +import { Router } from '@angular/router'; + +import * as draftActions from '../action/draft.action'; +import { MetadataProvider } from '../model/metadata-provider'; +import { EntityDraftService } from '../service/entity-draft.service'; + +@Injectable() +export class DraftEffects { + + @Effect() + loadDrafts$ = this.actions$ + .ofType(draftActions.LOAD_DRAFT_REQUEST) + .switchMap(() => + this.draftService + .query() + .map(descriptors => new draftActions.LoadDraftSuccess(descriptors)) + .catch(error => Observable.of(new draftActions.LoadDraftError(error))) + ); + + @Effect() + addDraft$ = this.actions$ + .ofType(draftActions.ADD_DRAFT) + .map(action => action.payload) + .switchMap(provider => { + return this.draftService + .save(provider) + .map(p => new draftActions.AddDraftSuccess(provider)); + }); + + @Effect() + addDraftSuccessReload$ = this.actions$ + .ofType(draftActions.ADD_DRAFT_SUCCESS) + .map(action => action.payload) + .switchMap(provider => + this.draftService + .find(provider.entityId) + .map(p => new draftActions.LoadDraftRequest()) + ); + + @Effect({ dispatch: false }) + addDraftSuccessRedirect$ = this.actions$ + .ofType(draftActions.ADD_DRAFT_SUCCESS) + .map(action => action.payload) + .do(provider => this.router.navigate(['provider', provider.entityId, 'wizard'])); + + @Effect() + updateDraft$ = this.actions$ + .ofType(draftActions.UPDATE_DRAFT_REQUEST) + .map(action => action.payload) + .switchMap(provider => { + return this.draftService + .update(provider) + .map(p => new draftActions.UpdateDraftSuccess(p)); + }); + + @Effect() + selectDraft$ = this.actions$ + .ofType(draftActions.SELECT) + .map(action => action.payload) + .switchMap(id => + this.draftService + .find(id) + .map(p => new draftActions.FindDraft(p.entityId)) + ); + + @Effect() + removeDraft$ = this.actions$ + .ofType(draftActions.REMOVE_DRAFT) + .map(action => action.payload) + .switchMap(provider => + this.draftService + .remove(provider) + .map(p => new draftActions.RemoveDraftSuccess(p)) + ); + @Effect() + removeDraftSuccessReload$ = this.actions$ + .ofType(draftActions.REMOVE_DRAFT) + .map(action => action.payload) + .map(provider => + new draftActions.LoadDraftRequest() + ); + + constructor( + private draftService: EntityDraftService, + private actions$: Actions, + private router: Router + ) { } +} /* istanbul ignore next */ diff --git a/ui/src/app/metadata-provider/effect/provider.effects.ts b/ui/src/app/metadata-provider/effect/provider.effects.ts new file mode 100644 index 000000000..e559e7517 --- /dev/null +++ b/ui/src/app/metadata-provider/effect/provider.effects.ts @@ -0,0 +1,122 @@ +import { Injectable } from '@angular/core'; +import { Effect, Actions } from '@ngrx/effects'; +import { Action } from '@ngrx/store'; +import { Observable } from 'rxjs/Observable'; +import { Router } from '@angular/router'; + +import * as providerActions from '../action/provider.action'; +import * as draftActions from '../action/draft.action'; +import { MetadataProvider } from '../model/metadata-provider'; +import { EntityDescriptorService } from '../service/entity-descriptor.service'; + +@Injectable() +export class ProviderEffects { + + @Effect() + loadProviders$ = this.actions$ + .ofType(providerActions.LOAD_PROVIDER_REQUEST) + .switchMap(() => + this.descriptorService + .query() + .map(descriptors => new providerActions.LoadProviderSuccess(descriptors)) + .catch(error => Observable.of(new providerActions.LoadProviderError(error))) + ); + + @Effect() + updateProvider$ = this.actions$ + .ofType(providerActions.UPDATE_PROVIDER_REQUEST) + .map(action => action.payload) + .switchMap(provider => { + delete provider.modifiedDate; + delete provider.createdDate; + return this.descriptorService + .update(provider) + .map(p => new providerActions.UpdateProviderSuccess(p)) + .catch(err => Observable.of(new providerActions.UpdateProviderFail(err))); + }); + @Effect({ dispatch: false }) + updateProviderSuccessRedirect$ = this.actions$ + .ofType(providerActions.UPDATE_PROVIDER_SUCCESS) + .map(action => action.payload) + .do(provider => { + this.router.navigate(['/dashboard']); + }); + @Effect() + updateProviderSuccessReload$ = this.actions$ + .ofType(providerActions.UPDATE_PROVIDER_SUCCESS) + .map(action => action.payload) + .map(provider => new providerActions.LoadProviderRequest()); + + @Effect() + selectProvider$ = this.actions$ + .ofType(providerActions.SELECT) + .map(action => action.payload) + .switchMap(id => + this.descriptorService + .find(id) + .map(p => new providerActions.SelectProviderSuccess(p)) + ); + + @Effect() + addProviderRequest$ = this.actions$ + .ofType(providerActions.ADD_PROVIDER) + .map(action => action.payload) + .map(provider => { + return { + ...provider, + relyingPartyOverrides: this.descriptorService.removeNulls(provider.relyingPartyOverrides) + }; + }) + .switchMap(provider => + this.descriptorService + .save(provider) + .map(p => new providerActions.AddProviderSuccess(p)) + .catch(() => Observable.of(new providerActions.AddProviderFail(provider))) + ); + + @Effect({ dispatch: false }) + addProviderSuccessRedirect$ = this.actions$ + .ofType(providerActions.ADD_PROVIDER_SUCCESS) + .map(action => action.payload) + .do(provider => { + this.router.navigate(['/dashboard']); + }); + @Effect() + addProviderSuccessReload$ = this.actions$ + .ofType(providerActions.ADD_PROVIDER_SUCCESS) + .map(action => action.payload) + .map(provider => new providerActions.LoadProviderRequest()); + + @Effect() + addProviderSuccessRemoveDraft$ = this.actions$ + .ofType(providerActions.ADD_PROVIDER_SUCCESS) + .map(action => action.payload) + .map(provider => new draftActions.RemoveDraftRequest(provider)); + + @Effect() + uploadProviderRequest$ = this.actions$ + .ofType(providerActions.UPLOAD_PROVIDER_REQUEST) + .map(action => action.payload) + .switchMap(file => + this.descriptorService + .upload(file.name, file.body) + .map(p => new providerActions.AddProviderSuccess(p)) + .catch(() => Observable.of(new providerActions.AddProviderFail(file))) + ); + + @Effect() + createProviderFromUrlRequest$ = this.actions$ + .ofType(providerActions.CREATE_PROVIDER_FROM_URL_REQUEST) + .map(action => action.payload) + .switchMap(file => + this.descriptorService + .createFromUrl(file.name, file.url) + .map(p => new providerActions.AddProviderSuccess(p)) + .catch(() => Observable.of(new providerActions.AddProviderFail(file))) + ); + constructor( + private descriptorService: EntityDescriptorService, + private actions$: Actions, + private router: Router + ) { } +} /* istanbul ignore next */ diff --git a/ui/src/app/metadata-provider/metadata-provider.module.ts b/ui/src/app/metadata-provider/metadata-provider.module.ts new file mode 100644 index 000000000..f4672e308 --- /dev/null +++ b/ui/src/app/metadata-provider/metadata-provider.module.ts @@ -0,0 +1,74 @@ +import { NgModule, ModuleWithProviders } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { CommonModule } from '@angular/common'; +import { HttpClientModule } from '@angular/common/http'; +import { ReactiveFormsModule, FormsModule } from '@angular/forms'; +import { StoreModule } from '@ngrx/store'; +import { EffectsModule } from '@ngrx/effects'; + +import { EntityDescriptorService } from './service/entity-descriptor.service'; +import { DraftEffects } from './effect/draft.effects'; +import { ProviderEffects } from './effect/provider.effects'; +import { reducers } from './reducer'; + +import { NewProviderComponent } from './container/new-provider.component'; + +import { ListValuesService } from './service/list-values.service'; +import { ProviderStatusEmitter, ProviderValueEmitter } from './service/provider-change-emitter.service'; +import { ProviderEditorFormModule } from './component'; +import { PreviewProviderDialogComponent } from './component/preview-provider-dialog.component'; +import { EntityDraftService } from './service/entity-draft.service'; +import { PretttyXml } from './pipe/pretty-xml.pipe'; +import { UploadProviderComponent } from './container/upload-provider.component'; +import { BlankProviderComponent } from './container/blank-provider.component'; + +@NgModule({ + declarations: [ + NewProviderComponent, + UploadProviderComponent, + BlankProviderComponent, + PreviewProviderDialogComponent, + PretttyXml, + ], + entryComponents: [ + PreviewProviderDialogComponent + ], + imports: [ + HttpClientModule, + CommonModule, + RouterModule, + ReactiveFormsModule, + FormsModule, + ProviderEditorFormModule + ], + exports: [ + ProviderEditorFormModule + ], + providers: [] +}) +export class MetadataProviderModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: RootProviderModule, + providers: [ + EntityDescriptorService, + EntityDraftService, + ProviderStatusEmitter, + ListValuesService, + ProviderValueEmitter + ] + }; + } +} + +@NgModule({ + imports: [ + MetadataProviderModule, + StoreModule.forFeature('providers', reducers), + EffectsModule.forFeature([DraftEffects, ProviderEffects]), + RouterModule.forChild([ + { path: 'new', component: NewProviderComponent } + ]), + ], +}) +export class RootProviderModule { } diff --git a/ui/src/app/metadata-provider/model/entity-descriptor.spec.ts b/ui/src/app/metadata-provider/model/entity-descriptor.spec.ts new file mode 100644 index 000000000..445a72f4a --- /dev/null +++ b/ui/src/app/metadata-provider/model/entity-descriptor.spec.ts @@ -0,0 +1,93 @@ +import { EntityDescriptor } from './entity-descriptor'; +import { MetadataProvider } from './metadata-provider'; +describe('Entity Desctiptor construct', () => { + + const config: MetadataProvider = { + id: 'foo', + entityId: 'string', + serviceProviderName: 'string', + organization: { + 'name': 'string', + 'displayName': 'string', + 'url': 'string' + }, + contacts: [ + { + 'name': 'string', + 'type': 'string', + 'emailAddress': 'string' + } + ], + mdui: { + 'displayName': 'string', + 'informationUrl': 'string', + 'privacyStatementUrl': 'string', + 'logoUrl': 'string', + 'logoHeight': 100, + 'logoWidth': 100, + 'description': 'string' + }, + securityInfo: { + 'x509CertificateAvailable': true, + 'authenticationRequestsSigned': true, + 'wantAssertionsSigned': true, + 'x509Certificates': [ + { + 'name': 'string', + 'type': 'string', + 'value': 'string' + } + ] + }, + assertionConsumerServices: [ + { + 'binding': 'string', + 'locationUrl': 'string', + 'makeDefault': true + } + ], + serviceProviderSsoDescriptor: { + 'protocolSupportEnum': 'string', + 'nameIdFormats': [ + 'string' + ] + }, + + logoutEndpoints: [ + { + 'url': 'string', + 'bindingType': 'string' + } + ], + serviceEnabled: true, + createdDate: 'string (date)', + modifiedDate: 'string (date)', + relyingPartyOverrides: { + 'signAssertion': true, + 'dontSignResponse': true, + 'turnOffEncryption': true, + 'useSha': true, + 'ignoreAuthenticationMethod': true, + 'omitNotBefore': true, + 'responderId': 'string', + 'nameIdFormats': [ + 'string' + ], + 'authenticationMethods': [ + 'string' + ] + }, + attributeRelease: [ + 'eduPersonPrincipalName', + 'uid', + 'mail' + ] + }; + const entity = new EntityDescriptor(config); + + it('should populate its own values', () => { + Object.keys(config).forEach(key => { + expect(entity[key]).toEqual(config[key]); + }); + }); +}); diff --git a/ui/src/app/metadata-provider/model/entity-descriptor.ts b/ui/src/app/metadata-provider/model/entity-descriptor.ts new file mode 100644 index 000000000..74d3193a4 --- /dev/null +++ b/ui/src/app/metadata-provider/model/entity-descriptor.ts @@ -0,0 +1,50 @@ +import { + MetadataProvider, + Organization, + Contact, + MDUI, + LogoutEndpoint, + SecurityInfo, + Certificate, + SsoService, + IdpSsoDescriptor +} from './metadata-provider'; + +export class EntityDescriptor implements MetadataProvider { + id = ''; + entityId = ''; + serviceProviderName = ''; + organization = {} as Organization; + contacts = [] as Contact[]; + mdui = {} as MDUI; + + securityInfo = { + x509CertificateAvailable: false, + authenticationRequestsSigned: false, + wantAssertionsSigned: false, + x509Certificates: [] as Certificate[] + } as SecurityInfo; + + assertionConsumerServices = [] as SsoService[]; + serviceProviderSsoDescriptor = { + nameIdFormats: [] + } as IdpSsoDescriptor; + + logoutEndpoints = [] as LogoutEndpoint[]; + + serviceEnabled = false; + + createdDate?: string; + modifiedDate?: string; + + relyingPartyOverrides = { + nameIdFormats: [] as string[], + authenticationMethods: [] as string[] + }; + + attributeRelease = [] as string[]; + + constructor(descriptor?: Partial) { + Object.assign(this, descriptor); + } +} diff --git a/ui/src/app/metadata-provider/model/metadata-provider.ts b/ui/src/app/metadata-provider/model/metadata-provider.ts new file mode 100644 index 000000000..3df3c8cf1 --- /dev/null +++ b/ui/src/app/metadata-provider/model/metadata-provider.ts @@ -0,0 +1,83 @@ +export interface MetadataProvider { + id?: string; + entityId: string; + serviceProviderName: string; + organization?: Organization; + contacts?: Contact[]; + mdui?: MDUI; + securityInfo?: SecurityInfo; + assertionConsumerServices?: SsoService[]; + serviceProviderSsoDescriptor?: IdpSsoDescriptor; + + logoutEndpoints: LogoutEndpoint[]; + + serviceEnabled?: boolean; + + createdDate?: string; + modifiedDate?: string; + + relyingPartyOverrides: { + signAssertion?: boolean; + dontSignResponse?: boolean; + turnOffEncryption?: boolean; + useSha?: boolean; + ignoreAuthenticationMethod?: boolean; + omitNotBefore?: boolean; + responderId?: string; + nameIdFormats: string[]; + authenticationMethods: string[]; + }; + + attributeRelease: string[]; +} + +export interface Organization { + name?: string; + displayName?: string; + url?: string; +} + +export interface Contact { + type: string; + name: string; + emailAddress: string; +} + +export interface MDUI { + displayName?: string; + informationUrl?: string; + privacyStatementUrl?: string; + logoUrl?: string; + logoHeight?: number; + logoWidth?: number; + description?: string; +} + +export interface LogoutEndpoint { + url: string; + bindingType: string; +} + +export interface SecurityInfo { + x509CertificateAvailable?: boolean; + authenticationRequestsSigned?: boolean; + wantAssertionsSigned?: boolean; + x509Certificates: Certificate[]; +} + +export interface Certificate { + name: string; + type: string; + value: string; +} + +export interface SsoService { + binding: string; + locationUrl: string; + makeDefault: boolean; +} + +export interface IdpSsoDescriptor { + protocolSupportEnum: string; + nameIdFormats: string[]; +} diff --git a/ui/src/app/metadata-provider/pipe/pretty-xml.pipe.ts b/ui/src/app/metadata-provider/pipe/pretty-xml.pipe.ts new file mode 100644 index 000000000..b74c605de --- /dev/null +++ b/ui/src/app/metadata-provider/pipe/pretty-xml.pipe.ts @@ -0,0 +1,12 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import XmlFormatter from 'xml-formatter'; + +@Pipe({ name: 'prettyXml' }) +export class PretttyXml implements PipeTransform { + transform(value: string): string { + if (!value) { + return value; + } + return XmlFormatter(value); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/metadata-provider/reducer/draft.reducer.spec.ts b/ui/src/app/metadata-provider/reducer/draft.reducer.spec.ts new file mode 100644 index 000000000..6cd8f807a --- /dev/null +++ b/ui/src/app/metadata-provider/reducer/draft.reducer.spec.ts @@ -0,0 +1,121 @@ +import { reducer } from './draft.reducer'; +import * as fromDrafts from './draft.reducer'; +import * as draftActions from '../action/draft.action'; +import { MetadataProvider } from '../model/metadata-provider'; + +let drafts: MetadataProvider[] = [ + { entityId: 'foo', serviceProviderName: 'bar' } as MetadataProvider, + { entityId: 'baz', serviceProviderName: 'fin' } as MetadataProvider + ], + snapshot: fromDrafts.DraftState = { + ids: [drafts[0].entityId, drafts[1].entityId], + drafts: { + [drafts[0].entityId]: drafts[0], + [drafts[1].entityId]: drafts[1] + }, + selectedDraftId: null + }; + +describe('Draft Reducer', () => { + const initialState: fromDrafts.DraftState = { + ids: [], + drafts: {}, + selectedDraftId: null + }; + + describe('undefined action', () => { + it('should return the default state', () => { + const result = reducer(undefined, {} as any); + + expect(result).toEqual(initialState); + }); + }); + + describe('Load Drafts: Success', () => { + it('should add the loaded drafts to the collection', () => { + const action = new draftActions.LoadDraftSuccess(drafts); + const result = reducer(initialState, action); + + expect(result).toEqual( + Object.assign({}, initialState, snapshot) + ); + }); + }); + + describe('Update Drafts: Success', () => { + it('should update the draft of the specified entityId', () => { + let changes = { ...drafts[1], serviceEnabled: true }, + expected = { + ids: [drafts[0].entityId, drafts[1].entityId], + drafts: { + [drafts[0].entityId]: drafts[0], + [drafts[1].entityId]: changes + }, + selectedDraftId: null + }; + const action = new draftActions.UpdateDraftSuccess(changes); + const result = reducer({...snapshot}, action); + + expect(result).toEqual( + Object.assign({}, initialState, expected) + ); + }); + + it('should return state if the entityId is not found', () => { + let changes = { ...drafts[1], serviceEnabled: true, entityId: 'bar' }; + const action = new draftActions.UpdateDraftSuccess(changes); + const result = reducer({ ...snapshot }, action); + + expect(result).toEqual(snapshot); + }); + }); + + describe('Select Draft', () => { + it('should update the selected draft id', () => { + let id = 'foo', + expected = { ...snapshot, selectedDraftId: id }; + const action = new draftActions.SelectDraft(id); + const result = reducer({ ...snapshot }, action); + + expect(result).toEqual( + Object.assign({}, initialState, expected) + ); + }); + }); + describe('Selectors', () => { + it('getEntities should return all drafts', () => { + expect(fromDrafts.getEntities({ + ids: [], + drafts: {}, + selectedDraftId: null, + })).toEqual({}); + expect(fromDrafts.getEntities(snapshot)).toEqual(snapshot.drafts); + }); + + it('getIds should return all Ids', () => { + expect(fromDrafts.getIds({ + ids: [], + drafts: {}, + selectedDraftId: null, + })).toEqual([]); + expect(fromDrafts.getIds(snapshot)).toEqual(snapshot.ids); + }); + + it('getSelectedDraftId should return the selected entityId', () => { + expect(fromDrafts.getSelectedId({ + ids: [], + drafts: {}, + selectedDraftId: null, + })).toBeNull(); + expect(fromDrafts.getSelectedId(Object.assign({}, snapshot, {selectedDraftId: 'foo'}))).toEqual('foo'); + }); + + it('getSelected should return the selected entity by id', () => { + expect(fromDrafts.getSelected(Object.assign({}, snapshot, { selectedDraftId: 'foo' }))).toEqual(drafts[0]); + }); + + it('getAll return all entities as an array', () => { + expect(fromDrafts.getAll(snapshot)).toEqual(drafts); + }); + }); +}); diff --git a/ui/src/app/metadata-provider/reducer/draft.reducer.ts b/ui/src/app/metadata-provider/reducer/draft.reducer.ts new file mode 100644 index 000000000..9dc61835a --- /dev/null +++ b/ui/src/app/metadata-provider/reducer/draft.reducer.ts @@ -0,0 +1,87 @@ +import { createSelector } from '@ngrx/store'; +import { MetadataProvider } from '../model/metadata-provider'; +import * as providerAction from '../action/provider.action'; +import * as draftAction from '../action/draft.action'; + +export interface DraftState { + ids: string[]; + drafts: { [id: string]: MetadataProvider }; + selectedDraftId: string | null; +} + +export const initialState: DraftState = { + ids: [], + drafts: {}, + selectedDraftId: null, +}; + +export function reducer(state = initialState, action: draftAction.Actions): DraftState { + switch (action.type) { + case draftAction.LOAD_DRAFT_SUCCESS: { + const providers = action.payload; + + const providerIds = providers.map(provider => provider.entityId); + const entities = providers.reduce( + (e: { [id: string]: MetadataProvider }, provider: MetadataProvider) => { + return Object.assign(e, { + [provider.entityId]: provider, + }); + }, + {} + ); + + return { + ids: [...providerIds], + drafts: Object.assign(entities), + selectedDraftId: state.selectedDraftId, + }; + } + + case draftAction.UPDATE_DRAFT_SUCCESS: { + const draft = action.payload; + + if (state.ids.indexOf(draft.entityId) < 0) { + return state; + } + + const original = state.drafts[draft.entityId], + updated = Object.assign({}, + { ...original }, + { ...draft } + ); + return { + ids: state.ids, + drafts: Object.assign({ ...state.drafts }, { + [draft.entityId]: updated, + }), + selectedDraftId: state.selectedDraftId, + }; + } + + case draftAction.SELECT: { + return { + ids: state.ids, + drafts: state.drafts, + selectedDraftId: action.payload, + }; + } + + default: { + return state; + } + } +} + +export const getEntities = (state: DraftState) => state.drafts; +export const getIds = (state: DraftState) => state.ids; +export const getSelectedId = (state: DraftState) => state.selectedDraftId; +export const getSelected = createSelector( + getEntities, + getSelectedId, + (entities, selectedId) => { + return entities[selectedId]; + } +); +export const getAll = createSelector(getEntities, getIds, (entities, ids) => { + return ids.map(id => entities[id]); +}); diff --git a/ui/src/app/metadata-provider/reducer/index.spec.ts b/ui/src/app/metadata-provider/reducer/index.spec.ts new file mode 100644 index 000000000..01494cdd5 --- /dev/null +++ b/ui/src/app/metadata-provider/reducer/index.spec.ts @@ -0,0 +1,39 @@ +import * as selectors from './index'; + +describe('Metadata Provider reducer functions', () => { + describe(`combineAllFn`, () => { + it('should combine arrays', () => { + let a = ['foo', 'bar'], + b = ['baz', 'hrm']; + expect(selectors.combineAllFn(a, b)).toEqual([...b, ...a]); + }); + }); + describe(`doesExistFn`, () => { + it('should test whether the provided index is in the provided collection', () => { + expect(selectors.doesExistFn(['foo', 'bar'], 'foo')).toBe(true); + expect(selectors.doesExistFn(['bar', 'baz'], 'foo')).toBe(false); + }); + }); + describe(`getInCollectionFn`, () => { + it('should retrieve the item from the provided collection by index', () => { + expect(selectors.getInCollectionFn({ foo: 'bar' }, 'foo')).toEqual('bar'); + }); + }); + describe(`getEntityIdsFn`, () => { + it('should create a collection of entity ids from the provided entities', () => { + expect(selectors.getEntityIdsFn([{entityId: 'foo'}])).toEqual(['foo']); + }); + }); + describe(`getProvidersFromStateFn`, () => { + it('should select the providers slice of state', () => { + let providers = []; + expect(selectors.getProvidersFromStateFn({ providers })).toBe(providers); + }); + }); + describe(`getDraftsFromStateFn`, () => { + it('should select the providers slice of state', () => { + let drafts = []; + expect(selectors.getDraftsFromStateFn({ drafts })).toBe(drafts); + }); + }); +}); diff --git a/ui/src/app/metadata-provider/reducer/index.ts b/ui/src/app/metadata-provider/reducer/index.ts new file mode 100644 index 000000000..b36639f60 --- /dev/null +++ b/ui/src/app/metadata-provider/reducer/index.ts @@ -0,0 +1,47 @@ +import { createSelector, createFeatureSelector } from '@ngrx/store'; +import * as fromProvider from './provider.reducer'; +import * as fromDraft from './draft.reducer'; +import * as fromRoot from '../../core/reducer'; +import { DraftState } from './draft.reducer'; + +export interface ProviderState { + providers: fromProvider.ProviderState; + drafts: fromDraft.DraftState; +} + +export interface State extends fromRoot.State { + providers: ProviderState; +} + +export const reducers = { + providers: fromProvider.reducer, + drafts: fromDraft.reducer +}; + +export const combineAllFn = (d, p) => [...p, ...d]; +export const doesExistFn = (ids, selected) => ids.indexOf(selected) > -1; +export const getInCollectionFn = (entities, selectedId) => selectedId && entities[selectedId]; +export const getEntityIdsFn = list => list.map(entity => entity.entityId); +export const getProvidersFromStateFn = state => state.providers; +export const getDraftsFromStateFn = state => state.drafts; + +export const getProviderState = createFeatureSelector('providers'); +export const getProviderEntityState = createSelector(getProviderState, getProvidersFromStateFn); +export const getProviderEntities = createSelector(getProviderEntityState, fromProvider.getEntities); +export const getSelectedProviderId = createSelector(getProviderEntityState, fromProvider.getSelectedId); +export const getSelectedProvider = createSelector(getProviderEntities, getSelectedProviderId, getInCollectionFn); +export const getProviderIds = createSelector(getProviderEntityState, fromProvider.getIds); +export const getProviderCollection = createSelector(getProviderEntityState, getProviderIds, fromProvider.getAll); +export const getDraftState = createFeatureSelector('providers'); +export const getDraftEntityState = createSelector(getDraftState, getDraftsFromStateFn); +export const getDraftEntities = createSelector(getDraftEntityState, fromDraft.getEntities); +export const getSelectedDraftId = createSelector(getDraftEntityState, fromDraft.getSelectedId); +export const getSelectedDraft = createSelector(getDraftEntities, getSelectedDraftId, getInCollectionFn); +export const getDraftIds = createSelector(getDraftEntityState, fromDraft.getIds); +export const getDraftCollection = createSelector(getDraftEntityState, getDraftIds, fromDraft.getAll); +export const isSelectedProviderInCollection = createSelector(getProviderIds, getSelectedProviderId, doesExistFn); +export const isSelectedDraftInCollection = createSelector(getDraftIds, getSelectedDraftId, doesExistFn); +export const getAllProviders = createSelector(getDraftCollection, getProviderCollection, combineAllFn); +export const getAllProviderIds = createSelector(getDraftIds, getProviderIds, combineAllFn); + +export const getAllEntityIds = createSelector(getAllProviders, getEntityIdsFn); diff --git a/ui/src/app/metadata-provider/reducer/provider.reducer.spec.ts b/ui/src/app/metadata-provider/reducer/provider.reducer.spec.ts new file mode 100644 index 000000000..c0330582a --- /dev/null +++ b/ui/src/app/metadata-provider/reducer/provider.reducer.spec.ts @@ -0,0 +1,109 @@ +import { reducer } from './provider.reducer'; +import * as fromProvider from './provider.reducer'; +import * as providerActions from '../action/provider.action'; +import { MetadataProvider } from '../model/metadata-provider'; + +let providers: MetadataProvider[] = [ + { id: '1', entityId: 'foo', serviceProviderName: 'bar' } as MetadataProvider, + { id: '2', entityId: 'baz', serviceProviderName: 'fin' } as MetadataProvider + ], + snapshot: fromProvider.ProviderState = { + ids: [providers[0].id, providers[1].id], + entities: { + [providers[0].id]: providers[0], + [providers[1].id]: providers[1] + }, + selectedProviderId: null + }; + +describe('Provider Reducer', () => { + const initialState: fromProvider.ProviderState = { + ids: [], + entities: {}, + selectedProviderId: null, + }; + + describe('undefined action', () => { + it('should return the default state', () => { + const result = reducer(undefined, {} as any); + + expect(result).toEqual(initialState); + }); + }); + + describe('Load Providers: Success', () => { + it('should add the loaded providers to the collection', () => { + const action = new providerActions.LoadProviderSuccess(providers); + const result = reducer(initialState, action); + + expect(result).toEqual( + Object.assign({}, initialState, snapshot) + ); + }); + }); + + describe('Update Providers: Success', () => { + it('should update the draft of the specified id', () => { + let changes = { ...providers[1], serviceEnabled: true }, + expected = { + ids: [providers[0].id, providers[1].id], + entities: { + [providers[0].id]: providers[0], + [providers[1].id]: changes + }, + selectedProviderId: null + }; + const action = new providerActions.UpdateProviderSuccess(changes); + const result = reducer({ ...snapshot }, action); + + expect(result).toEqual( + Object.assign({}, initialState, expected) + ); + }); + + it('should return state if the entityId is not found', () => { + let changes = { ...providers[1], serviceEnabled: true, id: '4' }; + const action = new providerActions.UpdateProviderSuccess(changes); + const result = reducer({ ...snapshot }, action); + + expect(result).toEqual(snapshot); + }); + }); + + describe('Select Provider', () => { + it('should update the selected draft id', () => { + let id = 'foo', + expected = { ...snapshot, selectedProviderId: id }; + const action = new providerActions.SelectProvider(id); + const result = reducer({ ...snapshot }, action); + + expect(result).toEqual( + Object.assign({}, initialState, expected) + ); + }); + }); + + describe('Select Provider Success', () => { + it('should update the selected draft id', () => { + let id = providers[0].id, + expected = { ...snapshot, selectedProviderId: id, ids: ['2', '1'] }; + const action = new providerActions.SelectProviderSuccess(providers[0]); + const result = reducer({ ...snapshot }, action); + + expect(result).toEqual( + Object.assign({}, initialState, expected) + ); + }); + + it('should not update state if the provider id does not exist', () => { + let id = providers[0].id, + expected = snapshot; + const action = new providerActions.SelectProviderSuccess({...providers[0], id: 'foo'}); + const result = reducer({ ...snapshot }, action); + + expect(result).toEqual( + Object.assign({}, initialState, expected) + ); + }); + }); +}); diff --git a/ui/src/app/metadata-provider/reducer/provider.reducer.ts b/ui/src/app/metadata-provider/reducer/provider.reducer.ts new file mode 100644 index 000000000..b99e22967 --- /dev/null +++ b/ui/src/app/metadata-provider/reducer/provider.reducer.ts @@ -0,0 +1,99 @@ +import { createSelector } from '@ngrx/store'; +import { MetadataProvider } from '../model/metadata-provider'; +import * as provider from '../action/provider.action'; + +export interface ProviderState { + ids: string[]; + entities: { [id: string]: MetadataProvider }; + selectedProviderId: string | null; +} + +export const initialState: ProviderState = { + ids: [], + entities: {}, + selectedProviderId: null +}; + +export function reducer(state = initialState, action: provider.Actions): ProviderState { + switch (action.type) { + case provider.LOAD_PROVIDER_SUCCESS: { + const providers = action.payload; + + const providerIds = providers.map(provider => provider.id); + const entities = providers.reduce( + (e: { [id: string]: MetadataProvider }, provider: MetadataProvider) => { + return Object.assign(e, { + [provider.id]: provider, + }); + }, + {} + ); + + return { + ...state, + ids: [...providerIds], + entities: Object.assign(entities) + }; + } + + case provider.SELECT_SUCCESS: { + const provider = action.payload; + + if (state.ids.indexOf(provider.id) < 0) { + return state; + } + return { + ids: [...state.ids.filter(id => id !== provider.id), provider.id], + entities: Object.assign({ ...state.entities }, { + [provider.id]: provider, + }), + selectedProviderId: provider.id + }; + } + + case provider.UPDATE_PROVIDER_SUCCESS: { + const provider = action.payload; + + if (state.ids.indexOf(provider.id) < 0) { + return state; + } + const original = state.entities[provider.id], + updated = Object.assign({}, + { ...original }, + { ...provider } + ); + return { + ...state, + ids: [...state.ids.filter(id => id !== provider.id), provider.id], + entities: Object.assign({ ...state.entities }, { + [provider.id]: provider, + }) + }; + } + + case provider.SELECT: { + return { + ...state, + selectedProviderId: action.payload, + }; + } + + default: { + return state; + } + } +} + +export const getEntities = (state: ProviderState) => state.entities; +export const getIds = (state: ProviderState) => state.ids; +export const getSelectedId = (state: ProviderState) => state.selectedProviderId; +export const getSelected = createSelector( + getEntities, + getSelectedId, + (entities, selectedId) => { + return entities[selectedId]; + } +); +export const getAll = createSelector(getEntities, getIds, (entities, ids) => { + return ids.map(id => entities[id]).filter(entity => entity); +}); diff --git a/ui/src/app/metadata-provider/service/entity-descriptor.service.spec.ts b/ui/src/app/metadata-provider/service/entity-descriptor.service.spec.ts new file mode 100644 index 000000000..2626f2e44 --- /dev/null +++ b/ui/src/app/metadata-provider/service/entity-descriptor.service.spec.ts @@ -0,0 +1,109 @@ +import { TestBed, async, inject } from '@angular/core/testing'; +import { HttpClientModule, HttpRequest, HttpParams } from '@angular/common/http'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { EntityDescriptorService } from './entity-descriptor.service'; + + +describe(`EntityDescriptorService`, () => { + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientModule, + HttpClientTestingModule + ], + providers: [ + EntityDescriptorService + ] + }); + }); + + afterEach(inject([HttpTestingController], (backend: HttpTestingController) => { + backend.verify(); + })); + + describe('query', () => { + it(`should send an expected query request`, async(inject([EntityDescriptorService, HttpTestingController], + (service: EntityDescriptorService, backend: HttpTestingController) => { + service.query().subscribe(); + + backend.expectOne((req: HttpRequest) => { + return req.url === '/api/EntityDescriptors' + && req.method === 'GET'; + }, `GET EntityDescriptors collection`); + } + ))); + + it(`should emit an empty array if an error is thrown`, async(inject([EntityDescriptorService, HttpTestingController], + (service: EntityDescriptorService, backend: HttpTestingController) => { + service.query().subscribe((next) => { + expect(next).toEqual([]); + }); + + backend.expectOne('/api/EntityDescriptors').flush(null, { status: 404, statusText: 'Not Found' }); + } + ))); + + it(`should emit 'true' for 200 Ok`, async(inject([EntityDescriptorService, HttpTestingController], + (service: EntityDescriptorService, backend: HttpTestingController) => { + service.query().subscribe((next) => { + expect(next).toBeTruthy(); + }); + + backend.expectOne('/api/EntityDescriptors').flush(null, { status: 200, statusText: 'Ok' }); + } + ))); + }); + + describe('find', () => { + let id = 'foo'; + + it(`should send an expected GET request`, async(inject([EntityDescriptorService, HttpTestingController], + (service: EntityDescriptorService, backend: HttpTestingController) => { + service.find(id).subscribe(); + + backend.expectOne((req: HttpRequest) => { + return req.url === `/api/EntityDescriptor/${id}` + && req.method === 'GET'; + }, `GET EntityDescriptor by id`); + } + ))); + + xit(`should emit an error is thrown`, async(inject([EntityDescriptorService, HttpTestingController], + (service: EntityDescriptorService, backend: HttpTestingController) => { + service.find(id).subscribe((next) => { + expect(next).toBeFalsy(); + }); + + backend.expectOne(`/api/EntityDescriptor/${id}`).flush(null, { status: 404, statusText: 'Not Found' }); + } + ))); + + xit(`should emit 'true' for 200 Ok`, async(inject([EntityDescriptorService, HttpTestingController], + (service: EntityDescriptorService, backend: HttpTestingController) => { + service.find(id).subscribe((next) => { + expect(next).toBeTruthy(); + }); + + backend.expectOne(`/api/EntityDescriptor/${id}`).flush(null, { status: 200, statusText: 'Ok' }); + } + ))); + }); + + describe('removeNulls', () => { + let obj = { + foo: null, + bar: 'baz' + }, + expected = { + bar: 'baz' + }; + it(`should remove null values from the object provided`, inject([EntityDescriptorService], (service) => { + expect(service.removeNulls(obj)).toEqual(expected); + })); + + it(`should return an empty object if passed a falsy value`, inject([EntityDescriptorService], (service) => { + expect(service.removeNulls(undefined)).toEqual({}); + })); + }); +}); diff --git a/ui/src/app/metadata-provider/service/entity-descriptor.service.ts b/ui/src/app/metadata-provider/service/entity-descriptor.service.ts new file mode 100644 index 000000000..68e6336e6 --- /dev/null +++ b/ui/src/app/metadata-provider/service/entity-descriptor.service.ts @@ -0,0 +1,85 @@ +import 'rxjs/add/observable/of'; +import 'rxjs/add/operator/concat'; +import { Injectable } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs/Observable'; +import { MetadataProvider } from '../model/metadata-provider'; +import { EntityDescriptor } from '../model/entity-descriptor'; +import { MOCK_DESCRIPTORS } from '../../../data/descriptors.mock'; +import { Storage } from '../../shared/storage'; +import { environment } from '../../../environments/environment'; + +@Injectable() +export class EntityDescriptorService { + + private endpoint = '/EntityDescriptor'; + private base = '/api'; + + constructor( + private http: HttpClient + ) {} + query(): Observable { + return this.http + .get(`${ this.base }${ this.endpoint }s`) + .catch(err => { + console.log('ERROR LOADING PROVIDERS:', err); + return Observable.of([] as MetadataProvider[]); + }); + } + + find(id: string): Observable { + return this + .http + .get(`${ this.base }${ this.endpoint }/${ id }`) + .catch(err => Observable.throw(err)); + } + + update(provider: MetadataProvider): Observable { + return this.http.put(`${this.base}${this.endpoint}/${provider.id}`, provider); + } + + save(provider: MetadataProvider): Observable { + if (!environment.production) { + console.log(JSON.stringify(provider)); + } + return this.http.post(`${this.base}${this.endpoint}`, provider); + } + + remove(provider: MetadataProvider): Observable { + return this.http.delete(`${this.base}${this.endpoint}/${provider.id}`); + } + + upload(name: string, xml: string): Observable { + return this.http.post(`${this.base}${this.endpoint}`, xml, { + headers: new HttpHeaders().set('Content-Type', 'application/xml'), + params: new HttpParams().set('spName', name) + }); + } + + createFromUrl(name: string, url: string): Observable { + let body = `metadataUrl=${url}`; + return this.http.post(`${this.base}${this.endpoint}`, body, { + headers: new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded'), + params: new HttpParams().set('spName', name) + }); + } + + preview(provider: MetadataProvider): Observable { + return this.http.get(`${this.base}${this.endpoint}/${provider.id}`, { + headers: new HttpHeaders({ + 'Accept': 'application/xml' + }), + responseType: 'text' + }); + } + + removeNulls(attribute): any { + if (!attribute) { return {}; } + return Object.keys(attribute).reduce((coll, val, index) => { + if (attribute[val]) { + coll[val] = attribute[val]; + } + return coll; + }, {}); + } +} diff --git a/ui/src/app/metadata-provider/service/entity-draft.service.ts b/ui/src/app/metadata-provider/service/entity-draft.service.ts new file mode 100644 index 000000000..3c38f96e6 --- /dev/null +++ b/ui/src/app/metadata-provider/service/entity-draft.service.ts @@ -0,0 +1,48 @@ +import 'rxjs/add/observable/of'; +import 'rxjs/add/operator/concat'; +import 'rxjs/add/operator/switchMap'; +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs/Observable'; +import { MetadataProvider } from '../model/metadata-provider'; +import { MOCK_DESCRIPTORS } from '../../../data/descriptors.mock'; +import { Storage } from '../../shared/storage'; + +@Injectable() +export class EntityDraftService { + + private storage: Storage; + + constructor(private http: HttpClient) { + this.storage = new Storage('provider_drafts'); + } + query(): Observable { + return Observable.of(this.storage.query()); + } + + find(entityId: string): Observable { + return this.query().switchMap( + list => Observable.of( + list.find(entity => entity.entityId === entityId) + ) + ); + } + + save(provider: MetadataProvider): Observable { + this.storage.add(provider); + return Observable.of(provider); + } + + remove(provider: MetadataProvider): Observable { + this.storage.removeByAttr(provider.entityId, 'entityId'); + return Observable.of(provider); + } + + update(provider: MetadataProvider): Observable { + let stored = this.storage.findByAttr(provider.id, 'entityId'); + stored = Object.assign({}, stored, provider); + this.storage.removeByAttr(provider.entityId, 'entityId'); + this.storage.add(stored); + return Observable.of(stored); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/metadata-provider/service/entity-validators.service.spec.ts b/ui/src/app/metadata-provider/service/entity-validators.service.spec.ts new file mode 100644 index 000000000..bfff3a6eb --- /dev/null +++ b/ui/src/app/metadata-provider/service/entity-validators.service.spec.ts @@ -0,0 +1,92 @@ +import { TestBed, async, inject } from '@angular/core/testing'; +import { EntityValidators } from './entity-validators.service'; +import { Observable } from 'rxjs/Observable'; +import { AbstractControl, FormBuilder, ReactiveFormsModule } from '@angular/forms'; +import 'rxjs/add/observable/of'; + +let ids = ['foo', 'bar', 'baz']; + +describe(`EntityDescriptorService`, () => { + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule + ], + providers: [ + EntityValidators + ] + }); + }); + + describe('createUniqueIdValidator', () => { + it('should detect that a provided id is already used', async(inject([FormBuilder], (fb) => { + let obs = Observable.of(ids), + validator = EntityValidators.createUniqueIdValidator(obs), + ctrl = fb.control('foo'); + validator(ctrl).subscribe(next => { + expect(next).toBeTruthy(); + }); + }))); + + it('should detect that a provided id is NOT already used', async(inject([FormBuilder], (fb) => { + let obs = Observable.of(ids), + validator = EntityValidators.createUniqueIdValidator(obs), + ctrl = fb.control('hi'); + validator(ctrl).subscribe(next => { + expect(next).toBeFalsy(); + }); + }))); + }); + + describe('createOrgValidator', () => { + it('should detect that all controls in a group have a value', async(inject([FormBuilder], (fb) => { + let validator = EntityValidators.createOrgValidator(), + group = fb.group({ + foo: '', + bar: '', + baz: '' + }); + group.get('baz').patchValue('123'); + group.updateValueAndValidity(); + validator(group).subscribe(next => { + expect(next).toBeTruthy(); + }); + }))); + + it('should not validate if all controls are empty', async(inject([FormBuilder], (fb) => { + let validator = EntityValidators.createOrgValidator(), + group = fb.group({ + foo: '', + bar: '', + baz: '' + }); + validator(group).subscribe(next => { + expect(next).toBeFalsy(); + }); + }))); + + it('should not validate if all controls are empty', async(inject([FormBuilder], (fb) => { + let validator = EntityValidators.createOrgValidator(), + group = fb.group({ + foo: '', + bar: '', + baz: '' + }); + group.get('foo').patchValue('123'); + group.get('bar').patchValue('456'); + group.get('baz').patchValue('789'); + group.updateValueAndValidity(); + validator(group).subscribe(next => { + expect(next).toBeFalsy(); + }); + }))); + + it('should return an empty observable when no control is provided', async(inject([FormBuilder], (fb) => { + let validator = EntityValidators.createOrgValidator(); + validator(null).subscribe(next => { + expect(next).toBeFalsy(); + }); + }))); + }); +}); diff --git a/ui/src/app/metadata-provider/service/entity-validators.service.ts b/ui/src/app/metadata-provider/service/entity-validators.service.ts new file mode 100644 index 000000000..95dae83f0 --- /dev/null +++ b/ui/src/app/metadata-provider/service/entity-validators.service.ts @@ -0,0 +1,45 @@ +import { Observable } from 'rxjs/Observable'; +import { AbstractControl, FormGroup } from '@angular/forms'; +import 'rxjs/add/operator/take'; +import 'rxjs/add/operator/startWith'; +import 'rxjs/add/operator/map'; + +export class EntityValidators { + static createUniqueIdValidator(ids$: Observable) { + return (control: AbstractControl) => { + return ids$ + .map(ids => ids.filter(id => id === control.value)) + .map(ids => !!ids.length) + .map((isTaken: boolean) => { + return isTaken ? { unique: true } : null; + }) + .take(1); + }; + } + + static createOrgValidator() { + return (control: AbstractControl) => { + if (!control || !control.valueChanges) { + return Observable.of(null); + } + return control.valueChanges + .startWith(control.value) + .map(values => { + let keys = Object.keys(values), + hasValue = keys.reduce((val, key) => val + (values[key] ? values[key] : ''), ''), + allHaveValue = keys.reduce((val, key) => { + return !values[key] ? false : val; + }, true); + if (!hasValue) { + return true; + } else { + return allHaveValue; + } + }) + .map(isValid => { + return !isValid ? { org: true } : null; + }) + .take(1); + }; + } +} diff --git a/ui/src/app/metadata-provider/service/list-values.service.spec.ts b/ui/src/app/metadata-provider/service/list-values.service.spec.ts new file mode 100644 index 000000000..662f81a72 --- /dev/null +++ b/ui/src/app/metadata-provider/service/list-values.service.spec.ts @@ -0,0 +1,50 @@ +import { TestBed, async, inject } from '@angular/core/testing'; +import { EntityValidators } from './entity-validators.service'; +import { Observable } from 'rxjs/Observable'; +import { ListValuesService } from './list-values.service'; + +import 'rxjs/add/observable/of'; + +describe(`ListValuesService`, () => { + let service: ListValuesService; + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [ + ListValuesService + ] + }); + service = TestBed.get(ListValuesService); + }); + + describe(`searchStringList method`, () => { + it('should match values', (done: DoneFn) => { + let list = Observable.of(['foo', 'bar', 'baz']), + query = Observable.of('foo'); + service.searchStringList(list)(query).subscribe((matches) => { + expect(matches.length).toBe(1); + done(); + }); + }); + }); + + describe(`searchFormats method`, () => { + it('should match the nameid formats', (done: DoneFn) => { + let query = Observable.of('unspecified'); + service.searchFormats(query).subscribe((matches) => { + expect(matches.length).toBe(1); + done(); + }); + }); + }); + + describe(`searchAuthenticationMethods method`, () => { + it('should match the nameid formats', (done: DoneFn) => { + let query = Observable.of('TimeSyncToken'); + service.searchAuthenticationMethods(query).subscribe((matches) => { + expect(matches.length).toBe(1); + done(); + }); + }); + }); +}); diff --git a/ui/src/app/metadata-provider/service/list-values.service.ts b/ui/src/app/metadata-provider/service/list-values.service.ts new file mode 100644 index 000000000..bdda90270 --- /dev/null +++ b/ui/src/app/metadata-provider/service/list-values.service.ts @@ -0,0 +1,56 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/observable/of'; +import 'rxjs/add/operator/combineLatest'; +import 'rxjs/add/operator/debounceTime'; +import 'rxjs/add/operator/distinctUntilChanged'; + +@Injectable() +export class ListValuesService { + constructor() {} + + readonly nameIdFormats: Observable = Observable.of([ + 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', + 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', + 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', + 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + ]); + + readonly authenticationMethods: Observable = Observable.of([ + 'https://refeds.org/profile/mfa', + 'urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken', + 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport' + ]); + + readonly attributesToRelease: Observable<{ key: string, label: string }[]> = Observable.of([ + { key: 'eduPersonPrincipalName', label: 'eduPersonPrincipalName (EPPN)' }, + { key: 'uid', label: 'uid' }, + { key: 'mail', label: 'mail' }, + { key: 'surname', label: 'surname' }, + { key: 'givenName', label: 'givenName' }, + { key: 'displayName', label: 'displayName' }, + { key: 'eduPersonAffiliation', label: 'eduPersonAffiliation' }, + { key: 'eduPersonScopedAffiliation', label: 'eduPersonScopedAffiliation' }, + { key: 'eduPersonPrimaryAffiliation', label: 'eduPersonPrimaryAffiliation' }, + { key: 'eduPersonEntitlement', label: 'eduPersonEntitlement' }, + { key: 'eduPersonAssurance', label: 'eduPersonAssurance' }, + { key: 'eduPersonUniqueId', label: 'eduPersonUniqueId' }, + { key: 'employeeNumber', label: 'employeeNumber' } + ]); + + searchStringList = (list: Observable): Function => + (text$: Observable) => + text$ + .debounceTime(100) + .distinctUntilChanged() + .combineLatest(list, (term, formats) => formats.filter( + v => v.toLowerCase().match(term.toLowerCase()) + ).slice(0, 4)) + + get searchFormats(): Function { + return this.searchStringList(this.nameIdFormats); + } + get searchAuthenticationMethods(): Function { + return this.searchStringList(this.authenticationMethods); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/metadata-provider/service/provider-change-emitter.service.spec.ts b/ui/src/app/metadata-provider/service/provider-change-emitter.service.spec.ts new file mode 100644 index 000000000..af7a40ce3 --- /dev/null +++ b/ui/src/app/metadata-provider/service/provider-change-emitter.service.spec.ts @@ -0,0 +1,41 @@ +import { TestBed, async, inject } from '@angular/core/testing'; +import { EntityValidators } from './entity-validators.service'; +import { Observable } from 'rxjs/Observable'; +import { AbstractControl, FormBuilder, ReactiveFormsModule } from '@angular/forms'; +import 'rxjs/add/observable/of'; +import { ProviderStatusEmitter, ProviderValueEmitter } from './provider-change-emitter.service'; + +describe(`EntityDescriptorService`, () => { + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule + ], + providers: [ + ProviderStatusEmitter, + ProviderValueEmitter + ] + }); + }); + + describe('ProviderStatusEmitter', () => { + it('should emit values', async(inject([ProviderStatusEmitter], (emitter) => { + let val = 'foo'; + emitter.changeEmitted$.subscribe(n => { + expect(n).toEqual(val); + }); + emitter.emit(val); + }))); + }); + + describe('ProviderValueEmitter', () => { + it('should emit values', async(inject([ProviderValueEmitter], (emitter) => { + let val = 'foo'; + emitter.changeEmitted$.subscribe(n => { + expect(n).toEqual(val); + }); + emitter.emit(val); + }))); + }); +}); diff --git a/ui/src/app/metadata-provider/service/provider-change-emitter.service.ts b/ui/src/app/metadata-provider/service/provider-change-emitter.service.ts new file mode 100644 index 000000000..63c24cd6e --- /dev/null +++ b/ui/src/app/metadata-provider/service/provider-change-emitter.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; + +@Injectable() +export class ProviderValueEmitter implements ChangeEmitter { + private subj = new Subject(); + changeEmitted$ = this.subj.asObservable(); + emit(change: any) { + this.subj.next(change); + } +} + +@Injectable() +export class ProviderStatusEmitter implements ChangeEmitter { + private subj = new Subject(); + changeEmitted$ = this.subj.asObservable(); + emit(change: string) { + this.subj.next(change); + } +} + +export interface ChangeEmitter { + changeEmitted$: Observable; + emit(change: string): void; +} /* istanbul ignore next */ diff --git a/ui/src/app/notification/action/notification.action.ts b/ui/src/app/notification/action/notification.action.ts new file mode 100644 index 000000000..3e5ada658 --- /dev/null +++ b/ui/src/app/notification/action/notification.action.ts @@ -0,0 +1,21 @@ +import { Action } from '@ngrx/store'; +import { Notification } from '../model/notification'; + +export const ADD_NOTIFICATION = '[Notification] Add Notification'; +export const CLEAR_NOTIFICATION = '[Metadata Draft] Clear Notification'; + +export class AddNotification implements Action { + readonly type = ADD_NOTIFICATION; + + constructor(public payload: Notification) { } +} + +export class ClearNotification implements Action { + readonly type = CLEAR_NOTIFICATION; + + constructor(public payload: Notification) { } +} + +export type Actions = + | AddNotification + | ClearNotification; diff --git a/ui/src/app/notification/component/notification-item.component.html b/ui/src/app/notification/component/notification-item.component.html new file mode 100644 index 000000000..e14cdf882 --- /dev/null +++ b/ui/src/app/notification/component/notification-item.component.html @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/ui/src/app/notification/component/notification-item.component.scss b/ui/src/app/notification/component/notification-item.component.scss new file mode 100644 index 000000000..ba091474a --- /dev/null +++ b/ui/src/app/notification/component/notification-item.component.scss @@ -0,0 +1,3 @@ +:host { + word-wrap: break-word; +} \ No newline at end of file diff --git a/ui/src/app/notification/component/notification-item.component.spec.ts b/ui/src/app/notification/component/notification-item.component.spec.ts new file mode 100644 index 000000000..9d3968f37 --- /dev/null +++ b/ui/src/app/notification/component/notification-item.component.spec.ts @@ -0,0 +1,60 @@ +import { TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import * as fromNotifications from '../reducer'; +import { NotificationItemComponent } from './notification-item.component'; +import { Notification, NotificationType } from '../model/notification'; + +describe('Notification List Component', () => { + let fixture: ComponentFixture; + let instance: NotificationItemComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [], + imports: [ + NoopAnimationsModule, + StoreModule.forRoot({ + 'notifications': combineReducers(fromNotifications.reducers), + }) + ], + declarations: [ + NotificationItemComponent + ], + }); + + fixture = TestBed.createComponent(NotificationItemComponent); + instance = fixture.componentInstance; + instance.notification = new Notification(); + fixture.detectChanges(); + }); + + it('should compile', () => { + expect(fixture).toBeDefined(); + }); + + describe('timeout', () => { + it('should timeout after the given number of seconds', fakeAsync(() => { + spyOn(instance.clear, 'emit'); + instance.timerCallback(); + expect(instance.clear.emit).toHaveBeenCalled(); + })); + + it('should call timeout after the given number of seconds', fakeAsync(() => { + spyOn(window, 'setTimeout'); + fixture = TestBed.createComponent(NotificationItemComponent); + instance = fixture.componentInstance; + instance.notification = new Notification(); + fixture.detectChanges(); + expect(window.setTimeout).toHaveBeenCalledWith(instance.timerCallback, instance.notification.timeout); + })); + + it('should NOT clear if 0 is passed as the timeout of the notification', async () => { + spyOn(window, 'setTimeout'); + instance.notification = new Notification(NotificationType.Info, 'foo', 0); + fixture.detectChanges(); + expect(window.setTimeout).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/ui/src/app/notification/component/notification-item.component.ts b/ui/src/app/notification/component/notification-item.component.ts new file mode 100644 index 000000000..6be297fee --- /dev/null +++ b/ui/src/app/notification/component/notification-item.component.ts @@ -0,0 +1,33 @@ +import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { Store } from '@ngrx/store'; + +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/switchMap'; + +import * as fromNotifications from '../reducer'; +import { Notification } from '../model/notification'; + +@Component({ + selector: 'notification-item', + templateUrl: './notification-item.component.html', + styleUrls: ['./notification-item.component.scss'] +}) +export class NotificationItemComponent implements OnInit { + @Input() notification: Notification; + @Output() clear: EventEmitter = new EventEmitter(); + types = { + '0': 'alert-success', + '1': 'alert-info', + '2': 'alert-warning', + '3': 'alert-danger' + }; + readonly timerCallback = () => this.clear.emit(this.notification); + constructor() {} + + ngOnInit(): void { + if (this.notification.timeout > 0) { + setTimeout(this.timerCallback, this.notification.timeout); + } + } +} /* istanbul ignore next */ diff --git a/ui/src/app/notification/component/notification-list.component.html b/ui/src/app/notification/component/notification-list.component.html new file mode 100644 index 000000000..5b6e7d621 --- /dev/null +++ b/ui/src/app/notification/component/notification-list.component.html @@ -0,0 +1,7 @@ +
+
    +
  • + +
  • +
+
\ No newline at end of file diff --git a/ui/src/app/notification/component/notification-list.component.scss b/ui/src/app/notification/component/notification-list.component.scss new file mode 100644 index 000000000..f90ee0ae8 --- /dev/null +++ b/ui/src/app/notification/component/notification-list.component.scss @@ -0,0 +1,7 @@ +:host { + & > .position-fixed { + bottom: 0px; + right: 0px; + z-index: 2000; + } +} \ No newline at end of file diff --git a/ui/src/app/notification/component/notification-list.component.spec.ts b/ui/src/app/notification/component/notification-list.component.spec.ts new file mode 100644 index 000000000..2f31d8f4d --- /dev/null +++ b/ui/src/app/notification/component/notification-list.component.spec.ts @@ -0,0 +1,87 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import * as fromNotifications from '../reducer'; +import { NotificationListComponent } from './notification-list.component'; +import { NotificationItemComponent } from './notification-item.component'; +import { Notification } from '../model/notification'; + +describe('Notification List Component', () => { + let fixture: ComponentFixture; + let instance: NotificationListComponent; + let store: Store; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [], + imports: [ + NoopAnimationsModule, + StoreModule.forRoot({ + 'notifications': combineReducers(fromNotifications.reducers), + }) + ], + declarations: [ + NotificationListComponent, + NotificationItemComponent + ], + }); + store = TestBed.get(Store); + spyOn(store, 'dispatch').and.callThrough(); + + fixture = TestBed.createComponent(NotificationListComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should compile', () => { + expect(fixture).toBeDefined(); + }); + + describe('clear function', () => { + it('should dispatch a clear action to the store', () => { + instance.clear(new Notification()); + expect(store.dispatch).toHaveBeenCalled(); + }); + }); + + describe('sorter function', () => { + it('should return -1 if the first parameter was created first', () => { + const a = new Notification(); + const b = new Notification(); + b.createdAt = Date.now() + 1; + expect(instance.sorter(a, b)).toBe(-1); + }); + it('should return -1 if the first parameter was created first', () => { + const a = new Notification(); + const b = new Notification(); + a.createdAt = Date.now() + 1; + expect(instance.sorter(a, b)).toBe(1); + }); + it('should return -1 if the first parameter was created first', () => { + const a = new Notification(); + const b = new Notification(); + expect(instance.sorter(a, b)).toBe(0); + }); + }); + + describe('limit function', () => { + it('should return true if index is > 5', () => { + expect(instance.filter(new Notification(), 4)).toBe(true); + }); + it('should return true if index is <= 5', () => { + expect(instance.filter(new Notification(), 5)).toBe(false); + }); + it('should return -1 if the first parameter was created first', () => { + const a = new Notification(); + const b = new Notification(); + a.createdAt = Date.now() + 1; + expect(instance.sorter(a, b)).toBe(1); + }); + it('should return -1 if the first parameter was created first', () => { + const a = new Notification(); + const b = new Notification(); + expect(instance.sorter(a, b)).toBe(0); + }); + }); +}); diff --git a/ui/src/app/notification/component/notification-list.component.ts b/ui/src/app/notification/component/notification-list.component.ts new file mode 100644 index 000000000..e1b3d8d3d --- /dev/null +++ b/ui/src/app/notification/component/notification-list.component.ts @@ -0,0 +1,42 @@ +import { Component } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { Store } from '@ngrx/store'; + +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/switchMap'; + +import * as fromNotifications from '../reducer'; +import { ClearNotification } from '../action/notification.action'; +import { Notification } from '../model/notification'; + +@Component({ + selector: 'notification-list', + templateUrl: './notification-list.component.html', + styleUrls: ['./notification-list.component.scss'] +}) +export class NotificationListComponent { + notifications$: Observable; + + max = 5; + + constructor( + private store: Store + ) { + this.notifications$ = this.store + .select(fromNotifications.getNotifications) + .map(notifications => notifications.sort(this.sorter)) + .map(notifications => notifications.filter(this.filter)); + } + + sorter(a: Notification, b: Notification): number { + return(a.createdAt < b.createdAt) ? - 1 : (a.createdAt > b.createdAt) ? 1 : 0; + } + + filter(n: Notification, index): boolean { + return index < 5; + } + + clear(event: Notification): void { + this.store.dispatch(new ClearNotification(event)); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/notification/model/notification.ts b/ui/src/app/notification/model/notification.ts new file mode 100644 index 000000000..7c1f5fb63 --- /dev/null +++ b/ui/src/app/notification/model/notification.ts @@ -0,0 +1,16 @@ +export class Notification { + createdAt: number = Date.now(); + constructor( + public type: NotificationType = NotificationType.Info, + public body: string = '', + public timeout: number = 8000, + public closeable: boolean = true + ) {} +} + +export enum NotificationType { + Success, + Info, + Warning, + Danger +} diff --git a/ui/src/app/notification/notification.module.ts b/ui/src/app/notification/notification.module.ts new file mode 100644 index 000000000..24b175df7 --- /dev/null +++ b/ui/src/app/notification/notification.module.ts @@ -0,0 +1,25 @@ +import { NgModule, ModuleWithProviders } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { StoreModule } from '@ngrx/store'; + +import { reducers } from './reducer'; +import { NotificationListComponent } from './component/notification-list.component'; +import { NotificationItemComponent } from './component/notification-item.component'; + +const COMPONENTS = [ + NotificationListComponent, + NotificationItemComponent +]; + +@NgModule({ + declarations: COMPONENTS, + entryComponents: COMPONENTS, + imports: [ + CommonModule, + StoreModule.forFeature('notifications', reducers) + ], + exports: COMPONENTS, + providers: [] +}) +export class NotificationModule {} + diff --git a/ui/src/app/notification/reducer/index.ts b/ui/src/app/notification/reducer/index.ts new file mode 100644 index 000000000..4298e3315 --- /dev/null +++ b/ui/src/app/notification/reducer/index.ts @@ -0,0 +1,23 @@ +import { createSelector, createFeatureSelector } from '@ngrx/store'; +import * as fromNotifications from './notification.reducer'; +import * as fromRoot from '../../core/reducer'; +import { Notification } from '../model/notification'; + +export interface State extends fromRoot.State { + notifications: NotificationState; +} + +export interface NotificationState { + notifications: fromNotifications.NotificationState; +} + +export const reducers = { + notifications: fromNotifications.reducer +}; + +export const getNotificationState = createFeatureSelector('notifications'); +export const getNotificationEntityState = createSelector(getNotificationState, (state: NotificationState) => state.notifications); +export const getNotifications = createSelector( + getNotificationEntityState, + (state: fromNotifications.NotificationState) => state.notifications +); diff --git a/ui/src/app/notification/reducer/notification.reducer.spec.ts b/ui/src/app/notification/reducer/notification.reducer.spec.ts new file mode 100644 index 000000000..50f43c869 --- /dev/null +++ b/ui/src/app/notification/reducer/notification.reducer.spec.ts @@ -0,0 +1,63 @@ +import { reducer } from './notification.reducer'; +import * as fromNotifications from './notification.reducer'; +import * as notificationActions from '../action/notification.action'; +import { Notification } from '../model/notification'; + +let notifications: Notification[] = [ + new Notification(), + new Notification() +], +snapshot: fromNotifications.NotificationState = { + notifications: [] +}; + +describe('Notification Reducer', () => { + const initialState: fromNotifications.NotificationState = { + notifications: [] + }; + + describe('undefined action', () => { + it('should return the default state', () => { + const result = reducer(undefined, {} as any); + + expect(result).toEqual(initialState); + }); + }); + + describe('create notification action', () => { + it('should update the status when a provider is saved', () => { + const n = new Notification(); + const action = new notificationActions.AddNotification(n); + const result = reducer(initialState, action); + expect(result).toEqual( + Object.assign({}, initialState, { + notifications: [n] + }) + ); + }); + }); + + describe('remove notification action', () => { + it('should update the status when a provider is saved', () => { + const n = new Notification(); + const action = new notificationActions.ClearNotification(n); + const state: fromNotifications.NotificationState = { + notifications: [n] + }; + const result = reducer(state, action); + expect(result).toEqual({notifications: []}); + }); + }); + + describe('get notifications selector', () => { + it('should update the status when a provider is saved', () => { + const n = new Notification(); + const action = new notificationActions.ClearNotification(n); + const state: fromNotifications.NotificationState = { + notifications: [n] + }; + const result = reducer(state, action); + expect(fromNotifications.getNotifications(state)).toEqual(state.notifications); + }); + }); +}); diff --git a/ui/src/app/notification/reducer/notification.reducer.ts b/ui/src/app/notification/reducer/notification.reducer.ts new file mode 100644 index 000000000..2a6ddbdde --- /dev/null +++ b/ui/src/app/notification/reducer/notification.reducer.ts @@ -0,0 +1,36 @@ +import { createSelector } from '@ngrx/store'; +import { Notification } from '../model/notification'; +import * as actions from '../action/notification.action'; + +export interface NotificationState { + notifications: Notification[]; +} + +export const initialState: NotificationState = { + notifications: [] +}; + +export function reducer(state = initialState, action: actions.Actions): NotificationState { + switch (action.type) { + case actions.ADD_NOTIFICATION: { + return { + notifications: [ + ...state.notifications, + action.payload + ] + }; + } + case actions.CLEAR_NOTIFICATION: { + return { + notifications: [ + ...state.notifications.filter(n => n !== action.payload) + ] + }; + } + default: { + return state; + } + } +} + +export const getNotifications = (state: NotificationState) => state.notifications; diff --git a/ui/src/app/routing.module.ts b/ui/src/app/routing.module.ts new file mode 100644 index 000000000..bca2b335a --- /dev/null +++ b/ui/src/app/routing.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +const routes: Routes = [ + { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, + { + path: 'dashboard', + loadChildren: './dashboard/dashboard.module#DashboardModule', + canActivate: [] + }, + { + path: 'provider', + loadChildren: './edit-provider/editor.module#EditorModule', + canActivate: [] + } +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule] +}) +export class AppRoutingModule { } diff --git a/ui/src/app/shared/client.ts b/ui/src/app/shared/client.ts new file mode 100644 index 000000000..9942538fb --- /dev/null +++ b/ui/src/app/shared/client.ts @@ -0,0 +1,3 @@ +export function isIosDevice() { + return !!(navigator.userAgent.match(/(iPod|iPhone|iPad)/g) && navigator.userAgent.match(/AppleWebKit/g)); +} diff --git a/ui/src/app/shared/constant.ts b/ui/src/app/shared/constant.ts new file mode 100644 index 000000000..a6e3fde7d --- /dev/null +++ b/ui/src/app/shared/constant.ts @@ -0,0 +1 @@ +export const DEFAULT_FIELD_MAX_LENGTH = 255; \ No newline at end of file diff --git a/ui/src/app/shared/keycodes.spec.ts b/ui/src/app/shared/keycodes.spec.ts new file mode 100644 index 000000000..d5c68f22c --- /dev/null +++ b/ui/src/app/shared/keycodes.spec.ts @@ -0,0 +1,55 @@ +import * as Keycodes from './keycodes'; + +function range (start, edge, step = 1) { + const ret = []; + edge = edge || 0; + step = step || 1; + for (ret; (edge - start) * step > 0; start += step) { + ret.push(start); + } + return ret; +} + +describe('Keycodes utility', () => { + describe('isPrintableKeyCode function', () => { + it('should return true if keycode is between (not including) 47 and 58', () => { + let rng = range(48, 57); + rng.forEach((num) => { + expect(Keycodes.isPrintableKeyCode(num)).toBe(true); + }); + }); + + it('should return true if keycode is between (not including) 64 and 91', () => { + let rng = range(64 + 1, 91 - 1); + rng.forEach((num) => { + expect(Keycodes.isPrintableKeyCode(num)).toBe(true); + }); + }); + + it('should return true if keycode is between (not including) 95 and 112', () => { + let rng = range(95 + 1, 112 - 1); + rng.forEach((num) => { + expect(Keycodes.isPrintableKeyCode(num)).toBe(true); + }); + }); + + it('should return true if keycode is between (not including) 185 and 193', () => { + let rng = range(185 + 1, 193 - 1); + rng.forEach((num) => { + expect(Keycodes.isPrintableKeyCode(num)).toBe(true); + }); + }); + + it('should return true if keycode is between (not including) 218 and 223', () => { + let rng = range(218 + 1, 223 - 1); + rng.forEach((num) => { + expect(Keycodes.isPrintableKeyCode(num)).toBe(true); + }); + }); + + it('should return true if keycode is 32 or 8', () => { + expect(Keycodes.isPrintableKeyCode(32)).toBe(true); + expect(Keycodes.isPrintableKeyCode(8)).toBe(true); + }); + }); +}); diff --git a/ui/src/app/shared/keycodes.ts b/ui/src/app/shared/keycodes.ts new file mode 100644 index 000000000..717c1bb22 --- /dev/null +++ b/ui/src/app/shared/keycodes.ts @@ -0,0 +1,18 @@ +export const keyCodes = { + 13: 'enter', + 27: 'escape', + 32: 'space', + 38: 'up', + 40: 'down' +}; + +export function isPrintableKeyCode (keyCode) { + return ( + (keyCode > 47 && keyCode < 58) || // number keys + keyCode === 32 || keyCode === 8 || // spacebar or backspace + (keyCode > 64 && keyCode < 91) || // letter keys + (keyCode > 95 && keyCode < 112) || // numpad keys + (keyCode > 185 && keyCode < 193) || // ;=,-./` (in order) + (keyCode > 218 && keyCode < 223) // [\]' (in order) + ); +} diff --git a/ui/src/app/shared/regex.ts b/ui/src/app/shared/regex.ts new file mode 100644 index 000000000..b5c6a1404 --- /dev/null +++ b/ui/src/app/shared/regex.ts @@ -0,0 +1,5 @@ +import { AbstractControl } from '@angular/forms'; + +export const URL_REGEX = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/; +export const EMAIL_REGEX = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{1,})+$/; +export const INTEGER_REGEX = /^[0-9]+$/; diff --git a/ui/src/app/shared/storage.ts b/ui/src/app/shared/storage.ts new file mode 100644 index 000000000..6672ee2d5 --- /dev/null +++ b/ui/src/app/shared/storage.ts @@ -0,0 +1,36 @@ +export class Storage { + constructor(readonly key: string) { + const list = localStorage.getItem(key); + if (!list || !Array.isArray(JSON.parse(list))) { + localStorage.setItem(key, JSON.stringify([])); + } + } + + add(obj): T[] { + const list = this.query(); + list.push(obj); + this.save(list); + return list; + } + + save(list): T[] { + localStorage.setItem(this.key, JSON.stringify(list)); + return list; + } + + findByAttr(val, attr: string = 'id'): T { + const list = this.query(); + return list.find(entity => entity[attr] === val); + } + + query(): T[] { + const list = JSON.parse(localStorage.getItem(this.key)); + return [...list]; + } + + removeByAttr(val, attr: string = 'id'): void { + const list = this.query().filter(entity => entity[attr] !== val); + this.save(list); + return null; + } +} /* istanbul ignore next */ diff --git a/ui/src/app/shared/util.ts b/ui/src/app/shared/util.ts new file mode 100644 index 000000000..e779c79d3 --- /dev/null +++ b/ui/src/app/shared/util.ts @@ -0,0 +1,16 @@ +import { RouterStateSerializer } from '@ngrx/router-store'; +import { RouterStateSnapshot, Params } from '@angular/router'; + +export interface RouterStateUrl { + url: string; + queryParams: Params; +} + +export class CustomRouterStateSerializer implements RouterStateSerializer { + serialize(routerState: RouterStateSnapshot): RouterStateUrl { + const { url } = routerState; + const queryParams = routerState.root.queryParams; + + return { url, queryParams }; + } +} /* istanbul ignore next */ diff --git a/ui/src/app/widget/autocomplete/autocomplete.component.html b/ui/src/app/widget/autocomplete/autocomplete.component.html new file mode 100644 index 000000000..34eb148f4 --- /dev/null +++ b/ui/src/app/widget/autocomplete/autocomplete.component.html @@ -0,0 +1,39 @@ + diff --git a/ui/src/app/widget/autocomplete/autocomplete.component.scss b/ui/src/app/widget/autocomplete/autocomplete.component.scss new file mode 100644 index 000000000..a491882aa --- /dev/null +++ b/ui/src/app/widget/autocomplete/autocomplete.component.scss @@ -0,0 +1,3 @@ +.dropdown-menu { + width: 100%; +} \ No newline at end of file diff --git a/ui/src/app/widget/autocomplete/autocomplete.component.spec.ts b/ui/src/app/widget/autocomplete/autocomplete.component.spec.ts new file mode 100644 index 000000000..72c39d088 --- /dev/null +++ b/ui/src/app/widget/autocomplete/autocomplete.component.spec.ts @@ -0,0 +1,240 @@ +import { Component, ViewChild } from '@angular/core'; +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { NgbPopoverModule, NgbPopoverConfig } from '@ng-bootstrap/ng-bootstrap/popover/popover.module'; +import { AutoCompleteComponent } from './autocomplete.component'; + +@Component({ + template: `` +}) +class TestHostComponent { + config: any = { + autoSelect: false, + options: [], + required: true, + defaultValue: '', + id: 'foo', + allowCustom: true, + noneFoundText: 'None Found' + }; + + @ViewChild(AutoCompleteComponent) + public autoCompleteUnderTest: AutoCompleteComponent; + + configure(opts: any): void { + this.config = Object.assign({}, this.config, opts); + } +} + +describe('AutoComplete Input Component', () => { + let testHostInstance: TestHostComponent; + let testHostFixture: ComponentFixture; + let instanceUnderTest: AutoCompleteComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [], + imports: [ + NoopAnimationsModule, + ReactiveFormsModule + ], + declarations: [ + AutoCompleteComponent, + TestHostComponent + ], + }).compileComponents(); + }); + + beforeEach(() => { + testHostFixture = TestBed.createComponent(TestHostComponent); + testHostInstance = testHostFixture.componentInstance; + instanceUnderTest = testHostInstance.autoCompleteUnderTest; + testHostFixture.detectChanges(); + }); + + it('should compile', () => { + expect(testHostFixture).toBeDefined(); + }); + + describe('ControlValueAccessor interface', () => { + describe('writeValue method', () => { + it('should set the value of the internal input element', () => { + let val = 'foo'; + instanceUnderTest.writeValue(val); + expect(instanceUnderTest.input.value).toBe(val); + }); + }); + + describe('registerOnChange method', () => { + it('should set the onChange callback method', () => { + let cb = jasmine.createSpy('registerOnChange'); + instanceUnderTest.registerOnChange(cb); + instanceUnderTest.propagateChange('foo'); + expect(cb).toHaveBeenCalledWith('foo'); + }); + }); + + describe('registerOnTouched method', () => { + it('should set the onTouched callback method', () => { + let cb = jasmine.createSpy('registerOnTouched'); + instanceUnderTest.registerOnTouched(cb); + instanceUnderTest.propagateTouched(null); + expect(cb).toHaveBeenCalledWith(null); + }); + }); + + describe('setDisabledState method', () => { + it('should set the disabled property', () => { + instanceUnderTest.setDisabledState(true); + expect(instanceUnderTest.disabled).toBe(true); + }); + it('should set the disabled property', () => { + instanceUnderTest.setDisabledState(); + expect(instanceUnderTest.disabled).toBe(false); + }); + }); + }); + + describe ('onChanges lifecycle event', () => { + it('should set disabled', () => { + spyOn(instanceUnderTest.input, 'disable'); + instanceUnderTest.setDisabledState(true); + instanceUnderTest.ngOnChanges({}); + expect(instanceUnderTest.input.disable).toHaveBeenCalled(); + }); + + it('should enable', () => { + spyOn(instanceUnderTest.input, 'enable'); + instanceUnderTest.setDisabledState(false); + instanceUnderTest.ngOnChanges({}); + expect(instanceUnderTest.input.enable).toHaveBeenCalled(); + }); + + it('should set required', () => { + spyOn(instanceUnderTest.input, 'setValidators'); + instanceUnderTest.required = true; + instanceUnderTest.ngOnChanges({}); + expect(instanceUnderTest.input.setValidators).toHaveBeenCalled(); + }); + + it('should remove required', () => { + spyOn(instanceUnderTest.input, 'clearValidators'); + instanceUnderTest.required = false; + instanceUnderTest.ngOnChanges({}); + expect(instanceUnderTest.input.clearValidators).toHaveBeenCalled(); + }); + + it('should update provided options', () => { + let opts = ['foo']; + spyOn(instanceUnderTest.state, 'setState'); + testHostInstance.configure({options: opts}); + testHostFixture.detectChanges(); + expect(instanceUnderTest.state.setState).toHaveBeenCalledWith({options: opts}); + }); + }); + + describe('handleKeyDown method', () => { + const keyCodes = { + 13: 'enter', + 27: 'escape', + 32: 'space', + 38: 'up', + 40: 'down' + }; + it('should call the handleUpArrow handler when the up arrow key is entered', () => { + spyOn(instanceUnderTest, 'handleUpArrow'); + instanceUnderTest.handleKeyDown({keyCode: 38} as KeyboardEvent); + expect(instanceUnderTest.handleUpArrow).toHaveBeenCalled(); + }); + it('should call the handleKeyDown handler when the down arrow key is entered', () => { + spyOn(instanceUnderTest, 'handleDownArrow'); + instanceUnderTest.handleKeyDown({ keyCode: 40 } as KeyboardEvent); + expect(instanceUnderTest.handleDownArrow).toHaveBeenCalled(); + }); + + it('should call the handleSpace handler when the arrow space is entered', () => { + spyOn(instanceUnderTest, 'handleSpace'); + instanceUnderTest.handleKeyDown({ keyCode: 32 } as KeyboardEvent); + expect(instanceUnderTest.handleSpace).toHaveBeenCalled(); + }); + + it('should call the handleEnter when the enter key is entered', () => { + spyOn(instanceUnderTest, 'handleEnter'); + instanceUnderTest.handleKeyDown({ keyCode: 13 } as KeyboardEvent); + expect(instanceUnderTest.handleEnter).toHaveBeenCalled(); + }); + it('should call the handleComponentBlur when the escape key is entered', () => { + spyOn(instanceUnderTest, 'handleComponentBlur'); + instanceUnderTest.handleKeyDown({ keyCode: 27 } as KeyboardEvent); + expect(instanceUnderTest.handleComponentBlur).toHaveBeenCalled(); + }); + }); + + xdescribe('handleEnter handler', () => { + + }); + + describe('handleOptionMouseDown method', () => { + it('should call the preventDefault method on the provided event', () => { + let spy = jasmine.createSpy('preventDefault'), + evt = {preventDefault: spy}; + instanceUnderTest.handleOptionMouseDown(evt); + expect(evt.preventDefault).toHaveBeenCalled(); + }); + }); + + describe('handleOptionMouseEnter method', () => { + it('should update the current state with the provided index', () => { + let index = 1; + spyOn(instanceUnderTest.state, 'setState'); + instanceUnderTest.handleOptionMouseEnter(index); + expect(instanceUnderTest.state.setState).toHaveBeenCalledWith({ hovered: index }); + }); + }); + + describe('handleOptionMouseOut method', () => { + it('should update the current state on the components model', () => { + spyOn(instanceUnderTest.state, 'setState'); + instanceUnderTest.handleOptionMouseOut(); + expect(instanceUnderTest.state.setState).toHaveBeenCalledWith({hovered: null}); + }); + }); + + describe('displayState getter function', () => { + it('should return the current state', () => { + let spy = spyOnProperty(instanceUnderTest.state, 'currentState', 'get'); + let state = instanceUnderTest.displayState; + expect(state).toEqual({options: []}); + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('hasAutoSelect getter', () => { + it('should return true if not an ios device and autoSelect is set to true', () => { + spyOn(instanceUnderTest, 'isIosDevice').and.returnValue(false); + testHostInstance.configure({autoSelect: true}); + console.log(testHostInstance.config); + testHostFixture.detectChanges(); + expect(instanceUnderTest.hasAutoselect).toBe(true); + }); + + it('should return false if an ios device', () => { + spyOn(instanceUnderTest, 'isIosDevice').and.returnValue(true); + expect(instanceUnderTest.hasAutoselect).toBe(false); + }); + }); + + describe('activeDescendant getter', () => { + it('should return a formatted string of an item is focused', () => { + let id = 'foo', + focused = 1, + matches = ['foo']; + testHostInstance.configure({id}); + spyOnProperty(instanceUnderTest.state, 'currentState', 'get').and.returnValue({focused, matches}); + testHostFixture.detectChanges(); + expect(instanceUnderTest.activeDescendant).toBe(`${id}__option--${focused}`); + }); + }); +}); diff --git a/ui/src/app/widget/autocomplete/autocomplete.component.ts b/ui/src/app/widget/autocomplete/autocomplete.component.ts new file mode 100644 index 000000000..2e3256a8a --- /dev/null +++ b/ui/src/app/widget/autocomplete/autocomplete.component.ts @@ -0,0 +1,368 @@ +import { + Component, + Input, + Output, + EventEmitter, + OnInit, + OnDestroy, + OnChanges, + AfterViewInit, + ViewChild, + ViewChildren, + QueryList, + ElementRef, + SimpleChanges, + forwardRef +} from '@angular/core'; +import { FormControl, ControlValueAccessor, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; +import { Subscription } from 'rxjs/Subscription'; + +import 'rxjs/add/observable/interval'; +import 'rxjs/add/operator/combineLatest'; + +import { keyCodes, isPrintableKeyCode } from '../../shared/keycodes'; +import { AutoCompleteState, AutoCompleteStateEmitter, defaultState } from './autocomplete.model'; + +const POLL_TIMEOUT = 1000; +const MIN_LENGTH = 2; +const INPUT_FIELD_INDEX = -1; + +@Component({ + selector: 'auto-complete', + templateUrl: './autocomplete.component.html', + styleUrls: ['./autocomplete.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AutoCompleteComponent), + multi: true + } + ] +}) +export class AutoCompleteComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit, ControlValueAccessor { + @Input() required: boolean | null = true; + @Input() defaultValue = ''; + @Input() options: string[] = []; + @Input() id: string; + @Input() autoSelect = false; + @Input() allowCustom = false; + @Input() noneFoundText = 'No Options Found'; + + @ViewChild('inputField') inputField: ElementRef; + @ViewChildren('matchElement', { read: ElementRef }) listItems: QueryList; + + focused: number; + selected: number; + disabled = false; + + isMenuOpen$: Observable; + query$: Observable; + + $pollInput: Observable; + $checkInputValue: Observable; + $pollSubscription: Subscription; + + matches$: Observable; + numMatches: number; + + input: FormControl = new FormControl(); + elementReferences: {[id: number]: ElementRef} = {}; + state: AutoCompleteStateEmitter = new AutoCompleteStateEmitter(); + + menuIsVisible$: Observable; + + private ngUnsubscribe: Subject = new Subject(); + + propagateChange = (_: any | null) => { }; + propagateTouched = (_: any | null) => { }; + + constructor() {} + + ngOnInit(): void { + this.$pollInput = Observable.interval(POLL_TIMEOUT); + this.$checkInputValue = this.$pollInput.takeUntil(this.ngUnsubscribe).combineLatest(this.input.valueChanges); + this.$pollSubscription = this.$checkInputValue.subscribe(([polled, value]) => { + const inputReference = this.inputField.nativeElement; + const queryHasChanged = inputReference && inputReference.value !== value; + if (queryHasChanged) { + this.input.setValue(inputReference.value); + this.input.updateValueAndValidity(); + } + }); + + this.input + .valueChanges + .subscribe((query) => { + let matches = []; + if (query && query.length >= MIN_LENGTH) { + matches = this.state.currentState.options + .filter((option: string) => option.toLocaleLowerCase().match(query.toLocaleLowerCase())); + } + this.state.setState({ matches }); + }); + this.input.valueChanges.subscribe(newValue => this.handleInputChange(newValue)); + this.input.setValue(this.defaultValue); + + this.menuIsVisible$ = this.state.changes$.map(state => state.menuOpen); + } + + ngOnDestroy(): void { + this.$pollSubscription.unsubscribe(); + this.ngUnsubscribe.next(); + this.ngUnsubscribe.complete(); + } + + ngAfterViewInit(): void { + this.listItems.changes.subscribe((changes) => this.setElementReferences(changes)); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.options) { + this.state.setState({options: this.options}); + } + const focusedChanged = !!changes.focused; + if (focusedChanged) { + const componentLostFocus = changes.focused.currentValue === null; + const focusDifferentElement = focusedChanged && !componentLostFocus; + if (focusDifferentElement) { + this.elementReferences[this.focused].nativeElement.focus(); + } + const focusedInput = this.focused === INPUT_FIELD_INDEX; + const componentGainedFocus = focusedChanged && changes.focused.previousValue === null; + const selectAllText = focusedInput && componentGainedFocus; + if (selectAllText) { + let inputElement = this.inputField.nativeElement; + inputElement.setSelectionRange(0, inputElement.value.length); + } + } + + if (this.required) { + this.input.setValidators([Validators.required]); + } else { + this.input.clearValidators(); + } + + if (this.disabled) { + this.input.disable(); + } else { + this.input.enable(); + } + } + + writeValue(value: any): void { + this.input.setValue(value); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + this.propagateTouched = fn; + } + + setDisabledState(isDisabled: boolean = false): void { + this.disabled = isDisabled; + } + + setElementReferences(changes): void { + this.elementReferences = { + [INPUT_FIELD_INDEX]: this.inputField + }; + this.listItems.map((item, index) => { + this.elementReferences[index] = item; + }); + } + + handleComponentBlur(newState: any = {}): void { + let { selected, options } = this.state.currentState, + query = newState.query || this.state.currentState.query; + if (options[selected]) { + this.propagateChange(options[selected]); + } else if (this.allowCustom) { + this.propagateChange(query); + } + this.state.setState({ + focused: null, + selected: null, + menuOpen: newState.menuOpen || false, + query: query + }); + this.propagateTouched(null); + } + + handleOptionBlur(event: FocusEvent, index): void { + const elm = event.relatedTarget as HTMLElement; + const { focused, menuOpen, options, selected } = this.state.currentState; + const focusingOutsideComponent = event.relatedTarget === null; + const focusingInput = elm.id === this.elementReferences[-1].nativeElement.id; + const focusingAnotherOption = focused !== index && focused !== -1; + const blurComponent = (!focusingAnotherOption && focusingOutsideComponent) || !(focusingAnotherOption || focusingInput); + if (blurComponent) { + const keepMenuOpen = menuOpen && this.isIosDevice(); + this.handleComponentBlur({ + menuOpen: keepMenuOpen, + query: options[selected] + }); + } + } + + handleInputBlur(event: FocusEvent): void { + const { focused, menuOpen, options, query, selected } = this.state.currentState; + const focusingAnOption = focused !== -1; + const isIosDevice = this.isIosDevice(); + if (!focusingAnOption) { + const keepMenuOpen = menuOpen && isIosDevice; + const newQuery = isIosDevice ? query : options[selected]; + this.handleComponentBlur({ + menuOpen: keepMenuOpen, + query: newQuery + }); + } + } + + handleInputChange(query: string): void { + const queryEmpty = query.length === 0; + const queryChanged = this.state.currentState.query.length !== query.length; + const queryLongEnough = query.length >= MIN_LENGTH; + const autoselect = this.hasAutoselect; + const optionsAvailable = this.state.currentState.matches.length > 0; + const searchForOptions = (!queryEmpty && queryChanged && queryLongEnough); + this.state.setState({ + menuOpen: searchForOptions, + selected: searchForOptions ? ((autoselect && optionsAvailable) ? 0 : -1) : null, + query + }); + if (this.allowCustom) { + this.propagateChange(query); + } + } + + handleInputFocus(event: FocusEvent): void { + this.state.setState({ + focused: INPUT_FIELD_INDEX + }); + } + handleOptionFocus(index: number) { + this.state.setState({ + focused: index, + selected: index + }); + } + handleOptionClick(event: MouseEvent | KeyboardEvent, index: number): void { + let { matches, options } = this.state.currentState; + const selectedOption = matches[index]; + if (selectedOption) { + this.propagateChange(selectedOption); + } + this.input.setValue(selectedOption); + this.state.setState({ + focused: -1, + selected: -1, + menuOpen: false, + query: selectedOption + }); + } + + handleKeyDown(event: KeyboardEvent): void { + switch (keyCodes[event.keyCode]) { + case 'up': + this.handleUpArrow(event); + break; + case 'down': + this.handleDownArrow(event); + break; + case 'space': + this.handleSpace(event); + break; + case 'enter': + this.handleEnter(event); + break; + case 'escape': + this.handleComponentBlur(); + break; + } + } + + handleUpArrow(event: KeyboardEvent): void { + event.preventDefault(); + const isNotAtTop = this.state.currentState.selected !== 0; + const allowMoveUp = isNotAtTop && this.state.currentState.menuOpen; + if (allowMoveUp) { + this.handleOptionFocus(this.state.currentState.selected - 1); + } + } + + handleDownArrow(event: KeyboardEvent): void { + event.preventDefault(); + const isNotAtBottom = this.state.currentState.selected !== this.state.currentState.matches.length - 1; + const allowMoveDown = isNotAtBottom && this.state.currentState.menuOpen; + if (allowMoveDown) { + this.handleOptionFocus(this.state.currentState.selected + 1); + } + } + + handleSpace(event: KeyboardEvent): void { + event.preventDefault(); + const focusIsOnOption = this.state.currentState.focused !== -1; + if (focusIsOnOption) { + this.handleOptionClick(event, this.state.currentState.focused); + } + } + + handleEnter(event: KeyboardEvent): void { + if (this.state.currentState.menuOpen) { + event.preventDefault(); + const hasSelectedOption = this.state.currentState.selected >= 0; + if (hasSelectedOption) { + this.handleOptionClick(event, this.state.currentState.selected); + } else { + this.handleComponentBlur({ + focused: -1, + selected: -1, + menuOpen: false + }); + } + } + } + + handleOptionMouseDown(event: MouseEvent | { preventDefault: () => {} }): void { + event.preventDefault(); + } + + handleOptionMouseEnter(index: number): void { + this.state.setState({ + hovered: index + }); + } + + handleOptionMouseOut(): void { + this.state.setState({ + hovered: null + }); + } + + isIosDevice() { + return !!(navigator.userAgent.match(/(iPod|iPhone|iPad)/g) && navigator.userAgent.match(/AppleWebKit/g)); + } + + get hasAutoselect(): boolean { + return this.isIosDevice() ? false : this.autoSelect; + } + + get activeDescendant (): string { + let state = this.state.currentState, + focused = state.focused, + optionFocused = focused !== -1 && focused !== null; + return optionFocused ? `${ this.id }__option--${ focused }` : 'false'; + } + + get displayState(): any { + return { + ...this.state.currentState, + options: [] + }; + } +} /* istanbul ignore next */ diff --git a/ui/src/app/widget/autocomplete/autocomplete.model.ts b/ui/src/app/widget/autocomplete/autocomplete.model.ts new file mode 100644 index 000000000..ac6389cc8 --- /dev/null +++ b/ui/src/app/widget/autocomplete/autocomplete.model.ts @@ -0,0 +1,48 @@ +import { ElementRef } from '@angular/core'; +import { Subject } from 'rxjs/Subject'; + +export const defaultState = { + focused: null, + selected: null, + hovered: null, + menuOpen: false, + query: '', + options: [], + matches: [] +}; + +export interface AutoCompleteState { + focused: number | null; + selected: number | null; + hovered: number | null; + menuOpen: boolean; + query: string; + options: string[]; + matches: string[]; +} + +export class AutoCompleteStateEmitter { + private subj = new Subject(); + + private state: AutoCompleteState; + + changes$ = this.subj.asObservable(); + + constructor( + private defaults: AutoCompleteState = defaultState + ) { + this.state = {...defaults}; + } + + get currentState(): AutoCompleteState { + return this.state; + } + + setState(change: Partial) { + this.state = { + ...this.state, + ...change + }; + this.subj.next(this.state); + } +} diff --git a/ui/src/app/widget/autocomplete/keys.service.ts b/ui/src/app/widget/autocomplete/keys.service.ts new file mode 100644 index 000000000..dca1f77ee --- /dev/null +++ b/ui/src/app/widget/autocomplete/keys.service.ts @@ -0,0 +1,4 @@ +export interface KeyService { + codes: any; + isPrintableKeyCode (): boolean; +} diff --git a/ui/src/app/widget/validation/validation-class.directive.spec.ts b/ui/src/app/widget/validation/validation-class.directive.spec.ts new file mode 100644 index 000000000..ab662feb5 --- /dev/null +++ b/ui/src/app/widget/validation/validation-class.directive.spec.ts @@ -0,0 +1,61 @@ +import { Component, DebugElement } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ReactiveFormsModule, FormsModule, NgControl, AbstractControl } from '@angular/forms'; +import { ValidationClassDirective } from './validation-class.directive'; + +import * as constants from '../../shared/constant'; + +@Component({ + template: `` +}) +class TestComponent { + foo = ''; +} + +describe('Validation Classes Directive', () => { + let component: TestComponent; + let fixture: ComponentFixture; + let inputEl: DebugElement; + let ctrl: AbstractControl; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [], + imports: [ + FormsModule, + ReactiveFormsModule + ], + declarations: [ + ValidationClassDirective, + TestComponent + ], + }); + + fixture = TestBed.createComponent(TestComponent); + inputEl = fixture.debugElement.query(By.css('input')); + ctrl = inputEl.injector.get(NgControl).control; + fixture.detectChanges(); + }); + + describe('directive', () => { + it('should compile', () => { + expect(inputEl).toBeDefined(); + }); + }); + + describe('validation classes', () => { + it('should add the is-invalid class when the control is invalid and has been touched', () => { + ctrl.markAsTouched(); + fixture.detectChanges(); + expect(inputEl.classes['is-invalid']).toBe(true); + }); + + it('should add the is-valid class when the control is valid and has been touched', () => { + ctrl.markAsTouched(); + ctrl.setValue('foo'); + fixture.detectChanges(); + expect(inputEl.classes['is-valid']).toBe(true); + }); + }); +}); diff --git a/ui/src/app/widget/validation/validation-class.directive.ts b/ui/src/app/widget/validation/validation-class.directive.ts new file mode 100644 index 000000000..5459731c9 --- /dev/null +++ b/ui/src/app/widget/validation/validation-class.directive.ts @@ -0,0 +1,30 @@ +import { Directive, Self, HostBinding, Input } from '@angular/core'; +import { NgControl } from '@angular/forms'; + +@Directive({ + selector: '[formControlName],[ngModel],[formControl]' +}) +export class ValidationClassDirective { + public constructor( + @Self() private ngCtrl: NgControl + ) { } + + @Input() disableValidation = false; + + @HostBinding('class.is-invalid') + get hasErrors() { + const ctrl = this.ngCtrl.control; + return (ctrl.invalid && this.isTouched) && !this.disableValidation; + } + + @HostBinding('class.is-valid') + get hasSuccess() { + const ctrl = this.ngCtrl.control; + return (ctrl.valid && this.isTouched) && !this.disableValidation; + } + + get isTouched() { + const ctrl = this.ngCtrl.control; + return ctrl.touched; + } +} /* istanbul ignore next */ diff --git a/ui/src/assets/favicon-96x96.png b/ui/src/assets/favicon-96x96.png new file mode 100644 index 000000000..e19516fff Binary files /dev/null and b/ui/src/assets/favicon-96x96.png differ diff --git a/ui/src/assets/shibboleth_icon_color_130x130.png b/ui/src/assets/shibboleth_icon_color_130x130.png new file mode 100644 index 000000000..313df313e Binary files /dev/null and b/ui/src/assets/shibboleth_icon_color_130x130.png differ diff --git a/ui/src/assets/shibboleth_logowordmark_color.png b/ui/src/assets/shibboleth_logowordmark_color.png new file mode 100644 index 000000000..bf1ff3b92 Binary files /dev/null and b/ui/src/assets/shibboleth_logowordmark_color.png differ diff --git a/ui/src/data/descriptors.mock.ts b/ui/src/data/descriptors.mock.ts new file mode 100644 index 000000000..acf15fc88 --- /dev/null +++ b/ui/src/data/descriptors.mock.ts @@ -0,0 +1,52 @@ +export const MOCK_DESCRIPTORS = [ + { + entityId: 'https://carmenwiki.osu.edu/enc/shibboleth', + serviceProviderName: 'Box.net Encrypted Metadata', + serviceEnabled: true + }, + { + entityId: 'https://carmenwiki.osu.edu/shibboleth', + serviceProviderName: 'Box.net Metadata', + serviceEnabled: true + }, + { + entityId: 'http://login.campustelevideo.com', + serviceProviderName: 'Campus Televideo Login SP Metadata', + serviceEnabled: false + }, + { + entityId: 'http://login.campustelevideo.com/test', + serviceProviderName: 'Campus Televideo Login TEST SP Metadata', + serviceEnabled: true + }, + { + entityId: 'http://corp.collegenet.com/shibboleth-sp', + serviceProviderName: 'Collegenet Corp Shibboleth SP', + serviceEnabled: true + }, + { + entityId: 'http://app.cvent.com', + serviceProviderName: 'Cvent App', + serviceEnabled: true + }, + { + entityId: 'http://casper.lmu.edu/shib-prot', + serviceProviderName: 'Casper IMU Shibboleth', + serviceEnabled: true + }, + { + entityId: 'http://confluence.unicon.net/shib', + serviceProviderName: 'Unicon Confluence', + serviceEnabled: true + }, + { + entityId: 'http://jira.unicon.net/shib', + serviceProviderName: 'Unicon Jira', + serviceEnabled: true + }, + { + entityId: 'http://web.unicon.net/shib', + serviceProviderName: 'Unicon Web', + serviceEnabled: true + } +]; diff --git a/ui/src/data/entities.json b/ui/src/data/entities.json new file mode 100644 index 000000000..ad972d8b0 --- /dev/null +++ b/ui/src/data/entities.json @@ -0,0 +1,94 @@ +{ + "entityId": "string", + "serviceProviderName": "string", + "organization": { + "name": "string", + "displayName": "string", + "url": "string" + }, + "contacts": [ + { + "name": "string", + "displayName": "string", + "url": "string" + } + ], + "mdui": { + "displayName": "string", + "informationUrl": "string", + "privacyStatementUrl": "string", + "logoUrl": "string", + "logoHeight": 100, + "logoWidth": 100, + "description": "string" + }, + "securityInfo": { + "x509CertificateAvailable": true, + "authenticationRequestsSigned": true, + "wantAssertionsSigned": true, + "x509Certificates": [ + { + "name": "string", + "type": "string", + "value": "string" + } + ] + }, + "assertionConsumerServices": [ + { + "binding": "string", + "locationUrl": "string", + "makeDefault": true + } + ], + "serviceProviderSsoDescriptor": { + "protocolSupportEnum": "string", + "nameIdFormats": [ + "string" + ] + }, + + "logoutEndpoints": [ + { + "url": "string", + "bindingType": "string" + } + ], + + "serviceEnabled": true, + + "createdDate": "string (date)", + "modifiedDate": "string (date)", + + "relyingPartyOverrides": { + "signAssertion": true, + "dontSignResponse": true, + "turnOffEncryption": true, + "useSha": true, + "ignoreAuthenticationMethod": true, + "omitNotBefore": true, + "responderId": "string", + "nameIdFormats": [ + "string" + ], + "authenticationMethods": [ + "string" + ] + }, + + "attributeRelease": { + "eduPersonPrincipalName": true, + "uid": true, + "mail": true, + "surname": true, + "givenName": true, + "displayName": true, + "eduPersonAffiliation": true, + "eduPersonScopedAffiliation": true, + "eduPersonPrimaryAffiliation": true, + "eduPersonEntitlement": true, + "eduPersonAssurance": true, + "eduPersonUniqueId": true, + "employeeNumber": true + } +} \ No newline at end of file diff --git a/ui/src/environments/environment.prod.ts b/ui/src/environments/environment.prod.ts new file mode 100644 index 000000000..3612073bc --- /dev/null +++ b/ui/src/environments/environment.prod.ts @@ -0,0 +1,3 @@ +export const environment = { + production: true +}; diff --git a/ui/src/environments/environment.ts b/ui/src/environments/environment.ts new file mode 100644 index 000000000..b7f639aec --- /dev/null +++ b/ui/src/environments/environment.ts @@ -0,0 +1,8 @@ +// The file contents for the current environment will overwrite these during build. +// The build system defaults to the dev environment which uses `environment.ts`, but if you do +// `ng build --env=prod` then `environment.prod.ts` will be used instead. +// The list of which env maps to which file can be found in `.angular-cli.json`. + +export const environment = { + production: false +}; diff --git a/ui/src/favicon.ico b/ui/src/favicon.ico new file mode 100644 index 000000000..9c1f419d3 Binary files /dev/null and b/ui/src/favicon.ico differ diff --git a/ui/src/index.html b/ui/src/index.html new file mode 100644 index 000000000..e63821220 --- /dev/null +++ b/ui/src/index.html @@ -0,0 +1,15 @@ + + + + + Ui + + + + + + + + + + diff --git a/ui/src/locale/en.xlf b/ui/src/locale/en.xlf new file mode 100644 index 000000000..be06dbe55 --- /dev/null +++ b/ui/src/locale/en.xlf @@ -0,0 +1,1976 @@ + + + + + + Metadata Provider Management + Metadata Provider Management + + app/app.component.ts + 7 + + + + Manage Providers + Manage Providers + + app/app.component.ts + 17 + + + + Add new provider + Add new provider + + app/app.component.ts + 23 + + + + Logout + Logout + + app/app.component.ts + 29 + + + + Links to Shibboleth resources: + Links to Shibboleth resources: + + app/app.component.ts + 42 + + + + Home Page + Home Page + + app/app.component.ts + 43 + + + + Wiki + Wiki + + app/app.component.ts + 44 + + + + Issue Tracker + Issue Tracker + + app/app.component.ts + 45 + + + + Mailing List + Mailing List + + app/app.component.ts + 46 + + + + Copyright + Copyright + + app/app.component.ts + 48 + + + + Service Provider Name (Dashboard Display Only) + Service Provider Name (Dashboard Display Only) + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 7 + + + app/metadata-provider/component/forms/finish-form.component.ts + 29 + + + app/metadata-provider/container/upload-provider.component.ts + 24 + + + app/metadata-provider/container/blank-provider.component.ts + 22 + + + + Service Provider Name (Dashboard Display Only) popover + Service Provider Name (Dashboard Display Only) popover + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 10 + + + + Service Provider Entity ID + Service Provider Entity ID + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 21 + + + app/metadata-provider/component/forms/finish-form.component.ts + 31 + + + app/metadata-provider/container/blank-provider.component.ts + 34 + + + + Service Provider Entity ID popover + Service Provider Entity ID popover + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 24 + + + + Enable this service? + Enable this service? + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 36 + + + app/metadata-provider/component/forms/finish-form.component.ts + 33 + + + + Organization Name + Organization Name + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 43 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 6 + + + app/metadata-provider/component/forms/finish-form.component.ts + 43 + + + + Organization Name popover + Organization Name popover + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 46 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 8 + + + + Organization Display Name + Organization Display Name + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 54 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 24 + + + app/metadata-provider/component/forms/finish-form.component.ts + 45 + + + + Organization Display Name popover + Organization Display Name popover + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 57 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 27 + + + + * These three fields must all be entered if any single field has a value. + * These three fields must all be entered if any single field has a value. + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 74 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 60 + + + + Organization URL + Organization URL + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 65 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 43 + + + app/metadata-provider/component/forms/finish-form.component.ts + 47 + + + + Organization URL popover + Organization URL popover + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 68 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 46 + + + + Contact Information: + Contact Information: + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 78 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 65 + + + app/metadata-provider/component/forms/finish-form.component.ts + 49 + + + + Add Contact + Add Contact + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 79 + + + + Name + Name + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 102 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 93 + + + + Name popover + Name popover + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 103 + + + + Type + Type + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 112 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 111 + + + app/metadata-provider/component/forms/key-info-form.component.ts + 120 + + + app/metadata-provider/component/forms/finish-form.component.ts + 155 + + + + Type popover + Type popover + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 113 + + + + Email Address + Email Address + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 128 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 135 + + + + Email Address popover + Email Address popover + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 129 + + + + Must be a valid Email Address + Must be a valid Email Address + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 134 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 149 + + + + Add Contact + Add Contact + + app/metadata-provider/component/forms/organization-info-form.component.ts + 66 + + + + Contact Name Popover + Contact Name Popover + + app/metadata-provider/component/forms/organization-info-form.component.ts + 94 + + + + Contact Type Popover + Contact Type Popover + + app/metadata-provider/component/forms/organization-info-form.component.ts + 112 + + + + Contact Email Popover + Contact Email Popover + + app/metadata-provider/component/forms/organization-info-form.component.ts + 136 + + + + Display Name + Display Name + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 6 + + + app/metadata-provider/component/forms/finish-form.component.ts + 77 + + + + Typically, the IdP Display Name field will be presented on IdP discovery service interfaces. + Typically, the IdP Display Name field will be presented on IdP discovery service interfaces. + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 7 + + + + Information URL + Information URL + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 22 + + + app/metadata-provider/component/forms/finish-form.component.ts + 79 + + + + The IdP Information URL is a link to a comprehensive information page about the IdP. This page should expand on the content of the IdP Description field. + The IdP Information URL is a link to a comprehensive information page about the IdP. This page should expand on the content of the IdP Description field. + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 23 + + + + Description + Description + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 39 + + + app/metadata-provider/component/forms/finish-form.component.ts + 81 + + + + The IdP Description is a brief description of the IdP service. On a well-designed discovery interface, the IdP Description will be presented to the user in addition to the IdP Display Name, and so the IdP Description helps disambiguate duplicate or similar IdP Display Names. + The IdP Description is a brief description of the IdP service. On a well-designed discovery interface, the IdP Description will be presented to the user in addition to the IdP Display Name, and so the IdP Description helps disambiguate duplicate or similar IdP Display Names. + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 40 + + + + Privacy Statement URL + Privacy Statement URL + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 67 + + + + The IdP Privacy Statement URL is a link to the IdP's Privacy Statement. The content of the Privacy Statement should be targeted at end users. + The IdP Privacy Statement URL is a link to the IdP's Privacy Statement. The content of the Privacy Statement should be targeted at end users. + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 68 + + + + Logo URL + Logo URL + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 84 + + + app/metadata-provider/component/forms/finish-form.component.ts + 85 + + + + The IdP Logo URL in metadata points to an image file on a remote server. A discovery service, for example, may rely on a visual cue (i.e., a logo) instead of or in addition to the IdP Display Name. + The IdP Logo URL in metadata points to an image file on a remote server. A discovery service, for example, may rely on a visual cue (i.e., a logo) instead of or in addition to the IdP Display Name. + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 85 + + + + Logo Width + Logo Width + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 103 + + + app/metadata-provider/component/forms/finish-form.component.ts + 87 + + + + The logo should have a minimum width of 100 pixels + The logo should have a minimum width of 100 pixels + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 104 + + + + Must be an integer equal to or greater than 0 + Must be an integer equal to or greater than 0 + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 108 + + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 127 + + + + Logo Height + Logo Height + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 114 + + + app/metadata-provider/component/forms/finish-form.component.ts + 89 + + + + The logo should have a minimum height of 75 pixels and a maximum height of 150 pixels (or the application will scale it proportionally) + The logo should have a minimum height of 75 pixels and a maximum height of 150 pixels (or the application will scale it proportionally) + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 115 + + + + Is there a X509 Certificate? + Is there a X509 Certificate? + + app/metadata-provider/component/forms/key-info-form.component.ts + 6 + + + app/metadata-provider/component/forms/finish-form.component.ts + 141 + + + + Is there a X509 Certificate popover + Is there a X509 Certificate popover + + app/metadata-provider/component/forms/key-info-form.component.ts + 7 + + + app/metadata-provider/component/forms/key-info-form.component.ts + 34 + + + + Yes + Yes + + app/metadata-provider/component/forms/key-info-form.component.ts + 21 + + + app/metadata-provider/component/forms/key-info-form.component.ts + 48 + + + app/metadata-provider/component/forms/key-info-form.component.ts + 75 + + + app/metadata-provider/component/forms/assertion-form.component.ts + 94 + + + app/metadata-provider/component/forms/attribute-release-form.component.ts + 8 + + + + No + No + + app/metadata-provider/component/forms/key-info-form.component.ts + 27 + + + app/metadata-provider/component/forms/key-info-form.component.ts + 54 + + + app/metadata-provider/component/forms/key-info-form.component.ts + 81 + + + + Authentication Requests Signed? + Authentication Requests Signed? + + app/metadata-provider/component/forms/key-info-form.component.ts + 33 + + + app/metadata-provider/component/forms/finish-form.component.ts + 143 + + + + Want Assertions Signed? + Want Assertions Signed? + + app/metadata-provider/component/forms/key-info-form.component.ts + 60 + + + app/metadata-provider/component/forms/finish-form.component.ts + 145 + + + + Want Assertions Signed + Want Assertions Signed + + app/metadata-provider/component/forms/key-info-form.component.ts + 61 + + + + X509 Certificates: + X509 Certificates: + + app/metadata-provider/component/forms/key-info-form.component.ts + 88 + + + app/metadata-provider/component/forms/finish-form.component.ts + 142 + + + app/metadata-provider/component/forms/finish-form.component.ts + 146 + + + + Add Certificate + Add Certificate + + app/metadata-provider/component/forms/key-info-form.component.ts + 89 + + + + Certificate Name (Display Only) + Certificate Name (Display Only) + + app/metadata-provider/component/forms/key-info-form.component.ts + 114 + + + app/metadata-provider/component/forms/finish-form.component.ts + 154 + + + + Certificate + Certificate + + app/metadata-provider/component/forms/key-info-form.component.ts + 129 + + + app/metadata-provider/component/forms/finish-form.component.ts + 156 + + + + Assertion Consumer Service Endpoints: + Assertion Consumer Service Endpoints: + + app/metadata-provider/component/forms/assertion-form.component.ts + 5 + + + app/metadata-provider/component/forms/finish-form.component.ts + 176 + + + + Add Endpoint + Add Endpoint + + app/metadata-provider/component/forms/assertion-form.component.ts + 6 + + + app/metadata-provider/component/forms/logout-form.component.ts + 6 + + + + (default) + (default) + + app/metadata-provider/component/forms/assertion-form.component.ts + 20 + + + + Assertion Consumer Service Location + Assertion Consumer Service Location + + app/metadata-provider/component/forms/assertion-form.component.ts + 32 + + + + Assertion Consumer Service Location popover + Assertion Consumer Service Location popover + + app/metadata-provider/component/forms/assertion-form.component.ts + 33 + + + + Must be a valid URL + Must be a valid URL + + app/metadata-provider/component/forms/assertion-form.component.ts + 46 + + + + Assertion Consumer Service Location Binding + Assertion Consumer Service Location Binding + + app/metadata-provider/component/forms/assertion-form.component.ts + 53 + + + app/metadata-provider/component/forms/finish-form.component.ts + 184 + + + + Assertion Consumer Service Location Binding + Assertion Consumer Service Location Binding + + app/metadata-provider/component/forms/assertion-form.component.ts + 54 + + + + Select Binding Type + Select Binding Type + + app/metadata-provider/component/forms/assertion-form.component.ts + 66 + + + app/metadata-provider/component/forms/logout-form.component.ts + 59 + + + + Mark as Default + Mark as Default + + app/metadata-provider/component/forms/assertion-form.component.ts + 76 + + + + Mark as Default + Mark as Default + + app/metadata-provider/component/forms/assertion-form.component.ts + 77 + + + + URL + URL + + app/metadata-provider/component/forms/logout-form.component.ts + 30 + + + + Logout Endpoints Url popover + Logout Endpoints Url popover + + app/metadata-provider/component/forms/logout-form.component.ts + 31 + + + + Binding Type + Binding Type + + app/metadata-provider/component/forms/logout-form.component.ts + 47 + + + app/metadata-provider/component/forms/finish-form.component.ts + 122 + + + + Logout Endpoints Binding Type popover + Logout Endpoints Binding Type popover + + app/metadata-provider/component/forms/logout-form.component.ts + 48 + + + + Protocol Support Enumeration + Protocol Support Enumeration + + app/metadata-provider/component/forms/descriptor-info-form.component.ts + 6 + + + app/metadata-provider/component/forms/finish-form.component.ts + 99 + + + + Protocol Support Enumeration popover + Protocol Support Enumeration popover + + app/metadata-provider/component/forms/descriptor-info-form.component.ts + 7 + + + + Select Protocol + Select Protocol + + app/metadata-provider/component/forms/descriptor-info-form.component.ts + 19 + + + + Add NameID Format + Add NameID Format + + app/metadata-provider/component/forms/descriptor-info-form.component.ts + 24 + + + app/metadata-provider/component/forms/relying-party-form.component.ts + 35 + + + + Sign the Assertion + Sign the Assertion + + app/metadata-provider/component/forms/relying-party-form.component.ts + 6 + + + app/metadata-provider/component/forms/finish-form.component.ts + 204 + + + + Don't Sign the Response + Don't Sign the Response + + app/metadata-provider/component/forms/relying-party-form.component.ts + 13 + + + app/metadata-provider/component/forms/finish-form.component.ts + 206 + + + + Turn Off Encryption of Response + Turn Off Encryption of Response + + app/metadata-provider/component/forms/relying-party-form.component.ts + 20 + + + app/metadata-provider/component/forms/finish-form.component.ts + 208 + + + + Use SHA1 Signing Algorithm + Use SHA1 Signing Algorithm + + app/metadata-provider/component/forms/relying-party-form.component.ts + 27 + + + app/metadata-provider/component/forms/finish-form.component.ts + 210 + + + + NameID Format to Send + NameID Format to Send + + app/metadata-provider/component/forms/relying-party-form.component.ts + 34 + + + app/metadata-provider/component/forms/finish-form.component.ts + 212 + + + + Authentication Methods to Use + Authentication Methods to Use + + app/metadata-provider/component/forms/relying-party-form.component.ts + 52 + + + + Add Authentication Method + Add Authentication Method + + app/metadata-provider/component/forms/relying-party-form.component.ts + 53 + + + + Ignore any SP-Requested Authentication Method + Ignore any SP-Requested Authentication Method + + app/metadata-provider/component/forms/relying-party-form.component.ts + 70 + + + app/metadata-provider/component/forms/finish-form.component.ts + 230 + + + + Omit Not Before Condition + Omit Not Before Condition + + app/metadata-provider/component/forms/relying-party-form.component.ts + 77 + + + app/metadata-provider/component/forms/finish-form.component.ts + 232 + + + + ResponderID + ResponderID + + app/metadata-provider/component/forms/relying-party-form.component.ts + 83 + + + app/metadata-provider/component/forms/finish-form.component.ts + 234 + + + + Attribute Name + Attribute Name + + app/metadata-provider/component/forms/attribute-release-form.component.ts + 7 + + + app/metadata-provider/component/forms/finish-form.component.ts + 246 + + + + Enable this service upon saving? + Enable this service upon saving? + + app/metadata-provider/component/forms/finish-form.component.ts + 9 + + + + Enable this service upon saving popover + Enable this service upon saving popover + + app/metadata-provider/component/forms/finish-form.component.ts + 11 + + + + Name and Entity ID + Name and Entity ID + + app/metadata-provider/component/forms/finish-form.component.ts + 26 + + + + Organization Information + Organization Information + + app/metadata-provider/component/forms/finish-form.component.ts + 40 + + + app/metadata-provider/container/blank-provider.component.ts + 11 + + + + Given Name + Given Name + + app/metadata-provider/component/forms/finish-form.component.ts + 55 + + + + Email Address + Email Address + + app/metadata-provider/component/forms/finish-form.component.ts + 56 + + + + Contact Type + Contact Type + + app/metadata-provider/component/forms/finish-form.component.ts + 57 + + label--contact-type + + + User Interface / MDUI Information + User Interface / MDUI Information + + app/metadata-provider/component/forms/finish-form.component.ts + 74 + + + + Privacy Statement URL + Privacy Statement URL + + app/metadata-provider/component/forms/finish-form.component.ts + 83 + + + + SP SSO Descriptor Information + SP SSO Descriptor Information + + app/metadata-provider/component/forms/finish-form.component.ts + 96 + + + + NameID Format + NameID Format + + app/metadata-provider/component/forms/finish-form.component.ts + 101 + + + + Security Information + Security Information + + app/metadata-provider/component/forms/finish-form.component.ts + 138 + + + + Assertion Consumer Service Location + Assertion Consumer Service Location + + app/metadata-provider/component/forms/finish-form.component.ts + 183 + + + + Default Authentication Method(s) + Default Authentication Method(s) + + app/metadata-provider/component/forms/finish-form.component.ts + 221 + + + + True + True + + app/metadata-provider/component/forms/finish-form.component.ts + 247 + + + + False + False + + app/metadata-provider/component/forms/finish-form.component.ts + 248 + + + + Add a new metadata provider + Add a new metadata provider + + app/metadata-provider/container/new-provider.component.ts + 6 + + + + How are you adding the metadata information? + How are you adding the metadata information? + + app/metadata-provider/container/new-provider.component.ts + 11 + + + + Upload/URL + Upload/URL + + app/metadata-provider/container/new-provider.component.ts + 16 + + + + Create File + Create File + + app/metadata-provider/container/new-provider.component.ts + 25 + + + + 1. Name and Upload Url + 1. Name and Upload Url + + app/metadata-provider/container/upload-provider.component.ts + 6 + + + + + Save Metadata Provider + + + Save Metadata Provider + + + app/metadata-provider/container/upload-provider.component.ts + 11 + + + app/edit-provider/component/wizard-nav.component.ts + 33 + + + + Save + Save + + app/metadata-provider/container/upload-provider.component.ts + 16 + + + app/edit-provider/container/editor.component.ts + 39 + + + app/edit-provider/component/wizard-nav.component.ts + 36 + + + + + Service Provider Name is required + + + Service Provider Name is required + + + app/metadata-provider/container/upload-provider.component.ts + 31 + + + app/metadata-provider/container/blank-provider.component.ts + 28 + + + + Select Provider Metadata File + Select Provider Metadata File + + app/metadata-provider/container/upload-provider.component.ts + 37 + + + + Note: You can only import a file with a single entityID (EntityDescriptor element) in it. Anything more in that file will result in an error. + Note: You can only import a file with a single entityID (EntityDescriptor element) in it. Anything more in that file will result in an error. + + app/metadata-provider/container/upload-provider.component.ts + 56 + + + + Choose File + Choose File + + app/metadata-provider/container/upload-provider.component.ts + 41 + + + app/metadata-provider/container/upload-provider.component.ts + 42 + + + + OR + OR + + app/metadata-provider/container/upload-provider.component.ts + 48 + + + + Service Provider Metadata URL + Service Provider Metadata URL + + app/metadata-provider/container/upload-provider.component.ts + 52 + + + + 1. Name and EntityId + 1. Name and EntityId + + app/metadata-provider/container/blank-provider.component.ts + 6 + + + + Next + Next + + app/metadata-provider/container/blank-provider.component.ts + 14 + + + app/metadata-provider/container/blank-provider.component.ts + 48 + + + app/edit-provider/component/wizard-nav.component.ts + 27 + + + + Entity ID is required + Entity ID is required + + app/metadata-provider/container/blank-provider.component.ts + 40 + + + + Entity ID must be unique + Entity ID must be unique + + app/metadata-provider/container/blank-provider.component.ts + 44 + + + + Preview Provider + Preview Provider + + app/metadata-provider/component/preview-provider-dialog.component.ts + 2 + + + + Download File + Download File + + app/metadata-provider/component/preview-provider-dialog.component.ts + 13 + + + + Cancel + Cancel + + app/metadata-provider/component/preview-provider-dialog.component.ts + 15 + + + app/dashboard/component/delete-dialog.component.ts + 9 + + + app/edit-provider/component/unsaved-dialog.component.ts + 18 + + + app/edit-provider/container/editor.component.ts + 43 + + + + Delete Metadata Provider? + Delete Metadata Provider? + + app/dashboard/component/delete-dialog.component.ts + 2 + + + + You are deleting a metadata provider. This cannot be undone. Continue? + You are deleting a metadata provider. This cannot be undone. Continue? + + app/dashboard/component/delete-dialog.component.ts + 5 + + + + Delete + Delete + + app/dashboard/component/delete-dialog.component.ts + 8 + + + + Current Metadata Sources + Current Metadata Sources + + app/dashboard/container/dashboard.component.ts + 6 + + + + Incomplete Form + Incomplete Form + + app/dashboard/component/provider-item.component.ts + 14 + + + app/dashboard/component/provider-item.component.ts + 52 + + + + Service Provider Name: + Service Provider Name: + + app/dashboard/component/provider-item.component.ts + 33 + + + + Created Date: + Created Date: + + app/dashboard/component/provider-item.component.ts + 35 + + + + Service Provider Entity ID: + Service Provider Entity ID: + + app/dashboard/component/provider-item.component.ts + 40 + + + + Service Provider Status: + Service Provider Status: + + app/dashboard/component/provider-item.component.ts + 42 + + + + Enabled + Enabled + + app/dashboard/component/provider-item.component.ts + 48 + + + + Disabled + Disabled + + app/dashboard/component/provider-item.component.ts + 49 + + + + Search for a provider + Search for a provider + + app/dashboard/component/provider-search.component.ts + 8 + + + + Clear + Clear + + app/dashboard/component/provider-search.component.ts + 19 + + + + Save your information? + Save your information? + + app/edit-provider/component/unsaved-dialog.component.ts + 3 + + + + + You have not completed the wizard! Do you wish to save this information? You can finish the wizard later by clicking the + "Edit" + icon on the dashboard. + + + You have not completed the wizard! Do you wish to save this information? You can finish the wizard later by clicking the + "Edit" + icon on the dashboard. + + + app/edit-provider/component/unsaved-dialog.component.ts + 6 + + + + You have not saved your changes. If you exit this screen, your changes will be lost. + You have not saved your changes. If you exit this screen, your changes will be lost. + + app/edit-provider/component/unsaved-dialog.component.ts + 11 + + + + Finish Later + Finish Later + + app/edit-provider/component/unsaved-dialog.component.ts + 15 + + + + Discard Changes + Discard Changes + + app/edit-provider/component/unsaved-dialog.component.ts + 16 + + + + Add a new metadata provider + Add a new metadata provider + + app/edit-provider/container/wizard.component.ts + 8 + + + + Step of + Step of + + app/edit-provider/container/wizard.component.ts + 11 + + + + Back + Back + + app/edit-provider/component/wizard-nav.component.ts + 7 + + + + Information icon - press spacebar to read additional information for this form field + Information icon - press spacebar to read additional information for this form field + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 13 + + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 30 + + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 55 + + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 69 + + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 83 + + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 122 + + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 134 + + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 153 + + + app/metadata-provider/component/forms/assertion-form.component.ts + 36 + + + app/metadata-provider/component/forms/assertion-form.component.ts + 52 + + + app/metadata-provider/component/forms/assertion-form.component.ts + 71 + + + app/metadata-provider/component/forms/descriptor-info-form.component.ts + 10 + + + app/metadata-provider/component/forms/descriptor-info-form.component.ts + 28 + + + app/metadata-provider/component/forms/key-info-form.component.ts + 10 + + + app/metadata-provider/component/forms/key-info-form.component.ts + 32 + + + app/metadata-provider/component/forms/key-info-form.component.ts + 54 + + + app/metadata-provider/component/forms/key-info-form.component.ts + 106 + + + app/metadata-provider/component/forms/key-info-form.component.ts + 120 + + + app/metadata-provider/component/forms/key-info-form.component.ts + 138 + + + app/metadata-provider/component/forms/logout-form.component.ts + 34 + + + app/metadata-provider/component/forms/logout-form.component.ts + 46 + + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 10 + + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 21 + + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 33 + + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 56 + + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 68 + + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 81 + + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 96 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 11 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 25 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 39 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 81 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 94 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 113 + + + app/metadata-provider/component/forms/relying-party-form.component.ts + 13 + + + app/metadata-provider/component/forms/relying-party-form.component.ts + 25 + + + app/metadata-provider/component/forms/relying-party-form.component.ts + 37 + + + app/metadata-provider/component/forms/relying-party-form.component.ts + 49 + + + app/metadata-provider/component/forms/relying-party-form.component.ts + 61 + + + app/metadata-provider/component/forms/relying-party-form.component.ts + 86 + + + app/metadata-provider/component/forms/relying-party-form.component.ts + 111 + + + app/metadata-provider/component/forms/relying-party-form.component.ts + 123 + + + app/metadata-provider/component/forms/relying-party-form.component.ts + 132 + + + app/metadata-provider/component/forms/finish-form.component.ts + 14 + + + + Add NameID Format Popover + Add NameID Format Popover + + app/metadata-provider/component/forms/descriptor-info-form.component.ts + 25 + + + app/metadata-provider/component/forms/relying-party-form.component.ts + 58 + + + + Sign Assertion popover + Sign Assertion popover + + app/metadata-provider/component/forms/relying-party-form.component.ts + 10 + + + + Don't Sign Response popover + Don't Sign Response popover + + app/metadata-provider/component/forms/relying-party-form.component.ts + 22 + + + + Turn Off Encryption of Response popover + Turn Off Encryption of Response popover + + app/metadata-provider/component/forms/relying-party-form.component.ts + 34 + + + + Use SHA1 Signing Algorithm popover + Use SHA1 Signing Algorithm popover + + app/metadata-provider/component/forms/relying-party-form.component.ts + 46 + + + + Authentication Methods to Use popover + Authentication Methods to Use popover + + app/metadata-provider/component/forms/relying-party-form.component.ts + 83 + + + + Ignore any SP-Requested Authentication Method popover + Ignore any SP-Requested Authentication Method popover + + app/metadata-provider/component/forms/relying-party-form.component.ts + 108 + + + + Omit Not Before Condition popover + Omit Not Before Condition popover + + app/metadata-provider/component/forms/relying-party-form.component.ts + 120 + + + + ResponderId popover + ResponderId popover + + app/metadata-provider/component/forms/relying-party-form.component.ts + 129 + + + + Certificate Name Popover + Certificate Name Popover + + app/metadata-provider/component/forms/key-info-form.component.ts + 103 + + + + Certificate Type Popover + Certificate Type Popover + + app/metadata-provider/component/forms/key-info-form.component.ts + 117 + + + + Certificate Popover + Certificate Popover + + app/metadata-provider/component/forms/key-info-form.component.ts + 135 + + + + * These three fields must all be entered if any single field has a value. + * These three fields must all be entered if any single field has a value. + + app/metadata-provider/component/forms/organization-info-form.component.ts + 45 + + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 89 + + + + ResponderID + ResponderID + + app/metadata-provider/component/forms/relying-party-form.component.ts + 128 + + + + User Interface / MDUI Information + User Interface / MDUI Information + + app/metadata-provider/component/i18n-text-info.component.ts + 3 + + + + SP SSO Descriptor Information + SP SSO Descriptor Information + + app/metadata-provider/component/i18n-text-info.component.ts + 4 + + + + Logout Endpoints + Logout Endpoints + + app/metadata-provider/component/i18n-text-info.component.ts + 5 + + + app/metadata-provider/component/forms/logout-form.component.ts + 5 + + + app/metadata-provider/component/forms/finish-form.component.ts + 115 + + + app/metadata-provider/component/forms/finish-form.component.ts + 121 + + + + Security Information + Security Information + + app/metadata-provider/component/i18n-text-info.component.ts + 6 + + + + Assertion Consumer Service + Assertion Consumer Service + + app/metadata-provider/component/i18n-text-info.component.ts + 7 + + + app/metadata-provider/component/forms/finish-form.component.ts + 173 + + + + Relying Party Overrides + Relying Party Overrides + + app/metadata-provider/component/i18n-text-info.component.ts + 8 + + + app/metadata-provider/component/forms/finish-form.component.ts + 201 + + + + Attribute Release + Attribute Release + + app/metadata-provider/component/i18n-text-info.component.ts + 9 + + + app/metadata-provider/component/forms/finish-form.component.ts + 241 + + + + SP/Organization Information + SP/Organization Information + + app/metadata-provider/component/i18n-text-info.component.ts + 10 + + + + Organization Information + Organization Information + + app/metadata-provider/component/i18n-text-info.component.ts + 11 + + + app/metadata-provider/container/blank-provider.component.ts + 13 + + + + Finished! + Finished! + + app/metadata-provider/component/i18n-text-info.component.ts + 12 + + + + New Endpoint + New Endpoint + + app/metadata-provider/component/forms/assertion-form.component.ts + 17 + + + app/metadata-provider/component/forms/logout-form.component.ts + 17 + + + + New Certificate + New Certificate + + app/metadata-provider/component/forms/key-info-form.component.ts + 107 + + + + Authentication Method + Authentication Method + + app/metadata-provider/component/forms/finish-form.component.ts + 108 + + + + NameId Format + NameId Format + + app/metadata-provider/component/forms/finish-form.component.ts + 76 + + + + Search + Search + + app/dashboard/component/provider-search.component.ts + 3 + + + + Signing + Signing + + app/metadata-provider/component/i18n-text-info.component.ts + 15 + + + + Encryption + Encryption + + app/metadata-provider/component/i18n-text-info.component.ts + 16 + + + + Both + Both + + app/metadata-provider/component/i18n-text-info.component.ts + 17 + + + + + diff --git a/ui/src/locale/es.xlf b/ui/src/locale/es.xlf new file mode 100644 index 000000000..077c9b8b6 --- /dev/null +++ b/ui/src/locale/es.xlf @@ -0,0 +1,1972 @@ + + + + + + Metadata Provider Management + Metadata Provider Management (es) + + app/app.component.ts + 7 + + + + Manage Providers + Manage Providers (es) + + app/app.component.ts + 17 + + + + Add new provider + Add new provider (es) + + app/app.component.ts + 23 + + + + Logout + Logout (es) + + app/app.component.ts + 29 + + + + Links to Shibboleth resources: + Links to Shibboleth resources: (es) + + app/app.component.ts + 42 + + + + Home Page + Home Page (es) + + app/app.component.ts + 43 + + + + Wiki + Wiki (es) + + app/app.component.ts + 44 + + + + Issue Tracker + Issue Tracker (es) + + app/app.component.ts + 45 + + + + Mailing List + Mailing List (es) + + app/app.component.ts + 46 + + + + Copyright + Copyright (es) + + app/app.component.ts + 48 + + + + Service Provider Name (Dashboard Display Only) + Service Provider Name (Dashboard Display Only) (es) + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 7 + + + app/metadata-provider/component/forms/finish-form.component.ts + 29 + + + app/metadata-provider/container/upload-provider.component.ts + 24 + + + app/metadata-provider/container/blank-provider.component.ts + 22 + + + + Service Provider Name (Dashboard Display Only) popover + Service Provider Name (Dashboard Display Only) popover (es) + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 10 + + + + Service Provider Entity ID + Service Provider Entity ID (es) + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 21 + + + app/metadata-provider/component/forms/finish-form.component.ts + 31 + + + app/metadata-provider/container/blank-provider.component.ts + 34 + + + + Service Provider Entity ID popover + Service Provider Entity ID popover (es) + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 24 + + + + Enable this service? + Enable this service? (es) + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 36 + + + app/metadata-provider/component/forms/finish-form.component.ts + 33 + + + + Organization Name + Organization Name (es) + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 43 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 6 + + + app/metadata-provider/component/forms/finish-form.component.ts + 43 + + + + Organization Name popover + Organization Name popover (es) + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 46 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 8 + + + + Organization Display Name + Organization Display Name (es) + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 54 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 24 + + + app/metadata-provider/component/forms/finish-form.component.ts + 45 + + + + Organization Display Name popover + Organization Display Name popover (es) + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 57 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 27 + + + + * These three fields must all be entered if any single field has a value. + * These three fields must all be entered if any single field has a value. (es) + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 74 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 60 + + + + Organization URL + Organization URL (es) + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 65 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 43 + + + app/metadata-provider/component/forms/finish-form.component.ts + 47 + + + + Organization URL popover + Organization URL popover (es) + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 68 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 46 + + + + Contact Information: + Contact Information: (es) + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 78 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 65 + + + app/metadata-provider/component/forms/finish-form.component.ts + 49 + + + + Add Contact + Add Contact (es) + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 79 + + + + Name + Name (es) + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 102 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 93 + + + + Name popover + Name popover (es) + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 103 + + + + Type + Type (es) + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 112 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 111 + + + app/metadata-provider/component/forms/key-info-form.component.ts + 120 + + + app/metadata-provider/component/forms/finish-form.component.ts + 155 + + + + Type popover + Type popover (es) + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 113 + + + + Email Address + Email Address (es) + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 128 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 135 + + + + Email Address popover + Email Address popover (es) + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 129 + + + + Must be a valid Email Address + Must be a valid Email Address (es) + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 134 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 149 + + + + Add Contact + Add Contact (es) + + app/metadata-provider/component/forms/organization-info-form.component.ts + 66 + + + + Contact Name Popover + Contact Name Popover (es) + + app/metadata-provider/component/forms/organization-info-form.component.ts + 94 + + + + Contact Type Popover + Contact Type Popover (es) + + app/metadata-provider/component/forms/organization-info-form.component.ts + 112 + + + + Contact Email Popover + Contact Email Popover (es) + + app/metadata-provider/component/forms/organization-info-form.component.ts + 136 + + + + Display Name + Display Name (es) + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 6 + + + app/metadata-provider/component/forms/finish-form.component.ts + 77 + + + + Typically, the IdP Display Name field will be presented on IdP discovery service interfaces. + Typically, the IdP Display Name field will be presented on IdP discovery service interfaces. (es) + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 7 + + + + Information URL + Information URL (es) + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 22 + + + app/metadata-provider/component/forms/finish-form.component.ts + 79 + + + + The IdP Information URL is a link to a comprehensive information page about the IdP. This page should expand on the content of the IdP Description field. + The IdP Information URL is a link to a comprehensive information page about the IdP. This page should expand on the content of the IdP Description field. (es) + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 23 + + + + Description + Description (es) + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 39 + + + app/metadata-provider/component/forms/finish-form.component.ts + 81 + + + + The IdP Description is a brief description of the IdP service. On a well-designed discovery interface, the IdP Description will be presented to the user in addition to the IdP Display Name, and so the IdP Description helps disambiguate duplicate or similar IdP Display Names. + The IdP Description is a brief description of the IdP service. On a well-designed discovery interface, the IdP Description will be presented to the user in addition to the IdP Display Name, and so the IdP Description helps disambiguate duplicate or similar IdP Display Names. (es) + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 40 + + + + Privacy Statement URL + Privacy Statement URL (es) + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 67 + + + + The IdP Privacy Statement URL is a link to the IdP's Privacy Statement. The content of the Privacy Statement should be targeted at end users. + The IdP Privacy Statement URL is a link to the IdP's Privacy Statement. The content of the Privacy Statement should be targeted at end users. (es) + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 68 + + + + Logo URL + Logo URL (es) + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 84 + + + app/metadata-provider/component/forms/finish-form.component.ts + 85 + + + + The IdP Logo URL in metadata points to an image file on a remote server. A discovery service, for example, may rely on a visual cue (i.e., a logo) instead of or in addition to the IdP Display Name. + The IdP Logo URL in metadata points to an image file on a remote server. A discovery service, for example, may rely on a visual cue (i.e., a logo) instead of or in addition to the IdP Display Name. (es) + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 85 + + + + Logo Width + Logo Width (es) + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 103 + + + app/metadata-provider/component/forms/finish-form.component.ts + 87 + + + + The logo should have a minimum width of 100 pixels + The logo should have a minimum width of 100 pixels (es) + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 104 + + + + Must be an integer equal to or greater than 0 + Must be an integer equal to or greater than 0 (es) + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 108 + + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 127 + + + + Logo Height + Logo Height (es) + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 114 + + + app/metadata-provider/component/forms/finish-form.component.ts + 89 + + + + The logo should have a minimum height of 75 pixels and a maximum height of 150 pixels (or the application will scale it proportionally) + The logo should have a minimum height of 75 pixels and a maximum height of 150 pixels (or the application will scale it proportionally) (es) + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 115 + + + + Is there a X509 Certificate? + Is there a X509 Certificate? (es) + + app/metadata-provider/component/forms/key-info-form.component.ts + 6 + + + app/metadata-provider/component/forms/finish-form.component.ts + 141 + + + + Is there a X509 Certificate popover + Is there a X509 Certificate popover (es) + + app/metadata-provider/component/forms/key-info-form.component.ts + 7 + + + app/metadata-provider/component/forms/key-info-form.component.ts + 34 + + + + Yes + Yes (es) + + app/metadata-provider/component/forms/key-info-form.component.ts + 21 + + + app/metadata-provider/component/forms/key-info-form.component.ts + 48 + + + app/metadata-provider/component/forms/key-info-form.component.ts + 75 + + + app/metadata-provider/component/forms/assertion-form.component.ts + 94 + + + app/metadata-provider/component/forms/attribute-release-form.component.ts + 8 + + + + No + No (es) + + app/metadata-provider/component/forms/key-info-form.component.ts + 27 + + + app/metadata-provider/component/forms/key-info-form.component.ts + 54 + + + app/metadata-provider/component/forms/key-info-form.component.ts + 81 + + + + Authentication Requests Signed? + Authentication Requests Signed? (es) + + app/metadata-provider/component/forms/key-info-form.component.ts + 33 + + + app/metadata-provider/component/forms/finish-form.component.ts + 143 + + + + Want Assertions Signed? + Want Assertions Signed? (es) + + app/metadata-provider/component/forms/key-info-form.component.ts + 60 + + + app/metadata-provider/component/forms/finish-form.component.ts + 145 + + + + Want Assertions Signed + Want Assertions Signed (es) + + app/metadata-provider/component/forms/key-info-form.component.ts + 61 + + + + X509 Certificates + X509 Certificates (es) + + app/metadata-provider/component/forms/key-info-form.component.ts + 88 + + + app/metadata-provider/component/forms/finish-form.component.ts + 142 + + + app/metadata-provider/component/forms/finish-form.component.ts + 146 + + + + Add Certificate + Add Certificate (es) + + app/metadata-provider/component/forms/key-info-form.component.ts + 89 + + + + Certificate Name (Display Only) + Certificate Name (Display Only) (es) + + app/metadata-provider/component/forms/key-info-form.component.ts + 114 + + + app/metadata-provider/component/forms/finish-form.component.ts + 154 + + + + Certificate + Certificate (es) + + app/metadata-provider/component/forms/key-info-form.component.ts + 129 + + + app/metadata-provider/component/forms/finish-form.component.ts + 156 + + + + Assertion Consumer Service Endpoints: + Assertion Consumer Service Endpoints: (es) + + app/metadata-provider/component/forms/assertion-form.component.ts + 5 + + + app/metadata-provider/component/forms/finish-form.component.ts + 176 + + + + Add Endpoint + Add Endpoint (es) + + app/metadata-provider/component/forms/assertion-form.component.ts + 6 + + + app/metadata-provider/component/forms/logout-form.component.ts + 6 + + + + (default) + (default) (es) + + app/metadata-provider/component/forms/assertion-form.component.ts + 20 + + + + Assertion Consumer Service Location + Assertion Consumer Service Location (es) + + app/metadata-provider/component/forms/assertion-form.component.ts + 32 + + + + Assertion Consumer Service Location popover + Assertion Consumer Service Location popover (es) + + app/metadata-provider/component/forms/assertion-form.component.ts + 33 + + + + Must be a valid URL + Must be a valid URL (es) + + app/metadata-provider/component/forms/assertion-form.component.ts + 46 + + + + Assertion Consumer Service Location Binding + Assertion Consumer Service Location Binding (es) + + app/metadata-provider/component/forms/assertion-form.component.ts + 53 + + + app/metadata-provider/component/forms/finish-form.component.ts + 184 + + + + Assertion Consumer Service Location Binding + Assertion Consumer Service Location Binding (es) + + app/metadata-provider/component/forms/assertion-form.component.ts + 54 + + + + Select Binding Type + Select Binding Type (es) + + app/metadata-provider/component/forms/assertion-form.component.ts + 66 + + + app/metadata-provider/component/forms/logout-form.component.ts + 59 + + + + Mark as Default + Mark as Default (es) + + app/metadata-provider/component/forms/assertion-form.component.ts + 76 + + + + Mark as Default + Mark as Default (es) + + app/metadata-provider/component/forms/assertion-form.component.ts + 77 + + + + URL + URL (es) + + app/metadata-provider/component/forms/logout-form.component.ts + 30 + + + + Logout Endpoints Url popover + Logout Endpoints Url popover (es) + + app/metadata-provider/component/forms/logout-form.component.ts + 31 + + + + Binding Type + Binding Type (es) + + app/metadata-provider/component/forms/logout-form.component.ts + 47 + + + app/metadata-provider/component/forms/finish-form.component.ts + 122 + + + + Logout Endpoints Binding Type popover + Logout Endpoints Binding Type popover (es) + + app/metadata-provider/component/forms/logout-form.component.ts + 48 + + + + Protocol Support Enumeration + Protocol Support Enumeration (es) + + app/metadata-provider/component/forms/descriptor-info-form.component.ts + 6 + + + app/metadata-provider/component/forms/finish-form.component.ts + 99 + + + + Protocol Support Enumeration popover + Protocol Support Enumeration popover (es) + + app/metadata-provider/component/forms/descriptor-info-form.component.ts + 7 + + + + Select Protocol + Select Protocol (es) + + app/metadata-provider/component/forms/descriptor-info-form.component.ts + 19 + + + + Add NameID Format + Add NameID Format (es) + + app/metadata-provider/component/forms/descriptor-info-form.component.ts + 24 + + + app/metadata-provider/component/forms/relying-party-form.component.ts + 35 + + + + Sign the Assertion + Sign the Assertion (es) + + app/metadata-provider/component/forms/relying-party-form.component.ts + 6 + + + app/metadata-provider/component/forms/finish-form.component.ts + 204 + + + + Don't Sign the Response + Don't Sign the Response (es) + + app/metadata-provider/component/forms/relying-party-form.component.ts + 13 + + + app/metadata-provider/component/forms/finish-form.component.ts + 206 + + + + Turn Off Encryption of Response + Turn Off Encryption of Response (es) + + app/metadata-provider/component/forms/relying-party-form.component.ts + 20 + + + app/metadata-provider/component/forms/finish-form.component.ts + 208 + + + + Use SHA1 Signing Algorithm + Use SHA1 Signing Algorithm (es) + + app/metadata-provider/component/forms/relying-party-form.component.ts + 27 + + + app/metadata-provider/component/forms/finish-form.component.ts + 210 + + + + NameID Format to Send + NameID Format to Send (es) + + app/metadata-provider/component/forms/relying-party-form.component.ts + 34 + + + app/metadata-provider/component/forms/finish-form.component.ts + 212 + + + + Authentication Methods to Use + Authentication Methods to Use (es) + + app/metadata-provider/component/forms/relying-party-form.component.ts + 52 + + + + Add Authentication Method + Add Authentication Method (es) + + app/metadata-provider/component/forms/relying-party-form.component.ts + 53 + + + + Ignore any SP-Requested Authentication Method + Ignore any SP-Requested Authentication Method (es) + + app/metadata-provider/component/forms/relying-party-form.component.ts + 70 + + + app/metadata-provider/component/forms/finish-form.component.ts + 230 + + + + Omit Not Before Condition + Omit Not Before Condition (es) + + app/metadata-provider/component/forms/relying-party-form.component.ts + 77 + + + app/metadata-provider/component/forms/finish-form.component.ts + 232 + + + + ResponderID + ResponderID (es) + + app/metadata-provider/component/forms/relying-party-form.component.ts + 83 + + + app/metadata-provider/component/forms/finish-form.component.ts + 234 + + + + Attribute Name + Attribute Name (es) + + app/metadata-provider/component/forms/attribute-release-form.component.ts + 7 + + + app/metadata-provider/component/forms/finish-form.component.ts + 246 + + + + Enable this service upon saving? + Enable this service upon saving? (es) + + app/metadata-provider/component/forms/finish-form.component.ts + 9 + + + + Enable this service upon saving popover + Enable this service upon saving popover (es) + + app/metadata-provider/component/forms/finish-form.component.ts + 11 + + + + Name and Entity ID + Name and Entity ID (es) + + app/metadata-provider/component/forms/finish-form.component.ts + 26 + + + + Organization Information + Organization Information (es) + + app/metadata-provider/component/forms/finish-form.component.ts + 40 + + + app/metadata-provider/container/blank-provider.component.ts + 11 + + + + Given Name + Given Name (es) + + app/metadata-provider/component/forms/finish-form.component.ts + 55 + + + + Email Address + Email Address (es) + + app/metadata-provider/component/forms/finish-form.component.ts + 56 + + + + Contact Type + Contact Type (es) + + app/metadata-provider/component/forms/finish-form.component.ts + 57 + + label--contact-type + + + User Interface / MDUI Information + User Interface / MDUI Information (es) + + app/metadata-provider/component/forms/finish-form.component.ts + 74 + + + + Privacy Statement URL + Privacy Statement URL (es) + + app/metadata-provider/component/forms/finish-form.component.ts + 83 + + + + SP SSO Descriptor Information + SP SSO Descriptor Information (es) + + app/metadata-provider/component/forms/finish-form.component.ts + 96 + + + + NameID Format + NameID Format (es) + + app/metadata-provider/component/forms/finish-form.component.ts + 101 + + + + Security Information + Security Information (es) + + app/metadata-provider/component/forms/finish-form.component.ts + 138 + + + + Assertion Consumer Service Location + Assertion Consumer Service Location (es) + + app/metadata-provider/component/forms/finish-form.component.ts + 183 + + + + Default Authentication Method(s) + Default Authentication Method(s) (es) + + app/metadata-provider/component/forms/finish-form.component.ts + 221 + + + + True + True (es) + + app/metadata-provider/component/forms/finish-form.component.ts + 247 + + + + False + False (es) + + app/metadata-provider/component/forms/finish-form.component.ts + 248 + + + + Add a new metadata provider + Add a new metadata provider (es) + + app/metadata-provider/container/new-provider.component.ts + 6 + + + + How are you adding the metadata information? + How are you adding the metadata information? (es) + + app/metadata-provider/container/new-provider.component.ts + 11 + + + + Upload/URL + Upload/URL (es) + + app/metadata-provider/container/new-provider.component.ts + 16 + + + + Create File + Create File (es) + + app/metadata-provider/container/new-provider.component.ts + 25 + + + + 1. Name and Upload Url + 1. Name and Upload Url (es) + + app/metadata-provider/container/upload-provider.component.ts + 6 + + + + + Save Metadata Provider + + + Save Metadata Provider + (es) + + app/metadata-provider/container/upload-provider.component.ts + 11 + + + app/edit-provider/component/wizard-nav.component.ts + 33 + + + + Save + Save (es) + + app/metadata-provider/container/upload-provider.component.ts + 16 + + + app/edit-provider/container/editor.component.ts + 39 + + + app/edit-provider/component/wizard-nav.component.ts + 36 + + + + + Service Provider Name is required + + + Service Provider Name is required + (es) + + app/metadata-provider/container/upload-provider.component.ts + 31 + + + app/metadata-provider/container/blank-provider.component.ts + 28 + + + + Select Provider Metadata File + Select Provider Metadata File (es) + + app/metadata-provider/container/upload-provider.component.ts + 37 + + + + Note: You can only import a file with a single entityID (EntityDescriptor element) in it. Anything more in that file will result in an error. + Note: You can only import a file with a single entityID (EntityDescriptor element) in it. Anything more in that file will result in an error. (es) + + app/metadata-provider/container/upload-provider.component.ts + 56 + + + + Choose File + Choose File (es) + + app/metadata-provider/container/upload-provider.component.ts + 41 + + + app/metadata-provider/container/upload-provider.component.ts + 42 + + + + OR + OR (es) + + app/metadata-provider/container/upload-provider.component.ts + 48 + + + + Service Provider Metadata URL + Service Provider Metadata URL (es) + + app/metadata-provider/container/upload-provider.component.ts + 52 + + + + 1. Name and EntityId + 1. Name and EntityId (es) + + app/metadata-provider/container/blank-provider.component.ts + 6 + + + + Next + Next (es) + + app/metadata-provider/container/blank-provider.component.ts + 14 + + + app/metadata-provider/container/blank-provider.component.ts + 48 + + + app/edit-provider/component/wizard-nav.component.ts + 27 + + + + Entity ID is required + Entity ID is required (es) + + app/metadata-provider/container/blank-provider.component.ts + 40 + + + + Entity ID must be unique + Entity ID must be unique (es) + + app/metadata-provider/container/blank-provider.component.ts + 44 + + + + Preview Provider + Preview Provider (es) + + app/metadata-provider/component/preview-provider-dialog.component.ts + 2 + + + + Download File + Download File (es) + + app/metadata-provider/component/preview-provider-dialog.component.ts + 13 + + + + Cancel + Cancel (es) + + app/metadata-provider/component/preview-provider-dialog.component.ts + 15 + + + app/dashboard/component/delete-dialog.component.ts + 9 + + + app/edit-provider/component/unsaved-dialog.component.ts + 18 + + + app/edit-provider/container/editor.component.ts + 43 + + + + Delete Metadata Provider? + Delete Metadata Provider? (es) + + app/dashboard/component/delete-dialog.component.ts + 2 + + + + You are deleting a metadata provider. This cannot be undone. Continue? + You are deleting a metadata provider. This cannot be undone. Continue? (es) + + app/dashboard/component/delete-dialog.component.ts + 5 + + + + Delete + Delete (es) + + app/dashboard/component/delete-dialog.component.ts + 8 + + + + Current Metadata Sources + Current Metadata Sources (es) + + app/dashboard/container/dashboard.component.ts + 6 + + + + Incomplete Form + Incomplete Form (es) + + app/dashboard/component/provider-item.component.ts + 14 + + + app/dashboard/component/provider-item.component.ts + 52 + + + + Service Provider Name: + Service Provider Name: (es) + + app/dashboard/component/provider-item.component.ts + 33 + + + + Created Date: + Created Date: (es) + + app/dashboard/component/provider-item.component.ts + 35 + + + + Service Provider Entity ID: + Service Provider Entity ID: (es) + + app/dashboard/component/provider-item.component.ts + 40 + + + + Service Provider Status: + Service Provider Status: (es) + + app/dashboard/component/provider-item.component.ts + 42 + + + + Enabled + Enabled (es) + + app/dashboard/component/provider-item.component.ts + 48 + + + + Disabled + Disabled (es) + + app/dashboard/component/provider-item.component.ts + 49 + + + + Search for a provider + Search for a provider (es) + + app/dashboard/component/provider-search.component.ts + 8 + + + + Clear + Clear (es) + + app/dashboard/component/provider-search.component.ts + 19 + + + + Save your information? + Save your information? (es) + + app/edit-provider/component/unsaved-dialog.component.ts + 3 + + + + + You have not completed the wizard! Do you wish to save this information? You can finish the wizard later by clicking the + "Edit" + icon on the dashboard. + + + You have not completed the wizard! Do you wish to save this information? You can finish the wizard later by clicking the + "Edit" + icon on the dashboard. + (es) + + app/edit-provider/component/unsaved-dialog.component.ts + 6 + + + + You have not saved your changes. If you exit this screen, your changes will be lost. + You have not saved your changes. If you exit this screen, your changes will be lost. (es) + + app/edit-provider/component/unsaved-dialog.component.ts + 11 + + + + Finish Later + Finish Later (es) + + app/edit-provider/component/unsaved-dialog.component.ts + 15 + + + + Discard Changes + Discard Changes (es) + + app/edit-provider/component/unsaved-dialog.component.ts + 16 + + + + Add a new metadata provider + Add a new metadata provider (es) + + app/edit-provider/container/wizard.component.ts + 8 + + + + Step of + Step of (es) + + app/edit-provider/container/wizard.component.ts + 11 + + + + Back + Back (es) + + app/edit-provider/component/wizard-nav.component.ts + 7 + + + + Information icon - press spacebar to read additional information for this form field + Information icon - press spacebar to read additional information for this form field (es) + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 13 + + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 30 + + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 55 + + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 69 + + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 83 + + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 122 + + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 134 + + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 153 + + + app/metadata-provider/component/forms/assertion-form.component.ts + 36 + + + app/metadata-provider/component/forms/assertion-form.component.ts + 52 + + + app/metadata-provider/component/forms/assertion-form.component.ts + 71 + + + app/metadata-provider/component/forms/descriptor-info-form.component.ts + 10 + + + app/metadata-provider/component/forms/descriptor-info-form.component.ts + 28 + + + app/metadata-provider/component/forms/key-info-form.component.ts + 10 + + + app/metadata-provider/component/forms/key-info-form.component.ts + 32 + + + app/metadata-provider/component/forms/key-info-form.component.ts + 54 + + + app/metadata-provider/component/forms/key-info-form.component.ts + 106 + + + app/metadata-provider/component/forms/key-info-form.component.ts + 120 + + + app/metadata-provider/component/forms/key-info-form.component.ts + 138 + + + app/metadata-provider/component/forms/logout-form.component.ts + 34 + + + app/metadata-provider/component/forms/logout-form.component.ts + 46 + + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 10 + + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 21 + + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 33 + + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 56 + + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 68 + + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 81 + + + app/metadata-provider/component/forms/metadata-ui-form.component.ts + 96 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 11 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 25 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 39 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 81 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 94 + + + app/metadata-provider/component/forms/organization-info-form.component.ts + 113 + + + app/metadata-provider/component/forms/relying-party-form.component.ts + 13 + + + app/metadata-provider/component/forms/relying-party-form.component.ts + 25 + + + app/metadata-provider/component/forms/relying-party-form.component.ts + 37 + + + app/metadata-provider/component/forms/relying-party-form.component.ts + 49 + + + app/metadata-provider/component/forms/relying-party-form.component.ts + 61 + + + app/metadata-provider/component/forms/relying-party-form.component.ts + 86 + + + app/metadata-provider/component/forms/relying-party-form.component.ts + 111 + + + app/metadata-provider/component/forms/relying-party-form.component.ts + 123 + + + app/metadata-provider/component/forms/relying-party-form.component.ts + 132 + + + + Add NameID Format Popover + Add NameID Format Popover (es) + + app/metadata-provider/component/forms/descriptor-info-form.component.ts + 25 + + + app/metadata-provider/component/forms/relying-party-form.component.ts + 58 + + + + Sign Assertion popover + Sign Assertion popover (es) + + app/metadata-provider/component/forms/relying-party-form.component.ts + 10 + + + + Don't Sign Response popover + Don't Sign Response popover (es) + + app/metadata-provider/component/forms/relying-party-form.component.ts + 22 + + + + Turn Off Encryption of Response popover + Turn Off Encryption of Response popover (es) + + app/metadata-provider/component/forms/relying-party-form.component.ts + 34 + + + + Use SHA1 Signing Algorithm popover + Use SHA1 Signing Algorithm popover (es) + + app/metadata-provider/component/forms/relying-party-form.component.ts + 46 + + + + Authentication Methods to Use popover + Authentication Methods to Use popover (es) + + app/metadata-provider/component/forms/relying-party-form.component.ts + 83 + + + + Ignore any SP-Requested Authentication Method popover + Ignore any SP-Requested Authentication Method popover (es) + + app/metadata-provider/component/forms/relying-party-form.component.ts + 108 + + + + Omit Not Before Condition popover + Omit Not Before Condition popover (es) + + app/metadata-provider/component/forms/relying-party-form.component.ts + 120 + + + + ResponderId popover + ResponderId popover (es) + + app/metadata-provider/component/forms/relying-party-form.component.ts + 129 + + + + Certificate Name Popover + Certificate Name Popover (es) + + app/metadata-provider/component/forms/key-info-form.component.ts + 103 + + + + Certificate Type Popover + Certificate Type Popover (es) + + app/metadata-provider/component/forms/key-info-form.component.ts + 117 + + + + Certificate Popover + Certificate Popover (es) + + app/metadata-provider/component/forms/key-info-form.component.ts + 135 + + + + * These three fields must all be entered if any single field has a value. + * These three fields must all be entered if any single field has a value. (es) + + app/metadata-provider/component/forms/organization-info-form.component.ts + 45 + + + app/metadata-provider/component/forms/advanced-info-form.component.ts + 89 + + + + ResponderID + ResponderID (es) + + app/metadata-provider/component/forms/relying-party-form.component.ts + 128 + + + + User Interface / MDUI Information + User Interface / MDUI Information (es) + + app/metadata-provider/component/i18n-text-info.component.ts + 3 + + + + SP SSO Descriptor Information + SP SSO Descriptor Information (es) + + app/metadata-provider/component/i18n-text-info.component.ts + 4 + + + + Logout Endpoints + Logout Endpoints (es) + + app/metadata-provider/component/i18n-text-info.component.ts + 5 + + + app/metadata-provider/component/forms/logout-form.component.ts + 5 + + + app/metadata-provider/component/forms/finish-form.component.ts + 115 + + + app/metadata-provider/component/forms/finish-form.component.ts + 121 + + + + Security Information + Security Information (es) + + app/metadata-provider/component/i18n-text-info.component.ts + 6 + + + + Assertion Consumer Service + Assertion Consumer Service (es) + + app/metadata-provider/component/i18n-text-info.component.ts + 7 + + + app/metadata-provider/component/forms/finish-form.component.ts + 173 + + + + Relying Party Overrides + Relying Party Overrides (es) + + app/metadata-provider/component/i18n-text-info.component.ts + 8 + + + app/metadata-provider/component/forms/finish-form.component.ts + 201 + + + + Attribute Release + Attribute Release (es) + + app/metadata-provider/component/i18n-text-info.component.ts + 9 + + + app/metadata-provider/component/forms/finish-form.component.ts + 241 + + + + SP/Organization Information + SP/Organization Information (es) + + app/metadata-provider/component/i18n-text-info.component.ts + 10 + + + + Organization Information + Organization Information (es) + + app/metadata-provider/component/i18n-text-info.component.ts + 11 + + + app/metadata-provider/container/blank-provider.component.ts + 13 + + + + Finished! + Finished! (es) + + app/metadata-provider/component/i18n-text-info.component.ts + 12 + + + + New Endpoint + New Endpoint (es) + + app/metadata-provider/component/forms/assertion-form.component.ts + 17 + + + app/metadata-provider/component/forms/logout-form.component.ts + 17 + + + + New Certificate + New Certificate (es) + + app/metadata-provider/component/forms/key-info-form.component.ts + 107 + + + + Authentication Method + Authentication Method (es) + + app/metadata-provider/component/forms/finish-form.component.ts + 108 + + + + NameId Format + NameId Format (es) + + app/metadata-provider/component/forms/finish-form.component.ts + 76 + + + + Search + Search (es) + + app/dashboard/component/provider-search.component.ts + 3 + + + + Signing + Signing (es) + + app/metadata-provider/component/i18n-text-info.component.ts + 15 + + + + Encryption + Encryption (es) + + app/metadata-provider/component/i18n-text-info.component.ts + 16 + + + + Both + Both (es) + + app/metadata-provider/component/i18n-text-info.component.ts + 17 + + + + + \ No newline at end of file diff --git a/ui/src/main.ts b/ui/src/main.ts new file mode 100644 index 000000000..401abe512 --- /dev/null +++ b/ui/src/main.ts @@ -0,0 +1,11 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/ui/src/polyfills.ts b/ui/src/polyfills.ts new file mode 100644 index 000000000..98fd84126 --- /dev/null +++ b/ui/src/polyfills.ts @@ -0,0 +1,72 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** IE9, IE10 and IE11 requires all of the following polyfills. **/ +import 'core-js/es6/symbol'; +import 'core-js/es6/object'; +import 'core-js/es6/function'; +import 'core-js/es6/parse-int'; +import 'core-js/es6/parse-float'; +import 'core-js/es6/number'; +import 'core-js/es6/math'; +import 'core-js/es6/string'; +import 'core-js/es6/date'; +import 'core-js/es6/array'; +import 'core-js/es6/regexp'; +import 'core-js/es6/map'; +import 'core-js/es6/weak-map'; +import 'core-js/es6/set'; + +/** IE10 and IE11 requires the following for NgClass support on SVG elements */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. + +/** Evergreen browsers require these. **/ +import 'core-js/es6/reflect'; +import 'core-js/es7/reflect'; + + +/** + * Required to support Web Animations `@angular/animation`. + * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation + **/ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + + + +/*************************************************************************************************** + * Zone JS is required by Angular itself. + */ +import 'zone.js/dist/zone'; // Included with Angular CLI. + + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ + +/** + * Date, currency, decimal and percent pipes. + * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 + */ +// import 'intl'; // Run `npm install --save intl`. +/** + * Need to import at least one locale-data with intl. + */ +// import 'intl/locale-data/jsonp/en'; diff --git a/ui/src/styles.scss b/ui/src/styles.scss new file mode 100644 index 000000000..eb7fba865 --- /dev/null +++ b/ui/src/styles.scss @@ -0,0 +1,45 @@ +@import './theme/bootstrap'; +@import './theme/buttons'; +@import './theme/forms'; +@import './theme/modal'; +@import './theme/tags'; + +body { + background-color: theme-color("light"); + padding-top: 56px; +} + +.section { + .section-body { + background: $white; + } +} + +nav.fixed-top { + border-bottom: 3px solid map-get($theme-colors, primary); +} + +footer { + background-color: $white; + padding: 0 20px; + font-size: $font-size-xs; + border-top: 3px solid map-get($theme-colors, primary); + + .copyright { + padding-top: 10px; + + a { + color: map-get($theme-colors, primary); + text-decoration: underline; + } + } + + img { + max-height: 88px; + } +} + +label { + display: flex; + justify-content: space-between; +} \ No newline at end of file diff --git a/ui/src/test.ts b/ui/src/test.ts new file mode 100644 index 000000000..03b44d82f --- /dev/null +++ b/ui/src/test.ts @@ -0,0 +1,34 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/dist/long-stack-trace-zone'; +import 'zone.js/dist/proxy.js'; +import 'zone.js/dist/sync-test'; +import 'zone.js/dist/jasmine-patch'; +import 'zone.js/dist/async-test'; +import 'zone.js/dist/fake-async-test'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +import 'rxjs/Rx'; + +// Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. +declare const __karma__: any; +declare const require: any; + +// Prevent Karma from running prematurely. +__karma__.loaded = function () { }; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); +// Finally, start Karma to run the tests. +__karma__.start(); diff --git a/ui/src/testing/activated-route.stub.ts b/ui/src/testing/activated-route.stub.ts new file mode 100644 index 000000000..511b78744 --- /dev/null +++ b/ui/src/testing/activated-route.stub.ts @@ -0,0 +1,30 @@ +/* istanbul ignore */ + +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +import { convertToParamMap, ParamMap } from '@angular/router'; + +@Injectable() +export class ActivatedRouteStub { + + // ActivatedRoute.paramMap is Observable + private subject = new BehaviorSubject(convertToParamMap(this.testParamMap)); + paramMap = this.subject.asObservable(); + + // Test parameters + private _testParamMap: ParamMap; + get testParamMap() { return this._testParamMap; } + set testParamMap(params: {}) { + this._testParamMap = convertToParamMap(params); + this.subject.next(this._testParamMap); + } + + // ActivatedRoute.snapshot.paramMap + get snapshot() { + return { paramMap: this.testParamMap }; + } + + get params() { + return this.paramMap; + } +} diff --git a/ui/src/testing/file.service.stub.ts b/ui/src/testing/file.service.stub.ts new file mode 100644 index 000000000..c492f004c --- /dev/null +++ b/ui/src/testing/file.service.stub.ts @@ -0,0 +1,11 @@ +import { Subject } from 'rxjs/Subject'; +import { Observable } from 'rxjs/Observable'; + +export class FileServiceStub { + readAsText(): Observable { + let subj = new Subject(); + subj.next('foo'); + subj.complete(); + return subj.asObservable(); + } +} diff --git a/ui/src/testing/modal.stub.ts b/ui/src/testing/modal.stub.ts new file mode 100644 index 000000000..ecdc072bc --- /dev/null +++ b/ui/src/testing/modal.stub.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +import { NgbModalOptions } from '@ng-bootstrap/ng-bootstrap/modal/modal'; + +@Injectable() +export class NgbModalStub { + open(content: any, options: NgbModalOptions): {result: Promise} { + return { + result: Promise.resolve(true) + }; + } +} diff --git a/ui/src/testing/provider.stub.ts b/ui/src/testing/provider.stub.ts new file mode 100644 index 000000000..095eaea71 --- /dev/null +++ b/ui/src/testing/provider.stub.ts @@ -0,0 +1,42 @@ +import { + MetadataProvider, + Contact, + SsoService, + Certificate, + SecurityInfo +} from '../app/metadata-provider/model/metadata-provider'; + +export const draft = { + entityId: 'foo', + serviceProviderName: 'bar' +} as MetadataProvider; + +export const provider = { + ...draft, + id: '1' +} as MetadataProvider; + +export const contact = { + type: 'support', + name: 'hithere yo', + emailAddress: 'somewhere@something.com' +} as Contact; + +export const endpoint = { + binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', + locationUrl: 'http://unicon.net/location', + makeDefault: false +} as SsoService; + +export const certificate = { + name: 'foo', + type: 'signing', + value: 'xyz' +} as Certificate; + +export const secInfo = { + x509CertificateAvailable: false, + authenticationRequestsSigned: true, + wantAssertionsSigned: true, + x509Certificates: [] +} as SecurityInfo; diff --git a/ui/src/testing/router.stub.ts b/ui/src/testing/router.stub.ts new file mode 100644 index 000000000..36e10de1c --- /dev/null +++ b/ui/src/testing/router.stub.ts @@ -0,0 +1,27 @@ +import { Component, Directive, Input, Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +import { NavigationExtras } from '@angular/router'; + +@Directive({ + selector: '[routerLink]', + host: { + '(click)': 'onClick()' + } +}) +export class RouterLinkStubDirective { + @Input('routerLink') linkParams: any; + navigatedTo: any = null; + onClick() { + this.navigatedTo = this.linkParams; + } +} +@Component({ + selector: 'router-outlet', + template: '' +}) +export class RouterOutletStubComponent {} + +@Injectable() +export class RouterStub { + navigate(commands: any[], extras?: NavigationExtras) { } +} diff --git a/ui/src/testing/utility.ts b/ui/src/testing/utility.ts new file mode 100644 index 000000000..4ac00e572 --- /dev/null +++ b/ui/src/testing/utility.ts @@ -0,0 +1,15 @@ +export function dispatchKeyboardEvent(elm: HTMLElement, eventName: string, keyName: string) { + let event; + try { + // PhantomJS + event = document.createEvent('KeyboardEvent'); + event.initEvent(eventName, true, false); + event.key = keyName; + } catch (e) { + // Chrome + event = new KeyboardEvent(eventName, { 'key': keyName }); + } + elm.dispatchEvent(event); +} + +export default dispatchKeyboardEvent; diff --git a/ui/src/theme/_palette.scss b/ui/src/theme/_palette.scss new file mode 100644 index 000000000..59fe75d45 --- /dev/null +++ b/ui/src/theme/_palette.scss @@ -0,0 +1,72 @@ +// +// Color system +// + +$white: #fff; +$gray-100: #f8f9fa; +$gray-200: #e9ecef; +$gray-300: #dee2e6; +$gray-400: #ced4da; +$gray-500: #adb5bd; +$gray-600: #868e96; +$gray-700: #495057; +$gray-800: #343a40; +$gray-900: #212529; +$black: #000; + +$grays: ( + 100: $gray-100, + 200: $gray-200, + 300: $gray-300, + 400: $gray-400, + 500: $gray-500, + 600: $gray-600, + 700: $gray-700, + 800: $gray-800, + 900: $gray-900 +); + +$blue: #00355f; +$light-blue: #007db1; +$indigo: #6610f2; +$purple: #6f42c1; +$pink: #e83e8c; +$red: #ef3e33; +$orange: #fd7e14; +$yellow: #ffc107; +$green: #6db300; +$teal: #20c997; +$cyan: #17a2b8; + +$dark-grey: #666666; +$light-grey: #EEEEEE; + +$colors: ( + blue: $blue, + indigo: $indigo, + purple: $purple, + pink: $pink, + red: $red, + orange: $orange, + yellow: $yellow, + green: $green, + teal: $teal, + cyan: $cyan, + white: $white, + gray: $gray-600, + gray-dark: $gray-800 +); + +$theme-colors: ( + primary: $blue, + secondary: $gray-600, + success: $green, + info: $light-blue, + warning: $yellow, + danger: $red, + light: $light-grey, + dark: $dark-grey +); + +// Set a specific jump point for requesting color jumps +$theme-color-interval: 8%; \ No newline at end of file diff --git a/ui/src/theme/_variables.scss b/ui/src/theme/_variables.scss new file mode 100644 index 000000000..8df205529 --- /dev/null +++ b/ui/src/theme/_variables.scss @@ -0,0 +1,2 @@ +$font-size-xs: .75rem !default; +$fa-font-path: "~font-awesome/fonts"; \ No newline at end of file diff --git a/ui/src/theme/bootstrap.scss b/ui/src/theme/bootstrap.scss new file mode 100644 index 000000000..b917f20fc --- /dev/null +++ b/ui/src/theme/bootstrap.scss @@ -0,0 +1,41 @@ +/* You can add global styles to this file, and also import other style files */ +@import "~bootstrap/scss/functions"; +@import "./palette"; +@import "./variables"; +@import "~bootstrap/scss/variables"; +@import "~bootstrap/scss/mixins"; + +@import "~bootstrap/scss/print"; +@import "~bootstrap/scss/reboot"; + +@import "~bootstrap/scss/type"; +@import "~bootstrap/scss/images"; +@import "~bootstrap/scss/code"; +@import "~bootstrap/scss/grid"; +@import "~bootstrap/scss/tables"; +@import "~bootstrap/scss/forms"; +@import "~bootstrap/scss/buttons"; +@import "~bootstrap/scss/transitions"; +@import "~bootstrap/scss/dropdown"; +// @import "~bootstrap/scss/button-group"; +@import "~bootstrap/scss/input-group"; +@import "~bootstrap/scss/custom-forms"; +@import "~bootstrap/scss/nav"; +@import "~bootstrap/scss/navbar"; +@import "~bootstrap/scss/card"; +// @import "~bootstrap/scss/breadcrumb"; +@import "~bootstrap/scss/pagination"; +@import "~bootstrap/scss/badge"; +// @import "~bootstrap/scss/jumbotron"; +@import "~bootstrap/scss/alert"; +// @import "~bootstrap/scss/progress"; +// @import "~bootstrap/scss/media"; +@import "~bootstrap/scss/list-group"; +@import "~bootstrap/scss/close"; +@import "~bootstrap/scss/modal"; +// @import "~bootstrap/scss/tooltip"; +@import "~bootstrap/scss/popover"; +// @import "~bootstrap/scss/carousel"; +@import "~bootstrap/scss/utilities"; + +@import '~font-awesome/scss/font-awesome'; \ No newline at end of file diff --git a/ui/src/theme/buttons.scss b/ui/src/theme/buttons.scss new file mode 100644 index 000000000..c2df1a38e --- /dev/null +++ b/ui/src/theme/buttons.scss @@ -0,0 +1,99 @@ +@import './_palette'; +@import './_variables'; +@import '~bootstrap/scss/_mixins'; + +.nav.nav-wizard { + .nav-item { + margin-right: $custom-control-spacer-x * 3; + + &:first-child .current { + margin-left: 34px; + } + + &:last-child { + margin-right: 0px; + margin-left: $custom-control-spacer-x * 2; + } + + .nav-link.btn { + @include button-size($input-btn-padding-y-lg, $input-btn-padding-x-lg, $font-size-sm, $line-height-lg, $btn-border-radius-lg); + white-space: normal; + height: 60px; + + &.previous { + $color: theme-color('secondary'); + @include button-variant($color, $color); + } + &.current { + $color: theme-color('primary'); + @include button-variant($color, $color, $color); + } + &.next { + $color: theme-color('success'); + @include button-variant($color, $color); + } + &.save { + $color: theme-color('info'); + @include button-variant($color, $color); + } + + & > .direction { + font-size: $font-size-xs; + } + + & > .label { + display: inline-block; + max-width: 160px; + text-align: left; + } + + &.next, &.save { + & > .direction { + border-left: 1px solid $white; + padding-left: $custom-control-spacer-x; + margin-left: $custom-control-spacer-x; + } + } + + &.previous { + & > .direction { + border-right: 1px solid $white; + padding-right: $custom-control-spacer-x; + margin-right: $custom-control-spacer-x; + } + } + } + } +} + +@media only screen and (max-width: 1024px) { + .nav.nav-wizard .nav-item { + &:not(:last-child) { + margin-right: $custom-control-spacer-x * 2; + } + &:last-child { + margin-left: $custom-control-spacer-x; + } + .nav-link.btn { + @include button-size($input-btn-padding-y-sm, $input-btn-padding-x-sm, $font-size-xs, $line-height-sm, $btn-border-radius-sm); + + &.next, &.save { + & > .direction { + padding-left: $custom-control-spacer-x / 2; + margin-left: $custom-control-spacer-x / 2; + } + } + + &.previous { + & > .direction { + padding-right: $custom-control-spacer-x / 2; + margin-right: $custom-control-spacer-x / 2; + } + } + + .label { + max-width: 130px; + } + } + } +} \ No newline at end of file diff --git a/ui/src/theme/forms.scss b/ui/src/theme/forms.scss new file mode 100644 index 000000000..d2f6860ad --- /dev/null +++ b/ui/src/theme/forms.scss @@ -0,0 +1,49 @@ +@import './_palette'; +@import './_variables'; + +.form-section { + padding-left: $grid-gutter-width; + padding-right: $grid-gutter-width; + + &:not(:first-child) { + border-left: 1px solid $hr-border-color; + } + + .btn-link { + &:focus { + outline: -webkit-focus-ring-color auto 5px; + } + } + + .form-check-label { + &:focus { + outline: -webkit-focus-ring-color auto 5px; + } + } +} + +.custom-control { + &.custom-control-reverse { + padding-right: 1.5em; + padding-left: 0; + + & > .custom-control-indicator { + right: 0px; + left: auto; + } + } +} + +.custom-file { + & > .custom-file-control { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } +} + +@media only screen and (max-width: 1200px) { + .form-section:not(:first-child) { + border-left: 0px; + } +} diff --git a/ui/src/theme/modal.scss b/ui/src/theme/modal.scss new file mode 100644 index 000000000..d2158f5f1 --- /dev/null +++ b/ui/src/theme/modal.scss @@ -0,0 +1,28 @@ +.modal-footer { + justify-content: space-between; +} + +.modal.modal-xl { + .modal-dialog { + width: 1200px; + max-width: 98%; + } +} + +delete-dialog { + .modal-body:before { + position: absolute; + font-family: 'FontAwesome'; + top: 0; + left: 10px; + content: "\f071"; + color: $red; + font-size: 3.5rem; + } + + .modal-body { + padding-left: 80px; + color: $red; + font-weight: 700; + } +} diff --git a/ui/src/theme/tags.scss b/ui/src/theme/tags.scss new file mode 100644 index 000000000..817fb34f1 --- /dev/null +++ b/ui/src/theme/tags.scss @@ -0,0 +1,97 @@ +@import './_palette'; +@import './_variables'; +@import '~bootstrap/scss/_mixins'; +@import '~bootstrap/scss/_utilities'; + +@mixin tag-variant($color) { + background-color: $color !important; + color: $white; + .index { + border-color: $color; + color: $color; + } + &::after { + border-left-color: $color; + } +} + +.tag { + $text-space: 8px; + $left-offset: 32px; + + font-size: $font-size-sm; + line-height: $line-height-lg; + position: relative; + padding: 0.5rem 0 0.5rem 1rem; + padding-left: 60px; + min-height: 58px; + display: inline-block; + // border-radius: $btn-border-radius-lg; + max-width: 220px; + color: theme-color('white'); + + &.tag-success { @include tag-variant(theme-color('success')); } + &.tag-primary { @include tag-variant(theme-color('primary')); } + + &:hover { + text-decoration: none; + } + + .index { + border-radius: 50%; + position: absolute; + width: 68px; + height: 68px; + background: $white; + color: theme-color('primary'); + left: -15px; + top: -5px; + border: 8px solid theme-color('primary'); + font-size: 36px; + display: block; + font-weight: bold; + text-align: center; + } + + &::after { + content:""; + position: absolute; + left: 100%; + top: 0; + width: 0; + height: 0; + border-top: 29px solid transparent; + border-left: 30px solid $white; + border-bottom: 29px solid transparent; + } + + &.tag-sm { + font-size: $font-size-xs; + padding: 0.25rem 0.5rem 0.25rem 40px; + + min-height: 44px; + display: inline-block; + max-width: 180px; + + .index { + width: 52px; + height: 52px; + left: -15px; + top: -5px; + font-size: 28px; + border-width: 5px; + } + &::after { + border-top-width: 22px; + border-left-width: 23px; + border-bottom-width: 22px; + } + } +} + +@media only screen and (max-width: 1024px) { + .tag { + font-size: $font-size-xs; + max-width: 200px; + } +} \ No newline at end of file diff --git a/ui/src/tsconfig.app.json b/ui/src/tsconfig.app.json new file mode 100644 index 000000000..1523e0307 --- /dev/null +++ b/ui/src/tsconfig.app.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/app", + "baseUrl": "./", + "module": "es2015", + "types": [] + }, + "exclude": [ + "test.ts", + "**/*.spec.ts", + "**/*.stub.ts" + ] +} \ No newline at end of file diff --git a/ui/src/tsconfig.spec.json b/ui/src/tsconfig.spec.json new file mode 100644 index 000000000..f32f96818 --- /dev/null +++ b/ui/src/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/spec", + "baseUrl": "./", + "module": "commonjs", + "target": "es5", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "test.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} \ No newline at end of file diff --git a/ui/src/typings.d.ts b/ui/src/typings.d.ts new file mode 100644 index 000000000..403b22fee --- /dev/null +++ b/ui/src/typings.d.ts @@ -0,0 +1,5 @@ +/* SystemJS module definition */ +declare var module: NodeModule; +interface NodeModule { + id: string; +} diff --git a/ui/tsconfig.json b/ui/tsconfig.json new file mode 100644 index 000000000..6314441c5 --- /dev/null +++ b/ui/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es5", + "typeRoots": [ + "node_modules/@types" + ], + "lib": [ + "es2017", + "dom" + ] + } +} \ No newline at end of file diff --git a/ui/tslint.json b/ui/tslint.json new file mode 100644 index 000000000..71d139914 --- /dev/null +++ b/ui/tslint.json @@ -0,0 +1,143 @@ +{ + "rulesDirectory": [ + "node_modules/codelyzer" + ], + "rules": { + "arrow-return-shorthand": true, + "callable-types": true, + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "curly": true, + "eofline": true, + "forin": true, + "import-blacklist": [ + true, + "rxjs" + ], + "import-spacing": true, + "indent": [ + true, + "spaces", + 4 + ], + "interface-over-type-literal": true, + "label-position": true, + "max-line-length": [ + true, + 140 + ], + "member-access": false, + "member-ordering": [ + true, + { + "order": [ + "static-field", + "instance-field", + "static-method", + "instance-method" + ] + } + ], + "no-arg": true, + "no-bitwise": true, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-debugger": true, + "no-duplicate-super": true, + "no-empty": false, + "no-empty-interface": true, + "no-eval": true, + "no-inferrable-types": [ + true, + "ignore-params" + ], + "no-misused-new": true, + "no-non-null-assertion": true, + "no-shadowed-variable": true, + "no-string-literal": false, + "no-string-throw": true, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": true, + "no-unnecessary-initializer": true, + "no-unused-expression": true, + "no-use-before-declare": true, + "no-var-keyword": true, + "object-literal-sort-keys": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "prefer-const": false, + "quotemark": [ + true, + "single" + ], + "radix": true, + "semicolon": [ + true, + "always" + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "typeof-compare": true, + "unified-signatures": true, + "variable-name": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ], + "directive-selector": [ + true, + "attribute", + "", + "camelCase" + ], + "component-selector": [ + true, + "element", + "", + "kebab-case" + ], + "use-input-property-decorator": true, + "use-output-property-decorator": true, + "use-host-property-decorator": true, + "no-input-rename": true, + "no-output-rename": true, + "use-life-cycle-interface": true, + "use-pipe-transform-interface": true, + "component-class-suffix": true, + "directive-class-suffix": true, + "no-access-missing-member": true, + "templates-use-public": true, + "invoke-injectable": true + } +} \ No newline at end of file