diff --git a/.github/workflows/docker-CI.yml b/.github/workflows/docker-CI.yml new file mode 100644 index 0000000..3f6d19c --- /dev/null +++ b/.github/workflows/docker-CI.yml @@ -0,0 +1,25 @@ +name: Docker Build and Push + +on: + push: + branches: [ main, develop ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Extract branch name + shell: bash + run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" + id: extract_branch + - name: Build the Docker image + run: | + docker build -t registry.tech4comp.dbis.rwth-aachen.de/rwthacis/las2peer-project-service:${{ steps.extract_branch.outputs.branch }} . + - name: Push to registry + env: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PW: ${{ secrets.DOCKER_PW }} + run: | + docker login -u $DOCKER_USERNAME -p $DOCKER_PW registry.tech4comp.dbis.rwth-aachen.de + docker push registry.tech4comp.dbis.rwth-aachen.de/rwthacis/las2peer-project-service:${{ steps.extract_branch.outputs.branch }} \ No newline at end of file diff --git a/.github/workflows/docker-ghcr.yml b/.github/workflows/docker-ghcr.yml new file mode 100644 index 0000000..33f33ce --- /dev/null +++ b/.github/workflows/docker-ghcr.yml @@ -0,0 +1,40 @@ +name: Build and publish docker image to ghcr.io + +on: + push: + branches: ["main", "develop"] + +env: + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Log in to the Container registry + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: ghcr.io/${{ env.IMAGE_NAME }} + + - name: Build and push Docker image + uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 0000000..af3bff3 --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,26 @@ +# This workflow will build a Java project with Gradle +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle + +name: Java CI with Gradle + +# Triggers the workflow on push or pull request events (on every branch) +on: [push, pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 17 + uses: actions/setup-java@v1 + with: + java-version: 17 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew build + - uses: codecov/codecov-action@v1 + with: + files: ./project_service/export/jacoco/test/jacocoTestReport.xml \ No newline at end of file diff --git a/.github/workflows/npmjs.yml b/.github/workflows/npmjs.yml new file mode 100644 index 0000000..05ae8ea --- /dev/null +++ b/.github/workflows/npmjs.yml @@ -0,0 +1,20 @@ +name: npmjs publish package +on: + release: + types: [created] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + # Setup .npmrc file to publish to npm + - uses: actions/setup-node@v2 + with: + node-version: '12.x' + registry-url: 'https://registry.npmjs.org' + - run: npm install + working-directory: frontend + - run: npm publish --access public + working-directory: frontend + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae27838 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +/project_service/output/ +/project_service/export/ +/node-storage/ +/node_modules +/.las2peer/ +/tmp/ +/log/ +/project_service/log/ +/lib/ +/service/ +/out/ +/.idea/ +*.iml +.DS_Store +/.settings/ +/junitvmwatcher*.properties +/junit*.properties +/etc/startup/ +*.secret +.classpath +.project +.settings + +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build +node_modules diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8a6ddb9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM openjdk:17-jdk-alpine + +ENV HTTP_PORT=8080 +ENV HTTPS_PORT=8443 +ENV LAS2PEER_PORT=9011 + +RUN apk add --update bash && rm -f /var/cache/apk/* +RUN addgroup -g 1000 -S las2peer && \ + adduser -u 1000 -S las2peer -G las2peer + +COPY --chown=las2peer:las2peer . /src +WORKDIR /src + +RUN chmod +x /src/docker-entrypoint.sh +# run the rest as unprivileged user +USER las2peer +RUN chmod +x gradlew && ./gradlew build --exclude-task test javadoc + +EXPOSE $HTTP_PORT +EXPOSE $HTTPS_PORT +EXPOSE $LAS2PEER_PORT +ENTRYPOINT ["/src/docker-entrypoint.sh"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..670154e --- /dev/null +++ b/LICENSE @@ -0,0 +1,116 @@ +CC0 1.0 Universal + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator and +subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific +works ("Commons") that the public can reliably and without fear of later +claims of infringement build upon, modify, incorporate in other works, reuse +and redistribute as freely as possible in any form whatsoever and for any +purposes, including without limitation commercial purposes. These owners may +contribute to the Commons to promote the ideal of a free culture and the +further production of creative, cultural and scientific works, or to gain +reputation or greater distribution for their Work in part through the use and +efforts of others. + +For these and/or other purposes and motivations, and without any expectation +of additional consideration or compensation, the person associating CC0 with a +Work (the "Affirmer"), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work +and publicly distribute the Work under its terms, with knowledge of his or her +Copyright and Related Rights in the Work and the meaning and intended legal +effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not limited +to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, communicate, + and translate a Work; + + ii. moral rights retained by the original author(s) and/or performer(s); + + iii. publicity and privacy rights pertaining to a person's image or likeness + depicted in a Work; + + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + + v. rights protecting the extraction, dissemination, use and reuse of data in + a Work; + + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + + vii. other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, +applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and +unconditionally waives, abandons, and surrenders all of Affirmer's Copyright +and Related Rights and associated claims and causes of action, whether now +known or unknown (including existing as well as future claims and causes of +action), in the Work (i) in all territories worldwide, (ii) for the maximum +duration provided by applicable law or treaty (including future time +extensions), (iii) in any current or future medium and for any number of +copies, and (iv) for any purpose whatsoever, including without limitation +commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes +the Waiver for the benefit of each member of the public at large and to the +detriment of Affirmer's heirs and successors, fully intending that such Waiver +shall not be subject to revocation, rescission, cancellation, termination, or +any other legal or equitable action to disrupt the quiet enjoyment of the Work +by the public as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be +judged legally invalid or ineffective under applicable law, then the Waiver +shall be preserved to the maximum extent permitted taking into account +Affirmer's express Statement of Purpose. In addition, to the extent the Waiver +is so judged Affirmer hereby grants to each affected person a royalty-free, +non transferable, non sublicensable, non exclusive, irrevocable and +unconditional license to exercise Affirmer's Copyright and Related Rights in +the Work (i) in all territories worldwide, (ii) for the maximum duration +provided by applicable law or treaty (including future time extensions), (iii) +in any current or future medium and for any number of copies, and (iv) for any +purpose whatsoever, including without limitation commercial, advertising or +promotional purposes (the "License"). The License shall be deemed effective as +of the date CC0 was applied by Affirmer to the Work. Should any part of the +License for any reason be judged legally invalid or ineffective under +applicable law, such partial invalidity or ineffectiveness shall not +invalidate the remainder of the License, and in such case Affirmer hereby +affirms that he or she will not (i) exercise any of his or her remaining +Copyright and Related Rights in the Work or (ii) assert any associated claims +and causes of action with respect to the Work, in either case contrary to +Affirmer's express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + + b. Affirmer offers the Work as-is and makes no representations or warranties + of any kind concerning the Work, express, implied, statutory or otherwise, + including without limitation warranties of title, merchantability, fitness + for a particular purpose, non infringement, or the absence of latent or + other defects, accuracy, or the present or absence of errors, whether or not + discoverable, all to the greatest extent permissible under applicable law. + + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without limitation + any person's Copyright and Related Rights in the Work. Further, Affirmer + disclaims responsibility for obtaining any necessary consents, permissions + or other rights required for any use of the Work. + + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to this + CC0 or use of the Work. + +For more information, please see + diff --git a/README.md b/README.md index 2734f97..8990f29 100644 --- a/README.md +++ b/README.md @@ -1 +1,116 @@ -# las2peer-project-service \ No newline at end of file +# las2peer-project-service + +[![Java CI with Gradle](https://github.com/rwth-acis/las2peer-project-service/actions/workflows/gradle.yml/badge.svg?branch=main)](https://github.com/rwth-acis/las2peer-project-service/actions/workflows/gradle.yml) +[![codecov](https://codecov.io/gh/rwth-acis/las2peer-project-service/branch/main/graph/badge.svg)](https://codecov.io/gh/rwth-acis/las2peer-project-service) +[![npmjs](https://img.shields.io/npm/v/@rwth-acis/las2peer-project-service-frontend?color=success)](https://www.npmjs.com/package/@rwth-acis/las2peer-project-service-frontend) + +A [las2peer](https://github.com/rwth-acis/las2peer) service for managing projects and their members. We provide a [project-list](/frontend) LitElement which can be used as a frontend for this service. + +Build +-------- +Execute the following command on your shell: +```shell +gradle clean build +``` +Make sure to use **Java 17** and **Gradle 7.2**. + +Service Properties +-------- +The project service uses a group agent to store envelopes containing the lists of available projects. +This allows, to access these envelopes using different service agents. +As an example, when updating your service, you do not loose access to the projects, but a new service agent can be added to the group agent instead (which will grant access to the envelopes). + +Depending on whether you are starting the service for the first time (and do not have a service group agent yet) or whether you are starting an updated version of the service, different properties (or environment variables) have to be set. + +When you are starting the service for the first time, you need to set the docker environment variable "NEW_GROUP_AGENT". Then on the first start of the service, a new group agent is generated for the service. In case you want to update your service at a later point and still want to have access to your projects, please note down the agent id of your service, the password for the service agent and also the id of the service group agent. +You can find the id of the service group agent in the etc/startup/group.xml file. + +When you want to restart your service after an update, you need to remove the "NEW_GROUP_AGENT" environment variable. +Instead, you now need to set the variables "SERVICE_GROUP_ID", "OLD_SERVICE_AGENT_ID" and "OLD_SERVICE_AGENT_PW" accordingly. +When using the service, your new service agent will be automatically added to the existing group by using the old service agent and it's password. + +Besides that, you need to configure the systems that the project service should handle projects for. +Therefore, you may use the "SYSTEMS" environment variable. +It should be set to a JSON object which could look as follows, if you are using two systems: +``` +{ + "SBF": { + "visibilityOfProjects": "own" + }, + "CAE": { + "visibilityOfProjects": "all", + "eventListenerService": "i5.las2peer.services.modelPersistenceService.ModelPersistenceService@0.1" + } +} +``` +In this example, we are supporting two systems, namely the "SBF" and "CAE". +In the CAE it is possible to view all projects, even the ones where a user is no member of. +The SBF only allows to read those projects, where the user is a member of. +Besides that, the ModelPersistenceService is called in the CAE, if specific events (such as project-creation) occur. For more information, see the section on the event listener service. + +GitHub Projects Extension (Optional) +------------------------------------ +It is possible to enable the GitHub projects extension for a system. +Then, for every las2peer project in the system, a corresponding GitHub project will be created and can be used for project management. +To enable the extension for a system, edit the "SYSTEMS" environment variable. +In the example from above, enabling it for the system named SBF would work as follows: +``` +... +"SBF": { + ... + "gitHubProjectsEnabled": true, + "gitHubOrganization": , + "gitHubPersonalAccessToken": +} +... +``` +The GitHub projects will be added within the given GitHub organization. +Therefore, a personal access token is required, that allows to use the GitHub API to create new GitHub projects in the organization and to add members to it. + +Event Listener Service +-------- +The project service provides the possibility to set another las2peer service as an event listener service. +On specific events (such as project-creation), the project service calls the configured event listener service via RMI. +In order to work properly, the event listener service needs to implement the following public methods for the specific events: + +| Event | Method | Description | +|-------------------|-------------------------------------------|----| +| Project creation | _onProjectCreated(JSONObject projectJSON) | Event gets fired whenever a new project gets created. The JSONObject will then be a JSON representation of the created project. | +| Project deletion | _onProjectDeleted(JSONObject projectJSON) | Event gets fired whenever a project gets deleted. The JSONObject will then be a JSON representation of the deleted project. | + +RMI Methods +-------- +Besides the event listener service, other services in general have the possibility to communicate with the project service via RMI. +Therefore, the project service provides the following methods: + +| Method | Description | +|------------------------------------------------|-------------| +| boolean hasAccessToProject(String system, String projectName) | This method may be used to verify if a user is allowed to write-access a project. Returns true, if the calling agent has access to project. Returns false otherwise (or if project with given name does not exist). | +| boolean changeMetadataRMI(String system, String body) | RMI wrapper for "/{system}/changeMetadata" REST method. Returns true if metadata could be updated successfully. | +| JSONObject getProjectMetadataRMI(String system, String projectName) | This method may be used to access the metadata of a project via RMI. Returns the metadata as a JSONObject or null if access to project was not granted. | + +Start +-------- + +First of all make sure that you have a running instance of the [Contact Service](https://github.com/rwth-acis/las2peer-contact-service) and the [User Information Service](https://github.com/rwth-acis/las2peer-user-information-service). + +To start the Project Service, use one of the available start scripts: + +Windows: + +```shell +bin/start_network.bat +``` + +Unix/Mac: +```shell +bin/start_network.sh +``` + +After successful start, Project Service is available under + +[http://localhost:8080/projects/](http://localhost:8080/projects/) + +Setup with docker +------------------ +To run the service for development purposes using docker, please refer to the [wiki](https://github.com/rwth-acis/las2peer-project-service/wiki/Local-development-instance-setup). diff --git a/bin/.gitignore b/bin/.gitignore new file mode 100644 index 0000000..896c87b --- /dev/null +++ b/bin/.gitignore @@ -0,0 +1,2 @@ +start_network.bat +start_network.sh diff --git a/bin/start_GroupAgentGenerator.bat b/bin/start_GroupAgentGenerator.bat new file mode 100644 index 0000000..cb8ce7c --- /dev/null +++ b/bin/start_GroupAgentGenerator.bat @@ -0,0 +1,14 @@ +@echo off + +cd %~dp0 +cd .. +set BASE=%CD% +set CLASSPATH="%BASE%/lib/*;" + +if "%~1"=="" ( + echo Syntax error! + echo. + echo Usage: start_GroupAgentGenerator filePathServiceAgent1 path2 +) else ( + java -cp %CLASSPATH% i5.las2peer.tools.GroupAgentGenerator %1 %2 +) diff --git a/bin/start_GroupAgentGenerator.sh b/bin/start_GroupAgentGenerator.sh new file mode 100644 index 0000000..6cff361 --- /dev/null +++ b/bin/start_GroupAgentGenerator.sh @@ -0,0 +1 @@ +java -cp "lib/*" i5.las2peer.tools.GroupAgentGenerator "$@" \ No newline at end of file diff --git a/bin/start_ServiceAgentGenerator.bat b/bin/start_ServiceAgentGenerator.bat new file mode 100644 index 0000000..914f27c --- /dev/null +++ b/bin/start_ServiceAgentGenerator.bat @@ -0,0 +1,14 @@ +@echo off + +cd %~dp0 +cd .. +set BASE=%CD% +set CLASSPATH="%BASE%/lib/*;" + +if "%~2"=="" ( + echo Syntax error! + echo. + echo Usage: start_ServiceAgentGenerator service.canonical.class.name service.password +) else ( + java -cp %CLASSPATH% i5.las2peer.tools.ServiceAgentGenerator %1 %2 +) diff --git a/bin/start_ServiceAgentGenerator.sh b/bin/start_ServiceAgentGenerator.sh new file mode 100644 index 0000000..4dd8481 --- /dev/null +++ b/bin/start_ServiceAgentGenerator.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +# this scripts generates a xml file for the specified ServiceClass with the desired ServicePass +# pls run the script form the root folder of your deployment, e. g. ./bin/start_ServiceAgentGenerator.sh + +java -cp "lib/*" i5.las2peer.tools.ServiceAgentGenerator "$@" diff --git a/bin/start_UserAgentGenerator.bat b/bin/start_UserAgentGenerator.bat new file mode 100644 index 0000000..4cf7360 --- /dev/null +++ b/bin/start_UserAgentGenerator.bat @@ -0,0 +1,14 @@ +@echo off + +cd %~dp0 +cd .. +set BASE=%CD% +set CLASSPATH="%BASE%/lib/*" + +if "%~2"=="" ( + echo Syntax error! + echo. + echo Usage: start_UserAgentGenerator user.name user.password user.mail +) else ( + java -cp %CLASSPATH% i5.las2peer.tools.UserAgentGenerator %2 %1 %3 +) \ No newline at end of file diff --git a/bin/start_UserAgentGenerator.sh b/bin/start_UserAgentGenerator.sh new file mode 100644 index 0000000..2356a67 --- /dev/null +++ b/bin/start_UserAgentGenerator.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +# this scripts generates an user agent as xml file in order to upload it via the startup folder +# pls run the script form the root folder of your deployment, e. g. ./bin/start_UserAgentGenerator.sh + +java -cp "lib/*" i5.las2peer.tools.UserAgentGenerator "$@" diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..7b68afd --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash + +set -e + +# print all comands to console if DEBUG is set +if [[ ! -z "${DEBUG}" ]]; then + set -x +fi + +# set some helpful variables +export SERVICE_PROPERTY_FILE='etc/i5.las2peer.services.projectService.ProjectService.properties' +export WEB_CONNECTOR_PROPERTY_FILE='etc/i5.las2peer.connectors.webConnector.WebConnector.properties' +export SERVICE_VERSION=$(awk -F "=" '/service.version/ {print $2}' gradle.properties) +export SERVICE_NAME=$(awk -F "=" '/service.name/ {print $2}' gradle.properties) +export SERVICE_CLASS=$(awk -F "=" '/service.class/ {print $2}' gradle.properties) +export SERVICE=${SERVICE_NAME}.${SERVICE_CLASS}@${SERVICE_VERSION} + +function set_in_service_config { + sed -i "s?${1}[[:blank:]]*=.*?${1}=${2}?g" ${SERVICE_PROPERTY_FILE} +} + +# check mandatory variables +[[ -z "${SYSTEMS}" ]] && \ + echo "Mandatory variable SYSTEMS is not set. Add -e SYSTEMS={...} to your arguments." && exit 1 + +set_in_service_config systems ${SYSTEMS} + +# check if a new group agent should be generated for the service +if [[ -z "${NEW_GROUP_AGENT}" ]]; then + # NEW_GROUP_AGENT is undefined + # we want to add the service agent to an existing group + [[ -z "${OLD_SERVICE_AGENT_ID}" ]] && \ + echo "Variable NEW_GROUP_AGENT is not set, but OLD_SERVICE_AGENT_ID is not set too. Cannot start service." && exit 1 + + [[ -z "${OLD_SERVICE_AGENT_PW}" ]] && \ + echo "Variable NEW_GROUP_AGENT is not set, but OLD_SERVICE_AGENT_PW is not set too. Cannot start service." && exit 1 + + [[ -z "${SERVICE_GROUP_ID}" ]] && \ + echo "Variable NEW_GROUP_AGENT is not set, but SERVICE_GROUP_ID is not set too. Cannot start service. Either set NEW_GROUP_AGENT or set SERVICE_GROUP_ID to the id of the previously used service group." && exit 1 + + set_in_service_config oldServiceAgentId ${OLD_SERVICE_AGENT_ID} + set_in_service_config oldServiceAgentPw ${OLD_SERVICE_AGENT_PW} + set_in_service_config serviceGroupId ${SERVICE_GROUP_ID} +else + # NEW_GROUP_AGENT is set + # Check if there does not exist a group agent yet + if [[ -f "etc/startup/group.xml" ]]; then + # group.xml exists + # do not do anything, we use the existing service and group agents + echo "There already exists a group.xml file. We use the existing service and group agent." + else + echo "Generating a new service agent..." + sh bin/start_ServiceAgentGenerator.sh ${SERVICE} ${SERVICE_PASSPHRASE} > "etc/startup/${SERVICE}.xml" + echo -e "\n${SERVICE}.xml;${SERVICE_PASSPHRASE}" >> "etc/startup/passphrases.txt" + echo "Generating a new group agent for the service..." + sh bin/start_GroupAgentGenerator.sh "etc/startup/${SERVICE}.xml" > "etc/startup/group.xml" + # extract id of group agent from group.xml file + groupId=`sed -n "s:.*\(.*\).*:\1:p" "etc/startup/group.xml"` + echo "Group agent was generated. Group agent identifier is: $groupId" + echo "Using this group id as the service group id." + set_in_service_config serviceGroupId $groupId + fi +fi + +# set defaults for optional web connector parameters +[[ -z "${START_HTTP}" ]] && export START_HTTP='TRUE' +[[ -z "${START_HTTPS}" ]] && export START_HTTPS='FALSE' +[[ -z "${SSL_KEYSTORE}" ]] && export SSL_KEYSTORE='' +[[ -z "${SSL_KEY_PASSWORD}" ]] && export SSL_KEY_PASSWORD='' +[[ -z "${CROSS_ORIGIN_RESOURCE_DOMAIN}" ]] && export CROSS_ORIGIN_RESOURCE_DOMAIN='*' +[[ -z "${CROSS_ORIGIN_RESOURCE_MAX_AGE}" ]] && export CROSS_ORIGIN_RESOURCE_MAX_AGE='60' +[[ -z "${ENABLE_CROSS_ORIGIN_RESOURCE_SHARING}" ]] && export ENABLE_CROSS_ORIGIN_RESOURCE_SHARING='TRUE' +[[ -z "${OIDC_PROVIDERS}" ]] && export OIDC_PROVIDERS='https://api.learning-layers.eu/o/oauth2,https://accounts.google.com' + +# configure web connector properties + +function set_in_web_config { + sed -i "s?${1}[[:blank:]]*=.*?${1}=${2}?g" ${WEB_CONNECTOR_PROPERTY_FILE} +} +set_in_web_config httpPort ${HTTP_PORT} +set_in_web_config httpsPort ${HTTPS_PORT} +set_in_web_config startHttp ${START_HTTP} +set_in_web_config startHttps ${START_HTTPS} +set_in_web_config sslKeystore ${SSL_KEYSTORE} +set_in_web_config sslKeyPassword ${SSL_KEY_PASSWORD} +set_in_web_config crossOriginResourceDomain "${CROSS_ORIGIN_RESOURCE_DOMAIN}" +set_in_web_config crossOriginResourceMaxAge ${CROSS_ORIGIN_RESOURCE_MAX_AGE} +set_in_web_config enableCrossOriginResourceSharing ${ENABLE_CROSS_ORIGIN_RESOURCE_SHARING} +set_in_web_config oidcProviders ${OIDC_PROVIDERS} + +# wait for any bootstrap host to be available +if [[ ! -z "${BOOTSTRAP}" ]]; then + echo "Waiting for any bootstrap host to become available..." + for host_port in ${BOOTSTRAP//,/ }; do + arr_host_port=(${host_port//:/ }) + host=${arr_host_port[0]} + port=${arr_host_port[1]} + if { /dev/null; then + echo "${host_port} is available. Continuing..." + break + fi + done +fi + +# prevent glob expansion in lib/* +set -f +LAUNCH_COMMAND='java -cp lib/* --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED i5.las2peer.tools.L2pNodeLauncher -s service -p '"${LAS2PEER_PORT} ${SERVICE_EXTRA_ARGS}" +if [[ ! -z "${BOOTSTRAP}" ]]; then + LAUNCH_COMMAND="${LAUNCH_COMMAND} -b ${BOOTSTRAP}" +fi + +# start the service within a las2peer node +if [[ -z "${@}" ]] +then + exec ${LAUNCH_COMMAND} uploadStartupDirectory startService\("'""${SERVICE}""'", "'""${SERVICE_PASSPHRASE}""'"\) startWebConnector +else + exec ${LAUNCH_COMMAND} ${@} +fi \ No newline at end of file diff --git a/etc/i5.las2peer.connectors.webConnector.WebConnector.properties b/etc/i5.las2peer.connectors.webConnector.WebConnector.properties new file mode 100644 index 0000000..65db529 --- /dev/null +++ b/etc/i5.las2peer.connectors.webConnector.WebConnector.properties @@ -0,0 +1,13 @@ +httpPort = 8080 +httpsPort = 8090 +startHttp = TRUE +startHttps = FALSE +sslKeystore = etc/example.jks +sslKeyPassword = secretpassword +crossOriginResourceDomain = * +crossOriginResourceMaxAge = 60 +enableCrossOriginResourceSharing = TRUE +onlyLocalServices = FALSE +defaultLoginUser = anonymous +defaultLoginPassword = anonymous +oidcProviders = https://api.learning-layers.eu/o/oauth2,https://accounts.google.com diff --git a/etc/i5.las2peer.services.projectService.ProjectService.properties b/etc/i5.las2peer.services.projectService.ProjectService.properties new file mode 100644 index 0000000..41c1fe3 --- /dev/null +++ b/etc/i5.las2peer.services.projectService.ProjectService.properties @@ -0,0 +1,4 @@ +serviceGroupId=3a46ce159327960a3249e1d4590860ba19600f18dcf70e9bcb511b57551a92adffc01694adcbbe99dee1094b64fd04baff1c23a9d7ffdf77d2ad7d084eb81632 +oldServiceAgentId= +oldServiceAgentPw= +systems= \ No newline at end of file diff --git a/etc/nodeInfo.xml b/etc/nodeInfo.xml new file mode 100644 index 0000000..d0d5c99 --- /dev/null +++ b/etc/nodeInfo.xml @@ -0,0 +1,6 @@ + + Admin + admin@mail.com + Advanced Community Information Systems (ACIS) Group, RWTH Aachen University + This node hosts a sample service. + diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..857c115 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,2 @@ +/node_modules/ +.idea diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..0296f08 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,91 @@ +# Project-List Frontend +[![npmjs](https://img.shields.io/npm/v/@rwth-acis/las2peer-project-service-frontend?color=success)](https://www.npmjs.com/package/@rwth-acis/las2peer-project-service-frontend) + +The project-list is a LitElement for listing projects given by the las2peer-project-service and for creating new projects. + +Basic Usage +------------------- + +To use the project-list LitElement in your project, just install it from [npmjs](https://www.npmjs.com/package/@rwth-acis/las2peer-project-service-frontend): + +``` +npm i @rwth-acis/las2peer-project-service-frontend +``` + +Then you can use the project-list element as follows: + +```html + +``` + +Here, SYSTEM_NAME needs to match with a system that is configured in the project-service. +For the configuration of systems, see the [main README](https://github.com/rwth-acis/las2peer-project-service). +Set PROJECT_SERVICE_URL and CONTACT_SERVICE_URL to the address of the webconnector, where the respective service can be found. + +The project-list element uses [Yjs](https://github.com/yjs/yjs) to share the metadata of the currently selected/opened project with other project members that have selected the same project. +This ensures, that every project member always gets the latest changes to the metadata. +To configure the used y-websockets-server instance, use the attributes `yjsAddress` and `yjsResourcePath`. +Please note: To keep the metadata of projects secured, the project-list uses an extended version of y-websockets-server and y-websockets-client. +This extension allows to use Yjs rooms that only project members can access. +The extensions can be found in the rwth-acis forks [rwth-acis/y-websockets-server#project-service](https://github.com/rwth-acis/y-websockets-server/tree/project-service) and [rwth-acis/y-websockets-client#v8](https://github.com/rwth-acis/y-websockets-client/tree/v8). + + +You may also use the `disableAllProjects` attribute to disable the tab where all available projects are listed. + +Events +------------- + +**Events fired by the project-list element:** + +`projects-loaded`: + +The project-list element fires this event whenever the projects are reloaded from the project-service. +The event details contain a list of all projects that the current user can access. +Depending on how the visibility of all projects is configured in the project-service, also projects where the current user is no member of will be part of the list. + +`project-selected`: + +Whenever the user clicks on one of the projects in the list, this event will be fired. +It contains detailed information on the selected project and its metadata. + +`metadata-changed`: + +If the metadata of the currently selected/opened project got changed, this event will be fired. +It contains the updated project metadata. +If you display the metadata in the UI and want to keep it up-to-date, then you can use this event. + +**Events that the project-list element listens for:** + +`metadata-change-request`: + +The project-list element listens to the event "metadata-change-request". +It can be used to update the metadata of the currently selected project. +If you send the event and set the event details to the updated metadata, the project-list element will send it to the project-service and after that the "metadata-changed" event will be fired. + +`metadata-reload-request`: + +If you updated the metadata without using the "metadata-change-request" event, as an example by using the RMI interface of the project-service, then you should use this event to inform the project-list element about it. +When receiving the event, the project-list will fetch the metadata from the project-service again and will also send a "metadata-changed" event. + +`projects-reload-request`: + +This event can be used to reload the list of projects. +It might be used after the user has logged in. + +Extension: SyncMeta Online User List +------------------------------------------- + +If your projects are using [SyncMeta](https://github.com/rwth-acis/syncmeta), you can use the online user list extension. +It allows to display the users that are currently online in a project / SyncMeta room. +To use the online user list, the project-list element needs to know which project is using which Yjs room(s) for modelling. +You can use the method "setOnlineUserListYjsRooms", that the project-list element provides, for this. +As a parameter you use a map that maps project names to a list of Yjs room names (which are used for modelling inside the project). + +Development +----------------------------- +For testing the element during development, run `npm i` and `npm run serve`. +There is a demo available that can be used during development. diff --git a/frontend/callbacks/openidconnect-icons.js b/frontend/callbacks/openidconnect-icons.js new file mode 100644 index 0000000..67524d4 --- /dev/null +++ b/frontend/callbacks/openidconnect-icons.js @@ -0,0 +1,14 @@ +/** +@license +Copyright (c) 2018 The Polymer Project Authors. All rights reserved. +This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt +The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt +The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt +Code distributed by Google as part of the polymer project is also +subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt +*/ + +import { html } from '@polymer/lit-element'; + +export const openidconnectIcon = html``; +export const signOutIcon = html``; diff --git a/frontend/callbacks/openidconnect-popup-signin-callback.js b/frontend/callbacks/openidconnect-popup-signin-callback.js new file mode 100644 index 0000000..9a688dd --- /dev/null +++ b/frontend/callbacks/openidconnect-popup-signin-callback.js @@ -0,0 +1,20 @@ +/** +@license +Copyright (c) 2018 Advanced Community Information Systems (ACIS) Group, Chair of Computer Science 5 (Databases & +Information Systems), RWTH Aachen University, Germany. All rights reserved. +*/ + +import {LitElement} from '@polymer/lit-element'; + +import 'oidc-client'; + +class OpenIDConnectPopupSigninCallback extends LitElement { + + constructor() { + super(); + new UserManager().signinPopupCallback(); + } + +} + +customElements.define('openidconnect-popup-signin-callback', OpenIDConnectPopupSigninCallback); diff --git a/frontend/callbacks/openidconnect-popup-signout-callback.js b/frontend/callbacks/openidconnect-popup-signout-callback.js new file mode 100644 index 0000000..e32b0f2 --- /dev/null +++ b/frontend/callbacks/openidconnect-popup-signout-callback.js @@ -0,0 +1,20 @@ +/** +@license +Copyright (c) 2018 Advanced Community Information Systems (ACIS) Group, Chair of Computer Science 5 (Databases & +Information Systems), RWTH Aachen University, Germany. All rights reserved. +*/ + +import {LitElement} from '@polymer/lit-element'; + +import 'oidc-client'; + +class OpenIDConnectPopupSignoutCallback extends LitElement { + + constructor() { + super(); + new UserManager().signoutPopupCallback(); + } + +} + +customElements.define('openidconnect-popup-signout-callback', OpenIDConnectPopupSignoutCallback); diff --git a/frontend/callbacks/openidconnect-signin-silent-callback.js b/frontend/callbacks/openidconnect-signin-silent-callback.js new file mode 100644 index 0000000..e79b638 --- /dev/null +++ b/frontend/callbacks/openidconnect-signin-silent-callback.js @@ -0,0 +1,20 @@ +/** +@license +Copyright (c) 2018 Advanced Community Information Systems (ACIS) Group, Chair of Computer Science 5 (Databases & +Information Systems), RWTH Aachen University, Germany. All rights reserved. +*/ + +import {LitElement} from '@polymer/lit-element'; + +import 'oidc-client'; + +class OpenIDConnectSigninSilentCallback extends LitElement { + + constructor() { + super(); + new UserManager().signinSilentCallback(); + } + +} + +customElements.define('openidconnect-signin-silent-callback', OpenIDConnectSigninSilentCallback); diff --git a/frontend/callbacks/popup-signin-callback.html b/frontend/callbacks/popup-signin-callback.html new file mode 100644 index 0000000..f03bf78 --- /dev/null +++ b/frontend/callbacks/popup-signin-callback.html @@ -0,0 +1,24 @@ + + + + + + openidconnect-signin demo + + + + + + + + + + + + diff --git a/frontend/callbacks/popup-signout-callback.html b/frontend/callbacks/popup-signout-callback.html new file mode 100644 index 0000000..5ed653a --- /dev/null +++ b/frontend/callbacks/popup-signout-callback.html @@ -0,0 +1,24 @@ + + + + + + openidconnect-signin demo + + + + + + + + + + + + diff --git a/frontend/callbacks/silent-callback.html b/frontend/callbacks/silent-callback.html new file mode 100644 index 0000000..6f5e070 --- /dev/null +++ b/frontend/callbacks/silent-callback.html @@ -0,0 +1,24 @@ + + + + + + openidconnect-signin demo + + + + + + + + + + + + diff --git a/frontend/dev/README.md b/frontend/dev/README.md new file mode 100644 index 0000000..34a238c --- /dev/null +++ b/frontend/dev/README.md @@ -0,0 +1,2 @@ + +This directory contains demo files containing the element for development. By running `npm run serve` you can edit and see changes. diff --git a/frontend/dev/demo-element.js b/frontend/dev/demo-element.js new file mode 100644 index 0000000..0e934f9 --- /dev/null +++ b/frontend/dev/demo-element.js @@ -0,0 +1,136 @@ +import { LitElement, html } from 'lit-element'; +import '../project-list.js'; +import Auth from "../util/auth"; +import Common from "../util/common"; +import 'las2peer-frontend-statusbar/las2peer-frontend-statusbar.js'; + +export class DemoElement extends LitElement { + + static get properties() { + return { + selectedProject: { + type: Object + } + } + } + + constructor() { + super(); + + } + // I didnt get how to use ready, so simply used firstUpdated which is always called after render... + firstUpdated(changedProperties){ + const statusBar = this.shadowRoot.querySelector("#statusBar"); + // in the following we use (event) => this.method(event) in order to be able to access + // this.shadowRoot in the handleLogin and handleLogout methods + statusBar.addEventListener('signed-in', (event) => this.handleLogin(event)); + statusBar.addEventListener('signed-out', (event) => this.handleLogout(event)); + } + + handleLogin(event) { + console.log(event.detail.access_token); + Auth.setAuthDataToLocalStorage(event.detail.access_token); + + var url = "https://api.learning-layers.eu/auth/realms/main/protocol/openid-connect/userinfo"; + fetch(url, {method: "GET", headers: { + "Authorization": "Bearer " + Auth.getAccessToken() + }}).then(response => { + if(response.ok) { + return response.json(); + } + }).then(data => { + console.log(data.name); + // const userInfo = Common.getUserInfo(); + //userInfo.sub = data.sub; + Common.storeUserInfo(data); + + // reload projects + window.dispatchEvent(new CustomEvent("projects-reload-request", { bubbles: true })); + }); + } + + handleLogout() { + Auth.removeAuthDataFromLocalStorage(); + + // remove userInfo from localStorage + Common.removeUserInfoFromStorage(); + } + + render() { + return html` + +

Project list with "All Projects" enabled

+
+ +
+

Demo information:

+

Selected project:

+

${this.selectedProject}

+ + +
+
+ +

Project list with "All Projects" disabled

+ + `; + } + + getStatusBarElement() { + return this.shadowRoot.querySelector("#statusBar"); + } + + _triggerChange(event){ + let events = new CustomEvent("metadata-change-request", { + detail: { + "random": this.shadowRoot.querySelector("#metadataInput").value + }, + bubbles: true + }); + window.dispatchEvent(events); + } + + /** + * For testing the "project-selected" event of the project list. + * @param event Event that contains the information on the selected project. + * @private + */ + _onProjectSelected(event) { + console.log("onProjectSelected called"); + this.selectedProject = JSON.stringify(event.detail.project); + console.log(this.selectedProject); + } + + /** + * Example for using the online user list. + * Make sure that _onProjectsLoaded is called on the projects-loaded event of the project-list. + */ + _onProjectsLoaded(event) { + let projects = event.detail.projects; + + window.addEventListener("metadata-changed", e => { + console.log("metadata has changed", e.detail); + const project = JSON.parse(this.selectedProject); + project.metadata = e.detail; + this.selectedProject = JSON.stringify(project); + }); + + // uncomment this, if you want to test the online user list + /*let mapProjectRooms = {}; + for(let project of projects) { + mapProjectRooms[project.name] = ["exampleYjsRoom"]; + } + this.shadowRoot.getElementById("pl1").setOnlineUserListYjsRooms(mapProjectRooms);*/ + } +} + +window.customElements.define('demo-element', DemoElement); diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..5ca7109 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,19 @@ + + + + + + project-list Demo + + + + + + + + +
+ +
+ + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..a09dba9 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2390 @@ +{ + "name": "@rwth-acis/las2peer-project-service-frontend", + "version": "0.3.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", + "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.14.5" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz", + "integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==", + "dev": true + }, + "@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + } + } + }, + "@polymer/font-roboto": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@polymer/font-roboto/-/font-roboto-3.0.2.tgz", + "integrity": "sha512-tx5TauYSmzsIvmSqepUPDYbs4/Ejz2XbZ1IkD7JEGqkdNUJlh+9KU85G56Tfdk/xjEZ8zorFfN09OSwiMrIQWA==" + }, + "@polymer/iron-a11y-announcer": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@polymer/iron-a11y-announcer/-/iron-a11y-announcer-3.2.0.tgz", + "integrity": "sha512-We+hyaFHcg7Ke8ovsoxUpYEXFIJLHxMCDaLehTB4dELS+C+K0zMnGSiqQvb/YzGS+nSYpAfkQIyg1msOCdHMtA==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-a11y-keys-behavior": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-a11y-keys-behavior/-/iron-a11y-keys-behavior-3.0.1.tgz", + "integrity": "sha512-lnrjKq3ysbBPT/74l0Fj0U9H9C35Tpw2C/tpJ8a+5g8Y3YJs1WSZYnEl1yOkw6sEyaxOq/1DkzH0+60gGu5/PQ==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-ajax": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-ajax/-/iron-ajax-3.0.1.tgz", + "integrity": "sha512-7+TPEAfWsRdhj1Y8UeF1759ktpVu+c3sG16rJiUC3wF9+woQ9xI1zUm2d59i7Yc3aDEJrR/Q8Y262KlOvyGVNg==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-autogrow-textarea": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@polymer/iron-autogrow-textarea/-/iron-autogrow-textarea-3.0.3.tgz", + "integrity": "sha512-5r0VkWrIlm0JIp5E5wlnvkw7slK72lFRZXncmrsLZF+6n1dg2rI8jt7xpFzSmUWrqpcyXwyKaGaDvUjl3j4JLA==", + "requires": { + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/iron-validatable-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-behaviors": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-behaviors/-/iron-behaviors-3.0.1.tgz", + "integrity": "sha512-IMEwcv1lhf1HSQxuyWOUIL0lOBwmeaoSTpgCJeP9IBYnuB1SPQngmfRuHKgK6/m9LQ9F9miC7p3HeQQUdKAE0w==", + "requires": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-checked-element-behavior": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-checked-element-behavior/-/iron-checked-element-behavior-3.0.1.tgz", + "integrity": "sha512-aDr0cbCNVq49q+pOqa6CZutFh+wWpwPMLpEth9swx+GkAj+gCURhuQkaUYhIo5f2egDbEioR1aeHMnPlU9dQZA==", + "requires": { + "@polymer/iron-form-element-behavior": "^3.0.0-pre.26", + "@polymer/iron-validatable-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-dropdown": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-dropdown/-/iron-dropdown-3.0.1.tgz", + "integrity": "sha512-22yLhepfcKjuQMfFmRHi/9MPKTqkzgRrmWWW0P5uqK++xle53k2QBO5VYUAYiCN3ZcxIi9lEhZ9YWGeQj2JBig==", + "requires": { + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-overlay-behavior": "^3.0.0-pre.27", + "@polymer/neon-animation": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-fit-behavior": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@polymer/iron-fit-behavior/-/iron-fit-behavior-3.1.0.tgz", + "integrity": "sha512-ABcgIYqrjhmUT8tiuolqeGttF/8pd3sEymUDrO1vXbZu4FWIvoLNndrMDFvs++AGd12Mjf5pYy84NJc6dB8Vig==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-flex-layout": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-flex-layout/-/iron-flex-layout-3.0.1.tgz", + "integrity": "sha512-7gB869czArF+HZcPTVSgvA7tXYFze9EKckvM95NB7SqYF+NnsQyhoXgKnpFwGyo95lUjUW9TFDLUwDXnCYFtkw==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-form": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-form/-/iron-form-3.0.1.tgz", + "integrity": "sha512-JwSQXHjYALsytCeBkXlY8aRwqgZuYIqzOk3iHuugb1RXOdZ7MZHyJhMDVBbscHjxqPKu/KaVzAjrcfwNNafzEA==", + "requires": { + "@polymer/iron-ajax": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-form-element-behavior": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-form-element-behavior/-/iron-form-element-behavior-3.0.1.tgz", + "integrity": "sha512-G/e2KXyL5AY7mMjmomHkGpgS0uAf4ovNpKhkuUTRnMuMJuf589bKqE85KN4ovE1Tzhv2hJoh/igyD6ekHiYU1A==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-icon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-icon/-/iron-icon-3.0.1.tgz", + "integrity": "sha512-QLPwirk+UPZNaLnMew9VludXA4CWUCenRewgEcGYwdzVgDPCDbXxy6vRJjmweZobMQv/oVLppT2JZtJFnPxX6g==", + "requires": { + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/iron-meta": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-icons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-icons/-/iron-icons-3.0.1.tgz", + "integrity": "sha512-xtEI8erH2GIBiF3QxEMyW81XuVjguu6Le5WjEEpX67qd9z7jjmc4T/ke3zRUlnDydex9p8ytcwVpMIKcyvjYAQ==", + "requires": { + "@polymer/iron-icon": "^3.0.0-pre.26", + "@polymer/iron-iconset-svg": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-iconset-svg": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-iconset-svg/-/iron-iconset-svg-3.0.1.tgz", + "integrity": "sha512-XNwURbNHRw6u2fJe05O5fMYye6GSgDlDqCO+q6K1zAnKIrpgZwf2vTkBd5uCcZwsN0FyCB3mvNZx4jkh85dRDw==", + "requires": { + "@polymer/iron-meta": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-image": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@polymer/iron-image/-/iron-image-3.0.2.tgz", + "integrity": "sha512-VyYtnewGozDb5sUeoLR1OvKzlt5WAL6b8Od7fPpio5oYL+9t061/nTV8+ZMrpMgF2WgB0zqM/3K53o3pbK5v8Q==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-input": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-input/-/iron-input-3.0.1.tgz", + "integrity": "sha512-WLx13kEcbH9GKbj9+pWR6pbJkA5kxn3796ynx6eQd2rueMyUfVTR3GzOvadBKsciUuIuzrxpBWZ2+3UcueVUQQ==", + "requires": { + "@polymer/iron-a11y-announcer": "^3.0.0-pre.26", + "@polymer/iron-validatable-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-list": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@polymer/iron-list/-/iron-list-3.1.0.tgz", + "integrity": "sha512-Eiv6xd3h3oPmn8SXFntXVfC3ZnegH+KHAxiKLKcOASFSRY3mHnr2AdcnExUJ9ItoCMA5UzKaM/0U22eWzGERtA==", + "requires": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/iron-resizable-behavior": "^3.0.0-pre.26", + "@polymer/iron-scroll-target-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-menu-behavior": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@polymer/iron-menu-behavior/-/iron-menu-behavior-3.0.2.tgz", + "integrity": "sha512-8dpASkFNBIkxAJWsFLWIO1M7tKM0+wKs3PqdeF/dDdBciwoaaFgC2K1XCZFZnbe2t9/nJgemXxVugGZAWpYCGg==", + "requires": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/iron-selector": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-meta": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-meta/-/iron-meta-3.0.1.tgz", + "integrity": "sha512-pWguPugiLYmWFV9UWxLWzZ6gm4wBwQdDy4VULKwdHCqR7OP7u98h+XDdGZsSlDPv6qoryV/e3tGHlTIT0mbzJA==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-overlay-behavior": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@polymer/iron-overlay-behavior/-/iron-overlay-behavior-3.0.3.tgz", + "integrity": "sha512-Q/Fp0+uOQQ145ebZ7T8Cxl4m1tUKYjyymkjcL2rXUm+aDQGb1wA1M1LYxUF5YBqd+9lipE0PTIiYwA2ZL/sznA==", + "requires": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/iron-fit-behavior": "^3.0.0-pre.26", + "@polymer/iron-resizable-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-resizable-behavior": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-resizable-behavior/-/iron-resizable-behavior-3.0.1.tgz", + "integrity": "sha512-FyHxRxFspVoRaeZSWpT3y0C9awomb4tXXolIJcZ7RvXhMP632V5lez+ch5G5SwK0LpnAPkg35eB0LPMFv+YMMQ==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-scroll-target-behavior": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-scroll-target-behavior/-/iron-scroll-target-behavior-3.0.1.tgz", + "integrity": "sha512-xg1WanG25BIkQE8rhuReqY9zx1K5M7F+YAIYpswEp5eyDIaZ1Y3vUmVeQ3KG+hiSugzI1M752azXN7kvyhOBcQ==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-selector": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-selector/-/iron-selector-3.0.1.tgz", + "integrity": "sha512-sBVk2uas6prW0glUe2xEJJYlvxmYzM40Au9OKbfDK2Qekou/fLKcBRyIYI39kuI8zWRaip8f3CI8qXcUHnKb1A==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-validatable-behavior": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-validatable-behavior/-/iron-validatable-behavior-3.0.1.tgz", + "integrity": "sha512-wwpYh6wOa4fNI+jH5EYKC7TVPYQ2OfgQqocWat7GsNWcsblKYhLYbwsvEY5nO0n2xKqNfZzDLrUom5INJN7msQ==", + "requires": { + "@polymer/iron-meta": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/lit-element": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@polymer/lit-element/-/lit-element-0.6.5.tgz", + "integrity": "sha512-KVjuU/5Ugp6PFob6YEe1/B4GCKjqhEy9Tj954shL6d3DohT2sNAmbX9QfbXvcZ8RhbVELK6dzbN3i2BRA3mOKg==", + "requires": { + "lit-html": "^1.0.0-rc.1" + } + }, + "@polymer/neon-animation": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/neon-animation/-/neon-animation-3.0.1.tgz", + "integrity": "sha512-cDDc0llpVCe0ATbDS3clDthI54Bc8YwZIeTGGmBJleKOvbRTUC5+ssJmRL+VwVh+VM5FlnQlx760ppftY3uprg==", + "requires": { + "@polymer/iron-resizable-behavior": "^3.0.0-pre.26", + "@polymer/iron-selector": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-badge": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@polymer/paper-badge/-/paper-badge-3.1.0.tgz", + "integrity": "sha512-5SH5Xw9ji16BjIZT2wY7oVgWX01fDyzm/nGnDi55iujPGsfaPV1itze7c9/3wlmgI+b28KBApUY9hW8f0h2V6g==", + "requires": { + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/iron-icon": "^3.0.0-pre.26", + "@polymer/iron-resizable-behavior": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-behaviors": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/paper-behaviors/-/paper-behaviors-3.0.1.tgz", + "integrity": "sha512-6knhj69fPJejv8qR0kCSUY+Q0XjaUf0OSnkjRjmTJPAwSrRYtgqE+l6P1FfA+py1X/cUjgne9EF5rMZAKJIg1g==", + "requires": { + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-checked-element-behavior": "^3.0.0-pre.26", + "@polymer/paper-ripple": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-button": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/paper-button/-/paper-button-3.0.1.tgz", + "integrity": "sha512-JRNBc+Oj9EWnmyLr7FcCr8T1KAnEHPh6mosln9BUdkM+qYaYsudSICh3cjTIbnj6AuF5OJidoLkM1dlyj0j6Zg==", + "requires": { + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/paper-behaviors": "^3.0.0-pre.27", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-card": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/paper-card/-/paper-card-3.0.1.tgz", + "integrity": "sha512-ZYzfA4kzP9niRO22wSOBL2RS+URZNUP5XmUCwN91fYPIGO0Qbimh7d1O2HpJD4cRCZhvGYn2CJMDMVmDm35vIg==", + "requires": { + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/iron-image": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-dialog": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/paper-dialog/-/paper-dialog-3.0.1.tgz", + "integrity": "sha512-KvglYbEq7AWJvui2j6WKLnOvgVMeGjovAydGrPRj7kVzCiD49Eq/hpYFJTRV5iDcalWH+mORUpw+jrFnG9+Kgw==", + "requires": { + "@polymer/iron-overlay-behavior": "^3.0.0-pre.27", + "@polymer/neon-animation": "^3.0.0-pre.26", + "@polymer/paper-dialog-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-dialog-behavior": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/paper-dialog-behavior/-/paper-dialog-behavior-3.0.1.tgz", + "integrity": "sha512-wbI4kCK8le/9MHT+IXzvHjoatxf3kd3Yn0tgozAiAwfSZ7N4Ubpi5MHrK0m9S9PeIxKokAgBYdTUrezSE5378A==", + "requires": { + "@polymer/iron-overlay-behavior": "^3.0.0-pre.27", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-dialog-scrollable": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/paper-dialog-scrollable/-/paper-dialog-scrollable-3.0.1.tgz", + "integrity": "sha512-1E8B9kNdL58jUrJ/BwqJeOoNVcxNrB559z//d1V0rVHWT5bWCCZegwS3G06iFK5MjxWFbIKzleVTLrT0opiZkA==", + "requires": { + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/paper-dialog-behavior": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-dropdown-menu": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@polymer/paper-dropdown-menu/-/paper-dropdown-menu-3.2.0.tgz", + "integrity": "sha512-2ohwSHF+RLSK6kA0UkkMiMQF6EZcaEYWAA25kfisI6DWie7yozKrpQNsqvwfOEHU6DdDMIotrOtH1TM88YS8Zg==", + "requires": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/iron-form-element-behavior": "^3.0.0-pre.26", + "@polymer/iron-icon": "^3.0.0-pre.26", + "@polymer/iron-iconset-svg": "^3.0.0-pre.26", + "@polymer/iron-validatable-behavior": "^3.0.0-pre.26", + "@polymer/paper-behaviors": "^3.0.0-pre.27", + "@polymer/paper-input": "^3.1.0", + "@polymer/paper-menu-button": "^3.1.0", + "@polymer/paper-ripple": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.3.1" + } + }, + "@polymer/paper-icon-button": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@polymer/paper-icon-button/-/paper-icon-button-3.0.2.tgz", + "integrity": "sha512-kOdxQgnKL097bggFF6PWvsBYuWg+MCcoHoTHX6bh/MuZoWFZNjrFntFqwuB4oEbpjCpfm4moA33muPJFj7CihQ==", + "requires": { + "@polymer/iron-icon": "^3.0.0-pre.26", + "@polymer/paper-behaviors": "^3.0.0-pre.27", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-input": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@polymer/paper-input/-/paper-input-3.2.1.tgz", + "integrity": "sha512-6ghgwQKM6mS0hAQxQqj+tkeEY1VUBqAsrasAm8V5RpNcfSWQC/hhRFxU0beGuKTAhndzezDzWYP6Zz4b8fExGg==", + "requires": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/iron-autogrow-textarea": "^3.0.0-pre.26", + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-form-element-behavior": "^3.0.0-pre.26", + "@polymer/iron-input": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-item": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/paper-item/-/paper-item-3.0.1.tgz", + "integrity": "sha512-KTk2N+GsYiI/HuubL3sxebZ6tteQbBOAp4QVLAnbjSPmwl+mJSDWk+omuadesU0bpkCwaWVs3fHuQsmXxy4pkw==", + "requires": { + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-listbox": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/paper-listbox/-/paper-listbox-3.0.1.tgz", + "integrity": "sha512-vMLWFpYcggAPmEDBmK+96fFefacOG3GLB1EguTn8+ZkqI+328hNfw1MzHjH68rgCIIUtjmm+9qgB1Sy/MN0a/A==", + "requires": { + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-menu-behavior": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-menu-button": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@polymer/paper-menu-button/-/paper-menu-button-3.1.0.tgz", + "integrity": "sha512-q0G0/rvYD/FFmIBMGCQWjfXzRqwFw9+WHSYV4uOQzM1Ln8LMXSAd+2CENsbVwtMh6fmBePj15ZlU8SM2dt1WDQ==", + "requires": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-dropdown": "^3.0.0-pre.26", + "@polymer/iron-fit-behavior": "^3.1.0", + "@polymer/neon-animation": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-ripple": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@polymer/paper-ripple/-/paper-ripple-3.0.2.tgz", + "integrity": "sha512-DnLNvYIMsiayeICroYxx6Q6Hg1cUU8HN2sbutXazlemAlGqdq80qz3TIaVdbpbt/pvjcFGX2HtntMlPstCge8Q==", + "requires": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-spinner": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@polymer/paper-spinner/-/paper-spinner-3.0.2.tgz", + "integrity": "sha512-XUzu8/4NH+pnNZUTI2MxtOKFAr0EOsW7eGhTg3VBhTh7DDW/q3ewzwYRWnqNJokX9BEnxKMiXXaIeTEBq4k2dw==", + "requires": { + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-styles": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/paper-styles/-/paper-styles-3.0.1.tgz", + "integrity": "sha512-y6hmObLqlCx602TQiSBKHqjwkE7xmDiFkoxdYGaNjtv4xcysOTdVJsDR/R9UHwIaxJ7gHlthMSykir1nv78++g==", + "requires": { + "@polymer/font-roboto": "^3.0.1", + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-tabs": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@polymer/paper-tabs/-/paper-tabs-3.1.0.tgz", + "integrity": "sha512-t8G+3CiyI0R+wA077UNQXR/oG9GlsqRRO1KMsFHHjBSsYqWXghNsqxUG827wEj+PafI5u9tZ3vVt1S++Lg4B2g==", + "requires": { + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/iron-icon": "^3.0.0-pre.26", + "@polymer/iron-iconset-svg": "^3.0.0-pre.26", + "@polymer/iron-menu-behavior": "^3.0.0-pre.26", + "@polymer/iron-resizable-behavior": "^3.0.0-pre.26", + "@polymer/paper-behaviors": "^3.0.0-pre.27", + "@polymer/paper-icon-button": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-toast": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/paper-toast/-/paper-toast-3.0.1.tgz", + "integrity": "sha512-pizuogzObniDdICUc6dSLrnDt2VzzoRne1gCmbD6sfOATVv5tc8UfrqhA2iHngbNBEbniBiciS3iogdp5KTVUQ==", + "requires": { + "@polymer/iron-a11y-announcer": "^3.0.0-pre.26", + "@polymer/iron-fit-behavior": "^3.0.0-pre.26", + "@polymer/iron-overlay-behavior": "^3.0.0-pre.27", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-toggle-button": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/paper-toggle-button/-/paper-toggle-button-3.0.1.tgz", + "integrity": "sha512-jadZB60fycT7YnSAH0H23LYo6/2HYmMZTtNr9LpdSIRFPLX6mqqxewex92cFz019bMKaRJgORn308hRlJo2u6A==", + "requires": { + "@polymer/iron-checked-element-behavior": "^3.0.0-pre.26", + "@polymer/paper-behaviors": "^3.0.0-pre.27", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/polymer": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@polymer/polymer/-/polymer-3.4.1.tgz", + "integrity": "sha512-KPWnhDZibtqKrUz7enIPOiO4ZQoJNOuLwqrhV2MXzIt3VVnUVJVG5ORz4Z2sgO+UZ+/UZnPD0jqY+jmw/+a9mQ==", + "requires": { + "@webcomponents/shadycss": "^1.9.1" + } + }, + "@rollup/plugin-node-resolve": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", + "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.19.0" + } + }, + "@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "requires": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + } + }, + "@types/accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/body-parser": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz", + "integrity": "sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/command-line-args": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.0.tgz", + "integrity": "sha512-UuKzKpJJ/Ief6ufIaIzr3A/0XnluX7RvFgwkV89Yzvm77wCh1kFaFmqN8XEnGcN62EuHdedQjEMb8mYxFLGPyA==", + "dev": true + }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-0mPF08jn9zYI0n0Q/Pnz7C4kThdSt+6LD4amsrYDDpgBfrVWa3TcCOxKX1zkGgYniGagRv8heN2cbh+CAn+uuQ==", + "dev": true + }, + "@types/cookies": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.7.tgz", + "integrity": "sha512-h7BcvPUogWbKCzBR2lY4oqaZbO3jXZksexYJVFvkrFeLgbZjQkU4x8pRq6eg2MHXQhY0McQdqmmsxRWlVAHooA==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", + "@types/node": "*" + } + }, + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz", + "integrity": "sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/http-assert": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.2.tgz", + "integrity": "sha512-Ddzuzv/bB2prZnJKlS1sEYhaeT50wfJjhcTTTQLjEsEZJlk3XB4Xohieyq+P4VXIzg7lrQ1Spd/PfRnBpQsdqA==", + "dev": true + }, + "@types/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-e+2rjEwK6KDaNOm5Aa9wNGgyS9oSZU/4pfSMMPYNOfjvFI0WVXm29+ITRFr6aKDvvKo7uU1jV68MW4ScsfDi7Q==", + "dev": true + }, + "@types/keygrip": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.2.tgz", + "integrity": "sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==", + "dev": true + }, + "@types/koa": { + "version": "2.13.4", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.13.4.tgz", + "integrity": "sha512-dfHYMfU+z/vKtQB7NUrthdAEiSvnLebvBjwHtfFmpZmB7em2N3WVQdHgnFq+xvyVgxW5jKDmjWfLD3lw4g4uTw==", + "dev": true, + "requires": { + "@types/accepts": "*", + "@types/content-disposition": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/http-errors": "*", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + } + }, + "@types/koa-compose": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.5.tgz", + "integrity": "sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==", + "dev": true, + "requires": { + "@types/koa": "*" + } + }, + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "dev": true + }, + "@types/node": { + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==", + "dev": true + }, + "@types/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-ARATsLdrGPUnaBvxLhUlnltcMgn7pQG312S8ccdYlnyijabrX9RN/KN/iGj9Am96CoW8e/K9628BA7Bv4XHdrA==", + "dev": true + }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true + }, + "@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "dev": true, + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "@types/ws": { + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@web/config-loader": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@web/config-loader/-/config-loader-0.1.3.tgz", + "integrity": "sha512-XVKH79pk4d3EHRhofete8eAnqto1e8mCRAqPV00KLNFzCWSe8sWmLnqKCqkPNARC6nksMaGrATnA5sPDRllMpQ==", + "dev": true, + "requires": { + "semver": "^7.3.4" + } + }, + "@web/dev-server": { + "version": "0.1.20", + "resolved": "https://registry.npmjs.org/@web/dev-server/-/dev-server-0.1.20.tgz", + "integrity": "sha512-jn+X91xfTlTtidQFPp/o9HvbYCdFMGwc7gA8NBHv+PTPSIu79wxQESV2DONKFMyR0RXshMvT3mwQwlIvPEzuBg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.11", + "@rollup/plugin-node-resolve": "^11.0.1", + "@types/command-line-args": "^5.0.0", + "@web/config-loader": "^0.1.3", + "@web/dev-server-core": "^0.3.12", + "@web/dev-server-rollup": "^0.3.7", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "command-line-args": "^5.1.1", + "command-line-usage": "^6.1.1", + "debounce": "^1.2.0", + "deepmerge": "^4.2.2", + "ip": "^1.1.5", + "open": "^8.0.2", + "portfinder": "^1.0.28" + } + }, + "@web/dev-server-core": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.3.13.tgz", + "integrity": "sha512-bGJHPeFRWATNfuL9Pp2LfqhnmqhBCc5eOO5AWQa0X+WQAwHiFo6xZNfsvsnJ1gvxXgsE4jKBAGu9lQRisvFRFA==", + "dev": true, + "requires": { + "@types/koa": "^2.11.6", + "@types/ws": "^7.4.0", + "@web/parse5-utils": "^1.2.0", + "chokidar": "^3.4.3", + "clone": "^2.1.2", + "es-module-lexer": "^0.4.0", + "get-stream": "^6.0.0", + "is-stream": "^2.0.0", + "isbinaryfile": "^4.0.6", + "koa": "^2.13.0", + "koa-etag": "^4.0.0", + "koa-send": "^5.0.1", + "koa-static": "^5.0.0", + "lru-cache": "^6.0.0", + "mime-types": "^2.1.27", + "parse5": "^6.0.1", + "picomatch": "^2.2.2", + "ws": "^7.4.2" + }, + "dependencies": { + "ws": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz", + "integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==", + "dev": true + } + } + }, + "@web/dev-server-rollup": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@web/dev-server-rollup/-/dev-server-rollup-0.3.8.tgz", + "integrity": "sha512-IoQh/mcqssyCL3nNB4dAwcV3VMZfwAcRw2TDFnY4bKnPnkZSOSFR3R+SuyvwhvvnG14NmnNE6RA2Lf7lt7AsmA==", + "dev": true, + "requires": { + "@web/dev-server-core": "^0.3.3", + "chalk": "^4.1.0", + "parse5": "^6.0.1", + "rollup": "^2.35.1", + "whatwg-url": "^9.0.0" + } + }, + "@web/parse5-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-1.3.0.tgz", + "integrity": "sha512-Pgkx3ECc8EgXSlS5EyrgzSOoUbM6P8OKS471HLAyvOBcP1NCBn0to4RN/OaKASGq8qa3j+lPX9H14uA5AHEnQg==", + "dev": true, + "requires": { + "@types/parse5": "^6.0.1", + "parse5": "^6.0.1" + } + }, + "@webcomponents/shadycss": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webcomponents/shadycss/-/shadycss-1.11.0.tgz", + "integrity": "sha512-L5O/+UPum8erOleNjKq6k58GVl3fNsEQdSOyh0EUhNmi7tHUyRuCJy1uqJiWydWcLARE5IPsMoPYMZmUGrz1JA==" + }, + "@webcomponents/webcomponentsjs": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.6.0.tgz", + "integrity": "sha512-Moog+Smx3ORTbWwuPqoclr+uvfLnciVd6wdCaVscHPrxbmQ/IJKm3wbB7hpzJtXWjAq2l/6QMlO85aZiOdtv5Q==" + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dev": true, + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" + }, + "after": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.1.tgz", + "integrity": "sha1-q11PuIP1loFtNRX495HAr0ht1ic=" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", + "dev": true + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "dev": true + }, + "arraybuffer.slice": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz", + "integrity": "sha1-8zshWfBTKj8xB6JywMz70a0peco=" + }, + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, + "base64-arraybuffer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.2.tgz", + "integrity": "sha1-R030qfLaJOBd8xWMOx2zw81GoVQ=" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "benchmark": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-1.0.0.tgz", + "integrity": "sha1-Lx4vpMNZ8REiqhgwgiGOlX45DHM=" + }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "requires": { + "callsite": "1.0.0" + } + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "bindings": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz", + "integrity": "sha1-FK1hE4EtLTfXLme0ystLtyZQXxE=", + "optional": true + }, + "blob": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz", + "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=" + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "bufferutil": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-1.2.1.tgz", + "integrity": "sha1-N75dNuHgZJIiHmjUdLGsWOUQy9c=", + "optional": true, + "requires": { + "bindings": "1.2.x", + "nan": "^2.0.5" + } + }, + "builtin-modules": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", + "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", + "dev": true + }, + "cache-content-type": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", + "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", + "dev": true, + "requires": { + "mime-types": "^2.1.18", + "ylru": "^1.2.0" + } + }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" + }, + "camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "chokidar": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "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 + }, + "command-line-args": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.0.tgz", + "integrity": "sha512-4zqtU1hYsSJzcJBOcNZIbW5Fbk9BkjCp1pZVhQKoRaWL5J7N4XphDLwo8aWwdQpTugxwu+jf9u2ZhkXiqp5Z6A==", + "dev": true, + "requires": { + "array-back": "^3.1.0", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" + } + }, + "command-line-usage": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.1.tgz", + "integrity": "sha512-F59pEuAR9o1SF/bD0dQBDluhpT4jJQNWUHEuVBqpDmCUo6gPjCi+m9fCWnWZVR/oG6cMTUms4h+3NPl74wGXvA==", + "dev": true, + "requires": { + "array-back": "^4.0.1", + "chalk": "^2.4.2", + "table-layout": "^1.0.1", + "typical": "^5.2.0" + }, + "dependencies": { + "array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true + } + } + }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" + }, + "component-emitter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz", + "integrity": "sha1-KWWU8nU9qmOZbSrwjRWpURbJrsM=" + }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "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 + }, + "cookies": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", + "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==", + "dev": true, + "requires": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + } + }, + "core-js": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.16.0.tgz", + "integrity": "sha512-5+5VxRFmSf97nM8Jr2wzOwLqRo6zphH2aX+7KsAUONObyzakDNq2G/bgbhinxB4PoV9L3aXQYhiDKyIKWd2c8g==" + }, + "crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" + }, + "debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true + }, + "debug": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz", + "integrity": "sha1-BuHqgILCyxTjmAbiLi9vdX+Srzk=" + }, + "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 + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true + }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true + }, + "define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, + "engine.io-client": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-1.5.4.tgz", + "integrity": "sha1-xq1lpldSopy5MMaRHledKyjREGw=", + "requires": { + "component-emitter": "1.1.2", + "component-inherit": "0.0.3", + "debug": "1.0.4", + "engine.io-parser": "1.2.2", + "has-cors": "1.0.3", + "indexof": "0.0.1", + "parsejson": "0.0.1", + "parseqs": "0.0.2", + "parseuri": "0.0.4", + "ws": "0.8.0", + "xmlhttprequest": "https://github.com/rase-/node-XMLHttpRequest/archive/a6b6f2.tar.gz" + }, + "dependencies": { + "debug": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-1.0.4.tgz", + "integrity": "sha1-W5wla9VLbsAigxdvqKDt5tFUy/g=", + "requires": { + "ms": "0.6.2" + } + }, + "parseuri": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.4.tgz", + "integrity": "sha1-gGWCo5iH4eoY3V4v4OAZAiaOk1A=", + "requires": { + "better-assert": "~1.0.0" + } + } + } + }, + "engine.io-parser": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-1.2.2.tgz", + "integrity": "sha1-zQgQQf7qOcZDI/95uCqQpyr8zN0=", + "requires": { + "after": "0.8.1", + "arraybuffer.slice": "0.0.6", + "base64-arraybuffer": "0.1.2", + "blob": "0.0.4", + "has-binary": "0.1.6", + "utf8": "2.1.0" + } + }, + "es-module-lexer": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.4.1.tgz", + "integrity": "sha512-ooYciCUtfw6/d2w56UVeqHPcoCFAiJdz5XOkYpv/Txl1HMUozpXjz/2RIQgqwKdXNDPSF1W7mJCFse3G+HDyAA==", + "dev": true + }, + "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 + }, + "estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", + "dev": true, + "requires": { + "array-back": "^3.0.1" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "global": { + "version": "https://github.com/component/global/archive/v2.0.1.tar.gz", + "integrity": "sha512-O91OcV/NbdmQJPHaRu2ekSP7bqFRLWgqSwaJvqHPZHUwmHBagQYTOra29+LnzzG3lZkXH1ANzHzfCxtAPM9HMA==" + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-binary": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.6.tgz", + "integrity": "sha1-JTJvOc+k9hath4eJTjryz7x7bhA=", + "requires": { + "isarray": "0.0.1" + } + }, + "has-cors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.0.3.tgz", + "integrity": "sha1-UCrLmzEE2sM90mMOry+IiwuvTLM=", + "requires": { + "global": "https://github.com/component/global/archive/v2.0.1.tar.gz" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "http-assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.4.1.tgz", + "integrity": "sha512-rdw7q6GTlibqVVbXr0CKelfV5iY8G2HqEUkhSk297BMbSpSL8crXC+9rjKoMcZZEsksX30le6f/4ul4E28gegw==", + "dev": true, + "requires": { + "deep-equal": "~1.0.1", + "http-errors": "~1.7.2" + }, + "dependencies": { + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + } + } + }, + "http-errors": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz", + "integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "dependencies": { + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + } + } + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-core-module": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.5.0.tgz", + "integrity": "sha512-TXCMSDsEHMEEZ6eCA8rwRDbLu55MRGmrctljsBX/2v1d9/GzqHOxW5c5oPSgrUt2vBFXebu9rGqckXGPWOlYpg==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "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-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "isbinaryfile": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.8.tgz", + "integrity": "sha512-53h6XFniq77YdW+spoRrebh0mnmTxRPTlcuIArO57lmMdq4uBKFKaeTjnb92oYWrSn/LVL+LT+Hap2tFQj8V+w==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "json3": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.2.6.tgz", + "integrity": "sha1-9u/JPAagTemuxTBT3yVZuxniA4s=" + }, + "keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "dev": true, + "requires": { + "tsscmp": "1.0.6" + } + }, + "koa": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.13.1.tgz", + "integrity": "sha512-Lb2Dloc72auj5vK4X4qqL7B5jyDPQaZucc9sR/71byg7ryoD1NCaCm63CShk9ID9quQvDEi1bGR/iGjCG7As3w==", + "dev": true, + "requires": { + "accepts": "^1.3.5", + "cache-content-type": "^1.0.0", + "content-disposition": "~0.5.2", + "content-type": "^1.0.4", + "cookies": "~0.8.0", + "debug": "~3.1.0", + "delegates": "^1.0.0", + "depd": "^2.0.0", + "destroy": "^1.0.4", + "encodeurl": "^1.0.2", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.3.0", + "http-errors": "^1.6.3", + "is-generator-function": "^1.0.7", + "koa-compose": "^4.1.0", + "koa-convert": "^1.2.0", + "on-finished": "^2.3.0", + "only": "~0.0.2", + "parseurl": "^1.3.2", + "statuses": "^1.5.0", + "type-is": "^1.6.16", + "vary": "^1.1.2" + }, + "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" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", + "dev": true + }, + "koa-convert": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", + "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", + "dev": true, + "requires": { + "co": "^4.6.0", + "koa-compose": "^3.0.0" + }, + "dependencies": { + "koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", + "dev": true, + "requires": { + "any-promise": "^1.1.0" + } + } + } + }, + "koa-etag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/koa-etag/-/koa-etag-4.0.0.tgz", + "integrity": "sha512-1cSdezCkBWlyuB9l6c/IFoe1ANCDdPBxkDkRiaIup40xpUub6U/wwRXoKBZw/O5BifX9OlqAjYnDyzM6+l+TAg==", + "dev": true, + "requires": { + "etag": "^1.8.1" + } + }, + "koa-send": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.1.tgz", + "integrity": "sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "http-errors": "^1.7.3", + "resolve-path": "^1.4.0" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "koa-static": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/koa-static/-/koa-static-5.0.0.tgz", + "integrity": "sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==", + "dev": true, + "requires": { + "debug": "^3.1.0", + "koa-send": "^5.0.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "las2peer-frontend-statusbar": { + "version": "github:rwth-acis/las2peer-frontend-statusbar#b969a8d05c208e6702bf650e5d6b6b3b42980890", + "from": "github:rwth-acis/las2peer-frontend-statusbar#0.3.1", + "requires": { + "@polymer/lit-element": "^0.6.3", + "las2peer-frontend-user-widget": "github:rwth-acis/las2peer-frontend-user-widget#0.3.0", + "openidconnect-signin": "github:rwth-acis/openidconnect-signin" + } + }, + "las2peer-frontend-user-widget": { + "version": "github:rwth-acis/las2peer-frontend-user-widget#63ad251f95b323938b78e0d68b14ec67a1a05511", + "from": "github:rwth-acis/las2peer-frontend-user-widget#0.3.0", + "requires": { + "@polymer/iron-ajax": "^3.0.1", + "@polymer/iron-dropdown": "^3.0.1", + "@polymer/iron-flex-layout": "^3.0.1", + "@polymer/iron-form": "^3.0.1", + "@polymer/iron-icon": "^3.0.1", + "@polymer/iron-icons": "^3.0.1", + "@polymer/iron-list": "^3.0.1", + "@polymer/paper-badge": "^3.0.1", + "@polymer/paper-button": "^3.0.1", + "@polymer/paper-card": "^3.0.1", + "@polymer/paper-dialog": "^3.0.1", + "@polymer/paper-dialog-scrollable": "^3.0.1", + "@polymer/paper-dropdown-menu": "^3.0.1", + "@polymer/paper-icon-button": "^3.0.1", + "@polymer/paper-input": "^3.0.1", + "@polymer/paper-item": "^3.0.1", + "@polymer/paper-spinner": "^3.0.1", + "@polymer/paper-styles": "^3.0.1", + "@polymer/paper-toast": "^3.0.1", + "@polymer/paper-toggle-button": "^3.0.1", + "@webcomponents/webcomponentsjs": "^2.2.1" + } + }, + "lit-element": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-2.5.1.tgz", + "integrity": "sha512-ogu7PiJTA33bEK0xGu1dmaX5vhcRjBXCFexPja0e7P7jqLhTpNKYRPmE+GmiCaRVAbiQKGkUgkh/i6+bh++dPQ==", + "requires": { + "lit-html": "^1.1.1" + } + }, + "lit-html": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.4.1.tgz", + "integrity": "sha512-B9btcSgPYb1q4oSOb/PrOT6Z/H+r6xuNzfH4lFli/AWhYwdtrgQkQWBbIc6mdnf6E2IL3gDXdkkqNktpU0OZQA==" + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": 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 + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "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 + }, + "mime-db": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz", + "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==", + "dev": true + }, + "mime-types": { + "version": "2.1.32", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz", + "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==", + "dev": true, + "requires": { + "mime-db": "1.49.0" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "ms": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.6.2.tgz", + "integrity": "sha1-2JwhJMb9wTU9Zai3e/GqxLGTcIw=" + }, + "nan": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", + "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", + "optional": true + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" + }, + "oidc-client": { + "version": "github:rwth-acis/oidc-client-js#d3205cf842a127bc645446a9aada46c9801a4709", + "from": "github:rwth-acis/oidc-client-js", + "requires": { + "acorn": "^7.4.1", + "base64-js": "^1.5.1", + "core-js": "^3.8.3", + "crypto-js": "^4.0.0", + "serialize-javascript": "^4.0.0" + } + }, + "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" + } + }, + "only": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=", + "dev": true + }, + "open": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/open/-/open-8.2.1.tgz", + "integrity": "sha512-rXILpcQlkF/QuFez2BJDf3GsqpjGKbkUUToAIGo9A0Q6ZkoSGogZJulrUdwRkrAsoQvoZsrjCYt8+zblOk7JQQ==", + "dev": true, + "requires": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + } + }, + "openidconnect-signin": { + "version": "github:rwth-acis/openidconnect-signin#f3ad5e95d3014ca0149615c17a12182de0ede3d3", + "from": "github:rwth-acis/openidconnect-signin", + "requires": { + "lit-element": "^2.5.1", + "oidc-client": "github:rwth-acis/oidc-client-js" + } + }, + "options": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", + "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=" + }, + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "parsejson": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.1.tgz", + "integrity": "sha1-mxDGwNglq1ieaFFTgm3go7oni8w=", + "requires": { + "better-assert": "~1.0.0" + } + }, + "parseqs": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.2.tgz", + "integrity": "sha1-nf5wss3aw4i95PNbHyQPpYrb5sc=", + "requires": { + "better-assert": "~1.0.0" + } + }, + "parseuri": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.2.tgz", + "integrity": "sha1-20GHjy1pZHGL6HCzFAlz2Ak74VY=", + "requires": { + "better-assert": "~1.0.0" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, + "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-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true + }, + "portfinder": { + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", + "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==", + "dev": true, + "requires": { + "async": "^2.6.2", + "debug": "^3.1.1", + "mkdirp": "^0.5.5" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "reduce-flatten": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", + "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==", + "dev": true + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "resolve-path": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/resolve-path/-/resolve-path-1.4.0.tgz", + "integrity": "sha1-xL2p9e+y/OZSR4c6s2u02DT+Fvc=", + "dev": true, + "requires": { + "http-errors": "~1.6.2", + "path-is-absolute": "1.0.1" + }, + "dependencies": { + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "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 + } + } + }, + "rollup": { + "version": "2.56.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.56.1.tgz", + "integrity": "sha512-KkrsNjeiTfGJMUFBi/PNfj3fnt70akqdoNXOjlzwo98uA1qrlkmgt6SGaK5OwhyDYCVnJb6jb2Xa2wbI47P4Nw==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "requires": { + "randombytes": "^2.1.0" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + }, + "socket.io-client": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-1.3.7.tgz", + "integrity": "sha1-erfAabjVBCXrJl8DH4Spfm6+cZw=", + "requires": { + "backo2": "1.0.2", + "component-bind": "1.0.0", + "component-emitter": "1.1.2", + "debug": "0.7.4", + "engine.io-client": "1.5.4", + "has-binary": "0.1.6", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseuri": "0.0.2", + "socket.io-parser": "2.2.4", + "to-array": "0.1.3" + } + }, + "socket.io-parser": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.2.4.tgz", + "integrity": "sha1-+c4ZvxkJYIzrFdl3IeI7/dHnz2U=", + "requires": { + "benchmark": "1.0.0", + "component-emitter": "1.1.2", + "debug": "0.7.4", + "isarray": "0.0.1", + "json3": "3.2.6" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "table-layout": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz", + "integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==", + "dev": true, + "requires": { + "array-back": "^4.0.1", + "deep-extend": "~0.6.0", + "typical": "^5.2.0", + "wordwrapjs": "^4.0.0" + }, + "dependencies": { + "array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", + "dev": true + }, + "typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true + } + } + }, + "to-array": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.3.tgz", + "integrity": "sha1-1F2txjY0F/YPKEdP6lDs3btPSZE=" + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "dev": true + }, + "tr46": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", + "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "dev": true, + "requires": { + "punycode": "^2.1.1" + } + }, + "tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typical": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", + "dev": true + }, + "ultron": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz", + "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=" + }, + "utf-8-validate": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-1.2.2.tgz", + "integrity": "sha1-i7hxpHQeCFxwSHynrNvX1tNgKes=", + "optional": true, + "requires": { + "bindings": "~1.2.1", + "nan": "~2.4.0" + }, + "dependencies": { + "nan": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.4.0.tgz", + "integrity": "sha1-+zxZ1F/k7/4hXwuJD4rfbrMtIjI=", + "optional": true + } + } + }, + "utf8": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-2.1.0.tgz", + "integrity": "sha1-DP7FyAUtRKI+OqqQgQToB1+V39U=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true + }, + "webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "dev": true + }, + "whatwg-url": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-9.1.0.tgz", + "integrity": "sha512-CQ0UcrPHyomtlOCot1TL77WyMIm/bCwrJ2D6AOKGwEczU9EpyoqAokfqrf/MioU9kHcMsmJZcg1egXix2KYEsA==", + "dev": true, + "requires": { + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" + } + }, + "wordwrapjs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz", + "integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==", + "dev": true, + "requires": { + "reduce-flatten": "^2.0.0", + "typical": "^5.2.0" + }, + "dependencies": { + "typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true + } + } + }, + "ws": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-0.8.0.tgz", + "integrity": "sha1-rGDrrTEhIdAeFswzg9fsZ60PDx8=", + "requires": { + "bufferutil": "1.2.x", + "options": ">=0.0.5", + "ultron": "1.0.x", + "utf-8-validate": "1.2.x" + } + }, + "xmlhttprequest": { + "version": "https://github.com/rase-/node-XMLHttpRequest/archive/a6b6f2.tar.gz", + "integrity": "sha512-GO6pmHif8rvZ9YddEoem4hQo0OvcTZJnPGyKxBNsFwgEwNYxbpfewye2ulTDAanWXTcfl2+XKE6/DK7SAoKqMw==" + }, + "y-map": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/y-map/-/y-map-10.1.3.tgz", + "integrity": "sha1-oVgCztusNp5Qa5b2je+PCi6DYZY=" + }, + "y-memory": { + "version": "8.0.9", + "resolved": "https://registry.npmjs.org/y-memory/-/y-memory-8.0.9.tgz", + "integrity": "sha512-OrcReh6DgZhz5R7JGXqAH53T0Ygw24qcxKj4jN9w2DIi2eIiKFCD5Y6apBTTNxiw2FaVP15F+M8phRRIMXFGBQ==" + }, + "y-websockets-client": { + "version": "github:rwth-acis/y-websockets-client#d29f50a4ce514313fc4813dae345d1bd31376de1", + "from": "github:rwth-acis/y-websockets-client#v8", + "requires": { + "socket.io-client": "1.3.7" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yjs": { + "version": "12.3.3", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-12.3.3.tgz", + "integrity": "sha1-e+wU1Zr+Fm1ozCsnQTGTwOW6ckw=", + "requires": { + "debug": "^2.6.3" + }, + "dependencies": { + "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" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "ylru": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.2.1.tgz", + "integrity": "sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==", + "dev": true + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..6397356 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,42 @@ +{ + "name": "@rwth-acis/las2peer-project-service-frontend", + "version": "0.3.1", + "description": "Frontend for las2peer project service.", + "main": "project-list.js", + "module": "project-list.js", + "type": "module", + "scripts": { + "serve": "web-dev-server --node-resolve --watch --open", + "build": "echo \"This is not a TypeScript project, so no need to build.\"" + }, + "repository": { + "type": "git", + "url": "https://github.com/rwth-acis/las2peer-project-service" + }, + "author": { + "name": "ACIS Group, RWTH Aachen", + "email": "acis@dbis.rwth-aachen.de" + }, + "dependencies": { + "@polymer/iron-icons": "^3.0.1", + "@polymer/paper-button": "^3.0.1", + "@polymer/paper-card": "^3.0.1", + "@polymer/paper-dialog": "^3.0.1", + "@polymer/paper-dropdown-menu": "^3.1.0", + "@polymer/paper-input": "^3.2.1", + "@polymer/paper-item": "^3.0.1", + "@polymer/paper-listbox": "^3.0.1", + "@polymer/paper-spinner": "^3.0.2", + "@polymer/paper-tabs": "^3.1.0", + "las2peer-frontend-statusbar": "github:rwth-acis/las2peer-frontend-statusbar#0.3.1", + "las2peer-frontend-user-widget": "github:rwth-acis/las2peer-frontend-user-widget#0.3.0", + "lit-element": "^2.4.0", + "y-map": "^10.1.3", + "y-memory": "^8.0.9", + "y-websockets-client": "rwth-acis/y-websockets-client#v8", + "yjs": "^12.3.3" + }, + "devDependencies": { + "@web/dev-server": "^0.1.8" + } +} diff --git a/frontend/project-list.js b/frontend/project-list.js new file mode 100644 index 0000000..c5c4660 --- /dev/null +++ b/frontend/project-list.js @@ -0,0 +1,985 @@ +import { LitElement, html, css } from 'lit-element'; +import '@polymer/paper-card/paper-card.js'; +import '@polymer/paper-button/paper-button.js'; +import '@polymer/paper-input/paper-input.js'; +import '@polymer/paper-dialog/paper-dialog.js'; +import '@polymer/paper-spinner/paper-spinner-lite.js'; +import '@polymer/paper-dropdown-menu/paper-dropdown-menu.js'; +import '@polymer/paper-item/paper-item.js'; +import '@polymer/paper-listbox/paper-listbox.js'; +import '@polymer/paper-tabs'; +import '@polymer/iron-icon/iron-icon.js'; +import '@polymer/iron-icons/social-icons.js'; +import OnlineUserListHelper from './util/online-user-list-helper' + +import Auth from './util/auth'; +import GitHub from "./util/github"; + + +/** + * The project list element provides the functionality to list existing projects and to create new ones. + * It provides several possibilities for configuration. The URL of the las2peer project service should be configured. + * It is also possible to disable the "All projects" tab, which allows hiding projects that the current user is no + * member of. + */ +export class ProjectList extends LitElement { + static get styles() { + return css` + .main { + width: 100%; + margin-top: 1em; + } + .paper-button-blue { + color: rgb(240,248,255); + background: rgb(30,144,255); + max-height: 50px; + } + .button-create-project { + margin-top: 0.5em; + margin-bottom: 0.5em; + } + .paper-button-blue:hover { + color: rgb(240,248,255); + background: rgb(65,105,225); + } + .paper-button-blue[disabled] { + background: #e1e1e1; + } + .button-danger { + height: 2.5em; + color: rgb(240,248,255); + background: rgb(255,93,84); + } + .button-danger:hover { + background: rgb(216,81,73); + } + .top-menu { + display: flex; + align-items: center; + } + .input-search-project { + border-radius: 3px; + border: thin solid #e1e1e1; + margin-top: 0.5em; + margin-bottom: 0.5em; + margin-left: auto; + height: 2.5em; + padding-left:5px; + } + /* Set outline to none, otherwise the border color changes when clicking on input field. */ + .input-search-project:focus { + outline: none; + } + .project-item-card { + display: flex; + width: 100%; + margin-top: 1em; + } + .project-item-card:hover { + background: #eeeeee; + } + .project-item-card-content { + width: 100%; + height: 100%; + align-items: center; + display: flex; + } + .project-item-name { + margin-left: 1em; + margin-top: 1em; + margin-bottom: 1em; + } + .project-item-user-list { + margin: 1em 1em 1em 0.5em; + } + .green-dot { + background-color: #c5e686; + height: 0.8em; + width: 0.8em; + border-radius: 50%; + display: inline-block; + margin-left: auto; + } + paper-tabs { + --paper-tabs-selection-bar-color: rgb(30,144,255); + } + .icon { + color: #000000; + } + .icon:hover { + color: #7c7c7c; + } + `; + } + + static get properties() { + return { + // Name of the system (needs to be existing in project service) + system: { + type: String + }, + // l2p groups + groups:{ + type: Array + }, + /** + * Array containing all the projects that were loaded from las2peer project service. + */ + projects: { + type: Array + }, + /** + * Array containing the projects that are currently listed/displayed in the frontend. This is used for the + * implementation of the project search. If the user searches for projects by name, then listedProjects + * only contains the projects that match the search input. If search is ended, listedProjects gets set to + * all projects again. + */ + listedProjects: { + type: Array + }, + /** + * The currently selected project, i.e. last one clicked on. + */ + selectedProject: { + type: Object + }, + /** + * This property allows to disable the "All projects" tab. It can be set to "true" if only the projects where + * the user is a member of should be listed. If set to "false", then all projects that are available will be + * listed in the "All projects" tab. + */ + disableAllProjects: { + type: Boolean + }, + /** + * If disableAllProjects is set to false, then we have two tabs for listing the projects - "My projects" and + * "All projects". This property stores the currently selected tab index and is either 0 (for "My projects") + * or 1 (for "All projects"). + */ + tabSelected: { + type: Number + }, + // TODO + projectsOnlineUser: { + type: Map + }, + /** + * URL where the frontend can access the las2peer project service REST API. + */ + projectServiceURL: { + type: String + }, + + /** + * URL where the frontend can access the las2peer contact service REST API. + */ + contactServiceURL: { + type: String + }, + + /** + * Yjs address that should be used for keeping the metadata in the frontend up-to-date. + * Can also be used for the online user list. + */ + yjsAddress: { + type: String + }, + + /** + * Yjs resource path used for sharing the metadata and for the online user list. + */ + yjsResourcePath: { + type: String + }, + + /** + * Yjs instance. + */ + y: { + type: Object + }, + + /** + * If the user opens the project options dialog, then the project + * for which the dialog is opened gets stored in this variable. + */ + projectOptionsSelected: { + type: Object + } + }; + } + + constructor() { + super(); + this.groups = []; + this.projects = []; + this.selectedProject = null; + this.listedProjects = ["sss"]; + this.projectsOnlineUser = new Object(); + // use a default value for project service URL for local testing + this.projectServiceURL = "http://127.0.0.1:8080"; + this.contactServiceURL = "http://127.0.0.1:8080/contactservice"; + window.addEventListener('metadata-change-request', this._changeMetadata.bind(this)); + window.addEventListener('metadata-reload-request', this._reloadMetadata.bind(this)); + window.addEventListener('projects-reload-request', (e) => this.showProjects(false)); + this.disableAllProjects = false; + this.yjsAddress = "http://127.0.0.1:1234"; + this.yjsResourcePath = "./socket.io"; + } + + connectedCallback() { + super.connectedCallback(); + + if(!GitHub.gitHubUsernameStored()) { + GitHub.hasUserGitHubAccountConnected().then(result => { + const connected = result[0]; + if(connected) { + const username = result[1]; + // store in localStorage + GitHub.storeGitHubUsername(username); + GitHub.sendGitHubUsernameToProjectService(this.projectServiceURL, this.system); + } + }); + } else { + GitHub.sendGitHubUsernameToProjectService(this.projectServiceURL, this.system); + } + + // here the properties are already set (otherwise this.system is undefinied in showProjects) + this.showProjects(false); + } + + _changeMetadata(event) { + console.log(event); + let newMetadata = event.detail; + + var project = this.selectedProject; + var projectName = project.name; + var oldMetadata = this.selectedProject.metadata; + + console.log("Project is: ", project); + console.log("New Metadata is: ", newMetadata); + + fetch(this.projectServiceURL + "/projects/" + this.system + "/changeMetadata/", { + method: "POST", + headers: Auth.getAuthHeaderWithSub(), + body: JSON.stringify({ + "access_token": Auth.getAccessToken(), + "projectName": projectName, + "oldMetadata": oldMetadata, + "newMetadata": newMetadata + }) + }).then( response => { + if(!response.ok) throw Error(response.status); + return response.json(); + }).then(data => { + console.log(data); + + if(this.y) { + // since the service successfully updated the metadata, we can also update it in the Yjs room + this.y.share.data.set("projectMetadata", newMetadata); + } + + }).catch(error => { + if(error.message == "401") { + // user is not authorized + // maybe the access token has expired + Auth.removeAuthDataFromLocalStorage(); + // location.reload(); + } else { + console.log(error); + } + }); + } + + /** + * Gets called on the "metadata-reload-request" event and re-fetches the metadata of the currently selected + * project. The fetched metadata gets put into the project's Yjs room. + * This event should be used if the metadata got updated from somewhere else than the frontend, because then + * using the "metadata-change-request" event is not working anymore as the project-list contains out-of-date + * metadata and is not able to send the update metadata request to the project service successfully. + * @param event + * @private + */ + _reloadMetadata(event) { + fetch(this.projectServiceURL + "/projects/" + this.system + "/" + this.selectedProject.name, { + method: "GET", + headers: Auth.getAuthHeaderWithSub() + }).then(response => { + if(!response.ok) throw Error(response.status); + return response.json(); + }).then(data => { + const metadata = data.metadata; + + if(this.y) { + // update metadata in yjs room => this will also update it in this.selectedProject automatically + this.y.share.data.set("projectMetadata", metadata); + } + }); + } + + render() { + return html` +
+
+ Create Project + +
+
+ ${this.disableAllProjects ? html`` : html` + + My Projects + All Projects + + `} +
+ + ${this.projectsLoading ? html` +
+ +
+ ` : html``} + ${this.listedProjects.map(project => html` + +
+

${project.name}

+
+ ${this.getListOfProjectOnlineUsers(project.name) ? html`` : html``} +

${this.getListOfProjectOnlineUsers(project.name)}

+ + ${project.gitHubProject ? html ` + + + + + + ` : html``} + ${project.is_member ? html ` + this.openProjectOptionsDialog(project)}> + ` : html``} +
+
+
+ `)} +
+ + + +

Create a Project

+ + + + ${this.groups.map(group => html` + ${group.name} + `)} + + +
+ Cancel + Create +
+
+ + +

Connected Group

+

The project Project name is connected to the las2peer group:

+
+

Group name

+ + +
+ +

Danger Zone

+
+

Delete this project. Please note that a project cannot be restored after deletion.

+ Delete +
+
+ OK +
+
+ + + +

Delete Project

+
+ Are you sure that you want to delete the project? +
+
+ Cancel + Yes +
+
+ + + + + + + + + + + + + + + + + `; + } + + /** + * Gets called by the "Create Project" button. Opens the dialog for creating a project, which then lets the user + * select a name for the project and a group that should be connected to the project. + * @private + */ + _onCreateProjectButtonClicked() { + // clear input fields of dialog + this.resetCreateProjectDialog(); + + // add statusbar to be able to get user infos for this step + console.log(this.contactServiceURL); + fetch(this.contactServiceURL + "/groups", { + method: "GET", + headers: Auth.getAuthHeaderWithSub() + }).then(response => { + if(!response.ok) throw Error(response.status); + console.log(typeof response) + console.log("ssssssss" + Object.keys(response)); + return response.json(); + }).then(data => { + // store loaded groups + // groups given by contact service as a JSONObject with key = group agent id and value = group name + // we create an array of objects with id and name attribute out of it + this.groups = []; + for(let key of Object.keys(data)) { + let group = { + "id": key, + "name": data[key] + }; + this.groups.push(group); + } + + console.log(this.groups); + // only open popup once group loaded + this.shadowRoot.getElementById("dialog-create-project").open(); + // disable create button until user entered a project name + this.shadowRoot.getElementById("dialog-button-create").disabled = true; + }).catch(error => { + if(error.message == "401") { + // user is not authorized + // maybe the access token has expired + Auth.removeAuthDataFromLocalStorage(); + // location.reload(); + } else { + console.log(error); + // in case of contactservice not running, which should not happen in real deployment + this.groups = []; + // only open popup once group loaded + this.shadowRoot.getElementById("dialog-create-project").open(); + // disable create button until user entered a project name + this.shadowRoot.getElementById("dialog-button-create").disabled = true; + } + + }); + } + + + /** + * Gets called when the search input gets updated by the user. Updates listedProjects array correspondingly. + * @param searchInput Input from the user entered in the input field for searching projects by name. + * @private + */ + _onSearchInputChanged(searchInput) { + if(searchInput) { + this.listedProjects = this.projects.filter(project => { + return project.name.toLowerCase().includes(searchInput.toLowerCase()); + }); + } else { + // no search input, show all projects that were loaded + this.listedProjects = this.projects; + } + } + + /** + * Gets called when the user switches the current tab. Depending on which tab is selected, "My projects" or + * "All projects" are loaded. + * @param tabIndex 0 = My Projects, 1 = All Projects + * @private + */ + _onTabChanged(tabIndex) { + this.tabSelected = tabIndex; + if(tabIndex == 0) { + // show users projects / projects where the user is a member of + this.showProjects(false); + } else { + // show all projects + this.showProjects(true); + } + } + + /** + * Loads and shows the projects that the user is a member of, or all existing projects. + * @param allProjects If all projects should be shown or only the ones where the + * current user is a member of. + */ + showProjects(allProjects) { + // set loading to true + this.projectsLoading = true; + // clear current project list + this.projects = []; + this.listedProjects = []; + + if(!Auth.userInfoAvailable()) { + // user is not logged in (cannot load projects) + this.projectsLoading = false; + return; + } + + fetch(this.projectServiceURL + "/projects/" + this.system, { + method: "GET", + headers: Auth.getAuthHeaderWithSub() + }).then(response => { + if(!response.ok) throw Error(response.status); + return response.json(); + }).then(data => { + console.log("Projects are", data.projects); + // set loading to false, then the spinner gets hidden + this.projectsLoading = false; + + // send event (containing every project, not only the ones from the user) + let event = new CustomEvent("projects-loaded", { + detail: { + projects: data.projects + }, + bubbles: true + }); + this.dispatchEvent(event); + + + // if we only want to show the projects, where the user is a member of, we need to filter out some projects + if(!allProjects) data.projects = data.projects.filter(project => project.is_member); + + // store loaded projects + this.projects = data.projects; + // set projects that should be shown (currently all) + this.listedProjects = data.projects; + }).catch(error => { + if(error.message == "401") { + // user is not authorized + // maybe the access token has expired + Auth.removeAuthDataFromLocalStorage(); + // location.reload(); + } else { + console.log(error); + } + }); + } + + /** + * Gets called when the user clicks on a project in the project list. Fires an event that notifies the parent + * elements that a project got selected. + * @param projectName Name of the project that got selected in the project list. + * @private + */ + _onProjectItemClicked(projectName) { + this.selectedProject = this.getProjectByName(projectName); + let event = new CustomEvent("project-selected", { + detail: { + message: "Selected project in project list.", + project: JSON.parse(JSON.stringify(this.selectedProject)) + }, + bubbles: true + }); + this.dispatchEvent(event); + + // join Yjs room for the project metadata + if(this.y) { + this.y.connector.disconnect(); + } + Y({ + db: { + name: "memory" // store the shared data in memory + }, + connector: { + name: "websockets-client", // use the websockets connector + room: "projects_" + this.system + "_" + projectName, + authInfo: { + accessToken: Auth.getAccessToken(), + basicAuth: Auth.getBasicAuthPart() + }, + //options: { resource: this.yjsResourcePath}, + url: this.yjsAddress + }, + share: { // specify the shared content + data: 'Map' + } + }).then(function(y) { + this.y = y; + const currentMetadataYjs = y.share.data.get("projectMetadata"); + + y.share.data.observe(event => { + if(event.name == "projectMetadata") { + this.selectedProject.metadata = y.share.data.get("projectMetadata"); + window.dispatchEvent(new CustomEvent("metadata-changed", { + detail: JSON.parse(JSON.stringify(this.selectedProject.metadata)), + bubbles: true + })); + } + }); + + if(!currentMetadataYjs) { + y.share.data.set("projectMetadata", JSON.parse(JSON.stringify(this.selectedProject.metadata))); + } + }.bind(this)); + } + + + getProjectByName(name) { + return this.listedProjects.find(x => x.name === name); + } + + /** + * Gets called when the user clicks on a project in the project list. Fires an event that notifies the parent + * elements that a project got selected. + * @param projectName Name of the project that got selected in the project list. + * @private + */ + _onGroupChangeDone(project) { + // TODO: give full information on the project and whether the user is a member of it + let event = new CustomEvent("project-selected", { + detail: { + message: "Selected project in project list.", + project: project + }, + bubbles: true + }); + this.dispatchEvent(event); + } + + /** + * Gets called when the user clicks on the "Close" button in the create project dialog. + * @private + */ + _closeCreateProjectDialogClicked() { + this.shadowRoot.getElementById("dialog-create-project").close(); + + // clear input field for project name in the dialog + this.shadowRoot.getElementById("input-project-name").value = ""; + } + + /** + * Gets called when the user changes the input of the project name input field in the create project dialog. + * Enables/disables the creation of projects depending on whether the name input is empty or not. + * @param projectName Input + * @private + */ + _onInputProjectNameChanged(projectName) { + if(projectName) { + this.shadowRoot.getElementById("dialog-button-create").disabled = false; + } else { + this.shadowRoot.getElementById("dialog-button-create").disabled = true; + } + } + + /** + * Get called when the user click on "create" in the create project dialog. + */ + _createProject() { + const projectName = this.shadowRoot.getElementById("input-project-name").value; + const linkedGroupName = this.shadowRoot.getElementById("input-group-name").value; + const linkedGroup = this.groups.find(group => group.name == linkedGroupName); + + // close dialog (then also the button is not clickable and user cannot create project twice or more often) + // important: get projectName before closing dialog, because when closing the dialog the input field gets cleared + this._closeCreateProjectDialogClicked(); + + // show loading dialog + this.shadowRoot.getElementById("dialog-loading").open(); + + // currently fetches members from contact service but does not check whether project already exists (code is there but commented) + if(projectName) { + fetch(this.contactServiceURL + "/groups/" + linkedGroupName + "/member", { + method: "GET", + headers: Auth.getAuthHeaderWithSub() + }).then( response => { + if(!response.ok) throw Error(response.status); + console.log(typeof response) + console.log("ssssssss" + Object.keys(response)); + return response.json(); + }).then(data => { + console.log(data); + const users = Object.values(data); + const body = { + "name": projectName, + "access_token": Auth.getAccessToken(), + "linkedGroup": linkedGroup, + "users": users + }; + if(GitHub.gitHubUsernameStored()) { + body.gitHubUsername = GitHub.getGitHubUsername(); + } + //const newProject = {"id":this.projects.length, "name":projectName, "Linked Group":linkedGroup, "Group Members":users}; + fetch(this.projectServiceURL + "/projects/" + this.system, { + method: "POST", + headers: Auth.getAuthHeaderWithSub(), + body: JSON.stringify(body) + }).then(response => { + console.log(response); + // close loading dialog + this.shadowRoot.getElementById("dialog-loading").close(); + + if(response.status == 201) { + // project got created successfully + this.shadowRoot.getElementById("toast-success").show(); + + // clear input field for project name in the dialog + this.shadowRoot.getElementById("input-project-name").value = ""; + + // since a new project exists, reload projects from server + this.showProjects(false); + // switch to tab "My Projects" + this.tabSelected = 0; + this.shadowRoot.getElementById("my-and-all-projects").selected = 0; + } else if(response.status == 409) { + // a project with the given name already exists + this.shadowRoot.getElementById("toast-already-existing").show(); + } else if(response.status == 401) { + Auth.removeAuthDataFromLocalStorage(); + // location.reload(); + } + // TODO: check what happens when access_token is missing in localStorage + }); + }); + } + } + + /** + * Call this method with a map, mapping project names to lists of Yjs room names, and then these Yjs room names + * will be used for the online user list. + * @param mapProjectRooms Map, mapping project names to lists of Yjs room names, where SyncMeta is running. + */ + setOnlineUserListYjsRooms(mapProjectRooms) { + this.projectsOnlineUser = {}; + + for(let projectName of Object.keys(mapProjectRooms)) { + let roomNames = mapProjectRooms[projectName]; + this.projectsOnlineUser[projectName] = []; + for(let roomName of roomNames) { + OnlineUserListHelper.loadListOfSyncMetaOnlineUsers(roomName, this.yjsAddress, this.yjsResourcePath).then(list => { + for(let username of list) { + if(!this.projectsOnlineUser[projectName].includes(username)) this.projectsOnlineUser[projectName].push(username); + } + this.requestUpdate(); + }); + } + } + } + + + /** + * Creates a string which contains a list of the users that are online in the + * project with the given name. + * @param projectName + * @returns {string} String containing a list of online users in the given project. + */ + getListOfProjectOnlineUsers(projectName) { + let s = ""; + for(let i in this.projectsOnlineUser[projectName]) { + s += this.projectsOnlineUser[projectName][i] + ","; + } + if(s) { + s = s.substr(0,s.length-1); + } + return s; + } + + /** + * Gets called when the user clicks on the edit-button for the group name in the "connected-group" dialog. + * @private + */ + _onEditConnectedGroupClicked() { + + fetch(this.contactServiceURL + "/groups", { + method: "GET", + headers: Auth.getAuthHeaderWithSub() + }).then(response => { + if(!response.ok) throw Error(response.status); + return response.json(); + }).then(data => { + // store loaded groups + // groups given by contact service as a JSONObject with key = group agent id and value = group name + // we create an array of objects with id and name attribute out of it + this.groups = []; + for(let key of Object.keys(data)) { + let group = { + "id": key, + "name": data[key] + }; + this.groups.push(group); + } + // hide current group name paragraph element + this.shadowRoot.getElementById("connected-group-name").style.setProperty("display", "none"); + + // show dropdown menu to select a different group, therefore remove display: none + this.shadowRoot.getElementById("input-edit-group-name").style.removeProperty("display"); + }); + + + + + /*const projectName = this.shadowRoot.getElementById("connected-group-project-name").value; + const newLinkedGroupName = this.shadowRoot.getElementById("input-edit-group-name").value; + + fetch(this.projectServiceURL + "/projects/changeGroup/", { + method: "POST", + headers: Auth.getAuthHeaderWithSub(), + body: JSON.stringify({ + "name": projectName, + "access_token": Auth.getAccessToken(), + "newGroupNameId": newLinkedGroupName + }) + }).then( response => { + if(!response.ok) throw Error(response.status); + console.log(typeof response) + console.log("ssssssss" + Object.keys(response)); + return response.json(); + })*/ + + } + + /** + * Gets called when the "Group" icon of one of the displayed projects gets clicked and opens a dialog with + * information on the group which is currently connected to the project. + * @param project + */ + _onGroupChanged() { + + const projectName = this.shadowRoot.getElementById("connected-group-project-name").innerText; + const newLinkedGroupName = this.shadowRoot.getElementById("input-edit-group-name").value; + if(newLinkedGroupName == undefined){ + return; + } + + var newLinkedGroupId = ""; + for(let key of Object.keys(this.groups)) { + if(this.groups[key].name == newLinkedGroupName){ + newLinkedGroupId = this.groups[key].id; + break; + } + } + fetch(this.projectServiceURL + "/projects/" + this.system + "/changeGroup/", { + method: "POST", + headers: Auth.getAuthHeaderWithSub(), + body: JSON.stringify({ + "name": projectName, + "access_token": Auth.getAccessToken(), + "projectName": projectName, + "newGroupName": newLinkedGroupName, + "newGroupId": newLinkedGroupId + }) + }).then( response => { + if(!response.ok) throw Error(response.status); + return response.json(); + }).then(data => { + this._onGroupChangeDone(data); + }).catch(error => { + if(error.message == "401") { + // user is not authorized + // maybe the access token has expired + Auth.removeAuthDataFromLocalStorage(); + // location.reload(); + } else { + console.log(error); + } + }); + } + + /** + * Gets called when the "options" icon of one of the displayed projects gets clicked and opens a dialog with + * information on the group which is currently connected to the project and the possibility to delete the project. + * @param project + */ + openProjectOptionsDialog(project) { + this.projectOptionsSelected = project; + + // reset the dialog + this.shadowRoot.getElementById("connected-group-name").style.removeProperty("display"); + this.shadowRoot.getElementById("input-edit-group-name").style.setProperty("display", "none"); + + this.shadowRoot.getElementById("connected-group-project-name").innerText = project.name; + this.shadowRoot.getElementById("connected-group-name").innerText = project.groupName; + + // open the dialog + this.shadowRoot.getElementById("dialog-project-options").open(); + } + + /** + * Gets called when the "delete project" button in the project option dialog is clicked. + * Opens another dialog to verify whether the project should really be deleted. + */ + showDeleteProjectDialog() { + // hide project options dialog + this.shadowRoot.getElementById("dialog-project-options").close(); + + // open delete dialog + this.shadowRoot.getElementById("dialog-delete-project").open(); + } + + /** + * Gets called if the user has verified that the project should be deleted. + */ + _deleteProject() { + let projectToDelete = this.projectOptionsSelected; + fetch(this.projectServiceURL + "/projects/" + this.system + "/" + projectToDelete.name, { + method: "DELETE", + headers: Auth.getAuthHeaderWithSub(), + body: JSON.stringify({ + "access_token": Auth.getAccessToken() + }) + }).then(response => { + if(response.ok) { + this.shadowRoot.getElementById("toast-success-deletion").show(); + + // since a project got deleted, reload projects from server + this.showProjects(false); + // switch to tab "My Projects" + this.tabSelected = 0; + this.shadowRoot.getElementById("my-and-all-projects").selected = 0; + } + else throw Error(response.status); + }).catch(error => { + + }); + + } + + /** + * Resets the input fields of the "Create Project" dialog. + * Clears the input for the project name and resets the dropdown menu for the group selection. + */ + resetCreateProjectDialog() { + // clear previous input (might not exist yet if dialog was never opened before, but just clear it anyway) + this.shadowRoot.getElementById("input-project-name").value = ""; + this.shadowRoot.getElementById("input-group-name")._setSelectedItem(null); + } +} + +window.customElements.define('project-list', ProjectList); diff --git a/frontend/util/auth.js b/frontend/util/auth.js new file mode 100644 index 0000000..d257871 --- /dev/null +++ b/frontend/util/auth.js @@ -0,0 +1,83 @@ +/** + * Helper class for user auth. + */ +export default class Auth { + + static KEY_ACCESS_TOKEN = "access_token"; + static KEY_USERINFO_ENDPOINT = "userinfo_endpoint"; + static KEY_USER_INFO = "userInfo"; + + /** + * Helper method for creating header for HTTP requests. + * Sets access token for las2peer OIDC auth to the access token + * stored in localeStorage. + * Also adds a "fake" basic auth since las2peer seems to need a password. + */ + static getAuthHeader() { + return { + "access-token": localStorage.getItem(this.KEY_ACCESS_TOKEN), + "Authorization": "Basic OnRlc3Q=", + "Content-Type": "application/json"} + } + + + /** + * Helper method for creating header for HTTP requests. + * Sets access token for las2peer OIDC auth to the access token + * stored in localeStorage. + * Also adds a "fake" basic auth since las2peer seems to need a password. + */ + static getAuthHeaderWithSub() { + var userInfo = JSON.parse(localStorage.getItem(this.KEY_USER_INFO)); + return { + "access-token": localStorage.getItem(this.KEY_ACCESS_TOKEN), + "Authorization": "Basic "+ btoa(userInfo.preferred_username + ":" + userInfo.sub) + } + } + + static userInfoAvailable() { + return localStorage.getItem(this.KEY_USER_INFO) !== null; + } + + static getBasicAuthPart() { + var userInfo = JSON.parse(localStorage.getItem(this.KEY_USER_INFO)); + return btoa(userInfo.preferred_username + ":" + userInfo.sub); + } + + + /** + * Loads the access token stored in localStorage. + * @returns {string} Access token which is stored in localStorage. + */ + static getAccessToken() { + return localStorage.getItem(this.KEY_ACCESS_TOKEN); + } + + /** + * Removes the access token and userinfo endpoint + * from localStorage. + * This can be used after user has logged out or when the access + * token expired. + */ + static removeAuthDataFromLocalStorage() { + localStorage.removeItem(this.KEY_ACCESS_TOKEN); + localStorage.removeItem(this.KEY_USERINFO_ENDPOINT); + } + + /** + * Stores the given access token and the userinfo endpoint to localStorage. + * @param access_token Access token to store. + */ + static setAuthDataToLocalStorage(access_token) { + localStorage.setItem("access_token", access_token); + localStorage.setItem("userinfo_endpoint", "https://api.learning-layers.eu/o/oauth2/userinfo"); + } + + /** + * Checks if access token is stored in localStorage. + * @returns {boolean} Whether access token is stored in localStorage. + */ + static isAccessTokenAvailable() { + return localStorage.getItem(this.KEY_ACCESS_TOKEN) !== null; + } +} diff --git a/frontend/util/common.js b/frontend/util/common.js new file mode 100644 index 0000000..b52dc02 --- /dev/null +++ b/frontend/util/common.js @@ -0,0 +1,37 @@ +/** + * Helper class for storing/loading of user info. + */ +export default class Common { + + /** + * Key used to store the information about the currently logged in user. + * @type {string} + */ + static KEY_USER_INFO = "userInfo"; + + /** + * Returns the information about the currently logged in user. + * @returns {string} + */ + static getUserInfo() { + return JSON.parse(localStorage.getItem(this.KEY_USER_INFO)); + } + + /** + * Stores the information about the currently logged in user. + * @param userInfo Info to store in localStorage. + */ + static storeUserInfo(userInfo) { + localStorage.setItem(this.KEY_USER_INFO, JSON.stringify(userInfo)); + } + + + /** + * Removes the userInfo from localStorage. + * This method may be used when user has logged out. + */ + static removeUserInfoFromStorage() { + localStorage.removeItem(this.KEY_USER_INFO); + } +} + diff --git a/frontend/util/github.js b/frontend/util/github.js new file mode 100644 index 0000000..c5b8196 --- /dev/null +++ b/frontend/util/github.js @@ -0,0 +1,79 @@ +import Auth from "./auth.js"; + +/** + * Helper class for GitHub projects connection. + * Helps to get the GitHub username and to inform the project-service about it. + */ +export default class GitHub { + + static GITHUB_ACCESS_TOKEN_URL = "https://api.learning-layers.eu/auth/realms/main/broker/github/token"; + static GITHUB_API_USER_URL = "https://api.github.com/user"; + static KEY_LOCALSTORAGE_USERNAME = "projectservice_github_username"; + + static gitHubUsernameStored() { + return localStorage.getItem(GitHub.KEY_LOCALSTORAGE_USERNAME) !== null; + } + + static storeGitHubUsername(username) { + localStorage.setItem(GitHub.KEY_LOCALSTORAGE_USERNAME, username); + } + + static getGitHubUsername() { + return localStorage.getItem(GitHub.KEY_LOCALSTORAGE_USERNAME); + } + + /** + * Sends the GitHub username (that is stored in localStorage) to the project-service. + * @param projectServiceURL URL of the project service webconnector. + * @param system Name of the system. + */ + static sendGitHubUsernameToProjectService(projectServiceURL, system) { + if(GitHub.gitHubUsernameStored()) { + const username = GitHub.getGitHubUsername(); + fetch(projectServiceURL + "/projects/" + system + "/user/githubinfo", { + method: "POST", + headers: Auth.getAuthHeaderWithSub(), + body: JSON.stringify({ + "gitHubUsername": username + }) + }); + } + } + + /** + * Tries to get a GitHub access token for the user from keycloak. + * If that works, the user has connected a GitHub account. + * @returns {Promise} + */ + static hasUserGitHubAccountConnected() { + return new Promise((resolve, reject) => fetch(GitHub.GITHUB_ACCESS_TOKEN_URL, { + method: "GET", + headers: { + "Authorization": "Bearer " + Auth.getAccessToken() + } + }).then(response => { + if(response.status == 200) { + return response.text(); + } else { + resolve([false]); + } + }).then(text => { + const gitHubAccessToken = text.split("access_token=")[1].split("&")[0]; + return fetch(GitHub.GITHUB_API_USER_URL, { + method: "GET", + headers: { + "Accept": "application/vnd.github.v3+json", + "Authorization": "Bearer " + gitHubAccessToken + } + }) + }).then(response => { + if(response.status == 200) { + return response.json(); + } else { + resolve([false]) + } + }).then(json => { + resolve([true, json.login]); + })); + } +} diff --git a/frontend/util/online-user-list-helper.js b/frontend/util/online-user-list-helper.js new file mode 100644 index 0000000..5e9a049 --- /dev/null +++ b/frontend/util/online-user-list-helper.js @@ -0,0 +1,57 @@ +export default class OnlineUserListHelper { + + /** + * Load users that are online in the given Yjs room which is used by a SyncMeta instance. + * @param yjsRoomName Name of the Yjs room which is used by SyncMeta. + * @param yjsAddress + * @param yjsResourcePath + */ + static loadListOfSyncMetaOnlineUsers(yjsRoomName, yjsAddress, yjsResourcePath) { + // get currently active users in yjs room + return new Promise((resolve) => Y({ + db: { + name: "memory" // store the shared data in memory + }, + connector: { + name: "websockets-client", // use the websockets connector + room: yjsRoomName, + options: { resource: yjsResourcePath }, + url: yjsAddress + }, + share: { // specify the shared content + userList: 'Map', // used to get full name of users + join: 'Map' // used to get currently online users + }, + type: ["Map"], + sourceDir: "node_modules" + }).then(function (y) { + const userList = y.share.userList; + + let list = []; + + // Start observing for join events. + // After that we will join the Yjs room with the username "invisible_user". + // When we join the Yjs room, then all the other users send a join event back to us. + // Thus, we wait for join events which tell us which users are online. + // We use "invisible_user" as username, because this is the only username where SyncMeta's + // activity list widget does not show the join/leave events for. + y.share.join.observe(event => { + if (userList.get(event.name)) { + const userFullName = userList.get(event.name)["http://purl.org/dc/terms/title"]; + if (y.share.userList.get(event.name)) { + if (!list.includes(userFullName)) { + list.push(userFullName); + } + } + } + }); + // now join the Yjs room + y.share.join.set("invisible_user", false); + + setTimeout(function () { + resolve(list); + }, 5000); + })); + } + +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..f710064 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,15 @@ +core.version=1.2.0 +service.name=i5.las2peer.services.projectService +service.class=ProjectService +service.version=1.0.0 +java.version=17 + +las2peer_user1.name=alice +las2peer_user1.password=pwalice +las2peer_user1.email=alice@example.org +las2peer_user2.name=bobby +las2peer_user2.password=pwbobby +las2peer_user2.email=bobby@example.org +las2peer_user3.name=joey +las2peer_user3.password=pwjoey +las2peer_user3.email=joey@example.org \ 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 0000000..7454180 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 0000000..ffed3a2 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..c53aefa --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${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='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# 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 ;; #( + MSYS* | 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" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@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 Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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="-Xmx64m" "-Xms64m" + +@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 execute + +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 execute + +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 + +: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 %* + +: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/project_service/build.gradle b/project_service/build.gradle new file mode 100644 index 0000000..a4a2bda --- /dev/null +++ b/project_service/build.gradle @@ -0,0 +1,281 @@ +plugins { + // Apply the application plugin to add support for building a CLI application in Java. + id 'application' + id 'eclipse' + id 'jacoco' +} + +repositories { + // Use maven central for resolving dependencies. + mavenCentral() + + // DBIS Archiva + maven { + url "https://archiva.dbis.rwth-aachen.de:9911/repository/internal/" + } + + maven { + url "https://archiva.dbis.rwth-aachen.de:9911/repository/snapshots/" + } +} + + +dependencies { + // Use JUnit test framework. + testImplementation "junit:junit:4.13.2" + + // https://mvnrepository.com/artifact/org.json/json + implementation group: 'org.json', name: 'json', version: '20201115' + + implementation 'com.googlecode.json-simple:json-simple:1.1.1' + // las2peer bundle which is not necessary in the runtime path + // compileOnly will be moved into the lib dir afterwards + implementation "i5:las2peer-bundle:${project.property('core.version')}" + +} + +tasks.withType(Jar) { duplicatesStrategy = DuplicatesStrategy.EXCLUDE } + + +configurations { + // This ensures las2peer is available in the tests, but won't be bundled + testImplementation.extendsFrom implementation +} + +jar { + manifest { + attributes "Main-Class": "${project.property('service.name')}.${project.property('service.class')}" + attributes "Library-Version": "${project.property('service.version')}" + attributes "Library-SymbolicName": "${project.property('service.name')}" + } + + from { (configurations.runtimeClasspath).collect { it.isDirectory() ? it : zipTree(it) } } { + // Exclude signatures to be able to natively bundle signed jars + exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA' + } +} + +application { + // Define the main class for the application. + mainClass = "${project.property('service.name')}.${project.property('service.class')}" + + group = "${project.property('service.name')}" + archivesBaseName = group + + version = "${project.property('service.version')}" + mainClass.set("i5.las2peer.tools.L2pNodeLauncher") + sourceCompatibility = "${project.property('java.version')}" + targetCompatibility = "${project.property('java.version')}" +} + +// put all .jar files into export/jars folder +tasks.withType(Jar) { + destinationDirectory = file("$projectDir/export/jars") +} + +javadoc { + destinationDir = file("$projectDir/export/doc") +} + +build.dependsOn "javadoc" + +compileJava { + dependsOn "copyMain" +} + +compileTestJava { + dependsOn "copyTest" +} + +// Copies .xml files into build directory +task copyMain(type: Copy) { + from "src/main/java" + include "**/*.xml" + into "$buildDir/classes/java/main" +} + +// Copies .xml files into build directory +task copyTest(type: Copy) { + from "src/test/java" + include "**/*.xml" + into "$buildDir/classes/java/test" +} + +// These two tasks restore the build and runtime environment used +// in the ant environment +task copyJar(type: Copy) { + from jar // here it automatically reads jar file produced from jar task + into "$rootDir/service" +} + +task copyToLib(type: Copy) { + from configurations.compileClasspath + into "$rootDir/lib" +} + +build.dependsOn copyJar +build.dependsOn copyToLib + +task startscripts { + new File("$rootDir/bin", "start_network.sh").text = """#!/bin/bash + +# this script is autogenerated by 'gradle startscripts' +# it starts a las2peer node providing the service '${project.property('service.name')}.${project.property('service.class')}' of this project +# pls execute it from the root folder of your deployment, e. g. ./bin/start_network.sh + +java -cp "lib/*" --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED i5.las2peer.tools.L2pNodeLauncher --port 9011 --service-directory service uploadStartupDirectory startService\\(\\'${project.property('service.name')}.${project.property('service.class')}@${project.property('service.version')}\\'\\) startWebConnector interactive +""" + new File("$rootDir/bin", "start_network.bat").text = """:: this script is autogenerated by 'gradle startscripts' +:: it starts a las2peer node providing the service '${project.property('service.name')}.${project.property('service.class')}' of this project +:: pls execute it from the bin folder of your deployment by double-clicking on it + +%~d0 +cd %~p0 +cd .. +set BASE=%CD% +set CLASSPATH="%BASE%/lib/*;" +set ADD_OPENS=--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED + +java -cp %CLASSPATH% %ADD_OPENS% i5.las2peer.tools.L2pNodeLauncher --port 9011 --service-directory service uploadStartupDirectory startService('${project.property('service.name')}.${project.property('service.class')}@${project.property('service.version')}') startWebConnector interactive + +pause +""" +} + +build.dependsOn "startscripts" + +def startup = "$rootDir/etc/startup" +def userAgent1Path = "${startup}/agent-user-${project.property('las2peer_user1.name')}.xml" +def userAgent2Path = "${startup}/agent-user-${project.property('las2peer_user2.name')}.xml" +def userAgent3Path = "${startup}/agent-user-${project.property('las2peer_user3.name')}.xml" +def passphrasesPath = "${startup}/passphrases.txt" + +task generateUserAgent1 { + dependsOn "jar" + + onlyIf { !(new File(userAgent1Path).exists()) } + + doLast { + tasks.create("generateUserAgent1Help", JavaExec) { + println "Writing User Agent xml to ${userAgent1Path}" + + main = "i5.las2peer.tools.UserAgentGenerator" + classpath = sourceSets.main.compileClasspath + args "${project.property('las2peer_user1.password')}", "${project.property('las2peer_user1.name')}", "${project.property('las2peer_user1.email')}" + mkdir "${startup}" + standardOutput new FileOutputStream(userAgent1Path) + }.exec() + } +} + +task generateUserAgent2 { + dependsOn "jar" + + onlyIf { !(new File(userAgent2Path).exists()) } + + doLast { + tasks.create("generateUserAgent2Help", JavaExec) { + println "Writing User Agent xml to ${userAgent2Path}" + + main = "i5.las2peer.tools.UserAgentGenerator" + classpath = sourceSets.main.compileClasspath + args "${project.property('las2peer_user2.password')}", "${project.property('las2peer_user2.name')}", "${project.property('las2peer_user2.email')}" + mkdir "${startup}" + standardOutput new FileOutputStream(userAgent2Path) + }.exec() + } +} + +task generateUserAgent3 { + dependsOn "jar" + + onlyIf { !(new File(userAgent3Path).exists()) } + + doLast { + tasks.create("generateUserAgent3Help", JavaExec) { + println "Writing User Agent xml to ${userAgent3Path}" + + main = "i5.las2peer.tools.UserAgentGenerator" + classpath = sourceSets.main.compileClasspath + args "${project.property('las2peer_user3.password')}", "${project.property('las2peer_user3.name')}", "${project.property('las2peer_user3.email')}" + mkdir "${startup}" + standardOutput new FileOutputStream(userAgent3Path) + }.exec() + } +} + +// generate example user agents +task generateAgents { + description "Generate example user agents" + dependsOn "generateUserAgent1" + dependsOn "generateUserAgent2" + dependsOn "generateUserAgent3" + + doLast { + new File(passphrasesPath).text = """agent-user-${project.property('las2peer_user1.name')}.xml;${project.property('las2peer_user1.password')} +agent-user-${project.property('las2peer_user2.name')}.xml;${project.property('las2peer_user2.password')} +agent-user-${project.property('las2peer_user3.name')}.xml;${project.property('las2peer_user3.password')} + """ + } +} + +build.dependsOn "generateAgents" + +clean.doLast { + file("$rootDir/tmp").deleteDir() + file("$rootDir/lib").deleteDir() + file("$rootDir/servicebundle").deleteDir() + file("$rootDir/service").deleteDir() + file("$rootDir/etc/startup").deleteDir() + file("$projectDir/export").deleteDir() +} + +task cleanAll { + dependsOn "clean" + + doLast { + file("$rootDir/log").deleteDir() + file("$rootDir/node-storage").deleteDir() + } +} + +jacoco { + toolVersion = "0.8.7" + reportsDirectory = file("$projectDir/export/jacoco") +} + +test { + finalizedBy jacocoTestReport // report is always generated after tests run + + jacoco { + destinationFile = file("$projectDir/export/jacoco.exec") + } +} + +jacocoTestReport { + dependsOn test // tests are required to run before generating the report + + // enable the xml report (html is also enabled) + reports { + xml.enabled true + } +} + +// configuration for eclipse (this allows to import the project service as a gradle project in eclipse without any problems) +eclipse { + classpath { + file { + whenMerged { + // change output directory for test, main, resources and default + def main = entries.find { it.path == "src/main/java" } + main.output = "output/main" + + def test = entries.find { it.path == "src/test/java" } + test.output = "output/test" + + def defaultEntry = entries.find { it.kind == "output" && it.path == "bin/default" } + defaultEntry.path = "output/default" + } + } + } +} \ No newline at end of file diff --git a/project_service/properties/i5.las2peer.services.projectService.ProjectService.properties b/project_service/properties/i5.las2peer.services.projectService.ProjectService.properties new file mode 100644 index 0000000..f6592e8 --- /dev/null +++ b/project_service/properties/i5.las2peer.services.projectService.ProjectService.properties @@ -0,0 +1,5 @@ +#Mon Mar 22 18:17:12 CET 2021 +oldServiceAgentPw= +systems={"test"\: {"visibilityOfProjects"\: "own", "eventListenerService"\: "i5.las2peer.services.projectService.RMITestService"},"othersystem"\:{}} +serviceGroupId=37d88b7f9b6a41f47bbf7e825fa642349fdc2d61ae8f319dc834ce9f7cf0f96a168ebff0ea376e657ae081915c44a55023719a1868c8c6c295a9769e7c23f0e6 +oldServiceAgentId= diff --git a/project_service/src/main/java/i5/las2peer/services/projectService/EventManager.java b/project_service/src/main/java/i5/las2peer/services/projectService/EventManager.java new file mode 100644 index 0000000..a8c9384 --- /dev/null +++ b/project_service/src/main/java/i5/las2peer/services/projectService/EventManager.java @@ -0,0 +1,85 @@ +package i5.las2peer.services.projectService; + +import java.io.Serializable; +import java.util.HashMap; + +import org.json.simple.JSONObject; + +import i5.las2peer.api.Context; +import i5.las2peer.api.execution.InternalServiceException; +import i5.las2peer.api.execution.ServiceAccessDeniedException; +import i5.las2peer.api.execution.ServiceInvocationFailedException; +import i5.las2peer.api.execution.ServiceMethodNotFoundException; +import i5.las2peer.api.execution.ServiceNotAuthorizedException; +import i5.las2peer.api.execution.ServiceNotAvailableException; +import i5.las2peer.api.execution.ServiceNotFoundException; + +/** + * Helper class used to send messages to the configured event listener services on specific events. + * @author Philipp + * + */ +public class EventManager { + + private static final String EVENT_METHOD_PROJECT_CREATED = "_onProjectCreated"; + private static final String EVENT_METHOD_PROJECT_DELETED = "_onProjectDeleted"; + + /** + * Map containing the name of the event listener service for every system. + * Name of the service is the one that should be called on specific events. + * Might be null for some systems if not set. + */ + private HashMap eventListenerServiceMap; + + public EventManager(HashMap eventListenerServiceMap) { + this.eventListenerServiceMap = eventListenerServiceMap; + } + + /** + * Sends the project-created event for the given project to the event listener service. + * @param context Context used for invoking the event listener service. + * @param system System that the project belongs to. + * @param projectJSON Project that got created as a JSONObject. + * @return If event listener is disabled, then always true. Otherwise only true, if event was sent successfully. + */ + public boolean sendProjectCreatedEvent(Context context, String system, JSONObject projectJSON) { + return invokeEventListenerService(context, system, EVENT_METHOD_PROJECT_CREATED, projectJSON); + } + + /** + * Sends the project-deleted event for the given project to the event listener service. + * @param context Context used for invoking the event listener service. + * @param system System that the project belongs to. + * @param projectJSON Project that got deleted as a JSONObject. + * @return If event listener is disabled, then always true. Otherwise only true, if event was sent successfully. + */ + public boolean sendProjectDeletedEvent(Context context, String system, JSONObject projectJSON) { + return invokeEventListenerService(context, system, EVENT_METHOD_PROJECT_DELETED, projectJSON); + } + + /** + * Helper method which uses the given context to invoke the given method of the event listener service (configured + * in properties file of service) using the given data. + * @param context Context used for invoking the event listener service. + * @param system System is required to find correct event listener service. + * @param method Method that should be called in the event listener service. + * @param data Data that should be used as parameters in the method call. + * @return If event listener is disabled, then always true. Otherwise only true, if method was called successfully. + */ + private boolean invokeEventListenerService(Context context, String system, String method, Serializable... data) { + String eventListenerService = this.eventListenerServiceMap.get(system); + boolean enabled = eventListenerService != null; + if(!enabled) return true; + + try { + context.invoke(eventListenerService, method, data); + return true; + } catch (ServiceNotFoundException | ServiceNotAvailableException | InternalServiceException + | ServiceMethodNotFoundException | ServiceInvocationFailedException | ServiceAccessDeniedException + | ServiceNotAuthorizedException e) { + return false; + } + } + + +} diff --git a/project_service/src/main/java/i5/las2peer/services/projectService/ProjectContainer.java b/project_service/src/main/java/i5/las2peer/services/projectService/ProjectContainer.java new file mode 100644 index 0000000..d2b1fa0 --- /dev/null +++ b/project_service/src/main/java/i5/las2peer/services/projectService/ProjectContainer.java @@ -0,0 +1,54 @@ +package i5.las2peer.services.projectService; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; + +import i5.las2peer.services.projectService.project.Project; + +/** + * This is an example object used to persist some data (in this case a simple + * String) to the network storage. It can be replaced with any type of + * Serializable or even with a plain String object. + * + */ +public class ProjectContainer implements Serializable { + + private static final long serialVersionUID = 1L; + + private HashSet userProjects; + + private HashMap allProjects; + + public ProjectContainer() { + userProjects = new HashSet<>(); + allProjects = new HashMap<>(); + } + + public HashSet getUserProjects() { + return userProjects; + } + + public void addProject(Project p) { + allProjects.put(p.getName(), p); + } + + public void removeProject(Project p) { + allProjects.remove(p.getName()); + } + + public void removeProject(String projectName) { + allProjects.remove(projectName); + } + + public Project getProjectByName(String projectName) { + return allProjects.get(projectName); + } + + public List getAllProjects() { + return new ArrayList<>(allProjects.values()); + } + +} \ No newline at end of file diff --git a/project_service/src/main/java/i5/las2peer/services/projectService/ProjectService.java b/project_service/src/main/java/i5/las2peer/services/projectService/ProjectService.java new file mode 100644 index 0000000..d6735df --- /dev/null +++ b/project_service/src/main/java/i5/las2peer/services/projectService/ProjectService.java @@ -0,0 +1,938 @@ +package i5.las2peer.services.projectService; + +import java.net.HttpURLConnection; +import java.util.ArrayList; +import java.util.List; + +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import i5.las2peer.api.Context; +import i5.las2peer.api.ManualDeployment; +import i5.las2peer.api.ServiceException; +import i5.las2peer.api.security.Agent; +import i5.las2peer.api.security.AgentNotFoundException; +import i5.las2peer.api.security.AgentAccessDeniedException; +import i5.las2peer.api.security.AgentOperationFailedException; +import i5.las2peer.api.security.AnonymousAgent; +import i5.las2peer.api.security.GroupAgent; +import i5.las2peer.api.security.ServiceAgent; +import i5.las2peer.api.logging.MonitoringEvent; +import i5.las2peer.api.persistency.Envelope; +import i5.las2peer.api.persistency.EnvelopeAccessDeniedException; +import i5.las2peer.api.persistency.EnvelopeNotFoundException; +import i5.las2peer.api.persistency.EnvelopeOperationFailedException; +import i5.las2peer.restMapper.RESTService; +import i5.las2peer.restMapper.annotations.ServicePath; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import io.swagger.annotations.Info; +import io.swagger.annotations.SwaggerDefinition; + +import org.json.simple.JSONObject; + +import org.json.simple.parser.ParseException; + +import org.json.simple.JSONValue; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; + +import i5.las2peer.services.projectService.project.Project; +import i5.las2peer.services.projectService.util.ProjectVisibility; +import i5.las2peer.services.projectService.util.SystemsConfig; +import i5.las2peer.services.projectService.util.github.GitHubException; +import i5.las2peer.services.projectService.util.github.GitHubHelper; + +/** + * las2peer-project-service + * + * A las2peer service for managing projects for las2peer groups. + * + */ +@Api +@SwaggerDefinition(info = @Info(title = "las2peer Project Service", version = "1.0.0", description = "A las2peer service for managing projects for las2peer groups.")) +@ServicePath("/projects") +@ManualDeployment +public class ProjectService extends RESTService { + public final static String projects_prefix = "projects"; + + /** + * If a system does not specify the "visibilityOfProjects" attribute, then this + * default value is used. + */ + public static final ProjectVisibility visibilityOfProjectsDefault = ProjectVisibility.OWN; + + // service that should be called on specific events such as project creation + private EventManager eventManager; + + private String serviceGroupId; + private String oldServiceAgentId; + private String oldServiceAgentPw; + + private String systems; + private SystemsConfig systemsConfig = null; + + @Override + protected void initResources() { + getResourceConfig().register(this); + } + + public ProjectService() throws ServiceException { + super(); + setFieldValues(); // This sets the values of the configuration file + System.out.println(serviceGroupId); + + // check if serviceGroupId is set. Otherwise do not let service start. + if(this.serviceGroupId == null || this.serviceGroupId.isEmpty()) + throw new ServiceException("Property 'serviceGroupId' is not set!"); + + // check if systems property is set. Otherwise service should not start. + if(this.systems == null || this.systems.isEmpty()) + throw new ServiceException("Property 'systems' is not set!"); + + try { + JSONObject systemsJSON = (JSONObject) JSONValue.parseWithException(this.systems); + systemsConfig = new SystemsConfig(systemsJSON); + } catch (ParseException e) { + throw new ServiceException("Property 'systems' is not well-formatted!"); + } + + // setup GitHubHelper + GitHubHelper gitHubHelper = GitHubHelper.getInstance(); + gitHubHelper.setSystemsConfig(systemsConfig); + + this.eventManager = new EventManager(systemsConfig.getSystemEventListenerServiceMap()); + } + + public GroupAgent getServiceGroupAgent() { + try { + return (GroupAgent) Context.get().requestAgent(this.serviceGroupId, Context.get().getServiceAgent()); + } catch (AgentAccessDeniedException | AgentNotFoundException | AgentOperationFailedException e) { + // TODO: error handling + try { + // Dont know if this is the best solution, but works, the user just needs to + // take care of the old service agent id field + pw + // Note: when calling this method at the same time, sometimes a problem occurs + // when trying to store groups at the same time + + System.out.println("Adding service agent " + Context.get().getServiceAgent().getIdentifier()); + + ServiceAgent sAgent = (ServiceAgent) Context.get().fetchAgent(this.oldServiceAgentId); + sAgent.unlock(this.oldServiceAgentPw); + GroupAgent gAgent = (GroupAgent) Context.get().requestAgent(this.serviceGroupId, sAgent); + gAgent.addMember(Context.get().getServiceAgent()); + Context.get().storeAgent(gAgent); + return gAgent; + } catch (Exception e1) { + System.out.println("Getting Service Group Agent failed because of:" + e1); + return null; + } + } + } + + /** + * This method can be used by other services, to verify if a user is allowed to + * write-access a project. + * + * @param system This prefix is used to store all the envelopes of a system. It should be + * unique for every system using the project service. + * @param projectName Project where the permission should be checked for. + * @return True, if agent has access to project. False otherwise (or if project + * with given name does not exist). + */ + public boolean hasAccessToProject(String system, String projectName) { + String identifier = getProjectIdentifier(system, projectName); + try { + Context.getCurrent().requestEnvelope(identifier); + } catch (EnvelopeAccessDeniedException e) { + return false; + } catch (EnvelopeNotFoundException | EnvelopeOperationFailedException e) { + return false; + } + return true; + } + + /** + * Wrapper for /changeMetadata method provided as part of the REST interface. + * This method can be used for RMI and does not return a Response object which + * breaks RMI because it is not serializable. + * @param system System where the metadata should be changed. + * @param body Body that gets forwarded to /changeMetadata method. + * @return Whether request was successful. + */ + public boolean changeMetadataRMI(String system, String body) { + Response r = this.changeMetadata(system, body); + return r.getStatus() == HttpURLConnection.HTTP_OK; + } + + /** + * Method for RMI to request the metadata of a project. + * @param system System where the metadata should be searched. + * @param projectName Name of the project + * @return Metadata as JSONObject or null if error occurred. + */ + public JSONObject getProjectMetadataRMI(String system, String projectName) { + Response r = this.getProjectByName(system, projectName); + if(r.getStatus() != 200) return null; + String entity = (String) r.getEntity(); + JSONObject projectJSON = (JSONObject) JSONValue.parse(entity); + JSONObject metadata = (JSONObject) projectJSON.get("metadata"); + return metadata; + } + + /** + * Creates a new project in the pastry storage. Therefore, the user needs to be + * authorized. First, checks if a project with the given name already exists. If + * not, then the new project gets stored into the pastry storage. + * + * @param system This prefix is used to store all the envelopes of a system. It should be + * unique for every system using the project service. + * @param inputProject JSON representation of the project to store (containing + * name and access token of user needed to create + * Requirements Bazaar category). + * @return Response containing the status code (and a message or the created + * project). + */ + @POST + @Path("/{system}/") + @Consumes(MediaType.TEXT_PLAIN) + @ApiOperation(value = "Creates a new project in the pastry storage if no project with the same name is already existing.") + @ApiResponses(value = { @ApiResponse(code = HttpURLConnection.HTTP_CREATED, message = "OK, project created."), + @ApiResponse(code = HttpURLConnection.HTTP_UNAUTHORIZED, message = "User not authorized."), + @ApiResponse(code = HttpURLConnection.HTTP_CONFLICT, message = "There already exists a project with the given name."), + @ApiResponse(code = HttpURLConnection.HTTP_BAD_REQUEST, message = "Input project is not well formatted or some attribute is missing."), + @ApiResponse(code = HttpURLConnection.HTTP_INTERNAL_ERROR, message = "Internal server error.") }) + public Response postProject(@PathParam("system") String system, String inputProject) { + Context.get().monitorEvent(MonitoringEvent.SERVICE_MESSAGE, "postProject: trying to store a new project"); + + if(!this.systemsConfig.isValidSystemName(system)) return Response.status(HttpURLConnection.HTTP_BAD_REQUEST) + .entity("Used system is not valid.").build(); + + if (Context.getCurrent().getMainAgent() instanceof AnonymousAgent) { + return Response.status(HttpURLConnection.HTTP_UNAUTHORIZED).entity("User not authorized.").build(); + } else { + GroupAgent serviceGroupAgent = getServiceGroupAgent(); + if (serviceGroupAgent == null) + return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR) + .entity("Cannot access service group agent.").build(); + + Agent agent = Context.getCurrent().getMainAgent(); + Envelope env = null; + Envelope env2 = null; + Project project; + String creatorGitHubUsername = null; + + try { + project = new Project(agent, inputProject); + JSONObject bodyJSON = (JSONObject) JSONValue.parseWithException(inputProject); + if(bodyJSON.containsKey("gitHubUsername")) { + creatorGitHubUsername = (String) bodyJSON.get("gitHubUsername"); + } + } catch (ParseException e) { + // JSON project given with the request is not well formatted or some attributes + // are missing + return Response.status(HttpURLConnection.HTTP_BAD_REQUEST).entity(e.getMessage()).build(); + } + + String identifier = getProjectIdentifier(system, project.getName()); + String identifier2 = getProjectListIdentifier(system); + + try { + Context.get().requestEnvelope(identifier); + // if requesting the envelope does not fail, then there already exists a project + // with the given name + return Response.status(HttpURLConnection.HTTP_CONFLICT).entity("Project already exists").build(); + } catch (EnvelopeNotFoundException e) { + // requesting the envelope failed, thus no project with the given name exists + // and we can create it + } catch (EnvelopeAccessDeniedException | EnvelopeOperationFailedException e) { + return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR).build(); + } + + GroupAgent groupAgent; + try { + // use main agent (user) to request the group agent + groupAgent = (GroupAgent) Context.get().requestAgent(project.getGroupIdentifier(), + Context.get().getMainAgent()); + } catch (AgentAccessDeniedException e) { + // could not unlock group agent => user is no group member + return Response.status(HttpURLConnection.HTTP_BAD_REQUEST) + .entity("User is no member of the group linked to the given project.").build(); + } catch (AgentNotFoundException e) { + // could not find group agent + return Response.status(HttpURLConnection.HTTP_BAD_REQUEST) + .entity("The group linked to the given project cannot be found.").build(); + } catch (AgentOperationFailedException e) { + return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR).build(); + } + + // check if GitHub project should be created for this las2peer project + if(this.systemsConfig.gitHubProjectsEnabled(system)) { + // create corresponding GitHub project + try { + project.createGitHubProject(system); + } catch (GitHubException e) { + return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR) + .entity("Creation of GitHub project failed.").build(); + } + } + + ProjectContainer cc = new ProjectContainer(); + + cc.addProject(project); + try { + // create envelope for project using the group agent + env = Context.get().createEnvelope(identifier, groupAgent); + // set the project container (which only contains the new project) as the + // envelope content + env.setContent(cc); + // store envelope using the group agent + Context.get().storeEnvelope(env, groupAgent); + + // writing to user + try { + // try to add project to project list (with service group agent) + env2 = Context.get().requestEnvelope(identifier2, serviceGroupAgent); + cc = (ProjectContainer) env2.getContent(); + cc.addProject(project); + env2.setContent(cc); + Context.get().storeEnvelope(env2, serviceGroupAgent); + } catch (EnvelopeNotFoundException e) { + // create new project list (with service group agent) + cc = new ProjectContainer(); + env2 = Context.get().createEnvelope(identifier2, serviceGroupAgent); + env2.setPublic(); + cc.addProject(project); + env2.setContent(cc); + Context.get().storeEnvelope(env2, serviceGroupAgent); + } + } catch (EnvelopeOperationFailedException | EnvelopeAccessDeniedException e1) { + System.out.println(e1); + return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR).build(); + } + + if(this.systemsConfig.gitHubProjectsEnabled(system) && creatorGitHubUsername != null) { + // project creator has sent a GitHub username + // grant access to corresponding GitHub project + try { + this.updateUserGitHubProjectsAccess(system, serviceGroupAgent, agent, creatorGitHubUsername); + } catch (Exception e) { + return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR).build(); + } + } + + if (this.eventManager.sendProjectCreatedEvent(Context.get(), system, project.toJSONObject())) { + return Response.status(HttpURLConnection.HTTP_CREATED).entity("Added Project To l2p Storage").build(); + } else { + // could not send event to event listener service + // delete project again + try { + // remove project from "project envelope" + this.deleteProjectFromProjectEnvelope(system, project.getName(), groupAgent); + + // also update project list and remove the project there + this.removeProjectFromProjectListEnvelope(system, project.getName(), serviceGroupAgent); + } catch (Exception e) {} + return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR) + .entity("Sending event to event listener service failed.").build(); + } + } + } + + /** + * Gets a user's projects Therefore, the user needs to be authorized. + * + * @param system This prefix is used to store all the envelopes of a system. It should be + * unique for every system using the project service. + * @return Response containing the status code + */ + @GET + @Path("/{system}/") + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Creates a new project in the database if no project with the same name is already existing.") + @ApiResponses(value = { @ApiResponse(code = HttpURLConnection.HTTP_CREATED, message = "OK, projects fetched."), + @ApiResponse(code = HttpURLConnection.HTTP_UNAUTHORIZED, message = "User not authorized."), + @ApiResponse(code = HttpURLConnection.HTTP_INTERNAL_ERROR, message = "Internal server error.") }) + public Response getProjects(@PathParam("system") String system) { + if(!this.systemsConfig.isValidSystemName(system)) return Response.status(HttpURLConnection.HTTP_BAD_REQUEST) + .entity("Used system is not valid.").build(); + + Agent agent = Context.getCurrent().getMainAgent(); + if (agent instanceof AnonymousAgent) { + return Response.status(HttpURLConnection.HTTP_UNAUTHORIZED).entity("User not authorized.").build(); + } + + GroupAgent serviceGroupAgent = getServiceGroupAgent(); + if (serviceGroupAgent == null) + return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR).entity("Cannot access service group agent.") + .build(); + + String identifier = getProjectListIdentifier(system); + JSONObject result = new JSONObject(); + try { + Envelope stored = Context.get().requestEnvelope(identifier, serviceGroupAgent); + ProjectContainer cc = (ProjectContainer) stored.getContent(); + // read all projects from the project list + List projects = cc.getAllProjects(); + // create another list for storing the projects that should be returned as JSON + // objects + List projectsJSON = new ArrayList<>(); + + for (Project project : projects) { + // To check whether the user is a member of the project/group, we need the group + // identifier + String groupId = project.getGroupIdentifier(); + JSONObject projectJSON = project.toJSONObject(); + try { + GroupAgent ga = (GroupAgent) Context.get().requestAgent(groupId, agent); + // user is allowed to access group agent => user is a project/group member + // add attribute to project JSON which tells that the user is a project member + projectJSON.put("is_member", true); + projectsJSON.add(projectJSON); + } catch (AgentAccessDeniedException e) { + // user is not allowed to access group agent => user is no project/group member + // only return this project if the service is configured that all projects are + // readable by any user + if (this.systemsConfig.getVisibilityOfProjectsBySystem(system) == ProjectVisibility.ALL) { + projectJSON.put("is_member", false); + projectsJSON.add(projectJSON); + } + } + } + + result.put("projects", projectsJSON); + // System.out.println(result); + return Response.status(Status.OK).entity(result).build(); + } catch (EnvelopeNotFoundException e) { + // return empty list of projects + result.put("projects", new ArrayList<>()); + return Response.status(Status.OK).entity(result).build(); + } catch (Exception e) { + // write error to logfile and console + // Couldnt build due to logging error so just left it out for now... + // logger.log(Level.SEVERE, "Can't persist to network storage!", e); + return Response.status(Status.BAD_REQUEST).entity("Unknown error occured: " + e.getMessage()).build(); + } + } + + /** + * Method for loading information on a single project. + * @param system This prefix is used to store all the envelopes of a system. It should be + * unique for every system using the project service. + * @param projectName Name of the project to load. + * @return JSON information on the requested project. + */ + @GET + @Path("/{system}/{projectName}") + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Returns the project with the given name if it exists and the user has read access.") + @ApiResponses(value = { @ApiResponse(code = HttpURLConnection.HTTP_OK, message = "OK, project fetched."), + @ApiResponse(code = HttpURLConnection.HTTP_FORBIDDEN, message = "Not allowed to access project."), + @ApiResponse(code = HttpURLConnection.HTTP_NOT_FOUND, message = "Could not find project with given name."), + @ApiResponse(code = HttpURLConnection.HTTP_INTERNAL_ERROR, message = "Internal server error.") }) + public Response getProjectByName(@PathParam("system") String system, @PathParam("projectName") String projectName) { + if(!this.systemsConfig.isValidSystemName(system)) return Response.status(HttpURLConnection.HTTP_BAD_REQUEST) + .entity("Used system is not valid.").build(); + + GroupAgent serviceGroupAgent = getServiceGroupAgent(); + if (serviceGroupAgent == null) + return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR).entity("Cannot access service group agent.") + .build(); + + String identifier = getProjectListIdentifier(system); + try { + Envelope stored = Context.get().requestEnvelope(identifier, serviceGroupAgent); + ProjectContainer cc = (ProjectContainer) stored.getContent(); + + Project p = cc.getProjectByName(projectName); + if(p != null) { + // To check whether the user is a member of the project/group, we need the group + // identifier + String groupId = p.getGroupIdentifier(); + JSONObject projectJSON = p.toJSONObject(); + try { + GroupAgent ga = (GroupAgent) Context.get().requestAgent(groupId, Context.get().getMainAgent()); + // user is allowed to access group agent => user is a project/group member + // add attribute to project JSON which tells that the user is a project member + projectJSON.put("is_member", true); + return Response.status(HttpURLConnection.HTTP_OK).entity(projectJSON.toJSONString()).build(); + } catch (AgentAccessDeniedException e) { + // user is not allowed to access group agent => user is no project/group member + // only return this project if the service is configured that all projects are + // readable by any user + if (this.systemsConfig.getVisibilityOfProjectsBySystem(system) == ProjectVisibility.ALL) { + projectJSON.put("is_member", false); + return Response.status(HttpURLConnection.HTTP_OK).entity(projectJSON.toJSONString()).build(); + } else { + // user is not allowed to read this project + return Response.status(HttpURLConnection.HTTP_FORBIDDEN).build(); + } + } + } else { + // found no project with given name + return Response.status(HttpURLConnection.HTTP_NOT_FOUND).build(); + } + } catch (Exception e) { + return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR).entity("Unknown error occured: " + e.getMessage()).build(); + } + } + + /** + * Deleted the project with the given name. + * @param system This prefix is used to store all the envelopes of a system. It should be + * unique for every system using the project service. + * @param projectName Name of the project that should be deleted. + * @return Response containing the status code + */ + @DELETE + @Path("/{system}/{projectName}") + @ApiOperation(value = "Deletes a project from storage.") + @ApiResponses(value = { @ApiResponse(code = HttpURLConnection.HTTP_OK, message = "OK, project deleted."), + @ApiResponse(code = HttpURLConnection.HTTP_FORBIDDEN, message = "Agent is no project member and not allowed to delete it."), + @ApiResponse(code = HttpURLConnection.HTTP_NOT_FOUND, message = "Could not find a project with the given name."), + @ApiResponse(code = HttpURLConnection.HTTP_INTERNAL_ERROR, message = "Internal server error.") }) + public Response deleteProject(@PathParam("system") String system, @PathParam("projectName") String projectName) { + if(!this.systemsConfig.isValidSystemName(system)) return Response.status(HttpURLConnection.HTTP_BAD_REQUEST) + .entity("Used system is not valid.").build(); + + Agent agent = Context.getCurrent().getMainAgent(); + + GroupAgent serviceGroupAgent = getServiceGroupAgent(); + if (serviceGroupAgent == null) + return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR).entity("Cannot access service group agent.") + .build(); + + Project deletedProject; + try { + // remove project from "project envelope" + deletedProject = this.deleteProjectFromProjectEnvelope(system, projectName, agent); + + // also update project list and remove the project there + this.removeProjectFromProjectListEnvelope(system, projectName, serviceGroupAgent); + + // delete corresponding GitHub project (if there exists one) + deletedProject.deleteGitHubProject(system); + } catch (EnvelopeAccessDeniedException e) { + return Response.status(HttpURLConnection.HTTP_FORBIDDEN) + .entity("Agent is no project member and not allowed to delete it.").build(); + } catch (EnvelopeNotFoundException e) { + return Response.status(HttpURLConnection.HTTP_NOT_FOUND) + .entity("Could not find a project with the given name.").build(); + } catch (EnvelopeOperationFailedException e) { + return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR).build(); + } catch (GitHubException e) { + return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR) + .entity("Deletion of corresponding GitHub project failed.").build(); + } + + if (this.eventManager.sendProjectDeletedEvent(Context.get(), system, deletedProject.toJSONObject())) { + return Response.status(HttpURLConnection.HTTP_OK).build(); + } else { + return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR) + .entity("Sending event to event listener service failed.").build(); + } + } + + private Project deleteProjectFromProjectEnvelope(String system, String projectName, Agent agent) + throws EnvelopeAccessDeniedException, EnvelopeNotFoundException, EnvelopeOperationFailedException { + Project deletedProject; + String projectIdentifier = getProjectIdentifier(system, projectName); + + Envelope env = Context.get().requestEnvelope(projectIdentifier, agent); + ProjectContainer cc = (ProjectContainer) env.getContent(); + deletedProject = cc.getProjectByName(projectName); + cc.removeProject(projectName); + env.setContent(cc); + Context.get().storeEnvelope(env, agent); + + return deletedProject; + } + + private void removeProjectFromProjectListEnvelope(String system, String projectName, GroupAgent serviceGroupAgent) + throws EnvelopeAccessDeniedException, EnvelopeNotFoundException, EnvelopeOperationFailedException { + Envelope envList = Context.get().requestEnvelope(getProjectListIdentifier(system), serviceGroupAgent); + ProjectContainer ccList = (ProjectContainer) envList.getContent(); + ccList.removeProject(projectName); + envList.setContent(ccList); + Context.get().storeEnvelope(envList, serviceGroupAgent); + } + + /** + * Changes the group linked to an existing project in the pastry storage. + * Therefore, the user needs to be authorized. + * + * @param system This prefix is used to store all the envelopes of a system. It should be + * unique for every system using the project service. + * @param body JSON representation of the project to store (containing name and + * access token of user needed to create Requirements Bazaar + * category). + * @return Response containing the status code (and a message or the created + * project). + */ + @POST + @Path("/{system}/changeGroup") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Creates a new project in the pastry storage if no project with the same name is already existing.") + @ApiResponses(value = { @ApiResponse(code = HttpURLConnection.HTTP_CREATED, message = "OK, group changed."), + @ApiResponse(code = HttpURLConnection.HTTP_UNAUTHORIZED, message = "User not authorized."), + @ApiResponse(code = HttpURLConnection.HTTP_CONFLICT, message = "The given group is already linked to the project."), + @ApiResponse(code = HttpURLConnection.HTTP_BAD_REQUEST, message = "Input project is not well formatted or some attribute is missing."), + @ApiResponse(code = HttpURLConnection.HTTP_INTERNAL_ERROR, message = "Internal server error.") }) + public Response changeGroup(@PathParam("system") String system, String body) { + if(!this.systemsConfig.isValidSystemName(system)) return Response.status(HttpURLConnection.HTTP_BAD_REQUEST) + .entity("Used system is not valid.").build(); + + Context.get().monitorEvent(MonitoringEvent.SERVICE_MESSAGE, "changeGroup: trying to change group of project"); + + if (Context.getCurrent().getMainAgent() instanceof AnonymousAgent) { + return Response.status(HttpURLConnection.HTTP_UNAUTHORIZED).entity("User not authorized.").build(); + } else { + Agent agent = Context.getCurrent().getMainAgent(); + try { + JSONObject jsonBody = (JSONObject) JSONValue.parseWithException(body); + + String projectName = (String) jsonBody.get("projectName"); + String newGroupId = (String) jsonBody.get("newGroupId"); + String newGroupName = (String) jsonBody.get("newGroupName"); + String identifier = getProjectListIdentifier(system); + + // check if user currently has access to project + if (!this.hasAccessToProject(system, projectName)) { + return Response.status(HttpURLConnection.HTTP_FORBIDDEN) + .entity("User is no member of the project and thus not allowed to edit its linked group.") + .build(); + } + + try { + Envelope stored = Context.get().requestEnvelope(identifier, Context.get().getServiceAgent()); + ProjectContainer cc = (ProjectContainer) stored.getContent(); + // read all projects from the project list + List projects = cc.getAllProjects(); + + for (Project project : projects) { + // To check whether the user is a member of the project/group, we need the group + // identifier + String groupId = project.getGroupIdentifier(); + // Search correct project + if (projectName.equals(project.getName())) { + // check if new group actually differs from old group + if (!newGroupId.equals(groupId)) { + try { + GroupAgent ga = (GroupAgent) Context.get().requestAgent(newGroupId, agent); + // user is allowed to access group agent => user is a project/group member + cc.removeProject(project); + project.changeGroup(newGroupId, newGroupName); + cc.addProject(project); + stored.setContent(cc); + Context.get().storeEnvelope(stored, Context.get().getServiceAgent()); + JSONObject response = new JSONObject(); + response.put("project", project); + return Response.status(Status.OK).entity("Group successfully changed!") + .entity(response).build(); + } catch (AgentAccessDeniedException e) { + // user is not allowed to access group agent => user is no project/group member + // cant use group which user is not a part of + return Response.status(HttpURLConnection.HTTP_BAD_REQUEST) + .entity("You are not a part of this group!").build(); + + } catch (AgentNotFoundException e) { + // or: group does not exist + return Response.status(HttpURLConnection.HTTP_BAD_REQUEST) + .entity("Non-existing group").build(); + } catch (AgentOperationFailedException e) { + return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR).entity(e).build(); + } + } + } + } + return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR).build(); + } catch (EnvelopeNotFoundException e) { + return Response.status(HttpURLConnection.HTTP_BAD_REQUEST).entity("No projects available.").build(); + } catch (EnvelopeAccessDeniedException | EnvelopeOperationFailedException e) { + return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR).build(); + } + } catch (ParseException e) { + // JSON project given with the request is not well formatted or some attributes + // are missing + return Response.status(HttpURLConnection.HTTP_BAD_REQUEST).entity(e.getMessage()).build(); + } + } + } + + /** + * Changes the group linked to an existing project in the pastry storage. + * Therefore, the user needs to be authorized. + * + * @param system This prefix is used to store all the envelopes of a system. It should be + * unique for every system using the project service. + * @param body JSON representation of the project to store (containing name and + * access token of user needed to create Requirements Bazaar + * category). + * @return Response containing the status code (and a message or the created + * project). + */ + @POST + @Path("/{system}/changeMetadata") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Change metadata corresponding to project.") + @ApiResponses(value = { @ApiResponse(code = HttpURLConnection.HTTP_CREATED, message = "OK, metadata changed."), + @ApiResponse(code = HttpURLConnection.HTTP_UNAUTHORIZED, message = "User not authorized."), + @ApiResponse(code = HttpURLConnection.HTTP_BAD_REQUEST, message = "Input project is not well formatted or some attribute is missing."), + @ApiResponse(code = HttpURLConnection.HTTP_INTERNAL_ERROR, message = "Internal server error.") }) + public Response changeMetadata(@PathParam("system") String system, String body) { + if(!this.systemsConfig.isValidSystemName(system)) return Response.status(HttpURLConnection.HTTP_BAD_REQUEST) + .entity("Used system is not valid.").build(); + + Context.get().monitorEvent(MonitoringEvent.SERVICE_MESSAGE, "changeGroup: trying to change group of project"); + + if (Context.getCurrent().getMainAgent() instanceof AnonymousAgent) { + return Response.status(HttpURLConnection.HTTP_UNAUTHORIZED).entity("User not authorized.").build(); + }; + Agent agent = Context.getCurrent().getMainAgent(); + try { + JSONObject jsonBody = (JSONObject) JSONValue.parseWithException(body); + String projectName = (String) jsonBody.get("projectName"); + String oldMetadata = jsonBody.get("oldMetadata").toString(); + String newMetadata = jsonBody.get("newMetadata").toString(); + String identifier = getProjectListIdentifier(system); + // check if user currently has access to project + if (!this.hasAccessToProject(system, projectName)) { + return Response.status(HttpURLConnection.HTTP_FORBIDDEN) + .entity("User is no member of the project and thus not allowed to edit its linked group.") + .build(); + } + + try { + Envelope stored = Context.get().requestEnvelope(identifier, Context.get().getServiceAgent()); + ProjectContainer cc = (ProjectContainer) stored.getContent(); + // read all projects from the project list + List projects = cc.getAllProjects(); + + for (Project project : projects) { + // To check whether there is an inconsistency, we compare the old metadata given + // as a parameter; + // Search correct project + if (projectName.equals(project.getName())) { + // check if new group actually differs from old group + if (oldMetadata.equals(project.getMetadataString())) { + try { + GroupAgent ga = (GroupAgent) Context.get().requestAgent(project.getGroupIdentifier(), + agent); + // user is allowed to access group agent => user is a project/group member + cc.removeProject(project); + project.changeMetadata(newMetadata); + cc.addProject(project); + stored.setContent(cc); + Context.get().storeEnvelope(stored, Context.get().getServiceAgent()); + JSONObject response = new JSONObject(); + response.put("project", project); + return Response.status(Status.OK).entity("Metadata successfully changed!") + .entity(response).build(); + } catch (AgentAccessDeniedException e) { + // user is not allowed to access group agent => user is no project/group member + // cant use group which user is not a part of + return Response.status(HttpURLConnection.HTTP_BAD_REQUEST) + .entity("You are not a part of this group!").build(); + + } catch (AgentNotFoundException e) { + // or: group does not exist + return Response.status(HttpURLConnection.HTTP_BAD_REQUEST).entity("Non-existing group") + .build(); + } catch (AgentOperationFailedException e) { + return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR).entity(e).build(); + } + } else { + return Response.status(HttpURLConnection.HTTP_BAD_REQUEST) + .entity("Inconsistency with old metadata, please reload page and try again!") + .build(); + } + } + } + return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR).build(); + } catch (EnvelopeNotFoundException e) { + return Response.status(HttpURLConnection.HTTP_BAD_REQUEST).entity("No projects available.").build(); + } catch (EnvelopeAccessDeniedException | EnvelopeOperationFailedException e) { + return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR).build(); + } + } catch (ParseException e) { + // JSON project given with the request is not well formatted or some attributes + // are missing + return Response.status(HttpURLConnection.HTTP_BAD_REQUEST).entity(e.getMessage()).build(); + } + } + + /** + * This method is used to notify the project-service about the GitHub username of a user. + * It checks every project where the user is a member of and grants the user access + * to the corresponding GitHub projects (if it is not already given). + * @param system Name of the system + * @param body JSON body that should contain the GitHub username. + * @return Response containing the status code. + */ + @POST + @Consumes(MediaType.TEXT_PLAIN) + @Path("/{system}/user/githubinfo") + public Response updateUserGitHubInfo(@PathParam("system") String system, String body) { + if(!this.systemsConfig.isValidSystemName(system)) return Response.status(HttpURLConnection.HTTP_BAD_REQUEST) + .entity("Used system is not valid.").build(); + + if(!this.systemsConfig.gitHubProjectsEnabled(system)) return Response.status(HttpURLConnection.HTTP_OK).build(); + + if (Context.getCurrent().getMainAgent() instanceof AnonymousAgent) { + return Response.status(HttpURLConnection.HTTP_UNAUTHORIZED).entity("User not authorized.").build(); + }; + + GroupAgent serviceGroupAgent = getServiceGroupAgent(); + if (serviceGroupAgent == null) + return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR).entity("Cannot access service group agent.") + .build(); + + Agent agent = Context.getCurrent().getMainAgent(); + + JSONObject jsonBody; + String gitHubUsername = null; + try { + jsonBody = (JSONObject) JSONValue.parseWithException(body); + if(jsonBody.containsKey("gitHubUsername")) { + gitHubUsername = (String) jsonBody.get("gitHubUsername"); + } else { + return Response.status(HttpURLConnection.HTTP_BAD_REQUEST).entity("GitHub username is missing in body.").build(); + } + } catch (ParseException e) { + return Response.status(HttpURLConnection.HTTP_BAD_REQUEST).entity("Cannot parse JSON body.").build(); + } + + try { + this.updateUserGitHubProjectsAccess(system, serviceGroupAgent, agent, gitHubUsername); + } catch (GitHubException e) { + return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR) + .entity("Communication with GitHub API failed.").build(); + } catch (Exception e) { + return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR).build(); + } + + return Response.status(HttpURLConnection.HTTP_OK).build(); + } + + /** + * Grants user access to all relevant GitHub projects (i.e., projects where the user + * is a member of) in the given system. + * Also checks if someone who still has access to the GitHub project is no group + * member anymore. In this case, access to GitHub project will be removed. + * @param system System + * @param serviceGroupAgent Group agent of the service used to access the project list envelope. + * @param userAgent Agent of the user + * @param gitHubUsername GitHub username of the user + * @throws EnvelopeAccessDeniedException Project list envelope or project envelope could not be accessed. + * @throws EnvelopeNotFoundException Project list envelope or project envelope not found. + * @throws EnvelopeOperationFailedException Updating one of the envelopes failed. + * @throws GitHubException Problem with communication with GitHub API. + */ + private void updateUserGitHubProjectsAccess(String system, GroupAgent serviceGroupAgent, Agent userAgent, String gitHubUsername) + throws EnvelopeAccessDeniedException, EnvelopeNotFoundException, EnvelopeOperationFailedException, GitHubException { + Envelope stored = null; + stored = Context.get().requestEnvelope(getProjectListIdentifier(system), serviceGroupAgent); + ProjectContainer cc = (ProjectContainer) stored.getContent(); + + // load all projects where the user is a member of + List userProjects = this.getProjectsOfUser(system, serviceGroupAgent, userAgent); + for(Project project : userProjects) { + if(!project.hasUserGitHubNameStored(userAgent)) { + // store the GitHub username of this user + project.addGitHubUsername(userAgent, gitHubUsername); + + // need to update project in envelope + cc.removeProject(project); + cc.addProject(project); + stored.setContent(cc); + Context.get().storeEnvelope(stored, Context.get().getServiceAgent()); + + // since we now know the GitHub username, we can add the user to the GitHub project + GitHubHelper.getInstance().grantUserAccessToProject(system, gitHubUsername, project.getConnectedGitHubProject()); + } + // check for other users, if someone left the group and still has access to GitHub project + String groupId = project.getGroupIdentifier(); + try { + GroupAgent ga = (GroupAgent) Context.get().requestAgent(groupId, userAgent); + boolean removedUser = project.removeNonGroupMembersGitHubAccess(system, ga.getMemberList()); + if(removedUser) { + // need to update project in envelope + cc.removeProject(project); + cc.addProject(project); + stored.setContent(cc); + Context.get().storeEnvelope(stored, Context.get().getServiceAgent()); + } + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } + + /** + * Returns the list of projects where the user is a member of. + * @param system Name of the system, where projects of the user should be searched. + * @param serviceGroupAgent Agent to access project list envelope. + * @param userAgent User agent to check if user has access to a project. + * @return List of projects where the user is a member of. + */ + private List getProjectsOfUser(String system, GroupAgent serviceGroupAgent, Agent userAgent) { + ArrayList userProjects = new ArrayList<>(); + + String identifier = getProjectListIdentifier(system); + + try { + Envelope stored = Context.get().requestEnvelope(identifier, serviceGroupAgent); + ProjectContainer cc = (ProjectContainer) stored.getContent(); + // read all projects from the project list + List projects = cc.getAllProjects(); + + for (Project project : projects) { + // To check whether the user is a member of the project/group, we need the group + // identifier + String groupId = project.getGroupIdentifier(); + try { + GroupAgent ga = (GroupAgent) Context.get().requestAgent(groupId, userAgent); + // user is allowed to access group agent => user is a project/group member + userProjects.add(project); + } catch (AgentAccessDeniedException e) { + // user is not allowed to access group agent => user is no project/group member + } + } + } catch (Exception e) {} + + return userProjects; + } + + /** + * Returns the identifier of the envelope for the project with the given name. + * @param system Prefix of the system which is used for all envelopes. Should be unique + * for every system using the project service. + * @param projectName Name of the project + * @return The identifier of the envelope for the project with the given name. + */ + public static String getProjectIdentifier(String system, String projectName) { + return system + "_" + projects_prefix + "_" + projectName; + } + + /** + * Returns the identifier of the envelope for the project list of the given system. + * @param system Prefix of the system which is used for all envelopes. Should be unique + * for every system using the project service. + * @return The identifier of the envelope for the project with the given name. + */ + public static String getProjectListIdentifier(String system) { + return system + "_" + projects_prefix; + } +} diff --git a/project_service/src/main/java/i5/las2peer/services/projectService/exception/GitHubException.java b/project_service/src/main/java/i5/las2peer/services/projectService/exception/GitHubException.java new file mode 100644 index 0000000..c12ab5f --- /dev/null +++ b/project_service/src/main/java/i5/las2peer/services/projectService/exception/GitHubException.java @@ -0,0 +1,10 @@ +package i5.las2peer.services.projectService.exception; + +public class GitHubException extends Exception { + + private static final long serialVersionUID = -2413185720239790874L; + + public GitHubException(String message) { + super(message); + } +} diff --git a/project_service/src/main/java/i5/las2peer/services/projectService/project/Project.java b/project_service/src/main/java/i5/las2peer/services/projectService/project/Project.java new file mode 100644 index 0000000..c207037 --- /dev/null +++ b/project_service/src/main/java/i5/las2peer/services/projectService/project/Project.java @@ -0,0 +1,289 @@ +package i5.las2peer.services.projectService.project; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import org.json.simple.JSONValue; +import org.json.simple.JSONObject; +import org.json.simple.parser.ParseException; + +import i5.las2peer.api.security.Agent; +import i5.las2peer.services.projectService.util.github.GitHubException; +import i5.las2peer.services.projectService.util.github.GitHubHelper; +import i5.las2peer.services.projectService.util.github.GitHubProject; + +/** + * (Data-)Class for Projects. Provides means to convert JSON to Object and + * Object to JSON. + */ +public class Project implements Serializable { + + /** + * Name of the project. + */ + private String name; + + /** + * Users that are part of the project. + */ + // private ArrayList users; + + /** + * Group linked to Project. + */ + private String groupName; + + /** + * Identifier of the group linked to the project. + */ + private String groupIdentifier; + + /** + * String containing the JSON representation of the project metadata. This + * metadata can be used to store additional information on the project, that + * might be system-specific. + */ + private String metadata; + + /** + * Information on the connected GitHub project (if there is one connected). + */ + private GitHubProject connectedGitHubProject = null; + + /** + * Maps user agent identifiers to their GitHub username. + * If the GitHub projects connection is disabled, this map might not be defined. + */ + private HashMap memberGitHubUsernames; + + /** + * Creates a project object from the given JSON string. This constructor should + * be used before storing new projects. + * + * @param creator User that creates the project. + * @param jsonProject JSON representation of the project to store. + * @throws ParseException If parsing went wrong or one of the keys is missing in + * the given JSON representation. + */ + public Project(Agent creator, String jsonProject) throws ParseException { + JSONObject project = (JSONObject) JSONValue.parseWithException(jsonProject); + + this.containsKeyWithException(project, "name"); + this.name = (String) project.get("name"); + + // this.users = new ArrayList<>(); + // this.users.add(creator); + // group and users to project from said group + this.containsKeyWithException(project, "linkedGroup"); + JSONObject linkedGroup = (JSONObject) project.get("linkedGroup"); + + // get name of linked group + this.containsKeyWithException(linkedGroup, "name"); + this.groupName = (String) linkedGroup.get("name"); + + // get id of linked group + this.containsKeyWithException(linkedGroup, "id"); + this.groupIdentifier = (String) linkedGroup.get("id"); + + // check if jsonProject contains metadata + if (project.containsKey("metadata")) { + // try converting to JSONObject (to check if valid JSON) + JSONObject metadataJSON = (JSONObject) project.get("metadata"); + this.metadata = metadataJSON.toJSONString(); + } else { + // no metadata given, just store an empty object + JSONObject empty = new JSONObject(); + this.metadata = empty.toJSONString(); + } + + this.memberGitHubUsernames = new HashMap<>(); + } + + /** + * Checks if the given JSONObject contains the given key. If key does not exist, + * then a ParseException is thrown. + * + * @param json JSONObject where the key should be searched. + * @param key Key that should be searched in given JSONObject. + * @throws ParseException If given JSONObject does not contain given key, a + * ParseException is thrown. + */ + private static void containsKeyWithException(JSONObject json, String key) throws ParseException { + if (!json.containsKey(key)) + throw new ParseException(0, "Attribute '" + key + "' of project is missing."); + } + + /** + * Changes linked group to given new group. + * + * @param groupIdentifier Groupagent id of new group + * @param groupName Groupname of new group + */ + public void changeGroup(String groupIdentifier, String groupName) { + this.groupIdentifier = groupIdentifier; + this.groupName = groupName; + } + + /** + * Changes metadata of project. + * + * @param newMetadata Metadata to replace the old one. + */ + public void changeMetadata(String newMetadata) { + this.metadata = newMetadata; + } + + /** + * Returns the JSON representation of this project. + * + * @return a JSON object representing a project + */ + @SuppressWarnings("unchecked") + public JSONObject toJSONObject() { + JSONObject jsonProject = new JSONObject(); + + // put attributes + jsonProject.put("name", this.name); + jsonProject.put("groupName", this.groupName); + jsonProject.put("groupIdentifier", this.groupIdentifier); + jsonProject.put("metadata", this.getMetadataAsJSONObject()); + if(this.gitHubProjectConnected()) { + jsonProject.put("gitHubProject", this.connectedGitHubProject.toJSONObject()); + } + + return jsonProject; + } + + /** + * Uses the GitHubHelper to create a GitHub project for this las2peer project. + * @param systemName Name of the system (used to find correct GitHub organization for GitHub project). + * @throws GitHubException If the project creation on GitHub failed. + */ + public void createGitHubProject(String systemName) throws GitHubException { + this.connectedGitHubProject = GitHubHelper.getInstance().createPublicGitHubProject(systemName, this.getName()); + } + + /** + * Uses the GitHubHelper to delete the corresponding GitHub project (if there exists one). + * @param systemName Name of the system (used to find correct GitHub organization for GitHub project). + * @throws GitHubException If the GitHub project deletion failed. + */ + public void deleteGitHubProject(String systemName) throws GitHubException { + if(this.gitHubProjectConnected()) { + GitHubHelper.getInstance().deleteGitHubProject(systemName, this.connectedGitHubProject); + } + } + + /** + * Getter for the name of the project. + * + * @return Name of the project. + */ + public String getName() { + return this.name; + } + + /** + * Getter for the name of the group connected to the project. + * + * @return Name of the group. + */ + public String getGroupName() { + return this.groupName; + } + + /** + * Getter for the identifier of the group connected to the project. + * + * @return Identifier of the group. + */ + public String getGroupIdentifier() { + return this.groupIdentifier; + } + + /** + * Getter for the project metadata as a String. + * + * @return JSON String representation of the project metadata. + */ + public String getMetadataString() { + return this.metadata; + } + + /** + * Getter for the project metadata as a JSONObject. + * + * @return Project metadata converted to JSONObject. + */ + public JSONObject getMetadataAsJSONObject() { + return (JSONObject) JSONValue.parse(this.metadata); + } + + /** + * Checks whether there is a GitHub project connected to this las2peer project. + * @return Whether there is a GitHub project connected to this las2peer project. + */ + public boolean gitHubProjectConnected() { + return this.connectedGitHubProject != null; + } + + public GitHubProject getConnectedGitHubProject() { + return this.connectedGitHubProject; + } + + /** + * Checks if the GitHub username of the given user is already stored inside this project. + * @param userAgent Agent of the user. + * @return Whether the GitHub username of the given user is already stored inside this project. + */ + public boolean hasUserGitHubNameStored(Agent userAgent) { + return this.memberGitHubUsernames.containsKey(userAgent.getIdentifier()); + } + + /** + * Stores the GitHub username of the user in the memberGitHubUsernames HashMap. + * @param userAgent Agent of the user. + * @param gitHubUsername GitHub username of the user. + */ + public void addGitHubUsername(Agent userAgent, String gitHubUsername) { + this.memberGitHubUsernames.put(userAgent.getIdentifier(), gitHubUsername); + } + + /** + * Checks if a user that is no group member anymore still has access to the GitHub project. + * In this case, access will be removed. + * @param system Name of the system. + * @param groupMemberIds Array containing the agent ids of the current group members. + * @return True if a group member was removed from access, false otherwise. + * @throws GitHubException If something with the communication with GitHub went wrong. + */ + public boolean removeNonGroupMembersGitHubAccess(String system, String[] groupMemberIds) throws GitHubException { + boolean changed = false; + List ghProjectMemberUserIds = new ArrayList<>(); + for(String userId : this.memberGitHubUsernames.keySet()) { + ghProjectMemberUserIds.add(userId); + } + + for(String userId : ghProjectMemberUserIds) { + // check if the user is still a member of the group + boolean stillMember = false; + for(String groupMemberId : groupMemberIds) { + if(groupMemberId.equals(userId)) { + stillMember = true; + break; + } + } + if(!stillMember) { + // user left the group + // remove access to GitHub project + String username = this.memberGitHubUsernames.get(userId); + this.memberGitHubUsernames.remove(userId); + GitHubHelper.getInstance().removeUserAccessToProject(system, username, this.connectedGitHubProject); + changed = true; + } + } + return changed; + } +} diff --git a/project_service/src/main/java/i5/las2peer/services/projectService/util/ProjectServiceSystem.java b/project_service/src/main/java/i5/las2peer/services/projectService/util/ProjectServiceSystem.java new file mode 100644 index 0000000..90bd480 --- /dev/null +++ b/project_service/src/main/java/i5/las2peer/services/projectService/util/ProjectServiceSystem.java @@ -0,0 +1,109 @@ +package i5.las2peer.services.projectService.util; + +import org.json.simple.JSONObject; + +import i5.las2peer.services.projectService.ProjectService; + +/** + * Data class to store information about one system that the project service + * handles projects for. Examples for a system could be the Social Bot Framework + * or the Community Application Editor. + * @author Philipp + * + */ +public class ProjectServiceSystem { + + private static final String JSON_KEY_VISIBILITY_OF_PROJECTS = "visibilityOfProjects"; + private static final String JSON_KEY_EVENT_LISTENER_SERVICE = "eventListenerService"; + private static final String JSON_KEY_GITHUB_PROJECTS_ENABLED = "gitHubProjectsEnabled"; + private static final String JSON_KEY_GITHUB_ORGANIZATION = "gitHubOrganization"; + private static final String JSON_KEY_GITHUB_PERSONAL_ACCESS_TOKEN = "gitHubPersonalAccessToken"; + + /** + * Name of the system. Example: SBF + */ + private String name; + + /** + * Whether projects of this system (and their metadata) can be read by everyone + * or only by project members. + */ + private ProjectVisibility visibilityOfProjects; + + /** + * Name of the event listener service of this system. + * This service will be notified about specific events (e.g., project-creation). + */ + private String eventListenerService; + + /** + * Whether the connection of las2peer projects within this system to GitHub + * projects should be enabled. + */ + private boolean gitHubProjectsEnabled = false; + + /** + * Name of the GitHub organization where GitHub projects (corresponding + * to projects of this system) should be stored. + */ + private String gitHubOrganization; + + /** + * Personal access token from GitHub that allows to use the API + * to create new GitHub projects in the used GitHub organization. + */ + private String gitHubPersonalAccessToken; + + public ProjectServiceSystem(String systemName, JSONObject systemJSON) { + this.name = systemName; + + if (systemJSON.containsKey(JSON_KEY_VISIBILITY_OF_PROJECTS)) { + String visibility = (String) systemJSON.get(JSON_KEY_VISIBILITY_OF_PROJECTS); + if (visibility.equals("all")) { + this.visibilityOfProjects = ProjectVisibility.ALL; + } else { + this.visibilityOfProjects = ProjectVisibility.OWN; + } + } else { + // use default value + this.visibilityOfProjects = ProjectService.visibilityOfProjectsDefault; + } + + if (systemJSON.containsKey(JSON_KEY_GITHUB_PROJECTS_ENABLED)) { + this.gitHubProjectsEnabled = (boolean) systemJSON.get(JSON_KEY_GITHUB_PROJECTS_ENABLED); + this.gitHubOrganization = (String) systemJSON.get(JSON_KEY_GITHUB_ORGANIZATION); + this.gitHubPersonalAccessToken = (String) systemJSON.get(JSON_KEY_GITHUB_PERSONAL_ACCESS_TOKEN); + } + + this.eventListenerService = (String) systemJSON.getOrDefault(JSON_KEY_EVENT_LISTENER_SERVICE, null); + } + + + public String getName() { + return this.name; + } + + public ProjectVisibility getVisibilityOfProjects() { + return this.visibilityOfProjects; + } + + public String getEventListenerService() { + return this.eventListenerService; + } + + public boolean hasEventListenerService() { + return this.eventListenerService != null; + } + + public boolean gitHubProjectsEnabled() { + return this.gitHubProjectsEnabled; + } + + public String getGitHubOrganization() { + return this.gitHubOrganization; + } + + public String getGitHubPersonalAccessToken() { + return this.gitHubPersonalAccessToken; + } +} diff --git a/project_service/src/main/java/i5/las2peer/services/projectService/util/ProjectVisibility.java b/project_service/src/main/java/i5/las2peer/services/projectService/util/ProjectVisibility.java new file mode 100644 index 0000000..98ce00f --- /dev/null +++ b/project_service/src/main/java/i5/las2peer/services/projectService/util/ProjectVisibility.java @@ -0,0 +1,11 @@ +package i5.las2peer.services.projectService.util; + +/** + * Whether projects should be visible for everyone (read-access) or + * only for the project members. + * @author Philipp + * + */ +public enum ProjectVisibility { + OWN, ALL +} diff --git a/project_service/src/main/java/i5/las2peer/services/projectService/util/SystemsConfig.java b/project_service/src/main/java/i5/las2peer/services/projectService/util/SystemsConfig.java new file mode 100644 index 0000000..a2fbfc2 --- /dev/null +++ b/project_service/src/main/java/i5/las2peer/services/projectService/util/SystemsConfig.java @@ -0,0 +1,94 @@ +package i5.las2peer.services.projectService.util; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import org.json.simple.JSONObject; + +import i5.las2peer.services.projectService.ProjectService; + +public class SystemsConfig { + + private List systems; + + public SystemsConfig(JSONObject systemsJSON) { + this.systems = new ArrayList<>(); + for(Object systemName : systemsJSON.keySet()) { + JSONObject systemJSON = (JSONObject) systemsJSON.get(systemName); + systems.add(new ProjectServiceSystem((String) systemName, systemJSON)); + } + } + + /** + * Returns a map consisting for every system (key) the corresponding event listener service name (as value). + * If the event listener service was not set in the properties file, then it is null. + * @return Map that maps system names to event listener service names. + */ + public HashMap getSystemEventListenerServiceMap() { + HashMap map = new HashMap<>(); + for(ProjectServiceSystem system : this.systems) { + map.put(system.getName(), system.getEventListenerService()); + } + return map; + } + + /** + * Checks if the given system name is valid, i.e. if it is part of the systems JSON given as a system property. + * @param systemName Name of the system. + * @return Whether the given system name is valid. + */ + public boolean isValidSystemName(String systemName) { + for(ProjectServiceSystem system : this.systems) { + if(system.getName().equals(systemName)) return true; + } + return false; + } + + /** + * Returns the value of the "visibilityOfProjects" attribute of the given system. + * @param systemName Name of the system. + * @return Value of "visibilityOfProjects" attribute set for this system. + */ + public ProjectVisibility getVisibilityOfProjectsBySystem(String systemName) { + for(ProjectServiceSystem system : this.systems) { + if(system.getName().equals(systemName)) return system.getVisibilityOfProjects(); + } + return ProjectService.visibilityOfProjectsDefault; + } + + /** + * Whether the GitHub projects connection is enabled for the system with the given name. + * @param systemName Name of the system. + * @return Whether the GitHub projects connection is enabled for the system with the given name. + */ + public boolean gitHubProjectsEnabled(String systemName) { + for(ProjectServiceSystem system : this.systems) { + if(system.getName().equals(systemName)) return system.gitHubProjectsEnabled(); + } + return false; + } + + /** + * Returns the name of the GitHub organization that is connected to the system. + * @param systemName System to search GitHub organization for. + * @return Name of the GitHub organization that is connected to the system. + */ + public String getGitHubOrganizationBySystem(String systemName) { + for(ProjectServiceSystem system : this.systems) { + if(system.getName().equals(systemName)) return system.getGitHubOrganization(); + } + return null; + } + + /** + * Returns the personal access token for GitHub related to the system. + * @param systemName System to search GitHub personal access token for. + * @return Personal access token for GitHub related to the system. + */ + public String getGitHubPATBySystem(String systemName) { + for(ProjectServiceSystem system : this.systems) { + if(system.getName().equals(systemName)) return system.getGitHubPersonalAccessToken(); + } + return null; + } +} diff --git a/project_service/src/main/java/i5/las2peer/services/projectService/util/github/GitHubException.java b/project_service/src/main/java/i5/las2peer/services/projectService/util/github/GitHubException.java new file mode 100644 index 0000000..4536e1d --- /dev/null +++ b/project_service/src/main/java/i5/las2peer/services/projectService/util/github/GitHubException.java @@ -0,0 +1,10 @@ +package i5.las2peer.services.projectService.util.github; + +public class GitHubException extends Exception { + + private static final long serialVersionUID = -2413185720139790874L; + + public GitHubException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/project_service/src/main/java/i5/las2peer/services/projectService/util/github/GitHubHelper.java b/project_service/src/main/java/i5/las2peer/services/projectService/util/github/GitHubHelper.java new file mode 100644 index 0000000..bb0c55c --- /dev/null +++ b/project_service/src/main/java/i5/las2peer/services/projectService/util/github/GitHubHelper.java @@ -0,0 +1,416 @@ +package i5.las2peer.services.projectService.util.github; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.util.Base64; +import java.net.http.*; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse.BodyHandlers; + +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; +import org.json.simple.parser.ParseException; + +import i5.las2peer.services.projectService.util.SystemsConfig; + + +/** + * Helper class for working with GitHub API. + * Currently supports creating new GitHub projects and update their + * visibility to public. + * @author Philipp + * + */ +public class GitHubHelper { + + private static GitHubHelper instance; + private static final String API_BASE_URL = "https://api.github.com"; + + // make sure that constructor cannot be accessed from outside + private GitHubHelper() {} + + public static GitHubHelper getInstance() { + if(GitHubHelper.instance == null) { + GitHubHelper.instance = new GitHubHelper(); + } + return GitHubHelper.instance; + } + + /** + * Systems configuration (that also contains the GitHub config). + */ + private SystemsConfig systemsConfig = null; + + public void setSystemsConfig(SystemsConfig systemsConfig) { + this.systemsConfig = systemsConfig; + } + + /** + * Creates a public GitHub project with the given name. + * @param systemName Name of the system, for which the GitHub project should be created. + * @param projectName Name of the GitHub project which should be created. + * @return The newly created GitHubProject object. + * @throws GitHubException If something with the requests to the GitHub API went wrong. + */ + public GitHubProject createPublicGitHubProject(String systemName, String projectName) throws GitHubException { + String gitHubOrganization = this.systemsConfig.getGitHubOrganizationBySystem(systemName); + String gitHubPersonalAccessToken = this.systemsConfig.getGitHubPATBySystem(systemName); + + if(gitHubPersonalAccessToken == null || gitHubOrganization == null) { + throw new GitHubException("One of the variables personal access token or organization are not set."); + } + + GitHubProject gitHubProject = createGitHubProject(systemName, projectName); + makeGitHubProjectPublic(systemName, gitHubProject.getId()); + + // create some predefined columns + createProjectColumn(systemName, gitHubProject.getId(), "To do"); + createProjectColumn(systemName, gitHubProject.getId(), "In progress"); + createProjectColumn(systemName, gitHubProject.getId(), "Done"); + + return gitHubProject; + } + + /** + * Gives the GitHub user with the given username access to the given GitHub project. + * @param systemName Name of the system, that the GitHub project belongs to. + * @param ghUsername Username of the GitHub user which should get access to the project. + * @param ghProject GitHubProject object + * @throws GitHubException If something with the API request went wrong + */ + public void grantUserAccessToProject(String systemName, String ghUsername, GitHubProject ghProject) throws GitHubException { + if(ghUsername == null) return; + + String authStringEnc = getAuthStringEnc(systemName); + + URL url; + try { + url = new URL(API_BASE_URL + "/projects/" + ghProject.getId() + "/collaborators/" + ghUsername); + + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("PUT"); + connection.setDoInput(true); + connection.setDoOutput(true); + connection.setUseCaches(false); + connection.setRequestProperty("Accept", "application/vnd.github.inertia-preview+json"); + connection.setRequestProperty("Authorization", "Basic " + authStringEnc); + + // forward (in case of) error + if (connection.getResponseCode() != 204) { + String message = getErrorMessage(connection); + throw new GitHubException(message); + } + } catch (MalformedURLException e) { + e.printStackTrace(); + throw new GitHubException(e.getMessage()); + } catch (IOException e) { + e.printStackTrace(); + throw new GitHubException(e.getMessage()); + } + } + + /** + * Removes users access to GitHub project. + * @param systemName Name of the system, that the GitHub project belongs to. + * @param ghUsername Username of the GitHub user who should be removed from project. + * @param ghProject GitHubProject object + * @throws GitHubException If something with the API request went wrong + */ + public void removeUserAccessToProject(String systemName, String ghUsername, GitHubProject ghProject) throws GitHubException { + if(ghUsername == null) return; + + String authStringEnc = getAuthStringEnc(systemName); + + URL url; + try { + url = new URL(API_BASE_URL + "/projects/" + ghProject.getId() + "/collaborators/" + ghUsername); + + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("DELETE"); + connection.setDoInput(true); + connection.setDoOutput(true); + connection.setUseCaches(false); + connection.setRequestProperty("Accept", "application/vnd.github.inertia-preview+json"); + connection.setRequestProperty("Authorization", "Basic " + authStringEnc); + + // forward (in case of) error + if (connection.getResponseCode() != 204) { + String message = getErrorMessage(connection); + throw new GitHubException(message); + } + } catch (MalformedURLException e) { + e.printStackTrace(); + throw new GitHubException(e.getMessage()); + } catch (IOException e) { + e.printStackTrace(); + throw new GitHubException(e.getMessage()); + } + } + + /** + * Deletes the given GitHub project. + * @param systemName Name of the system, to which the GitHub project belongs to. + * @param ghProject GitHub project which should be deleted. + * @throws GitHubException If something with the request to the GitHub API went wrong. + */ + public void deleteGitHubProject(String systemName, GitHubProject ghProject) throws GitHubException { + String authStringEnc = getAuthStringEnc(systemName); + + URL url; + try { + url = new URL(API_BASE_URL + "/projects/" + ghProject.getId()); + + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("DELETE"); + connection.setDoInput(true); + connection.setDoOutput(true); + connection.setUseCaches(false); + connection.setRequestProperty("Accept", "application/vnd.github.inertia-preview+json"); + connection.setRequestProperty("Authorization", "Basic " + authStringEnc); + + // forward (in case of) error + if (connection.getResponseCode() != 204) { + String message = getErrorMessage(connection); + throw new GitHubException(message); + } + } catch (MalformedURLException e) { + e.printStackTrace(); + throw new GitHubException(e.getMessage()); + } catch (IOException e) { + e.printStackTrace(); + throw new GitHubException(e.getMessage()); + } + } + + /** + * Creates a GitHub project in the GitHub organization given by the properties file. + * @param systemName Name of the system for which the GitHub project should be created. + * @param projectName Name of the GitHub project. + * @return The newly created GitHubProject object. + * @throws GitHubException If something with creating the new project went wrong. + */ + private GitHubProject createGitHubProject(String systemName, String projectName) throws GitHubException { + String body = getGitHubProjectBody(projectName); + String authStringEnc = getAuthStringEnc(systemName); + + String gitHubOrganization = this.systemsConfig.getGitHubOrganizationBySystem(systemName); + + URL url; + try { + url = new URL(API_BASE_URL + "/orgs/" + gitHubOrganization + "/projects"); + + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setDoInput(true); + connection.setDoOutput(true); + connection.setUseCaches(false); + connection.setRequestProperty("Accept", "application/vnd.github.inertia-preview+json"); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("Content-Length", String.valueOf(body.length())); + connection.setRequestProperty("Authorization", "Basic " + authStringEnc); + + writeRequestBody(connection, body); + + // forward (in case of) error + if (connection.getResponseCode() != 201) { + String message = getErrorMessage(connection); + throw new GitHubException(message); + } else { + // get response + String response = getResponseBody(connection); + + // convert to JSONObject + JSONObject json = (JSONObject) JSONValue.parseWithException(response); + int gitHubProjectId = ((Long) json.get("id")).intValue(); + String gitHubProjectHtmlUrl = (String) json.get("html_url"); + return new GitHubProject(gitHubProjectId, gitHubProjectHtmlUrl); + } + } catch (MalformedURLException e) { + e.printStackTrace(); + throw new GitHubException(e.getMessage()); + } catch (IOException e) { + e.printStackTrace(); + throw new GitHubException(e.getMessage()); + } catch (ParseException e) { + e.printStackTrace(); + throw new GitHubException(e.getMessage()); + } + } + + /** + * Changes the visibility of the GitHub project with the given id to "private: false". + * After calling this method, the GitHub project should be public and can be accessed by + * every GitHub user (accessed only means read-access). + * @param systemName Name of the system, for which the GitHub project should be updated. + * @param gitHubProjectId Id of the GitHub project id, whose visibility should be updated. + * @throws GitHubException If something with the request to the GitHub API went wrong. + */ + private void makeGitHubProjectPublic(String systemName, int gitHubProjectId) throws GitHubException { + String body = getVisibilityPublicBody(); + String authStringEnc = getAuthStringEnc(systemName); + + String url = API_BASE_URL + "/projects/" + gitHubProjectId; + + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .method("PATCH", BodyPublishers.ofString(body)) + .header("Content-Type", "application/json") + .header("Accept", "application/vnd.github.inertia-preview+json") + .header("Authorization", "Basic " + authStringEnc) + .build(); + + HttpResponse response; + try { + response = client.send(request, BodyHandlers.ofString()); + if(response.statusCode() != 200) { + String message = response.body(); + throw new GitHubException(message); + } + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + throw new GitHubException(e.getMessage()); + } + } + + /** + * Creates a new column with the given name in the GitHub project with the given id. + * @param systemName Name of the system, for which the column should be created. + * @param gitHubProjectId Id of the GitHub project, where the column should be added to. + * @param columnName Name of the column, which should be created. + * @throws GitHubException If something with the request to the GitHub API went wrong. + */ + private void createProjectColumn(String systemName, int gitHubProjectId, String columnName) throws GitHubException { + String body = getCreateColumnBody(columnName); + String authStringEnc = getAuthStringEnc(systemName); + + URL url; + try { + url = new URL(API_BASE_URL + "/projects/" + gitHubProjectId + "/columns"); + + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setDoInput(true); + connection.setDoOutput(true); + connection.setUseCaches(false); + connection.setRequestProperty("Accept", "application/vnd.github.inertia-preview+json"); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("Content-Length", String.valueOf(body.length())); + connection.setRequestProperty("Authorization", "Basic " + authStringEnc); + + writeRequestBody(connection, body); + + // forward (in case of) error + if (connection.getResponseCode() != 201) { + String message = getErrorMessage(connection); + throw new GitHubException(message); + } + } catch (MalformedURLException e) { + e.printStackTrace(); + throw new GitHubException(e.getMessage()); + } catch (IOException e) { + e.printStackTrace(); + throw new GitHubException(e.getMessage()); + } + } + + /** + * Creates the body needed to create a new column in a GitHub project. + * @param columnName Name of the column that should be created. + * @return Body as string. + */ + private String getCreateColumnBody(String columnName) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("name", columnName); + String body = JSONObject.toJSONString(jsonObject); + return body; + } + + /** + * Creates the body needed to update the visibility of the GitHub project. + * @return Body as String. + */ + private String getVisibilityPublicBody() { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("private", false); + String body = JSONObject.toJSONString(jsonObject); + return body; + } + + /** + * Creates the body needed for creating a new GitHub project. + * @param projectName Name of the project that should be created on GitHub. + * @return Body containing the information about the GitHub project which will be created. + */ + private String getGitHubProjectBody(String projectName) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("name", projectName); + jsonObject.put("body", "This GitHub project was auto-generated by las2peer."); + String body = JSONObject.toJSONString(jsonObject); + return body; + } + + /** + * Getter for encoded auth string. + * @param systemName Name of the system for which the request should be sent (relevant to choose correct PAT). + * @return Encoded auth string containing GitHub personal access token. + */ + private String getAuthStringEnc(String systemName) { + String authString = this.systemsConfig.getGitHubPATBySystem(systemName); + + byte[] authEncBytes = Base64.getEncoder().encode(authString.getBytes()); + return new String(authEncBytes); + } + + /** + * Extracts the error message from the response. + * @param connection HttpURLConnection object + * @return Error message as String. + * @throws IOException + */ + private String getErrorMessage(HttpURLConnection connection) throws IOException { + String message = "Error creating GitHub project at: "; + BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getErrorStream())); + for (String line; (line = reader.readLine()) != null;) { + message += line; + } + reader.close(); + return message; + } + + /** + * Getter for the body of the response. + * @param connection HttpURLConnection object + * @return Body of the response as string. + * @throws IOException + */ + private String getResponseBody(HttpURLConnection connection) throws IOException { + String response = ""; + BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + for (String line; (line = reader.readLine()) != null;) { + response += line; + } + reader.close(); + return response; + } + + /** + * Writes the request body. + * @param connection HttpURLConnection object + * @param body Body that should be written to the request. + * @throws IOException + */ + private void writeRequestBody(HttpURLConnection connection, String body) throws IOException { + OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream()); + writer.write(body); + writer.flush(); + writer.close(); + } + +} diff --git a/project_service/src/main/java/i5/las2peer/services/projectService/util/github/GitHubProject.java b/project_service/src/main/java/i5/las2peer/services/projectService/util/github/GitHubProject.java new file mode 100644 index 0000000..37d63c2 --- /dev/null +++ b/project_service/src/main/java/i5/las2peer/services/projectService/util/github/GitHubProject.java @@ -0,0 +1,45 @@ +package i5.las2peer.services.projectService.util.github; + +import java.io.Serializable; + +import org.json.simple.JSONObject; + +/** + * Contains the information about a GitHub project which is connected + * to a las2peer project. + * @author Philipp + * + */ +public class GitHubProject implements Serializable { + + /** + * The id of the GitHub project. + */ + private int id; + + /** + * The url to the GitHub project. + */ + private String url; + + public GitHubProject(int id, String url) { + this.id = id; + this.url = url; + } + + public int getId() { + return this.id; + } + + public String getUrl() { + return this.url; + } + + @SuppressWarnings("unchecked") + public JSONObject toJSONObject() { + JSONObject json = new JSONObject(); + json.put("id", this.id); + json.put("url", this.url); + return json; + } +} \ No newline at end of file diff --git a/project_service/src/test/java/i5/las2peer/services/projectService/RMITestService.java b/project_service/src/test/java/i5/las2peer/services/projectService/RMITestService.java new file mode 100644 index 0000000..d4b8388 --- /dev/null +++ b/project_service/src/test/java/i5/las2peer/services/projectService/RMITestService.java @@ -0,0 +1,119 @@ +package i5.las2peer.services.projectService; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.core.Response; + +import org.json.simple.JSONObject; + +import i5.las2peer.api.Context; +import i5.las2peer.api.execution.InternalServiceException; +import i5.las2peer.api.execution.ServiceAccessDeniedException; +import i5.las2peer.api.execution.ServiceInvocationFailedException; +import i5.las2peer.api.execution.ServiceMethodNotFoundException; +import i5.las2peer.api.execution.ServiceNotAuthorizedException; +import i5.las2peer.api.execution.ServiceNotAvailableException; +import i5.las2peer.api.execution.ServiceNotFoundException; +import i5.las2peer.restMapper.RESTService; +import i5.las2peer.restMapper.annotations.ServicePath; + +import java.net.HttpURLConnection; + +/** + * The RMI test service is a RESTService used to test the methods that the project service provides via RMI. + * Therefore, the RMI test service provides a RESTful interface that can be used by an agent during the tests. + * The different methods of this RESTful service will then invoke the methods that the project service provides for RMI. + * + * Besides that, the RMI test service provides the methods called by the EventManager of the project service. + * When testing the project service and setting the event listener service to the RMI test service, these + * methods will be called by the project service, when specific events occur. The RMI test service therefore also + * provides RESTful methods that can be used to check whether the event methods got called correctly by the project service. + * @author Philipp + * + */ +@ServicePath("/rmitestservice") +public class RMITestService extends RESTService { + + /** + * If the _onProjectCreated method got called, the data received will be stored in this variable. + */ + private JSONObject _onProjectCreatedData = null; + + /** + * If the _onProjectDeleted method got called, the data received will be stored in this variable. + */ + private JSONObject _onProjectDeletedData = null; + + @Override + protected void initResources() { + getResourceConfig().register(this); + } + + /** + * Method that is used to test the hasAccessToProject method provided by the project service for RMI. + * It uses the current agent to invoke the hasAccessToProject method and uses the given project name. + * @param projectName Name of the project, where access of the used agent should be checked for. + * @return Response with status code 200 and content containing the result of the invoked method, or 500 on error. + */ + @GET + @Path("/checkProjectAccess/{projectName}") + public Response checkProjectAccess(@PathParam("projectName") String projectName) { + boolean access; + String serviceMethod = "hasAccessToProject"; + try { + access = (boolean) Context.getCurrent().invoke("i5.las2peer.services.projectService.ProjectService@1.0.0", + serviceMethod, ServiceTest.system1, projectName); + } catch (ServiceNotFoundException | ServiceNotAvailableException e) { + return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR).entity("Service not found or not available.").build(); + } catch (ServiceMethodNotFoundException e) { + return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR).entity("Service has no method named " + serviceMethod + ".").build(); + } catch (InternalServiceException | ServiceInvocationFailedException | ServiceAccessDeniedException | ServiceNotAuthorizedException e) { + return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR).build(); + } + return Response.status(200).entity(access).build(); + } + + /** + * This is one of the methods, that the EventManager of the project service can call. + * It should be called whenever a project got created. + * @param projectJSON JSONObject containing the project that got created. + */ + public void _onProjectCreated(JSONObject projectJSON) { + this._onProjectCreatedData = projectJSON; + } + + /** + * This is one of the methods, that the EventManager of the project service can call. + * It should be called whenever a project got deleted. + * @param projectJSON JSONObject containing the project that got deleted. + */ + public void _onProjectDeleted(JSONObject projectJSON) { + this._onProjectDeletedData = projectJSON; + } + + /** + * This method may be used to verify, if the _onProjectCreated method got called correctly by the + * project service. + * @return 500 if _onProjectCreated was not called yet. 200 if it was already called. + */ + @GET + @Path("/onProjectCreated") + public Response onProjectCreated() { + if(this._onProjectCreatedData == null) return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR).build(); + return Response.status(200).entity(this._onProjectCreatedData.toJSONString()).build(); + } + + /** + * This method may be used to verify, if the _onProjectDeleted method got called correctly by the + * project service. + * @return 500 if _onProjectDeleted was not called yet. 200 if it was already called. + */ + @GET + @Path("/onProjectDeleted") + public Response onProjectDeleted() { + if(this._onProjectDeletedData == null) return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR).build(); + return Response.status(200).entity(this._onProjectDeletedData.toJSONString()).build(); + } + +} diff --git a/project_service/src/test/java/i5/las2peer/services/projectService/ServiceTest.java b/project_service/src/test/java/i5/las2peer/services/projectService/ServiceTest.java new file mode 100644 index 0000000..b33e11c --- /dev/null +++ b/project_service/src/test/java/i5/las2peer/services/projectService/ServiceTest.java @@ -0,0 +1,557 @@ +package i5.las2peer.services.projectService; + +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.PrintStream; +import java.net.HttpURLConnection; +import java.util.Properties; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import i5.las2peer.api.p2p.ServiceNameVersion; +import i5.las2peer.api.security.Agent; +import i5.las2peer.connectors.webConnector.WebConnector; +import i5.las2peer.connectors.webConnector.client.ClientResponse; +import i5.las2peer.connectors.webConnector.client.MiniClient; +import i5.las2peer.p2p.LocalNode; +import i5.las2peer.p2p.LocalNodeManager; +import i5.las2peer.security.GroupAgentImpl; +import i5.las2peer.security.ServiceAgentImpl; +import i5.las2peer.security.UserAgentImpl; +import i5.las2peer.testing.MockAgentFactory; + +/** + * Test Class for las2peer-project-service. + */ +public class ServiceTest { + + private static LocalNode node; + private static WebConnector connector; + private static ByteArrayOutputStream logStream; + + private static UserAgentImpl testAgentAdam; + private static final String testPassAdam = "adamspass"; + private static UserAgentImpl testAgentEve; + private static final String testPassEve = "evespass"; + + // important: the system names need to match the systems that are configured + // in the .properties file used for the ServiceTest + public static final String system1 = "test"; + public static final String system2 = "othersystem"; + private static final String mainPath = "projects/" + system1 + "/"; + private static final String mainPath2 = "projects/" + system2 + "/"; + + private String identifierGroupA; + private static final String nameGroupA = "groupA"; + private String identifierGroup1; + private static final String nameGroup1 = "group1"; + + // the used .properties file can be found in project_service/properties folder + private static final String projectServicePropertiesPath = "properties/i5.las2peer.services.projectService.ProjectService.properties"; + + private final ServiceNameVersion rmiTestServiceName = new ServiceNameVersion(RMITestService.class.getName(), "1.0.0"); + + /** + * Called before a test starts. + * + * Sets up the node, initializes connector and adds user agent that can be used + * throughout the test. + * + * @throws Exception + */ + @Before + public void startServer() throws Exception { + // start node + node = new LocalNodeManager().newNode(); + node.launch(); + + // MockAgentFactory provides 3 user agents: abel, adam, eve + // MockAgentFactory provides 4 groups: + // - Group1, Group2, Group3: all user agents are members + // - GroupA: eve is no member, abel and adam are members + + // add user agents to node (currently only adam and eve are used for testing) + testAgentAdam = MockAgentFactory.getAdam(); + testAgentAdam.unlock(testPassAdam); // agents must be unlocked in order to be stored + node.storeAgent(testAgentAdam); + + testAgentEve = MockAgentFactory.getEve(); + testAgentEve.unlock(testPassEve); + node.storeAgent(testAgentEve); + + // we only use abel as an admin for the service group which is used by the project service for storing envelopes + UserAgentImpl serviceGroupAdmin = MockAgentFactory.getAbel(); + serviceGroupAdmin.unlock("abelspass"); + node.storeAgent(serviceGroupAdmin); + + // add group agent to node + // use group A where adam is a member, but eve not + GroupAgentImpl groupA = MockAgentFactory.getGroupA(); + this.identifierGroupA = groupA.getIdentifier(); + groupA.unlock(testAgentAdam); + node.storeAgent(groupA); + + // use group 1 where adam and eve are member + GroupAgentImpl group1 = MockAgentFactory.getGroup1(); + this.identifierGroup1 = group1.getIdentifier(); + group1.unlock(testAgentAdam); + node.storeAgent(group1); + + // create group agent and add abel to this group agent + // we will later add the project service to this group and the project service will use this + // group agent for storing of envelopes + Agent[] members = new Agent[1]; + members[0] = serviceGroupAdmin; + GroupAgentImpl serviceGroup = GroupAgentImpl.createGroupAgent(members); + serviceGroup.unlock(serviceGroupAdmin); + node.storeAgent(serviceGroup); + + // now that we know the identifier of the group, we can set it in the properties file of the project service + // as the serviceGroupId + Properties props = new Properties(); + props.load(new FileInputStream(projectServicePropertiesPath)); + props.setProperty("serviceGroupId", serviceGroup.getIdentifier()); + props.store(new FileOutputStream(projectServicePropertiesPath), null); + + // start project service (which will automatically use the properties file) + // during testing, the specified service version does not matter + ServiceAgentImpl projectService = node.startService(new ServiceNameVersion(ProjectService.class.getName(), "1.0.0"), "a pass"); + // add the service agent to the service group + serviceGroup.addMember(projectService); + node.storeAgent(serviceGroup); + + + // also start RMI test service + node.startService(rmiTestServiceName, "a pass"); + + // start connector + connector = new WebConnector(true, 0, false, 0); // port 0 means use system defined port + logStream = new ByteArrayOutputStream(); + connector.setLogStream(new PrintStream(logStream)); + connector.start(node); + } + + /** + * Called after the test has finished. Shuts down the server and prints out the + * connector log file for reference. + * + * @throws Exception + */ + @After + public void shutDownServer() throws Exception { + if (connector != null) { + connector.stop(); + connector = null; + } + if (node != null) { + node.shutDown(); + node = null; + } + if (logStream != null) { + System.out.println("Connector-Log:"); + System.out.println("--------------"); + System.out.println(logStream.toString()); + logStream = null; + } + } + + /** + * Tests the method for fetching projects. + */ + @Test + public void testGetProjects() { + try { + MiniClient client = new MiniClient(); + client.setConnectorEndpoint(connector.getHttpEndpoint()); + + // first try without agent (this should not be possible) + ClientResponse result = client.sendRequest("GET", mainPath, ""); + Assert.assertEquals(HttpURLConnection.HTTP_UNAUTHORIZED, result.getHttpCode()); + + // now use an agent + client.setLogin(testAgentAdam.getIdentifier(), testPassAdam); + result = client.sendRequest("GET", mainPath, ""); + // we should get 200 and an empty list + Assert.assertEquals(HttpURLConnection.HTTP_OK, result.getHttpCode()); + Assert.assertEquals("{\"projects\":[]}", result.getResponse().trim()); + + // now add a project using adam and group A + result = client.sendRequest("POST", mainPath, + this.getProjectJSON("Project1_testGetProjects", this.nameGroupA, this.identifierGroupA)); + Assert.assertEquals(HttpURLConnection.HTTP_CREATED, result.getHttpCode()); + // get projects again + result = client.sendRequest("GET", mainPath, ""); + Assert.assertEquals(HttpURLConnection.HTTP_OK, result.getHttpCode()); + JSONObject resultJSON = (JSONObject) JSONValue.parse(result.getResponse().trim()); + JSONArray projectsJSON = (JSONArray) resultJSON.get("projects"); + int projectCountSystem1 = projectsJSON.size(); + Assert.assertEquals(1, projectCountSystem1); + + // now we are testing adding a project to the second system + // we use the same name, as this should not be a problem in a different system + result = client.sendRequest("POST", mainPath2, + this.getProjectJSON("Project1_testGetProjects", this.nameGroupA, this.identifierGroupA)); + Assert.assertEquals(HttpURLConnection.HTTP_CREATED, result.getHttpCode()); + // now we also test it with a different name + result = client.sendRequest("POST", mainPath2, + this.getProjectJSON("Project1_testGetProjects2", this.nameGroupA, this.identifierGroupA)); + Assert.assertEquals(HttpURLConnection.HTTP_CREATED, result.getHttpCode()); + + // now get projects of system1 again, to verify that no project was added there + result = client.sendRequest("GET", mainPath, ""); + Assert.assertEquals(HttpURLConnection.HTTP_OK, result.getHttpCode()); + resultJSON = (JSONObject) JSONValue.parse(result.getResponse().trim()); + projectsJSON = (JSONArray) resultJSON.get("projects"); + Assert.assertEquals(projectCountSystem1, projectsJSON.size()); + + // now check if projects in system2 exists + // here we also test the method getProjectByName + result = client.sendRequest("GET", "projects/" + system2 + "/Project1_testGetProjects", ""); + Assert.assertEquals(HttpURLConnection.HTTP_OK, result.getHttpCode()); + result = client.sendRequest("GET", "projects/" + system2 + "/Project1_testGetProjects2", ""); + Assert.assertEquals(HttpURLConnection.HTTP_OK, result.getHttpCode()); + + // now test getProjectByName with a non-existing project + result = client.sendRequest("GET", "projects/" + system2 + "/does_not_exist", ""); + Assert.assertEquals(HttpURLConnection.HTTP_NOT_FOUND, result.getHttpCode()); + + // now test getProjectByName method with an agent who is no member of groupA + client.setLogin(testAgentEve.getIdentifier(), testPassEve); + result = client.sendRequest("GET", "projects/" + system2 + "/Project1_testGetProjects", ""); + Assert.assertEquals(HttpURLConnection.HTTP_FORBIDDEN, result.getHttpCode()); + + // test with non-existing system (both GET methods, the one for all projects and the one for a single project) + result = client.sendRequest("GET", "projects/doesnotexist", ""); + Assert.assertEquals(HttpURLConnection.HTTP_BAD_REQUEST, result.getHttpCode()); + result = client.sendRequest("GET", "projects/doesnotexist/Project", ""); + Assert.assertEquals(HttpURLConnection.HTTP_BAD_REQUEST, result.getHttpCode()); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail(e.toString()); + } + } + + @Test + public void testPostProject() { + try { + MiniClient client = new MiniClient(); + client.setConnectorEndpoint(connector.getHttpEndpoint()); + + // first try without agent (this should not be possible) + ClientResponse result = client.sendRequest("POST", mainPath, ""); + Assert.assertEquals(HttpURLConnection.HTTP_UNAUTHORIZED, result.getHttpCode()); + + // now use an agent + client.setLogin(testAgentAdam.getIdentifier(), testPassAdam); + result = client.sendRequest("POST", mainPath, ""); + // bad request because of no body is sent + Assert.assertEquals(HttpURLConnection.HTTP_BAD_REQUEST, result.getHttpCode()); + + // test with a group that does not exist + result = client.sendRequest("POST", mainPath, + this.getProjectJSON("Project1_testPostProject", "groupName", "doesNotExist")); + Assert.assertEquals(HttpURLConnection.HTTP_BAD_REQUEST, result.getHttpCode()); + + // test with an existing group and user but the user is no group member + // in this case we use groupA and eve + client.setLogin(testAgentEve.getIdentifier(), testPassEve); + result = client.sendRequest("POST", mainPath, + this.getProjectJSON("Project1_testPostProject", "groupName", this.identifierGroupA)); + Assert.assertEquals(HttpURLConnection.HTTP_BAD_REQUEST, result.getHttpCode()); + + // now test with an existing group and a group member + client.setLogin(testAgentAdam.getIdentifier(), testPassAdam); + result = client.sendRequest("POST", mainPath, + this.getProjectJSON("Project1_testPostProject", "groupName", this.identifierGroupA)); + Assert.assertEquals(HttpURLConnection.HTTP_CREATED, result.getHttpCode()); + + // test again with same project name (should not be allowed, because project with that name already exists) + result = client.sendRequest("POST", mainPath, + this.getProjectJSON("Project1_testPostProject", "groupName", this.identifierGroupA)); + Assert.assertEquals(HttpURLConnection.HTTP_CONFLICT, result.getHttpCode()); + + // test again with incorrect project json (only containing project name, other attributes missing) + result = client.sendRequest("POST", mainPath, "{\"projectName\": \"should-fail\"}"); + Assert.assertEquals(HttpURLConnection.HTTP_BAD_REQUEST, result.getHttpCode()); + + // check if RMITestService event _onProjectCreated got called + result = client.sendRequest("GET", "rmitestservice/onProjectCreated", ""); + Assert.assertEquals(HttpURLConnection.HTTP_OK, result.getHttpCode()); + + // test with metadata + JSONObject metadata = new JSONObject(); + metadata.put("attr1", "value1"); + metadata.put("attr2", "value2"); + result = client.sendRequest("POST", mainPath, this.getProjectJSON("Project2_testPostProject", "groupName", + this.identifierGroupA, metadata.toJSONString())); + Assert.assertEquals(HttpURLConnection.HTTP_CREATED, result.getHttpCode()); + + // test with non-existing system + result = client.sendRequest("POST", "projects/doesnotexist", ""); + Assert.assertEquals(HttpURLConnection.HTTP_BAD_REQUEST, result.getHttpCode()); + + // now stop the RMITestService once (and verify that it is stopped) + node.stopService(rmiTestServiceName); + Assert.assertEquals(false, node.hasService(rmiTestServiceName)); + + // create a project (while event listener service or in this case RMITestService is not running anymore) + // this should not be possible (because the event cannot be sent) + result = client.sendRequest("POST", mainPath, + this.getProjectJSON("rmi-test-service-unavailable", "groupName", this.identifierGroupA)); + Assert.assertEquals(HttpURLConnection.HTTP_INTERNAL_ERROR, result.getHttpCode()); + // if event listener is not available, the project should not be created => verify this + result = client.sendRequest("GET", "projects/" + system1 + "/rmi-test-service-unavailable", ""); + Assert.assertEquals(HttpURLConnection.HTTP_NOT_FOUND, result.getHttpCode()); + + // start RMITestService again (maybe it is needed by other tests) + node.startService(rmiTestServiceName, "a pass"); + Assert.assertEquals(true, node.hasService(rmiTestServiceName)); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail(e.toString()); + } + } + + @Test + public void testRMIMethodsHasAccessToProject() { + try { + MiniClient client = new MiniClient(); + client.setConnectorEndpoint(connector.getHttpEndpoint()); + + client.setLogin(testAgentAdam.getIdentifier(), testPassAdam); + String projectName = "project1_testRMIMethodHasAccessToProject"; + ClientResponse result = client.sendRequest("GET", "rmitestservice/checkProjectAccess/" + projectName, ""); + // there should not exist a project with the given name yet, so user cannot have + // access to it + Assert.assertEquals(HttpURLConnection.HTTP_OK, result.getHttpCode()); + Assert.assertEquals("false", result.getResponse().trim()); + + // create a project + result = client.sendRequest("POST", mainPath, + this.getProjectJSON(projectName, this.nameGroupA, this.identifierGroupA)); + Assert.assertEquals(HttpURLConnection.HTTP_CREATED, result.getHttpCode()); + + // now check if the agent has access to this existing project + // test with adam first, adam is a member of group A + result = client.sendRequest("GET", "rmitestservice/checkProjectAccess/" + projectName, ""); + Assert.assertEquals(HttpURLConnection.HTTP_OK, result.getHttpCode()); + Assert.assertEquals("true", result.getResponse().trim()); + // now test with eve, eve is no member of group A + client.setLogin(testAgentEve.getIdentifier(), testPassEve); + result = client.sendRequest("GET", "rmitestservice/checkProjectAccess/" + projectName, ""); + Assert.assertEquals(HttpURLConnection.HTTP_OK, result.getHttpCode()); + Assert.assertEquals("false", result.getResponse().trim()); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail(e.toString()); + } + } + + @Test + public void testChangeGroup() { + try { + MiniClient client = new MiniClient(); + client.setConnectorEndpoint(connector.getHttpEndpoint()); + + client.setLogin(testAgentAdam.getIdentifier(), testPassAdam); + // create project using adam and group A + String projectName = "Project1_testGetChangeGroup"; + ClientResponse result = client.sendRequest("POST", mainPath, + this.getProjectJSON(projectName, this.nameGroupA, this.identifierGroupA)); + Assert.assertEquals(HttpURLConnection.HTTP_CREATED, result.getHttpCode()); + + JSONObject o = new JSONObject(); + o.put("projectName", projectName); + o.put("newGroupId", this.identifierGroup1); + o.put("newGroupName", this.nameGroup1); + + // try chaning group but use incorrect system name + result = client.sendRequest("POST", "projects/systemdoesnotexist/changeGroup", o.toJSONString()); + Assert.assertEquals(HttpURLConnection.HTTP_BAD_REQUEST, result.getHttpCode()); + + // try changing group without being a logged in user + client = new MiniClient(); + client.setConnectorEndpoint(connector.getHttpEndpoint()); + result = client.sendRequest("POST", mainPath + "changeGroup", o.toJSONString()); + Assert.assertEquals(HttpURLConnection.HTTP_UNAUTHORIZED, result.getHttpCode()); + + // try changing group using eve (who is no project member) + client.setLogin(testAgentEve.getIdentifier(), testPassEve); + result = client.sendRequest("POST", mainPath + "changeGroup", o.toJSONString()); + Assert.assertEquals(HttpURLConnection.HTTP_FORBIDDEN, result.getHttpCode()); + + // now change group using adam (who is a project member) + client.setLogin(testAgentAdam.getIdentifier(), testPassAdam); + result = client.sendRequest("POST", mainPath + "changeGroup", o.toJSONString()); + Assert.assertEquals(HttpURLConnection.HTTP_OK, result.getHttpCode()); + + // now test with no valid json + result = client.sendRequest("POST", mainPath + "changeGroup", "{fail"); + Assert.assertEquals(HttpURLConnection.HTTP_BAD_REQUEST, result.getHttpCode()); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail(e.toString()); + } + } + + @Test + public void testChangeMetadata() { + try { + MiniClient client = new MiniClient(); + client.setConnectorEndpoint(connector.getHttpEndpoint()); + + // try with invalid system name + ClientResponse result = client.sendRequest("POST", "projects/systemdoesnotexist/changeMetadata", ""); + Assert.assertEquals(HttpURLConnection.HTTP_BAD_REQUEST, result.getHttpCode()); + + // try to change metadata without being logged in + result = client.sendRequest("POST", mainPath + "changeMetadata", ""); + Assert.assertEquals(HttpURLConnection.HTTP_UNAUTHORIZED, result.getHttpCode()); + + client.setLogin(testAgentAdam.getIdentifier(), testPassAdam); + + // try to change metadata but let body empty + result = client.sendRequest("POST", mainPath + "changeMetadata", ""); + Assert.assertEquals(HttpURLConnection.HTTP_BAD_REQUEST, result.getHttpCode()); + + // create a new project for testing + String projectName = "testChangeMetadata_Project1"; + JSONObject metadata = new JSONObject(); + metadata.put("attr1", "value1"); + result = client.sendRequest("POST", mainPath, this.getProjectJSON(projectName, "groupName", + this.identifierGroupA, metadata.toJSONString())); + Assert.assertEquals(HttpURLConnection.HTTP_CREATED, result.getHttpCode()); + + // change metadata + JSONObject body = new JSONObject(); + body.put("projectName", projectName); + body.put("oldMetadata", metadata); + JSONObject metadataUpdated = new JSONObject(); + metadataUpdated.put("attr1", "value2"); + body.put("newMetadata", metadataUpdated); + result = client.sendRequest("POST", mainPath + "changeMetadata", body.toJSONString()); + Assert.assertEquals(HttpURLConnection.HTTP_OK, result.getHttpCode()); + + // get the project and check if metadata got really updated + result = client.sendRequest("GET", mainPath, ""); + Assert.assertEquals(HttpURLConnection.HTTP_OK, result.getHttpCode()); + JSONObject resultJSON = (JSONObject) JSONValue.parse(result.getResponse().trim()); + JSONArray projectsJSON = (JSONArray) resultJSON.get("projects"); + boolean containsProject = false; + for(Object p : projectsJSON) { + JSONObject projectJSON = (JSONObject) p; + if(((String)projectJSON.get("name")).equals(projectName)) { + String metadataString = ((JSONObject) projectJSON.get("metadata")).toJSONString(); + Assert.assertEquals(metadataUpdated.toJSONString(), metadataString); + containsProject = true; + break; + } + } + Assert.assertEquals(true, containsProject); + + // try to change metadata with a non-member of the project + client.setLogin(testAgentEve.getIdentifier(), testPassEve); + result = client.sendRequest("POST", mainPath + "changeMetadata", body.toJSONString()); + Assert.assertEquals(HttpURLConnection.HTTP_FORBIDDEN, result.getHttpCode()); + + // test with incorrect oldMetadata value + client.setLogin(testAgentAdam.getIdentifier(), testPassAdam); + body.put("oldMetadata", ""); + result = client.sendRequest("POST", mainPath + "changeMetadata", body.toJSONString()); + Assert.assertEquals(HttpURLConnection.HTTP_BAD_REQUEST, result.getHttpCode()); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail(e.toString()); + } + } + + @Test + public void testDeleteProject() { + try { + MiniClient client = new MiniClient(); + client.setConnectorEndpoint(connector.getHttpEndpoint()); + + client.setLogin(testAgentAdam.getIdentifier(), testPassAdam); + + // try with non-existing system name + ClientResponse result = client.sendRequest("DELETE", "projects/doesnotexist/not-existing-project", ""); + Assert.assertEquals(HttpURLConnection.HTTP_BAD_REQUEST, result.getHttpCode()); + + // try to delete a non-existing project + result = client.sendRequest("DELETE", mainPath + "not-existing-project", ""); + Assert.assertEquals(HttpURLConnection.HTTP_NOT_FOUND, result.getHttpCode()); + + // create project using adam and group A + String projectName = "Project1_testDeleteProject"; + result = client.sendRequest("POST", mainPath, + this.getProjectJSON(projectName, this.nameGroupA, this.identifierGroupA)); + Assert.assertEquals(HttpURLConnection.HTTP_CREATED, result.getHttpCode()); + + // count number of projects that adam has access to + result = client.sendRequest("GET", mainPath, ""); + JSONArray jsonProjects = (JSONArray) ((JSONObject) JSONValue.parse(result.getResponse().trim())).get("projects"); + int numProjects = jsonProjects.size(); + + // now try to delete this project using a non-member (e.g. eve is no member of group A) + // in this case, eve should not be allowed to delete the project + client.setLogin(testAgentEve.getIdentifier(), testPassEve); + result = client.sendRequest("DELETE", mainPath + projectName, ""); + Assert.assertEquals(HttpURLConnection.HTTP_FORBIDDEN, result.getHttpCode()); + + // now try to delete it again but now try with adam + client.setLogin(testAgentAdam.getIdentifier(), testPassAdam); + result = client.sendRequest("DELETE", mainPath + projectName, ""); + Assert.assertEquals(HttpURLConnection.HTTP_OK, result.getHttpCode()); + + // check if RMITestService event _onProjectCreated got called + result = client.sendRequest("GET", "rmitestservice/onProjectDeleted", ""); + Assert.assertEquals(HttpURLConnection.HTTP_OK, result.getHttpCode()); + + // count number of projects that adam has access to again + // should be one less than before + result = client.sendRequest("GET", mainPath, ""); + JSONArray jsonProjects2 = (JSONArray) ((JSONObject) JSONValue.parse(result.getResponse().trim())).get("projects"); + int numProjects2 = jsonProjects2.size(); + Assert.assertEquals(numProjects-1, numProjects2); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail(e.toString()); + } + } + + /** + * Helper method to get a JSON string representation of a project. + * + * @param projectName Name of the project. + * @param linkedGroupName Name of the group which gets linked to the project. + * @param linkedGroupId Id of the group which gets linked to the project. + * @param metadata JSON String representation of project metadata. + * @return JSON representation of project as string. + */ + private static final String getProjectJSON(String projectName, String linkedGroupName, String linkedGroupId, + String metadata) { + return "{\"name\": \"" + projectName + "\", \"linkedGroup\": { \"name\": \"" + linkedGroupName + + "\", \"id\": \"" + linkedGroupId + "\"}, \"users\": [], \"metadata\": " + metadata + "}"; + } + + /** + * Helper method to get a JSON string representation of a project. + * + * @param projectName Name of the project. + * @param linkedGroupName Name of the group which gets linked to the project. + * @param linkedGroupId Id of the group which gets linked to the project. + * @return JSON representation of project as string. Does not use any project + * metadata. + */ + private static final String getProjectJSON(String projectName, String linkedGroupName, String linkedGroupId) { + return "{\"name\": \"" + projectName + "\", \"linkedGroup\": { \"name\": \"" + linkedGroupName + + "\", \"id\": \"" + linkedGroupId + "\"}, \"users\": []}"; + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..72b347a --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'las2peer-project-service' +include('project_service')