diff --git a/.circleci/config.yml b/.circleci/config.yml
index a2d5e8651..46d2d2bfd 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,202 +1,205 @@
-
-version: 2.1
-
-executors:
- docker-image-golang:
- docker:
- - image: golang:1.11
- docker-publisher:
- environment:
- CONTAINER_IMAGE_NAME: proxeus/proxeus-core
- docker:
- - image: circleci/buildpack-deps:stretch
-
-jobs:
- validate:
- executor: docker-image-golang
- steps:
- - checkout
- - restore_cache:
- keys:
- - go-vendor-v1-{{ .Branch }}
- - run:
- command: make link-repo
- - run:
- command: curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
- - run:
- command: go get golang.org/x/tools/cmd/goimports
- - run:
- command: |
- cd /go/src/git.proxeus.com/core/central
- dep ensure -v
- - run:
- command: chmod -R +x ./build
- - run:
- command: |
- cd /go/src/git.proxeus.com/core/central
- make fmt
- - run:
- command: |
- cd /go/src/git.proxeus.com/core/central
- make validate
- - save_cache:
- key: go-vendor-v1-{{ .Branch }}
- paths:
- - vendor/
-
- test:
- executor: docker-image-golang
- steps:
- - checkout
- - restore_cache:
- keys:
- - go-vendor-v1-{{ .Branch }}
- - run:
- command: chmod -R +x ./build
- - run:
- command: make link-repo
- - run:
- command: |
- cd /go/src/git.proxeus.com/core/central
- make test
- - save_cache:
- key: go-vendor-v1-{{ .Branch }}
- paths:
- - vendor/
-
- build-ui:
- docker:
- - image: node:10
- steps:
- - checkout
- - restore_cache:
- keys:
- - yarn-v1-{{ .Branch }}
- - run:
- command: chmod -R +x ./build
- - run:
- command: make ui
- - save_cache:
- key: yarn-v1-{{ .Branch }}
- paths:
- - ui/.yarn/
- - ui/node_modules/
- - ui/core/node_modules/
- - ui/wallet/node_modules/
- - persist_to_workspace:
- root: .
- paths:
- - ./artifacts/dist
-
- build-go:
- executor: docker-image-golang
- steps:
- - checkout
- - attach_workspace:
- at: ~/project/
- - restore_cache:
- keys:
- - go-vendor-v1-{{ .Branch }}
- - run:
- command: chmod -R +x ./build
- - run:
- name:
- command: make link-repo
- - run:
- name:
- command: |
- cd /go/src/git.proxeus.com/core/central
- make server-docker
- - save_cache:
- key: go-vendor-v1-{{ .Branch }}
- paths:
- - vendor/
- - persist_to_workspace:
- root: .
- paths:
- - ./artifacts/server-docker
-
- build-docker:
- executor: docker-publisher
- steps:
- - checkout
- - attach_workspace:
- at: ~/project/
- - setup_remote_docker
- - run:
- name: Build Docker image
- command: |
- docker build -f Dockerfile -t $CONTAINER_IMAGE_NAME:latest -t $CONTAINER_IMAGE_NAME .
- - run:
- name: Archive Docker image
- command: docker save -o image.tar $CONTAINER_IMAGE_NAME
- - persist_to_workspace:
- root: .
- paths:
- - image.tar
-
- publish-latest:
- executor: docker-publisher
- steps:
- - attach_workspace:
- at: /tmp/workspace
- - setup_remote_docker
- - run:
- name: Load archived Docker image
- command: docker load -i /tmp/workspace/image.tar
- - run:
- name: Publish Docker Image to Docker Hub
- command: |
- echo "$DOCKERHUB_PASS" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin
- docker push $CONTAINER_IMAGE_NAME
-
- publish-demo:
- executor: docker-publisher
- steps:
- - attach_workspace:
- at: /tmp/workspace
- - setup_remote_docker
- - run:
- name: Load archived Docker image
- command: docker load -i /tmp/workspace/image.tar
- - run:
- name: Tag docker image
- command: docker tag $CONTAINER_IMAGE_NAME $CONTAINER_IMAGE_NAME:demo
- - run:
- name: Publish Docker Image to Docker Hub
- command: |
- echo "$DOCKERHUB_PASS" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin
- docker push $CONTAINER_IMAGE_NAME:demo
-
-
-workflows:
- version: 2
- build-master:
- jobs:
- - validate
- - test:
- requires:
- - validate
- - build-ui:
- requires:
- - test
- - build-go:
- requires:
- - build-ui
- - build-docker:
- requires:
- - build-ui
- - build-go
- - publish-latest:
- requires:
- - build-docker
- - hold:
- type: approval
- requires:
- - build-docker
- - publish-demo:
- requires:
- - hold
- filters:
- branches:
- only: master
+
+version: 2.1
+
+executors:
+ docker-image-golang:
+ environment:
+ PROJECT_ROOT_FOLDER: ~/project
+ docker:
+ - image: golang:1.11
+ docker-publisher:
+ environment:
+ PROJECT_ROOT_FOLDER: ~/project
+ CONTAINER_IMAGE_NAME: proxeus/proxeus-core
+ docker:
+ - image: circleci/buildpack-deps:stretch
+
+jobs:
+ validate:
+ executor: docker-image-golang
+ steps:
+ - checkout
+ - restore_cache:
+ keys:
+ - go-vendor-v1-{{ .Branch }}
+ - run:
+ command: make link-repo
+ - run:
+ command: curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
+ - run:
+ command: go get golang.org/x/tools/cmd/goimports
+ - run:
+ command: |
+ cd /go/src/git.proxeus.com/core/central
+ dep ensure -v
+ - run:
+ command: chmod -R +x ./build
+ - run:
+ command: |
+ cd /go/src/git.proxeus.com/core/central
+ make fmt
+ - run:
+ command: |
+ cd /go/src/git.proxeus.com/core/central
+ make validate
+ - save_cache:
+ key: go-vendor-v1-{{ .Branch }}
+ paths:
+ - vendor/
+
+ test:
+ executor: docker-image-golang
+ steps:
+ - checkout
+ - restore_cache:
+ keys:
+ - go-vendor-v1-{{ .Branch }}
+ - run:
+ command: chmod -R +x ./build
+ - run:
+ command: make link-repo
+ - run:
+ command: |
+ cd /go/src/git.proxeus.com/core/central
+ make test
+ - save_cache:
+ key: go-vendor-v1-{{ .Branch }}
+ paths:
+ - vendor/
+
+ build-ui:
+ docker:
+ - image: node:10
+ steps:
+ - checkout
+ - restore_cache:
+ keys:
+ - yarn-v1-{{ .Branch }}
+ - run:
+ command: chmod -R +x ./build
+ - run:
+ command: make ui
+ - save_cache:
+ key: yarn-v1-{{ .Branch }}
+ paths:
+ - ui/.yarn/
+ - ui/node_modules/
+ - ui/core/node_modules/
+ - ui/wallet/node_modules/
+ - persist_to_workspace:
+ root: .
+ paths:
+ - ./artifacts/dist
+
+ build-go:
+ executor: docker-image-golang
+ steps:
+ - checkout
+ - attach_workspace:
+ at: ~/project/
+ - restore_cache:
+ keys:
+ - go-vendor-v1-{{ .Branch }}
+ - run:
+ command: chmod -R +x ./build
+ - run:
+ name:
+ command: make link-repo
+ - run:
+ name:
+ command: |
+ cd /go/src/git.proxeus.com/core/central
+ make server-docker
+ - save_cache:
+ key: go-vendor-v1-{{ .Branch }}
+ paths:
+ - vendor/
+ - persist_to_workspace:
+ root: .
+ paths:
+ - ./artifacts/server-docker
+
+ build-docker:
+ executor: docker-publisher
+ steps:
+ - checkout
+ - attach_workspace:
+ at: ~/project/
+ - setup_remote_docker
+ - run:
+ name: Build Docker image
+ command: |
+ docker build -f Dockerfile -t $CONTAINER_IMAGE_NAME:latest -t $CONTAINER_IMAGE_NAME .
+ - run:
+ name: Archive Docker image
+ command: docker save -o image.tar $CONTAINER_IMAGE_NAME
+ - persist_to_workspace:
+ root: .
+ paths:
+ - image.tar
+
+ publish-latest:
+ executor: docker-publisher
+ steps:
+ - attach_workspace:
+ at: /tmp/workspace
+ - setup_remote_docker
+ - run:
+ name: Load archived Docker image
+ command: docker load -i /tmp/workspace/image.tar
+ - run:
+ name: Publish Docker Image to Docker Hub
+ command: |
+ echo "$DOCKERHUB_PASS" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin
+ docker push $CONTAINER_IMAGE_NAME
+
+ publish-demo:
+ executor: docker-publisher
+ steps:
+ - attach_workspace:
+ at: /tmp/workspace
+ - setup_remote_docker
+ - run:
+ name: Load archived Docker image
+ command: docker load -i /tmp/workspace/image.tar
+ - run:
+ name: Tag docker image
+ command: docker tag $CONTAINER_IMAGE_NAME $CONTAINER_IMAGE_NAME:demo
+ - run:
+ name: Publish Docker Image to Docker Hub
+ command: |
+ echo "$DOCKERHUB_PASS" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin
+ docker push $CONTAINER_IMAGE_NAME:demo
+
+
+workflows:
+ version: 2
+ build-master:
+ jobs:
+ - validate
+ - test:
+ requires:
+ - validate
+ - build-ui:
+ requires:
+ - test
+ - build-go:
+ requires:
+ - build-ui
+ - build-docker:
+ requires:
+ - build-ui
+ - build-go
+ - publish-latest:
+ requires:
+ - build-docker
+ - hold:
+ type: approval
+ requires:
+ - build-docker
+ - publish-demo:
+ requires:
+ - hold
+ filters:
+ branches:
+ only: master
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 000000000..b2766d843
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,175 @@
+variables:
+ DOCKER_HOST: tcp://localhost:2375
+ DOCKER_DRIVER: overlay2
+ CONTAINER_IMAGE_NAME: eu.gcr.io/blockfactory-01/proxeus-platform
+ PROJECT_ROOT_FOLDER: /builds/core/central
+
+stages:
+ - validate
+ - test
+ - build-ui
+ - build-go
+ - build-docker
+ - tag-docker
+
+validate:
+ image: golang:1.11
+ stage: validate
+ only:
+ - branches
+ before_script:
+ # install ssh-agent if not already installed
+ - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
+ # run ssh-agent
+ - eval $(ssh-agent -s)
+ # add the SSH key stored in SSH_DEPLOY_KEY_CORE_SYS
+ - ssh-add <(echo "$SSH_DEPLOY_KEY_CORE_SYS")
+ # for Docker builds disable host key checking
+ - mkdir -p ~/.ssh
+ - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
+ - make link-repo
+ - cd /go/src/git.proxeus.com/core/central
+ - curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
+ - go get golang.org/x/tools/cmd/goimports
+ - dep ensure -v
+ script:
+ - make validate
+ cache:
+ key: "go-vendor-$CI_COMMIT_REF_SLUG"
+ paths:
+ - vendor/
+
+# Removed dapp with PCO-1457 and ui isn't using it at the moment
+#validate-ui:
+# image: node
+# stage: validate
+# script:
+# - make validate-ui
+# cache:
+# key: "yarn-$CI_COMMIT_REF_SLUG"
+# paths:
+# - ui/.yarn/
+# - ui/node_modules/
+# - ui/core/node_modules/
+# - ui/wallet/node_modules/
+
+test:
+ image: golang:1.11
+ stage: test
+ only:
+ - branches
+ before_script:
+ - make link-repo
+ - cd /go/src/git.proxeus.com/core/central
+ script:
+ - make test
+ cache:
+ key: "go-vendor-$CI_COMMIT_REF_SLUG"
+ paths:
+ - vendor/
+
+#--------------------ui build part----------------------
+
+xes-platform-ui:
+ image: node:10
+ stage: build-ui
+ only:
+ - branches
+ script:
+ - make ui
+ artifacts:
+ paths:
+ - artifacts/dist
+ cache:
+ key: "yarn-$CI_COMMIT_REF_SLUG"
+ paths:
+ - ui/.yarn/
+ - ui/node_modules/
+ - ui/core/node_modules/
+ - ui/wallet/node_modules/
+
+#--------------------go build part----------------------
+
+xes-platform-go:
+ image: golang:1.11
+ stage: build-go
+ only:
+ - branches
+ before_script:
+ - make link-repo
+ - cd /go/src/git.proxeus.com/core/central
+ script:
+ - make server-docker
+ artifacts:
+ paths:
+ - artifacts/server-docker
+ cache:
+ key: "go-vendor-$CI_COMMIT_REF_SLUG"
+ paths:
+ - vendor/
+
+#------------------docker build part--------------------
+
+xes-platform-docker:
+ stage: build-docker
+ image: docker:latest
+ tags:
+ - dind
+ only:
+ - master
+ dependencies:
+ - xes-platform-go
+ services:
+ - docker:18.09.8-dind
+ before_script:
+ - docker login -u _json_key -p "$GCLOUD_SERVICE_KEY" https://eu.gcr.io
+ - docker pull $CONTAINER_IMAGE_NAME:latest || true
+ script:
+ - docker build --cache-from $CONTAINER_IMAGE_NAME:latest -t $CONTAINER_IMAGE_NAME:latest -t $CONTAINER_IMAGE_NAME:$CI_COMMIT_SHA .
+ - docker push $CONTAINER_IMAGE_NAME:latest
+ - docker push $CONTAINER_IMAGE_NAME:$CI_COMMIT_SHA
+
+#------------------docker tag part---------------------
+
+xes-platform-docker-tag-staging:
+ stage: tag-docker
+ image: docker:latest
+ tags:
+ - dind
+ variables:
+ GIT_STRATEGY: none
+ only:
+ - master
+ when: manual
+ dependencies:
+ - xes-platform-docker
+ services:
+ - docker:18.09.8-dind
+ before_script:
+ - docker login -u _json_key -p "$GCLOUD_SERVICE_KEY" https://eu.gcr.io
+ - docker pull $CONTAINER_IMAGE_NAME:$CI_COMMIT_SHA
+ script:
+ - docker tag $CONTAINER_IMAGE_NAME:$CI_COMMIT_SHA $CONTAINER_IMAGE_NAME:staging
+ - docker push $CONTAINER_IMAGE_NAME:staging
+
+xes-platform-docker-tag-tags:
+ stage: tag-docker
+ image: docker:latest
+ tags:
+ - dind
+ variables:
+ GIT_STRATEGY: none
+ only:
+ - tags
+ dependencies:
+ - xes-platform-docker
+ services:
+ - docker:18.09.8-dind
+ before_script:
+ - docker login -u _json_key -p "$GCLOUD_SERVICE_KEY" https://eu.gcr.io
+ - docker pull $CONTAINER_IMAGE_NAME:$CI_COMMIT_SHA
+ script:
+ - docker tag $CONTAINER_IMAGE_NAME:$CI_COMMIT_SHA $CONTAINER_IMAGE_NAME:$CI_COMMIT_REF_NAME
+ - docker push $CONTAINER_IMAGE_NAME:$CI_COMMIT_REF_NAME
+ - docker tag $CONTAINER_IMAGE_NAME:$CI_COMMIT_SHA $CONTAINER_IMAGE_NAME:stable
+ - docker push $CONTAINER_IMAGE_NAME:stable
diff --git a/Dockerfile b/Dockerfile
index e033deb27..758f62c83 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -12,6 +12,10 @@ WORKDIR /app
COPY artifacts/server-docker /app/server
+COPY demo/ /app/demo/
+
+RUN chmod +x ./demo/restore-demo.sh
+
RUN chmod +x ./server
CMD ["./server"]
diff --git a/Gopkg.lock b/Gopkg.lock
index cd99bead2..981308d42 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -2,398 +2,868 @@
[[projects]]
+ digest = "1:557f06e527ad555e77d2250bd8b2663cb691e811d4060750e21e214c8faa8b85"
name = "github.com/SparkPost/gosparkpost"
- packages = [".","events"]
+ packages = [
+ ".",
+ "events",
+ ]
+ pruneopts = "UT"
revision = "4c6d2a3be65810be883c869868a74e677df2881b"
version = "0.1.0"
[[projects]]
+ digest = "1:f780d408067189c4c42b53f7bb24ebf8fd2a1e4510b813ed6e79dd6563e38cc5"
+ name = "github.com/ajg/form"
+ packages = ["."]
+ pruneopts = "UT"
+ revision = "5c4e22684113ffc2a77577c178189940925f9aef"
+ version = "v1.5.1"
+
+[[projects]]
+ digest = "1:f96ba6ecca7ba87b1dddd70ae38cfc4ce5ea844f58d1f728e121d2e29cdfb8a2"
name = "github.com/allegro/bigcache"
- packages = [".","queue"]
+ packages = [
+ ".",
+ "queue",
+ ]
+ pruneopts = "UT"
revision = "84a0ff3f153cbd7e280a19029a864bb04b504e62"
version = "v1.2.0"
[[projects]]
branch = "master"
+ digest = "1:7d191fd0c54ff370eaf6116a14dafe2a328df487baea280699f597aae858d00d"
name = "github.com/aristanetworks/goarista"
packages = ["monotime"]
+ pruneopts = "UT"
revision = "004c259faaebbfec6dd9f9e23daad4027d8ba4f1"
[[projects]]
+ digest = "1:15e73447582bf3fcc69c90eea50469daff2668e1a9270a66b7d4b14adabc3aba"
name = "github.com/asdine/storm"
- packages = [".","codec","codec/json","codec/msgpack","index","internal","q"]
+ packages = [
+ ".",
+ "codec",
+ "codec/json",
+ "codec/msgpack",
+ "index",
+ "internal",
+ "q",
+ ]
+ pruneopts = "UT"
revision = "e0f77eada154c7c2670527a8566d3c045880224f"
version = "v2.2.1"
[[projects]]
branch = "master"
+ digest = "1:e77bac38acb4cb64b18dba9632a9702c861b5c925e407ea4cb57f870b9b60225"
name = "github.com/asticode/go-bindata"
packages = ["."]
+ pruneopts = "T"
revision = "a0ff2567cfb70903282db057e799fd826784d41d"
[[projects]]
+ digest = "1:0f98f59e9a2f4070d66f0c9c39561f68fcd1dc837b22a852d28d0003aebd1b1e"
name = "github.com/boltdb/bolt"
packages = ["."]
+ pruneopts = "UT"
revision = "2f1ce7a837dcb8da3ec595b1dac9d0632f0f99e8"
version = "v1.3.1"
[[projects]]
branch = "master"
+ digest = "1:093bf93a65962e8191e3e8cd8fc6c363f83d43caca9739c906531ba7210a9904"
name = "github.com/btcsuite/btcd"
packages = ["btcec"]
+ pruneopts = "UT"
revision = "16327141da8ce4b46b5bac57ba01352943465d9e"
[[projects]]
branch = "master"
+ digest = "1:e8891cd354f2af940345f895d5698ab1ed439196d4f69eb44817fc91e80354c1"
name = "github.com/c2h5oh/datasize"
packages = ["."]
+ pruneopts = "UT"
revision = "4eba002a5eaea69cf8d235a388fc6b65ae68d2dd"
[[projects]]
+ digest = "1:f2ac2c724fc8214bb7b9dd6d4f5b7a983152051f5133320f228557182263cb94"
name = "github.com/coreos/bbolt"
packages = ["."]
+ pruneopts = "UT"
revision = "a0458a2b35708eef59eb5f620ceb3cd1c01a824d"
version = "v1.3.3"
[[projects]]
branch = "master"
+ digest = "1:8645cabc6639b91c91275cc8b78cc34d30d1394146f713e0342e2b746f5121a9"
name = "github.com/cznic/b"
packages = ["."]
+ pruneopts = "UT"
revision = "a26611c4d92dc9181cdb5e8d67fb712700fcd4a3"
[[projects]]
branch = "master"
+ digest = "1:f1e43a2835867fefa3a284c92823a96cd8d34591cd9c90fa247c345d1cd08e08"
name = "github.com/cznic/fileutil"
packages = ["."]
+ pruneopts = "UT"
revision = "4d67cfea8c87a5ea35f02b7716ffe87753abb64a"
[[projects]]
branch = "master"
+ digest = "1:2a4afef2ef0e4e4ab66b4f4f6a70bf6551ae8b727184771b5547e2b90245fe58"
name = "github.com/cznic/golex"
packages = ["lex"]
+ pruneopts = "UT"
revision = "9c343928389cc59a56eb4494e0b781f1a9145ec0"
[[projects]]
+ digest = "1:a9e2943ed681d4758a43d1bd9e80d7e993b66ce5adc7aa5dcc083ba48a47a4eb"
name = "github.com/cznic/internal"
- packages = ["buffer","file","slice"]
+ packages = [
+ "buffer",
+ "file",
+ "slice",
+ ]
+ pruneopts = "UT"
revision = "cef02a853c3a93623c42eacd574e7ea05f55531b"
version = "1.0.0"
[[projects]]
+ digest = "1:ec7b58207ac8eee1c554b7947b3f5124f711238a80edb7224a53bae7263c95ae"
name = "github.com/cznic/lldb"
packages = ["."]
+ pruneopts = "UT"
revision = "bea8611dd5c407f3c5eab9f9c68e887a27dc6f0e"
version = "v1.1.0"
[[projects]]
branch = "master"
+ digest = "1:1e461a804fd370a051999c0e046d344c430af4aadb8cf0c3fdc2093d11ea6344"
name = "github.com/cznic/mathutil"
packages = ["."]
+ pruneopts = "UT"
revision = "297441e035482346a960053c9ea29669b13e1025"
[[projects]]
+ digest = "1:8d144f6cd80e8fc6e6adfe555ece503c93c5f8981be3760193354a9b13eadd70"
name = "github.com/cznic/ql"
- packages = [".","vendored/github.com/camlistore/go4/lock"]
+ packages = [
+ ".",
+ "vendored/github.com/camlistore/go4/lock",
+ ]
+ pruneopts = "UT"
revision = "47bf73cf8ed137969f12b223b7016784cdd9d42e"
version = "v1.2.0"
[[projects]]
branch = "master"
+ digest = "1:1ec94f18c3941cdbde18b11052791eed70cd8d175495348460aa47fadf0a9471"
name = "github.com/cznic/sortutil"
packages = ["."]
+ pruneopts = "UT"
revision = "f5f958428db822072e84a845d95c490468f5e96d"
[[projects]]
branch = "master"
+ digest = "1:c18715de7a83fd8048ffdfa316a4126da285a6f0bf4ecfb9b75949755e37614e"
name = "github.com/cznic/strutil"
packages = ["."]
+ pruneopts = "UT"
revision = "275e9034453778034cf2718cec00c840f5a34299"
[[projects]]
branch = "master"
+ digest = "1:cb3e2ad1c7a0273e0d9285acce9ee8e9c4336f301552fe57c068e95d31a2b387"
name = "github.com/cznic/zappy"
packages = ["."]
+ pruneopts = "UT"
revision = "ca47d358d4b1fee03c89617b25fc2d7bac45a9a6"
[[projects]]
+ digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec"
name = "github.com/davecgh/go-spew"
packages = ["spew"]
+ pruneopts = "UT"
revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73"
version = "v1.1.1"
[[projects]]
+ digest = "1:e47d51dab652d26c3fba6f8cba403f922d02757a82abdc77e90df7948daf296e"
name = "github.com/deckarep/golang-set"
packages = ["."]
+ pruneopts = "UT"
revision = "cbaa98ba5575e67703b32b4b19f73c91f3c4159e"
version = "v1.7.1"
[[projects]]
+ digest = "1:76dc72490af7174349349838f2fe118996381b31ea83243812a97e5a0fd5ed55"
name = "github.com/dgrijalva/jwt-go"
packages = ["."]
+ pruneopts = "UT"
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
version = "v3.2.0"
[[projects]]
+ digest = "1:47cebe27e59e4e0c239d0bf2419d6a46f0364a56efc43be702c32b82710b6ecf"
name = "github.com/disintegration/imaging"
packages = ["."]
+ pruneopts = "UT"
revision = "5362c131d56305ce787e79a5b94ffc956df00d62"
version = "v1.6.0"
[[projects]]
+ digest = "1:72dc2b6056e7097f829260e4a2ff08d32fec6017df1982a66e110ab4128486f8"
name = "github.com/dlclark/regexp2"
- packages = [".","syntax"]
+ packages = [
+ ".",
+ "syntax",
+ ]
+ pruneopts = "UT"
revision = "487489b64fb796de2e55f4e8a4ad1e145f80e957"
version = "v1.1.6"
[[projects]]
branch = "master"
+ digest = "1:715939d4cfa785d9b92bb0b4a929f5468cd8b90e8c2f11050763ae7533144dfd"
name = "github.com/dop251/goja"
- packages = [".","ast","file","parser","token"]
+ packages = [
+ ".",
+ "ast",
+ "file",
+ "parser",
+ "token",
+ ]
+ pruneopts = "UT"
revision = "084dd324c7243282cb38046d2b31196f88903242"
[[projects]]
+ digest = "1:edb569dd02419a41ddd98768cc0e7aec922ef19dae139731e5ca750afcf6f4c5"
name = "github.com/edsrzf/mmap-go"
packages = ["."]
+ pruneopts = "UT"
revision = "188cc3b666ba704534fa4f96e9e61f21f1e1ba7c"
version = "v1.0.0"
[[projects]]
+ digest = "1:56058c72bf5072f955ec109a1765aeed9a30f5ec254cfeaea6c4c6861f7c0d2f"
name = "github.com/ethereum/go-ethereum"
- packages = [".","accounts","accounts/abi","accounts/abi/bind","accounts/keystore","common","common/hexutil","common/math","common/mclock","common/prque","core/types","crypto","crypto/secp256k1","ethclient","ethdb","event","log","metrics","p2p/netutil","params","rlp","rpc","trie"]
+ packages = [
+ ".",
+ "accounts",
+ "accounts/abi",
+ "accounts/abi/bind",
+ "accounts/keystore",
+ "common",
+ "common/hexutil",
+ "common/math",
+ "common/mclock",
+ "common/prque",
+ "core/types",
+ "crypto",
+ "crypto/secp256k1",
+ "ethclient",
+ "ethdb",
+ "event",
+ "log",
+ "metrics",
+ "p2p/netutil",
+ "params",
+ "rlp",
+ "rpc",
+ "trie",
+ ]
+ pruneopts = "T"
revision = "4bcc0a37ab70cb79b16893556cffdaad6974e7d8"
version = "v1.8.27"
[[projects]]
+ digest = "1:af43bdaaf86655a2343f113e9b293bbc16b12099eaeb223982bbe4d4c22ba14d"
+ name = "github.com/fatih/structs"
+ packages = ["."]
+ pruneopts = "UT"
+ revision = "4966fc68f5b7593aafa6cbbba2d65ec6e1416047"
+ version = "v1.1.0"
+
+[[projects]]
+ digest = "1:3e658acf1e5e75c289ba64c08c6006382980f6cbcf3b17505adc95f4dafa40f6"
name = "github.com/go-sourcemap/sourcemap"
- packages = [".","internal/base64vlq"]
+ packages = [
+ ".",
+ "internal/base64vlq",
+ ]
+ pruneopts = "UT"
revision = "b019cc30c1eaa584753491b0d8f8c1534bf1eb44"
version = "v2.1.2"
[[projects]]
+ digest = "1:586ea76dbd0374d6fb649a91d70d652b7fe0ccffb8910a77468e7702e7901f3d"
name = "github.com/go-stack/stack"
packages = ["."]
+ pruneopts = "UT"
revision = "2fee6af1a9795aafbe0253a0cfbdf668e1fb8a9a"
version = "v1.8.0"
[[projects]]
+ digest = "1:be408f349cae090a7c17a279633d6e62b00068e64af66a582cae0983de8890ea"
name = "github.com/golang/mock"
packages = ["gomock"]
+ pruneopts = "UT"
revision = "9fa652df1129bef0e734c9cf9bf6dbae9ef3b9fa"
version = "1.3.1"
[[projects]]
+ digest = "1:318f1c959a8a740366fce4b1e1eb2fd914036b4af58fbd0a003349b305f118ad"
name = "github.com/golang/protobuf"
packages = ["proto"]
+ pruneopts = "UT"
revision = "b5d812f8a3706043e23a9cd5babf2e5423744d30"
version = "v1.3.1"
[[projects]]
+ digest = "1:e4f5819333ac698d294fe04dbf640f84719658d5c7ce195b10060cc37292ce79"
name = "github.com/golang/snappy"
packages = ["."]
+ pruneopts = "UT"
revision = "2a8bb927dd31d8daada140a5d09578521ce5c36a"
version = "v0.0.1"
[[projects]]
+ digest = "1:a63cff6b5d8b95638bfe300385d93b2a6d9d687734b863da8e09dc834510a690"
+ name = "github.com/google/go-querystring"
+ packages = ["query"]
+ pruneopts = "UT"
+ revision = "44c6ddd0a2342c386950e880b658017258da92fc"
+ version = "v1.0.0"
+
+[[projects]]
+ digest = "1:582b704bebaa06b48c29b0cec224a6058a09c86883aaddabde889cd1a5f73e1b"
name = "github.com/google/uuid"
packages = ["."]
+ pruneopts = "UT"
revision = "0cd6bf5da1e1c83f8b45653022c74f71af0538a4"
version = "v1.1.1"
[[projects]]
+ digest = "1:c79fb010be38a59d657c48c6ba1d003a8aa651fa56b579d959d74573b7dff8e1"
name = "github.com/gorilla/context"
packages = ["."]
+ pruneopts = "UT"
revision = "08b5f424b9271eedf6f9f0ce86cb9396ed337a42"
version = "v1.1.1"
[[projects]]
+ digest = "1:e72d1ebb8d395cf9f346fd9cbc652e5ae222dd85e0ac842dc57f175abed6d195"
name = "github.com/gorilla/securecookie"
packages = ["."]
+ pruneopts = "UT"
revision = "e59506cc896acb7f7bf732d4fdf5e25f7ccd8983"
version = "v1.1.1"
[[projects]]
+ digest = "1:e5bf52fd66a2e984b57b4c0f2c4ee024ed749a19886246240629998dc0cf31ce"
name = "github.com/gorilla/sessions"
packages = ["."]
+ pruneopts = "UT"
revision = "f57b7e2d29c6211d16ffa52a0998272f75799030"
version = "v1.1.3"
[[projects]]
+ digest = "1:e62657cca9badaa308d86e7716083e4c5933bb78e30a17743fc67f50be26f6f4"
+ name = "github.com/gorilla/websocket"
+ packages = ["."]
+ pruneopts = "UT"
+ revision = "c3e18be99d19e6b3e8f1559eea2c161a665c4b6b"
+ version = "v1.4.1"
+
+[[projects]]
+ digest = "1:4442692c7525ec403b23a5b3e169a089bb1e6ed3b69795be877d39578744bf7a"
name = "github.com/h2non/filetype"
- packages = [".","matchers","matchers/isobmff","types"]
+ packages = [
+ ".",
+ "matchers",
+ "matchers/isobmff",
+ "types",
+ ]
+ pruneopts = "UT"
revision = "2248f2e2f77cd8cf9694216e3f9589d71005de37"
version = "v1.0.8"
+[[projects]]
+ digest = "1:a3ce4de79566c21e93cb6934797fdaa587ad3fc6a964708ab77babe54ea67188"
+ name = "github.com/imkira/go-interpol"
+ packages = ["."]
+ pruneopts = "UT"
+ revision = "5accad8134979a6ac504d456a6c7f1c53da237ca"
+ version = "v1.1.0"
+
[[projects]]
branch = "master"
+ digest = "1:1aa40fe85400837932c4e74c8829f7d2a48483cb497f8902993c8d2a829066c3"
name = "github.com/jteeuwen/go-bindata"
packages = ["."]
+ pruneopts = "UT"
revision = "6025e8de665b31fa74ab1a66f2cddd8c0abf887e"
[[projects]]
+ digest = "1:caa1da085cc12cd39d3ddb267434902f621050e1b46f7e9477b2b933df680193"
+ name = "github.com/klauspost/compress"
+ packages = [
+ "flate",
+ "gzip",
+ "zlib",
+ ]
+ pruneopts = "UT"
+ revision = "fdc46f09595ea8241b9bfe291bbc868a7af19e10"
+ version = "v1.8.2"
+
+[[projects]]
+ digest = "1:923c4d7194b42e054b2eb8a6c62824ac55e23ececc1c7e48d4da69c971c55954"
+ name = "github.com/klauspost/cpuid"
+ packages = ["."]
+ pruneopts = "UT"
+ revision = "05a8198c0f5a27739aec358908d7e12c64ce6eb7"
+ version = "v1.2.1"
+
+[[projects]]
+ digest = "1:efd601ad1d1189884f37ed075fadb5bbd17f1f5acf5862827247c6da56d125ba"
name = "github.com/labstack/echo"
- packages = [".","middleware"]
+ packages = [
+ ".",
+ "middleware",
+ ]
+ pruneopts = "UT"
revision = "38772c686c76b501f94bd6cd5b77f5842e93b559"
version = "v3.3.10"
[[projects]]
+ digest = "1:431646838dda93dfb384c1010fca1bb0353c1ee030910d926bb64b32f1ba253a"
name = "github.com/labstack/echo-contrib"
packages = ["session"]
+ pruneopts = "UT"
revision = "7d9d9632a4aadf9b026436e43d5f96eca2d895a6"
version = "0.5.2"
[[projects]]
+ digest = "1:764bce605f1c70823a567ac3205a03e6b7f375ca747d8820fded0b0abddda802"
name = "github.com/labstack/gommon"
- packages = ["bytes","color","log","random"]
+ packages = [
+ "bytes",
+ "color",
+ "log",
+ "random",
+ ]
+ pruneopts = "UT"
revision = "7fd9f68ece0bcb1a905fac8f1549f0083f71c51b"
version = "v0.2.8"
[[projects]]
+ digest = "1:7c084e0e780596dd2a7e20d25803909a9a43689c153de953520dfbc0b0e51166"
name = "github.com/mattn/go-colorable"
packages = ["."]
+ pruneopts = "UT"
revision = "8029fb3788e5a4a9c00e415f586a6d033f5d38b3"
version = "v0.1.2"
[[projects]]
+ digest = "1:9b90c7639a41697f3d4ad12d7d67dfacc9a7a4a6e0bbfae4fc72d0da57c28871"
name = "github.com/mattn/go-isatty"
packages = ["."]
+ pruneopts = "UT"
revision = "1311e847b0cb909da63b5fecfb5370aa66236465"
version = "v0.0.8"
[[projects]]
+ digest = "1:7aefb397a53fc437c90f0fdb3e1419c751c5a3a165ced52325d5d797edf1aca6"
+ name = "github.com/moul/http2curl"
+ packages = ["."]
+ pruneopts = "UT"
+ revision = "9ac6cf4d929b2fa8fd2d2e6dec5bb0feb4f4911d"
+ version = "v1.0.0"
+
+[[projects]]
+ digest = "1:e5d0bd87abc2781d14e274807a470acd180f0499f8bf5bb18606e9ec22ad9de9"
name = "github.com/pborman/uuid"
packages = ["."]
+ pruneopts = "UT"
revision = "adf5a7427709b9deb95d29d3fa8a2bf9cfd388f1"
version = "v1.2"
[[projects]]
+ digest = "1:cf31692c14422fa27c83a05292eb5cbe0fb2775972e8f1f8446a71549bd8980b"
name = "github.com/pkg/errors"
packages = ["."]
+ pruneopts = "UT"
revision = "ba968bfe8b2f7e042a574c888954fccecfa385b4"
version = "v0.8.1"
[[projects]]
+ digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe"
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
+ pruneopts = "UT"
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
branch = "master"
+ digest = "1:b590e9af2aa114018c32477dd0109fd4678b7a1326a025e6640d4fe501dccd38"
name = "github.com/remyoudompheng/bigfft"
packages = ["."]
+ pruneopts = "UT"
revision = "babf20351dd7e3ac320adedbbe5eb311aec8763c"
[[projects]]
branch = "master"
+ digest = "1:4293d7afbe7f9a4255b7f3fce291652eae1e08b105c88222505adc5075e0cb55"
name = "github.com/rjeczalik/notify"
packages = ["."]
+ pruneopts = "UT"
revision = "629144ba06a1c6af28c1e42c228e3d42594ce081"
[[projects]]
branch = "master"
+ digest = "1:dbfe572cc258e5bcf54cb650a06d90edd0da04e42ca1ed909cc1d49f00011c63"
name = "github.com/robertkrimen/otto"
- packages = [".","ast","dbg","file","parser","registry","token"]
+ packages = [
+ ".",
+ "ast",
+ "dbg",
+ "file",
+ "parser",
+ "registry",
+ "token",
+ ]
+ pruneopts = "UT"
revision = "15f95af6e78dcd2030d8195a138bd88d4f403546"
[[projects]]
+ digest = "1:b0c25f00bad20d783d259af2af8666969e2fc343fa0dc9efe52936bbd67fb758"
name = "github.com/rs/cors"
packages = ["."]
+ pruneopts = "UT"
revision = "9a47f48565a795472d43519dd49aac781f3034fb"
version = "v1.6.0"
[[projects]]
+ digest = "1:274f67cb6fed9588ea2521ecdac05a6d62a8c51c074c1fccc6a49a40ba80e925"
name = "github.com/satori/go.uuid"
packages = ["."]
+ pruneopts = "UT"
revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3"
version = "v1.2.0"
[[projects]]
+ digest = "1:d917313f309bda80d27274d53985bc65651f81a5b66b820749ac7f8ef061fd04"
+ name = "github.com/sergi/go-diff"
+ packages = ["diffmatchpatch"]
+ pruneopts = "UT"
+ revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
+ version = "v1.0.0"
+
+[[projects]]
+ digest = "1:5da8ce674952566deae4dbc23d07c85caafc6cfa815b0b3e03e41979cedb8750"
name = "github.com/stretchr/testify"
- packages = ["assert"]
+ packages = [
+ "assert",
+ "require",
+ ]
+ pruneopts = "UT"
revision = "ffdc059bfe9ce6a4e144ba849dbedead332c6053"
version = "v1.3.0"
[[projects]]
+ digest = "1:5b180f17d5bc50b765f4dcf0d126c72979531cbbd7f7929bf3edd87fb801ea2d"
name = "github.com/syndtr/goleveldb"
- packages = ["leveldb","leveldb/cache","leveldb/comparer","leveldb/errors","leveldb/filter","leveldb/iterator","leveldb/journal","leveldb/memdb","leveldb/opt","leveldb/storage","leveldb/table","leveldb/util"]
+ packages = [
+ "leveldb",
+ "leveldb/cache",
+ "leveldb/comparer",
+ "leveldb/errors",
+ "leveldb/filter",
+ "leveldb/iterator",
+ "leveldb/journal",
+ "leveldb/memdb",
+ "leveldb/opt",
+ "leveldb/storage",
+ "leveldb/table",
+ "leveldb/util",
+ ]
+ pruneopts = "UT"
revision = "9d007e481048296f09f59bd19bb7ae584563cd95"
version = "v1.0.0"
[[projects]]
+ digest = "1:c468422f334a6b46a19448ad59aaffdfc0a36b08fdcc1c749a0b29b6453d7e59"
name = "github.com/valyala/bytebufferpool"
packages = ["."]
+ pruneopts = "UT"
revision = "e746df99fe4a3986f4d4f79e13c1e0117ce9c2f7"
version = "v1.0.0"
[[projects]]
+ digest = "1:15ad8a80098fcc7a194b9db6b26d74072a852e4faa957848c8118193d3c69230"
+ name = "github.com/valyala/fasthttp"
+ packages = [
+ ".",
+ "fasthttputil",
+ "stackless",
+ ]
+ pruneopts = "UT"
+ revision = "e5f51c11919d4f66400334047b897ef0a94c6f3c"
+ version = "v20180529"
+
+[[projects]]
+ digest = "1:4d29fdc69817829d8c78473d61613d984ce59675110cee7a2f0314f332cc70a2"
name = "github.com/valyala/fasttemplate"
packages = ["."]
+ pruneopts = "UT"
revision = "8b5e4e491ab636663841c42ea3c5a9adebabaf36"
version = "v1.0.1"
[[projects]]
+ digest = "1:01d5dc3bfb14cf8fb2c924dcf026231218604b8ed15daaa028875d49e7f09071"
name = "github.com/vmihailenco/msgpack"
- packages = [".","codes"]
+ packages = [
+ ".",
+ "codes",
+ ]
+ pruneopts = "UT"
revision = "c2fc210f30a2aca9db880cc017a92c169c999253"
version = "v4.0.4"
[[projects]]
+ branch = "master"
+ digest = "1:87fe9bca786484cef53d52adeec7d1c52bc2bfbee75734eddeb75fc5c7023871"
+ name = "github.com/xeipuuv/gojsonpointer"
+ packages = ["."]
+ pruneopts = "UT"
+ revision = "02993c407bfbf5f6dae44c4f4b1cf6a39b5fc5bb"
+
+[[projects]]
+ branch = "master"
+ digest = "1:dc6a6c28ca45d38cfce9f7cb61681ee38c5b99ec1425339bfc1e1a7ba769c807"
+ name = "github.com/xeipuuv/gojsonreference"
+ packages = ["."]
+ pruneopts = "UT"
+ revision = "bd5ef7bd5415a7ac448318e64f11a24cd21e594b"
+
+[[projects]]
+ digest = "1:1c898ea6c30c16e8d55fdb6fe44c4bee5f9b7d68aa260cfdfc3024491dcc7bea"
+ name = "github.com/xeipuuv/gojsonschema"
+ packages = ["."]
+ pruneopts = "UT"
+ revision = "f971f3cd73b2899de6923801c147f075263e0c50"
+ version = "v1.1.0"
+
+[[projects]]
+ branch = "master"
+ digest = "1:ac3d942a027d57fbfc5c13791cfaaa4b30729674fea88f2e03190b777c2b674e"
+ name = "github.com/yalp/jsonpath"
+ packages = ["."]
+ pruneopts = "UT"
+ revision = "5cc68e5049a040829faef3a44c00ec4332f6dec7"
+
+[[projects]]
+ digest = "1:52ccbcf36804b0beb5677a8994bd4ac740b71d1d6fe38c02b113dabdda51bf6d"
+ name = "github.com/yudai/gojsondiff"
+ packages = [
+ ".",
+ "formatter",
+ ]
+ pruneopts = "UT"
+ revision = "7b1b7adf999dab73a6eb02669c3d82dbb27a3dd6"
+ version = "1.0.0"
+
+[[projects]]
+ branch = "master"
+ digest = "1:0d4822d3440c9b5992704bb357061fff7ab60daa85d92dec02b81b78e4908db7"
+ name = "github.com/yudai/golcs"
+ packages = ["."]
+ pruneopts = "UT"
+ revision = "ecda9a501e8220fae3b4b600c3db4b0ba22cfc68"
+
+[[projects]]
+ digest = "1:f2ac2c724fc8214bb7b9dd6d4f5b7a983152051f5133320f228557182263cb94"
name = "go.etcd.io/bbolt"
packages = ["."]
+ pruneopts = "UT"
revision = "a0458a2b35708eef59eb5f620ceb3cd1c01a824d"
version = "v1.3.3"
[[projects]]
branch = "master"
+ digest = "1:200c02a0b7bdf750212f2c515b084fb687fcde570b1df7385a5b34b943775254"
name = "golang.org/x/crypto"
- packages = ["acme","acme/autocert","bcrypt","blowfish","pbkdf2","scrypt","sha3"]
+ packages = [
+ "acme",
+ "acme/autocert",
+ "bcrypt",
+ "blowfish",
+ "pbkdf2",
+ "scrypt",
+ "sha3",
+ ]
+ pruneopts = "UT"
revision = "22d7a77e9e5f409e934ed268692e56707cd169e5"
[[projects]]
branch = "master"
+ digest = "1:4c5fae3d31eb72f59429f9218ff258128ef37edcae9785f93bb9f1609b392c51"
name = "golang.org/x/image"
- packages = ["bmp","tiff","tiff/lzw"]
+ packages = [
+ "bmp",
+ "tiff",
+ "tiff/lzw",
+ ]
+ pruneopts = "UT"
revision = "f03afa92d3ffad9e5ff762aa549908afe674f01e"
[[projects]]
branch = "master"
+ digest = "1:0c01f457a20cc8802fcd49ded23ddf3c45f6bcf5002f5866f7fa1ad70ece365b"
name = "golang.org/x/net"
- packages = ["context","idna","websocket"]
+ packages = [
+ "context",
+ "idna",
+ "publicsuffix",
+ "websocket",
+ ]
+ pruneopts = "UT"
revision = "f3200d17e092c607f615320ecaad13d87ad9a2b3"
[[projects]]
branch = "master"
+ digest = "1:1a012d0fbd62ad6cf1df182d9e5875672c722098b1a032757ead9538bbd9edf2"
name = "golang.org/x/sys"
- packages = ["cpu","unix","windows"]
+ packages = [
+ "cpu",
+ "unix",
+ "windows",
+ ]
+ pruneopts = "UT"
revision = "dbbf3f1254d491605cf4a0034ce25d0dc71b0c58"
[[projects]]
+ digest = "1:899611736f6c2bec18cdb819e0b6b7b4fcf8dd955fa90d39741100dfd719f938"
name = "golang.org/x/text"
- packages = ["cases","collate","collate/build","internal","internal/colltab","internal/gen","internal/language","internal/language/compact","internal/tag","internal/triegen","internal/ucd","language","secure/bidirule","transform","unicode/bidi","unicode/cldr","unicode/norm","unicode/rangetable"]
+ packages = [
+ "cases",
+ "collate",
+ "collate/build",
+ "internal",
+ "internal/colltab",
+ "internal/gen",
+ "internal/language",
+ "internal/language/compact",
+ "internal/tag",
+ "internal/triegen",
+ "internal/ucd",
+ "language",
+ "secure/bidirule",
+ "transform",
+ "unicode/bidi",
+ "unicode/cldr",
+ "unicode/norm",
+ "unicode/rangetable",
+ ]
+ pruneopts = "UT"
revision = "342b2e1fbaa52c93f31447ad2c6abc048c63e475"
version = "v0.3.2"
[[projects]]
+ digest = "1:6f2e5316818a04be180ab68722b2eba1f25af244fab309736313d2baa22532fb"
name = "google.golang.org/appengine"
- packages = [".","datastore","datastore/internal/cloudkey","datastore/internal/cloudpb","internal","internal/app_identity","internal/base","internal/datastore","internal/log","internal/modules","internal/remote_api"]
+ packages = [
+ ".",
+ "datastore",
+ "datastore/internal/cloudkey",
+ "datastore/internal/cloudpb",
+ "internal",
+ "internal/app_identity",
+ "internal/base",
+ "internal/datastore",
+ "internal/log",
+ "internal/modules",
+ "internal/remote_api",
+ ]
+ pruneopts = "UT"
revision = "4c25cacc810c02874000e4f7071286a8e96b2515"
version = "v1.6.0"
+[[projects]]
+ digest = "1:74aa17994c6e50827ff025c0bb699e88174e295e43c2ab60b4a545f9bc9c48b1"
+ name = "gopkg.in/gavv/httpexpect.v2"
+ packages = ["."]
+ pruneopts = "UT"
+ revision = "30ddbb4755a80fb6a97cc4c26274250bae331cd5"
+ version = "v2.0.0"
+
[[projects]]
branch = "v2"
+ digest = "1:3d3f9391ab615be8655ae0d686a1564f3fec413979bb1aaf018bac1ec1bb1cc7"
name = "gopkg.in/natefinch/npipe.v2"
packages = ["."]
+ pruneopts = "UT"
revision = "c1b8fa8bdccecb0b8db834ee0b92fdbcfa606dd6"
[[projects]]
+ digest = "1:9935525a8c49b8434a0b0a54e1980e94a6fae73aaff45c5d33ba8dff69de123e"
name = "gopkg.in/sourcemap.v1"
- packages = [".","base64vlq"]
+ packages = [
+ ".",
+ "base64vlq",
+ ]
+ pruneopts = "UT"
revision = "6e83acea0053641eff084973fee085f0c193c61a"
version = "v1.0.5"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
- inputs-digest = "c9a0d5a8d2bc986f57fe07f9e65f83e2554bbc431133bcd47870ac931c99a92e"
+ input-imports = [
+ "github.com/SparkPost/gosparkpost",
+ "github.com/asdine/storm",
+ "github.com/asdine/storm/codec/msgpack",
+ "github.com/asdine/storm/q",
+ "github.com/asticode/go-bindata",
+ "github.com/boltdb/bolt",
+ "github.com/c2h5oh/datasize",
+ "github.com/coreos/bbolt",
+ "github.com/cznic/ql",
+ "github.com/davecgh/go-spew/spew",
+ "github.com/disintegration/imaging",
+ "github.com/dop251/goja",
+ "github.com/ethereum/go-ethereum",
+ "github.com/ethereum/go-ethereum/accounts/abi",
+ "github.com/ethereum/go-ethereum/accounts/abi/bind",
+ "github.com/ethereum/go-ethereum/accounts/keystore",
+ "github.com/ethereum/go-ethereum/common",
+ "github.com/ethereum/go-ethereum/core/types",
+ "github.com/ethereum/go-ethereum/crypto",
+ "github.com/ethereum/go-ethereum/ethclient",
+ "github.com/ethereum/go-ethereum/event",
+ "github.com/golang/mock/gomock",
+ "github.com/gorilla/sessions",
+ "github.com/h2non/filetype",
+ "github.com/h2non/filetype/matchers",
+ "github.com/h2non/filetype/types",
+ "github.com/jteeuwen/go-bindata",
+ "github.com/labstack/echo",
+ "github.com/labstack/echo-contrib/session",
+ "github.com/labstack/echo/middleware",
+ "github.com/labstack/gommon/log",
+ "github.com/pkg/errors",
+ "github.com/rjeczalik/notify",
+ "github.com/robertkrimen/otto",
+ "github.com/satori/go.uuid",
+ "github.com/stretchr/testify/assert",
+ "go.etcd.io/bbolt",
+ "golang.org/x/crypto/acme/autocert",
+ "golang.org/x/crypto/bcrypt",
+ "gopkg.in/gavv/httpexpect.v2",
+ ]
solver-name = "gps-cdcl"
solver-version = 1
diff --git a/Makefile b/Makefile
index 6144160ed..ed19d7108 100644
--- a/Makefile
+++ b/Makefile
@@ -1,3 +1,5 @@
+PROXEUS_TEST_MODE?=false
+
# required tooling
init:
./build/deps.sh
@@ -29,17 +31,28 @@ test:
./build/test.sh
test-payment:
- go test ./main/handlers/api ./main/handlers/workflow ./main/handlers/blockchain
+ go test ./main/handlers/payment ./main/handlers/blockchain
+
+test/bindata.go: $(wildcard ./test/assets/**)
+ go-bindata ${DEBUG_FLAG} -pkg test -o ./test/bindata.go ./test/assets
+
+test-api: test/bindata.go
+ go clean -testcache && go test ./test
clean:
cd artifacts && rm -rf `ls . | grep -v 'cache'`
all: ui server
+run:
+ artifacts/server -DataDir ./data/proxeus-platform/data/ -DocumentServiceUrl=http://document-service:2115 \
+ -BlockchainContractAddress=${PROXEUS_CONTRACT_ADDRESS} -InfuraApiKey=${PROXEUS_INFURA_KEY} \
+ -SparkpostApiKey=${PROXEUS_SPARKPOST_KEY} -EmailFrom=${PROXEUS_EMAIL_FROM} -TestMode=${PROXEUS_TEST_MODE}
+
# used by CI
link-repo:
mkdir -p /go/src/git.proxeus.com/core
- ln -s ~/project /go/src/git.proxeus.com/core/central
+ ln -s ${PROJECT_ROOT_FOLDER} /go/src/git.proxeus.com/core/central
.PHONY: init main all all-debug generate test clean fmt validate link-repo
.PHONY: ui server
diff --git a/README.md b/README.md
index 987bf331e..39f3ed09c 100644
--- a/README.md
+++ b/README.md
@@ -7,10 +7,12 @@ Main repository of the proxeus platform.
Proxeus combines a powerful document automation tool with the wide-ranging
blockchain functionalities, enabling users to digitize and monetize their IP.
-## Quick Start
+## Quick Start with docker
The quickest way to try Proxeus is to use `docker-compose`.
+(If you are a developer and want to build the project form the source code follow the instructions in [Build all](docs/build_all.md))
+
### Install docker and docker-compose
1. [Install Docker Engine](https://docs.docker.com/install/)
2. [Install docker-compose](https://docs.docker.com/compose/install/)
@@ -21,7 +23,7 @@ for Ethereum and email integration respectively.
Please create an account on those platform and get an API Keys.
-### Proxeus Demo Etherum Smart Contract
+### Proxeus Demo Ethereum Smart Contract
For your convenience, a demo smart contract is deployed on the Ropsten network at the following address:
@@ -41,17 +43,20 @@ docker-compose up
Proxeus should be available at http://localhost:1323
-The next step is to [configure](configure.md) your instance for the first time.
+The next step is to [configure](docs/configure.md) your instance for the first time.
-## User manual
+## Build Proxeus Platform from the source code
-The user manual is available here: [User Manual](https://docs.google.com/document/d/1SP0ZimG7uemfZ2cF2JkY5enUZnBJLDyfcJGZnyWOejQ)
+If you are a developer and want to build the project form the source code follow the instructions in [Build all](docs/build_all.md)
## Developer manual
Please read the [Developer Manual](docs/_sidebar.md) to learn more about the
Proxeus platform. *TODO: link to the github pages documentation site when ready*
+## User manual
+
+The user manual is available here: [User Manual](https://docs.google.com/document/d/1SP0ZimG7uemfZ2cF2JkY5enUZnBJLDyfcJGZnyWOejQ)
## 3 Misc
diff --git a/build/deps.sh b/build/deps.sh
old mode 100644
new mode 100755
index 26dd46969..1f8c0ecc9
--- a/build/deps.sh
+++ b/build/deps.sh
@@ -21,7 +21,13 @@ go get golang.org/x/tools/cmd/goimports
echo "Installing dependencies. This may take a while."
dep ensure
-mkdir -p /data/hosted
+
+if [[ "$OSTYPE" == "darwin"* ]]; then
+ sudo mkdir -p /data/hosted
+ sudo chown $(id -u):$(id -g) /data/hosted
+else
+ mkdir -p /data/hosted
+fi
# on macs use brew
diff --git a/build/run-in-docker.sh b/build/run-in-docker.sh
old mode 100644
new mode 100755
diff --git a/build/server.sh b/build/server.sh
old mode 100644
new mode 100755
diff --git a/build/test.sh b/build/test.sh
old mode 100644
new mode 100755
diff --git a/build/validate.sh b/build/validate.sh
old mode 100644
new mode 100755
diff --git a/demo/demo.sh b/demo/demo.sh
new file mode 100644
index 000000000..c50bb6dfa
--- /dev/null
+++ b/demo/demo.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+if [ `id -u` -ne 0 ]; then
+ echo "Execute only as root, Exiting.."
+ exit 1
+fi
+
+CRON_FILE="/etc/cron.d/demo"
+dockerImageToStop="proxeus/proxeus-core:latest"
+RESTORE_COMMAND="/usr/bin/docker exec \$(/usr/bin/docker ps -a -q --filter ancestor=$dockerImageToStop --format='{{.ID}}') /app/demo/restore-demo.sh && /usr/bin/docker container restart \$(/usr/bin/docker ps -a -q --filter ancestor=$dockerImageToStop --format='{{.ID}}')"
+
+case "$1" in
+ install)
+ if [ ! -f $CRON_FILE ]; then
+ echo "Cron file for demo not present, creating.."
+ touch $CRON_FILE
+ chown root:root $CRON_FILE
+ fi
+
+ grep -qi "root" $CRON_FILE
+
+ if [ $? != 0 ]; then
+ echo "Create cron task to clean environment every 3 days"
+ /bin/echo "* * */3 * * root "$RESTORE_COMMAND" >/dev/null 2>&1" >> $CRON_FILE
+ fi
+ ;;
+
+ clean)
+ rm $CRON_FILE
+ ;;
+
+ *)
+ echo $"Usage: $0 {install|clean}"
+ exit 1
+
+esac
+
diff --git a/demo/restore-demo.sh b/demo/restore-demo.sh
new file mode 100644
index 000000000..cacc5d809
--- /dev/null
+++ b/demo/restore-demo.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+# In order to reset Proxeus to a pre-defined status, this script
+# copies the databases from an origin and copies them to the destination
+echo "Restoring database.."
+originDataDir="/app/demo/restore_db/*"
+destinationDataDir="/data/hosted/"
+
+echo "Removing $destinationDataDir"
+
+rm -R ${destinationDataDir}
+
+echo "Resetting to initial status from $originDataDir.."
+cp -R ${originDataDir} ${destinationDataDir}
+echo "Done.."
diff --git a/demo/restore_db/cache b/demo/restore_db/cache
new file mode 100644
index 000000000..e449c2898
Binary files /dev/null and b/demo/restore_db/cache differ
diff --git a/demo/restore_db/document_template/document_templates b/demo/restore_db/document_template/document_templates
new file mode 100644
index 000000000..71191b41e
Binary files /dev/null and b/demo/restore_db/document_template/document_templates differ
diff --git a/demo/restore_db/form/forms b/demo/restore_db/form/forms
new file mode 100644
index 000000000..5d6508661
Binary files /dev/null and b/demo/restore_db/form/forms differ
diff --git a/demo/restore_db/i18n b/demo/restore_db/i18n
new file mode 100644
index 000000000..f0958a7e3
Binary files /dev/null and b/demo/restore_db/i18n differ
diff --git a/demo/restore_db/sessions/sessions b/demo/restore_db/sessions/sessions
new file mode 100644
index 000000000..e449c2898
Binary files /dev/null and b/demo/restore_db/sessions/sessions differ
diff --git a/demo/restore_db/signaturerequests/signaturerequestsdb b/demo/restore_db/signaturerequests/signaturerequestsdb
new file mode 100644
index 000000000..3956621e0
Binary files /dev/null and b/demo/restore_db/signaturerequests/signaturerequestsdb differ
diff --git a/demo/restore_db/user/users b/demo/restore_db/user/users
new file mode 100644
index 000000000..f26bfc8ce
Binary files /dev/null and b/demo/restore_db/user/users differ
diff --git a/demo/restore_db/userdata/usrdb b/demo/restore_db/userdata/usrdb
new file mode 100644
index 000000000..927a0f2ae
Binary files /dev/null and b/demo/restore_db/userdata/usrdb differ
diff --git a/demo/restore_db/workflow/workflows b/demo/restore_db/workflow/workflows
new file mode 100644
index 000000000..64aefed69
Binary files /dev/null and b/demo/restore_db/workflow/workflows differ
diff --git a/demo/restore_db/workflowpayments/workflowpaymentsdb b/demo/restore_db/workflowpayments/workflowpaymentsdb
new file mode 100644
index 000000000..7c84bf5f6
Binary files /dev/null and b/demo/restore_db/workflowpayments/workflowpaymentsdb differ
diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml
index 70b53a75f..507f0b899 100644
--- a/docker-compose-dev.yml
+++ b/docker-compose-dev.yml
@@ -17,8 +17,11 @@ services:
DocumentServiceUrl: "http://document-service:2115/"
BlockchainContractAddress: "${PROXEUS_CONTRACT_ADDRESS}"
InfuraApiKey: "${PROXEUS_INFURA_KEY}"
- SparkpostApiKey: "${PROXEUS_SPARKPOST_KEY}"
+ SparkpostApiKey: "${PROXEUS_SPARKPOST_KEY}"
EmailFrom: "${PROXEUS_EMAIL_FROM:-no-reply@proxeus.com}"
+ AirdropWalletfile: "${PROXEUS_AIRDROP_WALLET_FILE:-/root/.proxeus/settings/airdropwallet.json}"
+ AirdropWalletkey: "${PROXEUS_AIRDROP_WALLET_KEY:-/root/.proxeus/settings/airdropwallet.key}"
+ TestMode: "${PROXEUS_TEST_MODE:-false}"
ports:
- "1323:1323"
volumes:
diff --git a/docker-compose.yml b/docker-compose.yml
index 21518ee3e..5363e327c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -17,10 +17,12 @@ services:
TZ: Europe/Zurich
DataDir: "/data/hosted"
DocumentServiceUrl: "http://document-service:2115/"
- BlockchainContractAddress: "${PROXEUS_CONTRACT_ADDRESS}"
InfuraApiKey: "${PROXEUS_INFURA_KEY}"
SparkpostApiKey: "${PROXEUS_SPARKPOST_KEY}"
+ BlockchainContractAddress: "${PROXEUS_CONTRACT_ADDRESS}"
EmailFrom: "${PROXEUS_EMAIL_FROM:-no-reply@proxeus.com}"
+ AirdropWalletfile: "${PROXEUS_AIRDROP_WALLET_FILE:-./data/proxeus-platform/settings/airdropwallet.json}"
+ AirdropWalletkey: "${PROXEUS_AIRDROP_WALLET_KEY:-./data/proxeus-platform/settings/airdropwallet.key}"
ports:
- "1323:1323"
volumes:
diff --git a/docs/_coverpage.md b/docs/_coverpage.md
index 4edab8e5c..6c82d0109 100644
--- a/docs/_coverpage.md
+++ b/docs/_coverpage.md
@@ -1,12 +1,9 @@
![logo](_media/proxeus_logo.svg)
# 1.0.0
+> Blockchain-enabled documents and workflows
-> Bringing blockchain compatibility to traditional companies.
-
-Proxeus is all about document-centered processes that interact with the Blockchain.
-Our app lets you drag & drop a workflow in the wink of an eye. Proxeus workflows may comprise forms, document templates and conditions.
-Proxeus enables you to create tamper-proof and easily verifiable documents by registering them on the Ethereum Blockchain.
+Proxeus combines a powerful document automation tool with the wide-ranging blockchain functionalities, enabling users to digitize and monetize their IP.
[GitHub](https://github.com/ProxeusApp/proxeus-core)
[Getting Started](quickstart.md)
diff --git a/docs/_media/xes_payment_sequence_diagram.png b/docs/_media/xes_payment_sequence_diagram.png
new file mode 100644
index 000000000..dd66eb258
Binary files /dev/null and b/docs/_media/xes_payment_sequence_diagram.png differ
diff --git a/docs/_media/xes_payment_state_model.png b/docs/_media/xes_payment_state_model.png
new file mode 100644
index 000000000..3d8d67384
Binary files /dev/null and b/docs/_media/xes_payment_state_model.png differ
diff --git a/docs/_sidebar.md b/docs/_sidebar.md
index 8afd63a3c..461f9c34d 100644
--- a/docs/_sidebar.md
+++ b/docs/_sidebar.md
@@ -4,12 +4,6 @@
- [Quick start](quickstart.md)
-- Deployment
-
- - [Components](components.md)
- - [Docker](docker.md)
- - [Smart Contract](contract_deployment.md)
-
- Build and Run
- [Build All](build_all.md)
@@ -25,6 +19,12 @@
- [Back End](backend.md)
- [Front End](frontend.md)
+- Deployment
+
+ - [Components](components.md)
+ - [Docker](docker.md)
+ - [Smart Contract](contract_deployment.md)
+
- API
- [Authentication](api_auth.md)
@@ -36,7 +36,7 @@
- Workflow Extension
- - [Customer Workflow Nodes](custom_workflow_nodes.md)
+ - [Custom Workflow Nodes](custom_workflow_nodes.md)
- Contributing
diff --git a/docs/api_auth.md b/docs/api_auth.md
index 2543146cd..762080fef 100644
--- a/docs/api_auth.md
+++ b/docs/api_auth.md
@@ -1,12 +1,73 @@
# API Authentication
-
+## Create an API Key
You need first to get an `API Key` from your Proxeus account.
The key can then be used in your HTTP header as following:
+## Get a Session Token
+You get a session token using your login and API Key using basic HTTP
+authentication:
+
+### Request
+```
+GET /api/session/token
+
+Authorization: BASIC BASE64(username:password)
+```
+### Response
+```
+{
+ "token": "1e17b274-9f83-4348-8742-b28fb624cef6"
+}
```
-Authorization: Basic de52a38002d24c31743b7a25914fb39e121a45a7
+
+The username can be either
+* your account email or
+* the public Ethereum ID associated with your account
+
+The returned token can be used to access the API as described below.
+
+
+## Use the token to access the API
+Use the token create using the step above to access the API by adding a
+bearer authorization header as the following example:
+
+### Request
+```
+GET /api/user/workflow/list
+
+Authorization: Bearer 1e17b274-9f83-4348-8742-b28fb624cef6
+```
+
+### Response
+```
+[
+ {
+ "owner": "af25eed6-aa6d-47d0-8b31-86ca845335cc",
+ "groupAndOthers": {},
+ "published": false,
+ "id": "3784b001-e461-4ae6-879d-ff7b0d94af9c",
+ "name": "test",
+ "detail": "",
+ "updated": "2019-09-10T14:28:33.09133+02:00",
+ "created": "2019-09-06T17:18:06.790217+02:00",
+ "price": 0,
+ "data": null,
+ "ownerEthAddress": "",
+ "deactivated": false
+ }
+]
+```
+
+## Delete the Token
+To delete the session associated with the token, use the following request:
+
+### Request
+```
+DELETE /api/session/token
+
+Authorization: Bearer 1e17b274-9f83-4348-8742-b28fb624cef6
```
diff --git a/docs/api_get_workflow_schema.md b/docs/api_get_workflow_schema.md
index cceca026d..c41fbe5dc 100644
--- a/docs/api_get_workflow_schema.md
+++ b/docs/api_get_workflow_schema.md
@@ -41,77 +41,13 @@ GET /api/document/3e6ece3d-6b5d-4e79-aea0-0c06e14935cb/allAtOnce/schema
"AutoSteer":{
"required":false
},
- "AutoSteer2":{
- "required":false
- },
- "AutoSteer4":{
- "required":false
- },
"CHFXES":{
"required":true
},
"ETD":{
"datePattern":"dd.MM.yyyy",
"required":true
- },
- "ETD4":{
- "datePattern":"dd.MM.yyyy",
- "required":true
- },
- "EyeColor":{
- "required":true
- },
- "EyeColor2":{
- "required":true
- },
- "EyeColor4":{
- "required":true
- },
- "Hurts":{
- "required":true
- },
- "Hurts2":{
- "required":true
- },
- "Hurts4":{
- "required":true
- },
- "Name":{
- "required":false
- },
- "Name2":{
- "required":false
- },
- "Name4":{
- "required":false
- },
- "SkinType":{
- "required":false
- },
- "SkinType2":{
- "required":false
- },
- "SkinType4":{
- "required":false
- },
- "Status":{
- "required":true
- },
- "Status2":{
- "required":true
- },
- "Status4":{
- "required":true
- },
- "Zodiac":{
- "required":false
- },
- "Zodiac2":{
- "required":false
- },
- "Zodiac4":{
- "required":false
- }
+ }
}
}
}
diff --git a/docs/build_all.md b/docs/build_all.md
index f6c40bfc6..36845c02d 100644
--- a/docs/build_all.md
+++ b/docs/build_all.md
@@ -36,7 +36,7 @@ PATH=$PATH:$(go env GOPATH)/bin
### Clone repository
Clone the repository to the right location below your GOPATH:
```
-git clone git@git.proxeus.com:core/central.git $(go env GOPATH)/src/git.proxeus.com/core/central
+git clone git@github.com:ProxeusApp/proxeus-core.git $(go env GOPATH)/src/git.proxeus.com/core/central
cd $(go env GOPATH)/src/git.proxeus.com/core/central
```
diff --git a/docs/quickstart.md b/docs/quickstart.md
index 9f56af9f1..76b3af9e7 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -12,7 +12,7 @@ for Ethereum and email integration respectively.
Please create an account on those platform and get an API Keys.
-## Proxeus Demo Etherum Smart Contract
+## Proxeus Demo Ethereum Smart Contract
For your convenience, a demo smart contract is deployed on the Ropsten network at the following address:
diff --git a/docs/xes-payment.md b/docs/xes-payment.md
index 3a379a0e5..b19d7d93f 100644
--- a/docs/xes-payment.md
+++ b/docs/xes-payment.md
@@ -1,13 +1,6 @@
-# Proxeus platform
+# XES Payment Specification
-## Payment
-
-### Run payment tests
-```
-make test-payment
-```
-
-### Requirements
+## Requirements
* The user pays for each start of a workflow (Exception: user is owner of the workflow or the workflow price is 0).
* The payment is made before the workflow is initiated (e.g. before the first form is displayed).
* A successful payment (confirmed transaction) starts the workflow. A failed transaction shows an error message on the payment page.
@@ -15,14 +8,47 @@ make test-payment
* The default price is 0 XES. If the price is 0, the payment is skipped and the workflow starts right away. The price relevant for the workflow execution is the one that was valid at the time of transaction.
* The price is displayed where you choose the workflow to be started and on the payment page.
-### Technical concept
+## Technical concept
+
+### Outline
* A Workflow-Payments is an ERC-20 Transfer from the buying user's eth-address to the selling user's eth-address
* When a payment is submitted to the blockchain the platform-backend and the platform-frontend listens to the `Transfer`-Event, that is emitted by the blockchain as soon as the payment is confirmed.
* When the backend receives the event it persists the successful payment.
-* When the frontend receives the event it sends a "AddWorkflowPayment"-Request to the backend and claims the payment by sending the transaction hash of the payment and workflowId of the workflow the user wants to start. In case the request fails the frontend retries the request up to 10 times in an interval of 2 seconds.
+* When the frontend receives the event it sends a Request to the backend and redeems the payment by sending the transaction hash of the payment and workflowId of the workflow the user wants to start. In case the request fails the frontend retries the request multiple times.
* The backend checks for the validity of the transaction parameters. This check includes checking whether from-address, to-address and xes-amount match and that the payment has not been claimed before.
-* The payment is persisted as long as the workflow has not been finished by the user.
-* When the user finishes the workflow the payment is removed. When the user starts the same or any other workflow a new payment is due.
+* The payment is redeemed as soon as the workflow is started.
+* When a workflow is finished or has been deleted before finishing it and the user starts the same or any other workflow a new payment is due.
+
+### State model
+
+The following table shows the different states of a workflow payment:
+
+State|Description
+---|---
+created|Initial state of a *new workflow payment*. Used to capture payment intent for a specific workflow.
Attribute: workflowId from to price
+pending|State indicating a *successful payment transaction submission* (payment not confirmed yet). Used to associate payment transaction from the Blockchain event with a workflow purchase.
Attribute: transcationId
+confirmed|State indicating a *successfully completed payment transaction*.
+redeemed|State indicating the *use of a payment for a workflow execution*.
+timeout|State indicating either an *abandoned or unsuccessful payment attempt* that has timed out.
-### Known Issues
+The following diagram shows the state transitions of a workflow payment:
+
+![payment state model diagram](_media/xes_payment_state_model.png)
+
+### Sequence Diagram
+![Sequence Diagram payment flow xes](_media/xes_payment_sequence_diagram.png)
+
+## Known Issues
Due to the distributed and shared nature of the blockchain, in case multiple Proxeus Platform instances would be running, all payments would be shared between these Proxeus Platform instances. Therefore in the rare case where the same buyer and seller (same eth-address in metamask) use various Platform instances it would be possible to pay for a workflow on one instance and use the workflow without payment on another instance. We mitigate the risk of exploiting this behaviour by checking the from-address, to-address and xes-amount in the backend of the Platform. Thanks to this measure the described issue could potentially only arise, in a scenario where the same buyer and same seller of a workflow would be registered on multiple Proxeus Platform Instances and buyer and seller would both have to be registered on 2 or more of the same Proxeus Platform Instances. In addition to that the price of the workflow would have to exactly match the price on the other instances.
+
+## Payment tests
+To run the tests locally:
+```
+make test-payment
+```
+Expected result:
+```
+ ok git.proxeus.com/core/central/main/handlers/api 0.023s
+ ok git.proxeus.com/core/central/main/handlers/workflow 0.026s
+ ok git.proxeus.com/core/central/main/handlers/blockchain 0.025s
+```
diff --git a/main/app/document.go b/main/app/document.go
index 83ff74053..d766f4498 100644
--- a/main/app/document.go
+++ b/main/app/document.go
@@ -126,7 +126,6 @@ func (me *DocumentFlowInstance) init(wfd *workflow.Workflow, state []workflow.St
State: state,
GetData: me.getData, // for condition execution
NodeImpl: map[string]*workflow.NodeDef{
- "ibmsender": {InitImplFunc: me.newIBMSenderNodeImpl, Background: true},
"mailsender": {InitImplFunc: me.newMailSender, Background: true},
"priceretriever": {InitImplFunc: me.newPriceRetriever, Background: true},
"form": {InitImplFunc: me.newFormNodeImpl, Background: false},
@@ -203,10 +202,6 @@ func (me *DocumentFlowInstance) getDataByPath(dataPath string) (interface{}, err
return me.system.DB.UserData.GetData(me.auth, me.DataID, dataPath)
}
-func (me *DocumentFlowInstance) newIBMSenderNodeImpl(n *workflow.Node) (workflow.NodeIF, error) {
- return &IBMSenderNodeImpl{ctx: me}, nil
-}
-
func (me *DocumentFlowInstance) newMailSender(n *workflow.Node) (workflow.NodeIF, error) {
return &mailSenderNode{ctx: me}, nil
}
diff --git a/main/app/ibm_sender.go b/main/app/ibm_sender.go
deleted file mode 100644
index fd97a1d73..000000000
--- a/main/app/ibm_sender.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package app
-
-import (
- "bytes"
- "encoding/json"
- "io"
- "io/ioutil"
- "net/http"
-
- "log"
-
- "git.proxeus.com/core/central/sys/workflow"
-)
-
-const (
- forwardURL = ""
-)
-
-type IBMSenderNodeImpl struct {
- ctx *DocumentFlowInstance
-}
-
-func newIBMSenderNodeImpl(n *workflow.Node) (workflow.NodeIF, error) {
- return &IBMSenderNodeImpl{}, nil
-}
-
-func addConfigHeaders(req *http.Request) {
- req.Header.Set("clientid", "")
- req.Header.Set("tenantid", "")
- req.Header.Set("secret", "")
- req.Header.Set("oauthserverurl", "")
-}
-
-//data changes requested by customer
-func changeDataBeforeSend(dat interface{}) interface{} {
- if m, ok := dat.(map[string]interface{}); ok {
- if d, ok := m["input"]; ok {
- bts, _ := json.Marshal(d)
- var dataCopy map[string]interface{}
- json.Unmarshal(bts, &dataCopy)
- if cs, ok := dataCopy["CapitalSource"]; ok {
- bts, _ := json.Marshal(cs)
- dataCopy["CapitalSource"] = string(bts)
- }
- return dataCopy
- }
- }
- return dat
-}
-
-func (me *IBMSenderNodeImpl) Execute(n *workflow.Node) (proceed bool, err error) {
- b, err := json.Marshal(changeDataBeforeSend(me.ctx.getData()))
- if err != nil {
- return false, err
- }
- req, err := http.NewRequest("POST", forwardURL, bytes.NewReader(b))
- if err != nil {
- return false, err
- }
- req.Header.Set("Content-Type", "application/json")
- addConfigHeaders(req)
- resp, err := http.DefaultClient.Do(req)
- if err != nil {
- b2, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 100*1024))
- log.Printf("SERVER NOT ACCEPTED '%s', RESPONSE '%s'\n", b, b2)
- return false, err
- }
- return true, nil
-}
-
-func (me *IBMSenderNodeImpl) Remove(n *workflow.Node) {}
-func (me *IBMSenderNodeImpl) Close() {}
diff --git a/main/app/ibm_sender_test.go b/main/app/ibm_sender_test.go
deleted file mode 100644
index cc9561ef6..000000000
--- a/main/app/ibm_sender_test.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package app
-
-import (
- "encoding/json"
- "testing"
-)
-
-func TestChangeDataBeforeSend(t *testing.T) {
- dat := map[string]interface{}{"input": map[string]interface{}{"CapitalSource": []interface{}{"Andere"}}}
- newDat := changeDataBeforeSend(dat)
- bts, _ := json.Marshal(newDat)
- if string(bts) != `{"CapitalSource":"[\"Andere\"]"}` {
- t.Error(string(bts))
- }
-}
diff --git a/main/config/configuration.go b/main/config/configuration.go
index 73e71f282..b991aa7c8 100644
--- a/main/config/configuration.go
+++ b/main/config/configuration.go
@@ -23,6 +23,9 @@ type Configuration struct {
XESContractAddress string `json:"XESContractAddress" default:"0x84E0b37e8f5B4B86d5d299b0B0e33686405A3919"`
+ AirdropWalletfile string `json:"airdropWalletfile" usage:"Path to File containing Private Key of the Wallet to fund Airdrops of XES and Ether."`
+ AirdropWalletkey string `json:"airdropWalletkey" usage:"Path to File containing the Key for the Airdrop Private Key."`
+
model.Settings // extend cmd line args with settings
}
diff --git a/main/customNode/repository.go b/main/customNode/repository.go
index 4aedf529e..067aee2c6 100644
--- a/main/customNode/repository.go
+++ b/main/customNode/repository.go
@@ -6,12 +6,6 @@ import (
func List(nodeType string) *workflow.Node {
var repositories = make(map[string]*workflow.Node)
- repositories["ibmsender"] = &workflow.Node{
- ID: "1234123-1234123",
- Name: "IBM Sender",
- Detail: "sends all workflow data to an IBM service",
- Type: "ibmsender",
- }
repositories["mailsender"] = &workflow.Node{
ID: "1234123-1234124",
diff --git a/main/handlers/api/handlers.go b/main/handlers/api/handlers.go
index 14bb46033..6deb952d8 100644
--- a/main/handlers/api/handlers.go
+++ b/main/handlers/api/handlers.go
@@ -12,6 +12,10 @@ import (
"strings"
"time"
+ workflow2 "git.proxeus.com/core/central/sys/workflow"
+
+ "git.proxeus.com/core/central/main/handlers/payment"
+
"git.proxeus.com/core/central/main/handlers/blockchain"
"git.proxeus.com/core/central/sys/utils"
@@ -48,7 +52,6 @@ import (
)
var filenameRegex = regexp.MustCompile(`^[^\s][\p{L}\d.,_\-&: ]{3,}[^\s]$`)
-var ServerVersion string
func html(c echo.Context, p string) error {
bts, err := sys.ReadAllFile(p)
@@ -249,7 +252,6 @@ func GetInit(e echo.Context) error {
if len(settings.PlatformDomain) == 0 {
settings.PlatformDomain = e.Request().Host
}
-
return c.JSON(http.StatusOK, map[string]interface{}{"settings": settings, "configured": configured})
}
@@ -305,20 +307,22 @@ func PostInit(e echo.Context) error {
return c.NoContent(http.StatusOK)
}
-func ConfigHandler(e echo.Context) error {
- c := e.(*www.Context)
- sess := c.Session(false)
- var roles []model.RoleSet
- if sess != nil {
- roles = sess.AccessRights().RolesInRange()
+func ConfigHandler(version string) echo.HandlerFunc {
+ return func(e echo.Context) error {
+ c := e.(*www.Context)
+ sess := c.Session(false)
+ var roles []model.RoleSet
+ if sess != nil {
+ roles = sess.AccessRights().RolesInRange()
+ }
+ stngs := c.System().GetSettings()
+ return c.JSON(http.StatusOK, map[string]interface{}{
+ "roles": roles,
+ "blockchainNet": strings.Replace(stngs.BlockchainNet, "mainnet", "main", 1),
+ "blockchainProxeusFSAddress": stngs.BlockchainContractAddress,
+ "version": version,
+ })
}
- stngs := c.System().GetSettings()
- return c.JSON(http.StatusOK, map[string]interface{}{
- "roles": roles,
- "blockchainNet": strings.Replace(stngs.BlockchainNet, "mainnet", "main", 1),
- "blockchainProxeusFSAddress": stngs.BlockchainContractAddress,
- "version": ServerVersion,
- })
}
type loginForm struct {
@@ -468,10 +472,62 @@ func LoginWithWallet(c *www.Context, challenge, signature string) (bool, *model.
}
created = true
usr, err = c.System().DB.User.GetByBCAddress(address)
+ if err == nil && c.System().GetSettings().BlockchainNet == "ropsten" && c.System().GetSettings().AirdropEnabled == "true" {
+ go func() {
+ defer func() {
+ if r := recover(); r != nil {
+ log.Println("airdrop recover with err ", r)
+ }
+ }()
+ blockchain.GiveTokens(address)
+ }()
+ }
}
return created, usr, err
}
+func GetSessionTokenHandler(e echo.Context) (err error) {
+ c := e.(*www.Context)
+
+ username, apiKey := c.BasicAuth()
+
+ if username == "" || apiKey == "" {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ user, err := c.System().DB.User.APIKey(apiKey)
+ if err != nil {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ if user == nil {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ if user.Email != username && user.EthereumAddr != username {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ //create a new session only if role, id or name has changed
+ sess := c.SessionWithUser(user)
+ if sess == nil {
+ return c.NoContent(http.StatusBadRequest)
+ }
+ sess.Put("user", user)
+
+ c.Response().Header().Del("Set-Cookie")
+
+ return c.JSON(http.StatusOK, map[string]string{
+ "token": sess.ID(),
+ })
+}
+
+func DeleteSessionTokenHandler(e echo.Context) (err error) {
+ c := e.(*www.Context)
+ c.EndSession()
+ return c.NoContent(http.StatusOK)
+}
+
type TokenRequest struct {
Email string `json:"email" validate:"email=true,required=true"`
Token string `json:"token"`
@@ -545,38 +601,50 @@ func RegisterRequest(e echo.Context) (err error) {
stngs := c.System().GetSettings()
m.Role = stngs.DefaultRole
- if usr, err := c.System().DB.User.GetByEmail(m.Email); usr == nil {
- resetKey := m.Email + "_register"
- var token *TokenRequest
- err = c.System().Cache.Get(resetKey, &token)
+ if usr, _ := c.System().DB.User.GetByEmail(m.Email); usr != nil {
+ // always return ok if provided email was valid
+ // otherwise public users can test what email accounts exist
+ return c.NoContent(http.StatusOK)
+ }
+
+ resetKey := m.Email + "_register"
+ var token *TokenRequest
+
+ err = c.System().Cache.Get(resetKey, &token)
+ if err == nil {
+ return c.NoContent(http.StatusOK)
+ }
+
+ token = m
+ u2 := uuid.NewV4()
+ token.Token = u2.String()
+
+ if c.System().TestMode {
+ c.Response().Header().Set("X-Test-Token", token.Token)
+ } else {
+ err = c.System().EmailSender.Send(&email.Email{
+ From: stngs.EmailFrom,
+ To: []string{m.Email},
+ Subject: c.I18n().T("Register"),
+ Body: fmt.Sprintf(
+ "Hi there,\n\nplease proceed with your registration by visiting this link:\n%s\n\nIf you didn't request this, please ignore this email.\n\nProxeus",
+ helpers.AbsoluteURL(c, "/register/", token.Token),
+ ),
+ })
if err != nil {
- token = m
- u2 := uuid.NewV4()
- token.Token = u2.String()
- err = c.System().EmailSender.Send(&email.Email{
- From: stngs.EmailFrom,
- To: []string{m.Email},
- Subject: c.I18n().T("Register"),
- Body: fmt.Sprintf(
- "Hi there,\n\nplease proceed with your registration by visiting this link:\n%s\n\nIf you didn't request this, please ignore this email.\n\nProxeus",
- helpers.AbsoluteURL(c, "/register/", token.Token),
- ),
- })
- if err != nil {
- return c.NoContent(http.StatusExpectationFailed)
- }
- err = c.System().Cache.Put(resetKey, token)
- if err != nil {
- return c.NoContent(http.StatusInternalServerError)
- }
- err = c.System().Cache.Put(token.Token, token)
- if err != nil {
- return c.NoContent(http.StatusInternalServerError)
- }
+ return c.NoContent(http.StatusExpectationFailed)
}
}
- // always return ok if provided email was valid
- // otherwise public users can test what email accounts exist
+
+ err = c.System().Cache.Put(resetKey, token)
+ if err != nil {
+ return c.NoContent(http.StatusInternalServerError)
+ }
+ err = c.System().Cache.Put(token.Token, token)
+ if err != nil {
+ return c.NoContent(http.StatusInternalServerError)
+ }
+
return c.NoContent(http.StatusOK)
}
@@ -602,6 +670,71 @@ func Register(e echo.Context) error {
if err != nil {
return c.NoContent(http.StatusExpectationFailed)
}
+
+ // If some default workflows have to be assigned to the user, then clone them
+ workflowIds := strings.Split(c.System().GetSettings().DefaultWorkflowIds, ",")
+ workflows, err := c.System().DB.Workflow.GetList(root, workflowIds)
+ if err != nil {
+ log.Printf("Can't retrieve list of workflows (%v). Please check the ids exist. Error: %s", workflowIds, err.Error())
+ }
+ for _, workflow := range workflows {
+ w := workflow.Clone()
+ w.OwnerEthAddress = newUser.EthereumAddr
+ w.Owner = newUser.ID
+ newNodes := make(map[string]*workflow2.Node)
+ oldToNewIdsMap := make(map[string]string)
+ for oldId, node := range w.Data.Flow.Nodes {
+ if node.Type == "form" {
+ form, er := c.System().DB.Form.Get(root, node.ID)
+ if er != nil {
+ log.Println(err.Error())
+ }
+ f := form.Clone()
+ er = c.System().DB.Form.Put(newUser, &f)
+ if er != nil {
+ log.Println("can't put form" + err.Error())
+ }
+
+ oldToNewIdsMap[node.ID] = f.ID
+ node.ID = f.ID
+ newNodes[node.ID] = node
+ delete(w.Data.Flow.Nodes, oldId)
+
+ } else if node.Type == "template" {
+ template, er := c.System().DB.Template.Get(root, node.ID)
+ if er != nil {
+ log.Println(err.Error())
+ }
+ t := template.Clone()
+ er = c.System().DB.Template.Put(newUser, &t)
+ if er != nil {
+ log.Println("can't put template" + err.Error())
+ }
+ oldToNewIdsMap[node.ID] = t.ID
+ node.ID = t.ID
+ newNodes[node.ID] = node
+ delete(w.Data.Flow.Nodes, oldId)
+ } else {
+ newNodes[node.ID] = node
+ }
+ }
+ oldStartNodeId := w.Data.Flow.Start.NodeID
+ if _, ok := oldToNewIdsMap[oldStartNodeId]; ok {
+ w.Data.Flow.Start.NodeID = oldToNewIdsMap[oldStartNodeId]
+ }
+
+ // Now go through all connections and map them with the new ids
+ for _, node := range newNodes {
+ for _, connection := range node.Connections {
+ if _, ok := oldToNewIdsMap[connection.NodeID]; ok {
+ connection.NodeID = oldToNewIdsMap[connection.NodeID]
+ }
+ }
+ }
+ w.Data.Flow.Nodes = newNodes
+ c.System().DB.Workflow.Put(newUser, &w)
+ }
+
err = c.System().DB.User.PutPw(newUser.ID, p.Password)
if err != nil {
return c.NoContent(http.StatusExpectationFailed)
@@ -902,6 +1035,44 @@ func GetProfilePhotoHandler(e echo.Context) error {
return c.NoContent(http.StatusOK)
}
+// Check if a payment is required for current user for the workflow.
+// Return http OK if no payment is required or if payment is required and a payment a matching payment with status = "confirmed" is found
+func CheckForWorkflowPayment(e echo.Context) error {
+ c := e.(*www.Context)
+ sess := c.Session(true)
+ if sess == nil {
+ return c.NoContent(http.StatusNotFound)
+ }
+ workflowId := strings.TrimSpace(c.QueryParam("workflowId"))
+
+ if workflowId == "" {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ user, err := c.System().DB.User.Get(sess, sess.UserID())
+ if err != nil {
+ return c.NoContent(http.StatusUnauthorized)
+ }
+
+ paymentRequired, err := payment.CheckIfWorkflowPaymentRequired(c, workflowId)
+ if err != nil {
+ return c.NoContent(http.StatusBadRequest)
+ }
+ if paymentRequired {
+ _, err := c.System().DB.WorkflowPaymentsDB.GetByWorkflowIdAndFromEthAddress(workflowId, user.EthereumAddr, []string{model.PaymentStatusConfirmed})
+ if err != nil {
+ if err == strm.ErrNotFound {
+ return c.NoContent(http.StatusNotFound)
+ }
+ return c.NoContent(http.StatusBadRequest)
+ }
+ }
+
+ return c.NoContent(http.StatusOK)
+}
+
+var errNoPaymentFound = errors.New("no payment for workflow")
+
func DocumentHandler(e echo.Context) error {
c := e.(*www.Context)
ID := c.Param("ID")
@@ -918,13 +1089,44 @@ func DocumentHandler(e echo.Context) error {
if err != nil {
return c.String(http.StatusNotFound, err.Error())
}
+
docApp := getDocApp(c, sess, ID)
if docApp == nil {
- var usrDataItem *model.UserDataItem
- usrDataItem, err = c.System().DB.UserData.GetByWorkflow(sess, wf, false)
+ paymentRequired, err := payment.CheckIfWorkflowPaymentRequired(c, ID)
if err != nil {
return c.String(http.StatusNotFound, err.Error())
}
+
+ if paymentRequired {
+ sess := c.Session(false)
+ user, err := c.System().DB.User.Get(sess, sess.UserID())
+ if err != nil {
+ return c.NoContent(http.StatusBadRequest)
+ }
+ err = payment.RedeemPayment(c.System().DB.WorkflowPaymentsDB, wf.ID, user.EthereumAddr)
+ if err != nil {
+ log.Println("[redeemPayment] ", err.Error())
+ return c.String(http.StatusUnprocessableEntity, errNoPaymentFound.Error())
+ }
+ }
+
+ usrDataItem, _, err := c.System().DB.UserData.GetByWorkflow(sess, wf, false)
+ if err != nil {
+ if err != strm.ErrNotFound {
+ return c.String(http.StatusNotFound, err.Error())
+ }
+
+ usrDataItem = &model.UserDataItem{
+ WorkflowID: wf.ID,
+ Name: wf.Name,
+ Detail: wf.Detail,
+ }
+ err := c.System().DB.UserData.Put(sess, usrDataItem)
+ if err != nil {
+ return c.String(http.StatusInternalServerError, err.Error())
+ }
+ }
+
docApp, err = app.NewDocumentApp(usrDataItem, sess, c.System(), ID, sess.SessionDir())
if err != nil {
return c.String(http.StatusUnprocessableEntity, err.Error())
@@ -932,20 +1134,6 @@ func DocumentHandler(e echo.Context) error {
sess.Put("docApp_"+ID, docApp)
}
- err = checkIfWorkflowNeedsPayment(c.System().DB.WorkflowPaymentsDB, wf, sess.UserID())
- if err != nil {
- log.Println("[checkIfWorkflowNeedsPayment] ", err.Error())
- return c.String(http.StatusUnprocessableEntity, err.Error())
- }
-
- //check payment if not owner and not free
- if wf.Owner != sess.UserID() && wf.Price != 0 {
- workflowPaymentItem, err := c.System().DB.WorkflowPaymentsDB.GetByWorkflowId(ID)
- if err != nil || workflowPaymentItem == nil {
- return c.String(http.StatusUnprocessableEntity, "no payment for workflow")
- }
- }
-
st, err = docApp.Current(nil)
if err == nil {
return c.JSON(http.StatusOK, map[string]interface{}{"name": docApp.WF().Name, "status": st})
@@ -960,18 +1148,6 @@ func DocumentHandler(e echo.Context) error {
}
}
-var errNoPaymentFound = errors.New("no payment for workflow")
-
-func checkIfWorkflowNeedsPayment(WorkflowPaymentsDB storm.WorkflowPaymentsDBInterface, wf *model.WorkflowItem, userId string) error {
- if wf.Owner != userId && wf.Price != 0 {
- workflowPaymentItem, err := WorkflowPaymentsDB.GetByWorkflowId(wf.ID)
- if err != nil || workflowPaymentItem == nil {
- return errNoPaymentFound
- }
- }
- return nil
-}
-
func DocumentDeleteHandler(e echo.Context) error {
c := e.(*www.Context)
ID := c.Param("ID")
@@ -1084,17 +1260,6 @@ func DocumentNextHandler(e echo.Context) error {
return c.String(http.StatusBadRequest, err.Error())
}
- user, err := c.System().DB.User.Get(sess, sess.UserID())
- if err != nil {
- return c.NoContent(http.StatusBadRequest)
- }
-
- // Invalidate payment by removing from WorkflowPaymentsDB once workflow is finished.
- err = DeletePaymentIfExists(c, ID, user.EthereumAddr)
- if err != nil {
- return c.String(http.StatusBadRequest, err.Error())
- }
-
return c.JSON(http.StatusOK, map[string]interface{}{"id": dataID})
}
}
@@ -1116,24 +1281,6 @@ func DocumentNextHandler(e echo.Context) error {
return c.JSON(http.StatusOK, resData)
}
-// Remove payment from workflowPaymentsDB if exists
-func DeletePaymentIfExists(c *www.Context, workflowID, ethereumAddr string) error {
- workflowPaymentsDB := c.System().DB.WorkflowPaymentsDB
- workflowPaymentItem, err := c.System().DB.WorkflowPaymentsDB.GetByWorkflowIdAndFromEthAddress(workflowID, ethereumAddr)
- if err != nil {
- if err.Error() != "not found" { //if workflow is free or started by owner no payment will be found
- return err
- }
- } else {
- err = workflowPaymentsDB.Delete(workflowPaymentItem.TxHash)
- if err != nil {
- return err
- }
- }
-
- return nil
-}
-
func DocumentPrevHandler(e echo.Context) error {
c := e.(*www.Context)
ID := c.Param("ID")
@@ -1289,13 +1436,13 @@ func getDocApp(c *www.Context, sess *session.Session, ID string) *app.DocumentFl
func UserDocumentListHandler(e echo.Context) error {
c := e.(*www.Context)
- a, err := c.Auth()
- if err != nil {
+ sess := c.Session(false)
+ if sess == nil {
return c.NoContent(http.StatusUnauthorized)
}
contains := c.QueryParam("c")
settings := helpers.ReadReqSettings(c)
- items, err := c.System().DB.UserData.List(a, contains, settings, false)
+ items, err := c.System().DB.UserData.List(sess, contains, settings, false)
if err == nil && items != nil {
return c.JSON(http.StatusOK, items)
}
@@ -1304,12 +1451,12 @@ func UserDocumentListHandler(e echo.Context) error {
func UserDocumentGetHandler(e echo.Context) error {
c := e.(*www.Context)
- a, err := c.Auth()
- if err != nil {
+ sess := c.Session(false)
+ if sess == nil {
return c.NoContent(http.StatusUnauthorized)
}
id := c.Param("ID")
- items, err := c.System().DB.UserData.Get(a, id)
+ items, err := c.System().DB.UserData.Get(sess, id)
if err == nil && items != nil {
return c.JSON(http.StatusOK, items)
}
@@ -1418,7 +1565,7 @@ func UserDeleteHandler(e echo.Context) error {
//set workflow templates to deactivated
workflowDB := c.System().DB.Workflow
workflows, err := workflowDB.List(sess, "", map[string]interface{}{})
- if err != nil {
+ if err != nil && err.Error() != "not found" {
return c.NoContent(http.StatusInternalServerError)
}
for _, workflow := range workflows {
@@ -1832,16 +1979,16 @@ func AdminUserListHandler(e echo.Context) error {
func WorkflowSchema(e echo.Context) error {
c := e.(*www.Context)
- a, err := c.Auth()
- if err != nil {
+ sess := c.Session(false)
+ if sess == nil {
return c.NoContent(http.StatusUnauthorized)
}
id := c.Param("ID")
- wf, err := c.System().DB.Workflow.Get(a, id)
+ wf, err := c.System().DB.Workflow.Get(sess, id)
if err != nil {
return c.NoContent(http.StatusNotFound)
}
- fieldsAndRules := utils.GetAllFormFieldsWithRulesOf(wf.Data, a, c.System())
+ fieldsAndRules := utils.GetAllFormFieldsWithRulesOf(wf.Data, sess, c.System())
wfDetails := &struct {
*model.WorkflowItem
Data interface{} `json:"data"`
@@ -1854,8 +2001,8 @@ func WorkflowSchema(e echo.Context) error {
func WorkflowExecuteAtOnce(e echo.Context) error {
c := e.(*www.Context)
- a, err := c.Auth()
- if err != nil {
+ sess := c.Session(false)
+ if sess == nil {
return c.NoContent(http.StatusUnauthorized)
}
inputData, err := helpers.ParseDataFromReq(c)
@@ -1863,11 +2010,11 @@ func WorkflowExecuteAtOnce(e echo.Context) error {
return c.NoContent(http.StatusBadRequest)
}
id := c.Param("ID")
- wItem, err := c.System().DB.Workflow.Get(a, id)
+ wItem, err := c.System().DB.Workflow.Get(sess, id)
if err != nil || wItem.Data == nil {
return c.NoContent(http.StatusNotFound)
}
- err = app.ExecuteWorkflowAtOnce(c, a, wItem, inputData)
+ err = app.ExecuteWorkflowAtOnce(c, sess, wItem, inputData)
if err != nil {
if er, ok := err.(validate.ErrorMap); ok {
er.Translate(func(key string, args ...string) string {
@@ -1919,16 +2066,6 @@ func DeleteApiKeyHandler(e echo.Context) error {
return c.NoContent(http.StatusOK)
}
-func ManagementListHandler(e echo.Context) error {
- c := e.(*www.Context)
- length := random(10, 40)
- var a []map[string]interface{}
- for i := 0; i < length; i++ {
- a = append(a, map[string]interface{}{"id": fmt.Sprintf("id %v", i), "owner": "owner", "consignmentID": "cons", "timestamp": "time", "signatory": "sig"})
- }
- return c.JSON(http.StatusOK, a)
-}
-
func random(min, max int) int {
rand.Seed(time.Now().Unix())
return rand.Intn(max-min) + min
diff --git a/main/handlers/api/handlers_test.go b/main/handlers/api/handlers_test.go
deleted file mode 100644
index 378d8923f..000000000
--- a/main/handlers/api/handlers_test.go
+++ /dev/null
@@ -1,142 +0,0 @@
-package api
-
-import (
- "errors"
- "testing"
-
- "github.com/golang/mock/gomock"
- "github.com/stretchr/testify/assert"
-
- "git.proxeus.com/core/central/main/www"
- "git.proxeus.com/core/central/sys"
- "git.proxeus.com/core/central/sys/db/storm"
- "git.proxeus.com/core/central/sys/model"
-)
-
-func TestCheckIfWorkflowNeedsPayment(t *testing.T) {
- mockCtrl := gomock.NewController(t)
- defer mockCtrl.Finish()
-
- t.Run("CheckIfWorkflowNeedsPaymentShouldSucceedIfPaymentFound", func(t *testing.T) {
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2000000000000000000, From: "0x1", To: "0x3", TxHash: "0x5", WorkflowID: "3"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByWorkflowId("3").Return(workflowPaymentItem, nil).Times(1)
-
- workflow := &model.WorkflowItem{ID: "3", Price: 2000000000000000000}
- workflow.Owner = "33"
-
- result := checkIfWorkflowNeedsPayment(workflowPaymentsDBMock, workflow, "44")
-
- assert.NoError(t, result)
- })
-
- t.Run("CheckIfWorkflowNeedsPaymentShouldSucceedIfFreeWorkflow", func(t *testing.T) {
-
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
-
- workflow := &model.WorkflowItem{ID: "3", Price: 0}
- workflow.Owner = "33"
-
- result := checkIfWorkflowNeedsPayment(workflowPaymentsDBMock, workflow, "44")
-
- assert.NoError(t, result)
- })
-
- t.Run("CheckIfWorkflowNeedsPaymentShouldSucceedIfOwnerStartsWorkflow", func(t *testing.T) {
-
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
-
- workflow := &model.WorkflowItem{ID: "3", Price: 2000000000000000000}
- workflow.Owner = "33"
-
- result := checkIfWorkflowNeedsPayment(workflowPaymentsDBMock, workflow, "33")
-
- assert.NoError(t, result)
- })
-
- t.Run("CheckIfWorkflowNeedsPaymentShouldFailIfNoPaymentFound", func(t *testing.T) {
-
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByWorkflowId("3").Return(nil, nil).Times(1)
-
- workflow := &model.WorkflowItem{ID: "3", Price: 2000000000000000000}
- workflow.Owner = "33"
-
- result := checkIfWorkflowNeedsPayment(workflowPaymentsDBMock, workflow, "44")
-
- assert.EqualError(t, result, errNoPaymentFound.Error())
- })
-}
-
-func TestDeletePaymentIfExists(t *testing.T) {
-
- mockCtrl := gomock.NewController(t)
- defer mockCtrl.Finish()
-
- t.Run("DeletePaymentIfExistsShouldSucceedIfPaymentDeleted", func(t *testing.T) {
- wwwContext := &www.Context{}
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2000000000000000000, From: "0x1", To: "0x3", TxHash: "0x5"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByWorkflowIdAndFromEthAddress(gomock.Eq("1"), gomock.Eq("0x1")).Return(workflowPaymentItem, nil).Times(1)
- workflowPaymentsDBMock.EXPECT().Delete(gomock.Eq(workflowPaymentItem.TxHash)).Return(nil).Times(1)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock}
- www.SetSystem(system)
-
- result := DeletePaymentIfExists(wwwContext, "1", "0x1")
- assert.NoError(t, result)
- })
-
- t.Run("DeletePaymentIfExistsShouldSucceedIfNoPaymentToDelete", func(t *testing.T) {
- wwwContext := &www.Context{}
-
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- err := errors.New("not found")
- workflowPaymentsDBMock.EXPECT().GetByWorkflowIdAndFromEthAddress(gomock.Eq("1"), gomock.Eq("0x1")).Return(nil, err).Times(1)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock}
- www.SetSystem(system)
-
- result := DeletePaymentIfExists(wwwContext, "1", "0x1")
- assert.NoError(t, result)
- })
-
- t.Run("DeletePaymentIfExistsShouldFailOnGetError", func(t *testing.T) {
- wwwContext := &www.Context{}
-
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- err := errors.New("some error")
- workflowPaymentsDBMock.EXPECT().GetByWorkflowIdAndFromEthAddress(gomock.Eq("1"), gomock.Eq("0x1")).Return(nil, err).Times(1)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock}
- www.SetSystem(system)
-
- result := DeletePaymentIfExists(wwwContext, "1", "0x1")
- assert.Error(t, result)
- })
-
- t.Run("DeletePaymentIfExistsShouldFailOnDeleteError", func(t *testing.T) {
- wwwContext := &www.Context{}
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2000000000000000000, From: "0x1", To: "0x3", TxHash: "0x5"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByWorkflowIdAndFromEthAddress(gomock.Eq("1"), gomock.Eq("0x1")).Return(workflowPaymentItem, nil).Times(1)
-
- err := errors.New("some error")
-
- workflowPaymentsDBMock.EXPECT().Delete(gomock.Eq(workflowPaymentItem.TxHash)).Return(err).Times(1)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock}
- www.SetSystem(system)
-
- result := DeletePaymentIfExists(wwwContext, "1", "0x1")
- assert.Error(t, result)
- })
-
-}
diff --git a/main/handlers/blockchain/airdrop.go b/main/handlers/blockchain/airdrop.go
new file mode 100644
index 000000000..603fc4817
--- /dev/null
+++ b/main/handlers/blockchain/airdrop.go
@@ -0,0 +1,144 @@
+package blockchain
+
+import (
+ "context"
+ "encoding/json"
+ "io/ioutil"
+ "log"
+ "math/big"
+ "os"
+ "strconv"
+ "sync"
+
+ "github.com/ethereum/go-ethereum/accounts/keystore"
+
+ "git.proxeus.com/core/central/main/config"
+
+ "github.com/ethereum/go-ethereum/accounts/abi/bind"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/ethclient"
+
+ "git.proxeus.com/core/central/main/ethglue"
+)
+
+const etherUnit = 1000000000000000000.0
+
+var conn *ethclient.Client
+var nonceManager ethglue.NonceManager
+
+var mu sync.Mutex
+
+func GiveTokens(toWallet string) {
+ var err error
+ conn, err = ethglue.Dial(config.Config.EthClientURL)
+ if err != nil {
+ log.Panic("[airdrop] Failed to connect to the Ethereum client:", err)
+ }
+ nonceManager.OnDial(conn)
+
+ type Web3Keystore struct {
+ Address string `json:"address"`
+ }
+ var keystore Web3Keystore
+
+ keystoreJSON, err := ioutil.ReadFile(config.Config.AirdropWalletfile)
+ if err != nil {
+ log.Panic("[airdrop] Failed to read keystore:", err)
+ }
+ err = json.Unmarshal(keystoreJSON, &keystore)
+ if err != nil {
+ log.Panic("[airdrop] Failed to parse keystore:", err)
+ }
+
+ address := keystore.Address
+ nonceManager.OnAccountChange(address)
+ mu.Lock()
+ defer mu.Unlock()
+ FreeXES(toWallet)
+ FreeEth(toWallet)
+}
+
+// FreeXES sends ropsten XES to given wallet address
+func FreeXES(walletAddress string) {
+ log.Println("[airdrop] [Ropsten] Prepare XES for addr:", walletAddress)
+ amount := new(big.Int)
+ f, err := strconv.ParseFloat(config.Config.AirdropAmountXES, 64)
+
+ amount.SetInt64(int64(f * etherUnit))
+
+ // make transfer
+ token, err := NewToken(common.HexToAddress(config.Config.XESContractAddress), conn)
+ if err != nil {
+ log.Panic("[airdrop] Failed to instantiate a Token contract:", err)
+ }
+
+ keystorereader, err := os.Open(config.Config.AirdropWalletfile)
+ if err != nil {
+ log.Panic("[airdrop] Failed to read keystore:", err)
+ }
+
+ keystorekey, err := ioutil.ReadFile(config.Config.AirdropWalletkey)
+ if err != nil {
+ log.Panic("[airdrop] Failed to read keystore key:", err)
+ }
+
+ // Create an authorized transactor and spend Amount XES
+ auth, err := bind.NewTransactor(keystorereader, string(keystorekey[:len(keystorekey)-1]))
+ if err != nil {
+ log.Panic("[airdrop] Failed to create authorized transactor:", err)
+ }
+
+ auth.Nonce = nonceManager.NextNonce()
+ tx, err := token.Transfer(auth, common.HexToAddress(walletAddress), amount)
+ nonceManager.OnError(err)
+ if err != nil {
+ log.Panic("[airdrop] Failed to request token transfer:", err)
+ }
+ log.Println("[airdrop] [Ropsten] Sending XES with tx:", tx.Hash().String())
+}
+
+func FreeEth(walletAddress string) {
+ log.Println("[airdrop] [Ropsten] Prepare ETH for addr:", walletAddress)
+ amount := new(big.Int)
+ f, err := strconv.ParseFloat(config.Config.AirdropAmountEther, 64)
+ amount.SetInt64(int64(f * etherUnit))
+
+ var gasLimit = uint64(21000)
+ gasPrice, err := conn.SuggestGasPrice(context.Background())
+ nonceManager.OnError(err)
+ if err != nil {
+ log.Panic(err)
+ }
+
+ keystoreJSON, err := ioutil.ReadFile(config.Config.AirdropWalletfile)
+ if err != nil {
+ log.Panic("[airdrop] Failed to read keystore:", err)
+ }
+
+ keystorekey, err := ioutil.ReadFile(config.Config.AirdropWalletkey)
+ if err != nil {
+ log.Panic("[airdrop] Failed to read keystore key:", err)
+ }
+
+ unlockedKey, err := keystore.DecryptKey(keystoreJSON, string(keystorekey[:len(keystorekey)-1]))
+ if err != nil {
+ log.Panic("[airdrop] Failed to create authorized transactor:", err)
+ }
+
+ nonce := nonceManager.NextNonce()
+ tx := types.NewTransaction(nonce.Uint64(), common.HexToAddress(walletAddress), amount, gasLimit, gasPrice, nil)
+ // chainid 3 = ropsten, see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md#list-of-chain-ids
+ signedTx, err := types.SignTx(tx, types.NewEIP155Signer(big.NewInt(3)), unlockedKey.PrivateKey)
+ if err != nil {
+ log.Panic("[airdrop] Failed to sign transaction:", err)
+ }
+
+ err = conn.SendTransaction(context.Background(), signedTx)
+ nonceManager.OnError(err)
+ if err != nil {
+ log.Panic("[airdrop] Failed to send transaction:", err)
+ }
+
+ log.Println("[airdrop] [Ropsten] Sending ETH with tx:", signedTx.Hash().String())
+}
diff --git a/main/handlers/blockchain/listener_test.go b/main/handlers/blockchain/listener_test.go
deleted file mode 100644
index f2311d8f2..000000000
--- a/main/handlers/blockchain/listener_test.go
+++ /dev/null
@@ -1,115 +0,0 @@
-package blockchain
-
-import (
- "log"
- "math/big"
- "testing"
-
- "github.com/ethereum/go-ethereum/common"
- "github.com/ethereum/go-ethereum/core/types"
- "github.com/golang/mock/gomock"
-
- "git.proxeus.com/core/central/sys/db/storm"
- "git.proxeus.com/core/central/sys/model"
-)
-
-type (
- workflowPaymentItemMatcher struct {
- transactionHash string
- from string
- to string
- xes uint64
- }
-)
-
-func (me *workflowPaymentItemMatcher) Matches(x interface{}) bool {
- workflowPaymentItem, ok := x.(*model.WorkflowPaymentItem)
- if !ok {
- log.Fatal("workflowPaymentItemMatcher cast error")
- }
- return workflowPaymentItem.WorkflowID == "" &&
- me.transactionHash == workflowPaymentItem.TxHash &&
- me.from == workflowPaymentItem.From &&
- me.to == workflowPaymentItem.To &&
- me.xes == workflowPaymentItem.Xes
-}
-func (me *workflowPaymentItemMatcher) String() string {
- return "workflowPaymentItem needs to match"
-}
-
-func TestCheckIfWorkflowNeedsPayment(t *testing.T) {
- mockCtrl := gomock.NewController(t)
- defer mockCtrl.Finish()
-
- t.Run("CheckIfEventsHandlerSavesBlockchainEvent1Xes", func(t *testing.T) {
- err := runTest(mockCtrl, big.NewInt(1), true)
- if err != nil {
- log.Fatal(err.Error())
- }
- })
- t.Run("CheckIfEventsHandlerSavesBlockchainEvent130Xes", func(t *testing.T) {
- err := runTest(mockCtrl, big.NewInt(130), true)
- if err != nil {
- log.Fatal(err.Error())
- }
- })
- t.Run("CheckIfEventsHandlerSavesBlockchainEvent100000000000000000Xes", func(t *testing.T) {
- err := runTest(mockCtrl, big.NewInt(100000000000000000), true)
- if err != nil {
- log.Fatal(err.Error())
- }
- })
- t.Run("CheckIfEventsHandlerShowsOverflowError", func(t *testing.T) {
- xesTmp := big.NewInt(1000000000000000000)
- xes := xesTmp.Mul(xesTmp, big.NewInt(1000000000000000000))
- err := runTest(mockCtrl, xes, false)
- if err != xesOverflowError {
- if err != nil {
- log.Fatal("expected xesOverflowError but got: ", err.Error())
- }
- log.Fatal("expected xesOverflowError but got: nil")
- }
- })
-}
-
-func runTest(mockCtrl *gomock.Controller, xesAmount *big.Int, addPaymentExpected bool) error {
- adapterMock := NewMockadapter(mockCtrl)
- adapterMock.EXPECT().eventFromLog(gomock.Any(), gomock.Any(), gomock.Eq("Transfer")).Return(nil).Times(1)
-
- transactionHash := "0x04f1bbf224b5876d91c74984c4d7f7768c5cc9da5b7f7afe1a31ef9115310f67"
- from := "0xe4f2604bc8300004aa4477af5f2AdBd37765F3F7"
- to := "0xe902Fb81617079236cB6eF8f34b2A1e759ef676D"
-
- xesUint := xesAmount.Uint64()
-
- xes := xesAmount.Mul(xesAmount, big.NewInt(1000000000000000000))
-
- workflowPaymentItemMatcher := &workflowPaymentItemMatcher{
- transactionHash: transactionHash,
- from: from,
- to: to,
- xes: xesUint,
- }
- paymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
-
- if addPaymentExpected {
- paymentsDBMock.EXPECT().Add(workflowPaymentItemMatcher)
- }
-
- event := &XesMainTokenTransfer{
- Value: xes,
- FromAddress: common.HexToAddress(from),
- ToAddress: common.HexToAddress(to),
- }
-
- listener := &Paymentlistener{
- xesAdapter: adapterMock,
- workflowPaymentsDB: paymentsDBMock,
- }
-
- ethLog := &types.Log{
- TxHash: common.HexToHash(transactionHash),
- }
-
- return listener.eventsHandler(ethLog, event)
-}
diff --git a/main/handlers/blockchain/payment_listener.go b/main/handlers/blockchain/payment_listener.go
new file mode 100644
index 000000000..abc207f68
--- /dev/null
+++ b/main/handlers/blockchain/payment_listener.go
@@ -0,0 +1,162 @@
+package blockchain
+
+import (
+ "context"
+ "errors"
+ "log"
+ "math/big"
+ "time"
+
+ "github.com/ethereum/go-ethereum"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
+
+ "git.proxeus.com/core/central/main/ethglue"
+ "git.proxeus.com/core/central/sys/db/storm"
+
+ strm "github.com/asdine/storm"
+)
+
+type (
+ listener struct {
+ logs chan types.Log
+ ethWebSocketURL string
+ ethURL string
+ sub ethereum.Subscription
+ }
+ PaymentListener struct {
+ listener
+ workflowPaymentsDB storm.WorkflowPaymentsDBInterface
+ xesAdapter adapter
+ }
+)
+
+func NewPaymentListener(xesAdapter adapter, ethWebSocketURL, ethURL string, workflowPaymentsDB storm.WorkflowPaymentsDBInterface) *PaymentListener {
+ me := &PaymentListener{}
+ me.xesAdapter = xesAdapter
+ me.ethWebSocketURL = ethWebSocketURL
+ me.ethURL = ethURL
+ me.workflowPaymentsDB = workflowPaymentsDB
+ me.logs = make(chan types.Log, 200)
+ return me
+}
+
+func (me *PaymentListener) Listen(ctx context.Context) {
+ var readyCh <-chan struct{}
+
+ for {
+ readyCh = me.ethConnectWebSocketsAsync(ctx)
+ select {
+ case <-readyCh:
+ log.Println("[paymentlistener] listen on contract started. contract address: ", me.xesAdapter.getContractAddress())
+ reconnect := me.listenLoop(ctx)
+ if !reconnect {
+ log.Printf("[paymentlistener][eventHandler] finished")
+ return
+ }
+ case <-ctx.Done():
+ log.Printf("[paymentlistener][eventHandler] done")
+ return
+ }
+ }
+ return
+}
+
+func (me *PaymentListener) listenLoop(ctx context.Context) (shouldReconnect bool) {
+ for {
+ select {
+ case <-ctx.Done():
+ return false
+ case err, ok := <-me.sub.Err():
+ if !ok {
+ return true
+ }
+ log.Println("ERROR sub", err)
+ return true
+ case vLog, ok := <-me.logs:
+ if !ok {
+ return true
+ }
+ event := new(XesMainTokenTransfer)
+ err := me.eventsHandler(&vLog, event)
+ if err != nil {
+ if err != strm.ErrNotFound { //ErrNotFound already logged in eventsHandler
+ if err == xesOverflowError {
+ log.Fatal("[blockchain][listener] overflow err: ", err.Error())
+ }
+ log.Println("[blockchain][listener] err: ", err.Error())
+ }
+ }
+ }
+ }
+}
+
+func (me *PaymentListener) ethConnectWebSocketsAsync(ctx context.Context) <-chan struct{} {
+
+ filterAddresses := []common.Address{common.HexToAddress(me.xesAdapter.getContractAddress())}
+
+ readyCh := make(chan struct{})
+ go func() {
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ default:
+ var err error
+ ethwsconn, err := ethglue.DialContext(ctx, me.ethWebSocketURL)
+ if err != nil {
+ log.Printf("failed to dial for eth events, will retry (%s)\n", err)
+ time.Sleep(time.Second * 4)
+ continue
+ }
+ query := ethereum.FilterQuery{
+ Addresses: filterAddresses,
+ }
+ ctx, cancel := context.WithTimeout(ctx, time.Duration(10*time.Second))
+ me.sub, err = ethwsconn.SubscribeFilterLogs(ctx, query, me.logs)
+ cancel()
+ if err != nil {
+ log.Printf("failed to subscribe for eth events, will retry (%s)\n", err)
+ time.Sleep(time.Second * 4)
+ continue
+ }
+ // success!
+ readyCh <- struct{}{}
+ return
+ }
+ }
+ }()
+ return readyCh
+}
+
+var xesOverflowError = errors.New("overflow on xes event")
+
+func (me *PaymentListener) eventsHandler(lg *types.Log, event *XesMainTokenTransfer) error {
+ log.Printf("[PaymentListener][eventHandler] txHash: %s, value: %s, %v",
+ lg.TxHash.String(), event.Value.String(), lg)
+ err := me.xesAdapter.eventFromLog(event, lg, "Transfer")
+ if err != nil {
+ return err
+ }
+
+ bigXes := event.Value.Div(event.Value, big.NewInt(1000000000000000000)) //to xes-ether
+
+ if !bigXes.IsUint64() {
+ log.Println("[PaymentListener][eventHandler] error overflow on transfer event value:", event.Value)
+ return xesOverflowError
+ }
+
+ err = me.workflowPaymentsDB.ConfirmPayment(lg.TxHash.String(), event.FromAddress.String(), event.ToAddress.String(), bigXes.Uint64())
+ if err != nil {
+ if err == strm.ErrNotFound {
+ log.Printf(" [PaymentListener][eventHandler] info: no matching payment found for txHash: %s, reason: %s", lg.TxHash.String(), err.Error())
+ return err
+ }
+
+ log.Printf(" [PaymentListener][eventHandler] err: workflowPaymentsDB.ConfirmPayment for txHash: %s, err: %s", lg.TxHash.String(), err.Error())
+ return err
+ }
+ log.Println("[PaymentListener][eventHandler] confirmed payment with hash: ", lg.TxHash.String())
+
+ return nil
+}
diff --git a/main/handlers/blockchain/payment_listener_test.go b/main/handlers/blockchain/payment_listener_test.go
new file mode 100644
index 000000000..ba0b383b2
--- /dev/null
+++ b/main/handlers/blockchain/payment_listener_test.go
@@ -0,0 +1,315 @@
+package blockchain
+
+import (
+ "errors"
+ "math/big"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/golang/mock/gomock"
+
+ strm "github.com/asdine/storm"
+
+ "git.proxeus.com/core/central/sys/model"
+
+ "git.proxeus.com/core/central/sys/db/storm"
+)
+
+var errCleanupTestData = errors.New("db data has not been cleanup up after finishing tests")
+
+func removePaymentIfExists(workflowPaymentsDB storm.WorkflowPaymentsDBInterface,
+ persistedPaymentItem **model.WorkflowPaymentItem) {
+
+ if *persistedPaymentItem != nil {
+ err := workflowPaymentsDB.Remove(*persistedPaymentItem)
+ if err != nil {
+ panic(err.Error())
+ }
+ }
+}
+
+func TestPaymentEventHandling(t *testing.T) {
+ mockCtrl := gomock.NewController(t)
+ defer mockCtrl.Finish()
+
+ workflowPaymentsDB, err := storm.NewWorkflowPaymentDB(".test_data")
+ if err != nil {
+ panic(err)
+ }
+
+ defer func() {
+ payments, err := workflowPaymentsDB.All()
+ if err != nil {
+ panic(err)
+ }
+ if len(payments) != 0 {
+ panic(errCleanupTestData)
+ }
+
+ err = os.Remove(filepath.Join(".test_data", storm.WorkflowPaymentDBDir, storm.WorkflowPaymentDB))
+ if err != nil {
+ panic(err.Error())
+ }
+ err = os.Remove(filepath.Join(".test_data", storm.WorkflowPaymentDBDir))
+ if err != nil {
+ panic(err.Error())
+ }
+ err = os.Remove(".test_data")
+ if err != nil {
+ panic(err.Error())
+ }
+
+ }()
+
+ t.Run("ShouldSetPendingPaymentWithTxHashToConfirmed", func(t *testing.T) {
+
+ var persistedPaymentItem *model.WorkflowPaymentItem
+ defer removePaymentIfExists(workflowPaymentsDB, &persistedPaymentItem)
+
+ paymentID := "1"
+ paymentTxHash := "0x04f1bbf224b5876d91c74984c4d7f7768c5cc9da5b7f7afe1a31ef9115310f67"
+ from := "0xe4f2604bc8300004aa4477af5f2AdBd37765F3F7"
+ to := "0xe902Fb81617079236cB6eF8f34b2A1e759ef676D"
+ eventTxHash := paymentTxHash
+ expectedStatus := model.PaymentStatusConfirmed
+ xesAmount := big.NewInt(1)
+
+ err = runTest(mockCtrl, workflowPaymentsDB, xesAmount, eventTxHash, paymentID, paymentTxHash, from,
+ to, model.PaymentStatusPending, xesAmount)
+ if err != nil {
+ panic(err.Error())
+ }
+
+ persistedPaymentItem, err := workflowPaymentsDB.Get(paymentID)
+ if err != nil {
+ panic(err)
+ }
+
+ if persistedPaymentItem.Status != expectedStatus {
+ t.Errorf("Expected persistedPaymentItem to have status %s but got: %s",
+ expectedStatus, persistedPaymentItem.Status)
+ }
+
+ if persistedPaymentItem.TxHash != eventTxHash {
+ t.Errorf("Expected persistedPaymentItem to have TxHash %s but got: %s",
+ eventTxHash, persistedPaymentItem.TxHash)
+ }
+ })
+
+ t.Run("ShouldSetCreatedPaymentWithoutTxHashToConfirmed", func(t *testing.T) {
+
+ var persistedPaymentItem *model.WorkflowPaymentItem
+ defer removePaymentIfExists(workflowPaymentsDB, &persistedPaymentItem)
+
+ paymentID := "2"
+ eventTxHash := "0x94420cb493b721c627feb8f911df8546bba0f911cb4433dfabd4c9c65012593c"
+ paymentTxHash := ""
+ from := "0xe4f2604bc8300004aa4477af5f2AdBd37765F3F7"
+ to := "0xe902Fb81617079236cB6eF8f34b2A1e759ef676D"
+ expectedStatus := model.PaymentStatusConfirmed
+ xesAmount := big.NewInt(100000000000000000)
+
+ err = runTest(mockCtrl, workflowPaymentsDB, xesAmount, eventTxHash, paymentID,
+ paymentTxHash, from, to, model.PaymentStatusCreated, xesAmount)
+ if err != nil {
+ panic(err.Error())
+ }
+
+ persistedPaymentItem, err := workflowPaymentsDB.Get(paymentID)
+ if err != nil {
+ panic(err)
+ }
+
+ if persistedPaymentItem.Status != expectedStatus {
+ t.Errorf("Expected persistedPaymentItem to have status %s but got: %s",
+ expectedStatus, persistedPaymentItem.Status)
+ }
+
+ if persistedPaymentItem.TxHash != eventTxHash {
+ t.Errorf("Expected persistedPaymentItem to have TxHash %s but got: %s",
+ eventTxHash, persistedPaymentItem.TxHash)
+ }
+ })
+
+ t.Run("ShouldIgnoreAlreadyRedeemedPayment", func(e *testing.T) {
+
+ var persistedPaymentItem *model.WorkflowPaymentItem
+ defer removePaymentIfExists(workflowPaymentsDB, &persistedPaymentItem)
+
+ paymentID := "3"
+ eventTxHash := "0x8c962ca22918cf37e89a7bef93efe2938320c38ec113321d847d6fc48f2ba2fa"
+ paymentTxHash := ""
+ from := "0xe4f2604bc8300004aa4477af5f2AdBd37765F3F7"
+ to := "0xe902Fb81617079236cB6eF8f34b2A1e759ef676D"
+ expectedStatus := model.PaymentStatusRedeemed
+ xesAmount := big.NewInt(1)
+
+ err = runTest(mockCtrl, workflowPaymentsDB, xesAmount, eventTxHash, paymentID,
+ paymentTxHash, from, to, model.PaymentStatusRedeemed, xesAmount)
+
+ if err != strm.ErrNotFound {
+ if err != nil {
+ t.Errorf("Expected to have %s but got: %s", strm.ErrNotFound, err.Error())
+ }
+ t.Errorf("Expected to have %s but got: nil", strm.ErrNotFound)
+ }
+
+ persistedPaymentItem, err = workflowPaymentsDB.Get(paymentID)
+ if err != nil {
+ panic(err)
+ }
+
+ if persistedPaymentItem.Status != expectedStatus {
+ t.Errorf("Expected persistedPaymentItem to have status %s but got: %s",
+ expectedStatus, persistedPaymentItem.Status)
+ }
+ })
+
+ t.Run("ShouldIgnorePaymentOnNotMatchingXesAmount", func(e *testing.T) {
+
+ var persistedPaymentItem *model.WorkflowPaymentItem
+ defer removePaymentIfExists(workflowPaymentsDB, &persistedPaymentItem)
+
+ paymentID := "4"
+ paymentTxHash := "0x04f1bbf224b5876d91c74984c4d7f7768c5cc9da5b7f7afe1a31ef9115310f67"
+ from := "0xe4f2604bc8300004aa4477af5f2AdBd37765F3F7"
+ to := "0xe902Fb81617079236cB6eF8f34b2A1e759ef676D"
+ eventTxHash := paymentTxHash
+ expectedStatus := model.PaymentStatusCreated
+ xesAmount := big.NewInt(2)
+ xesAmountEvent := big.NewInt(1)
+
+ err = runTest(mockCtrl, workflowPaymentsDB, xesAmountEvent, eventTxHash, paymentID, paymentTxHash, from,
+ to, model.PaymentStatusCreated, xesAmount)
+ if err != strm.ErrNotFound {
+ if err != nil {
+ t.Errorf("Expected to have %s but got: %s", strm.ErrNotFound, err.Error())
+ }
+ t.Errorf("Expected to have %s but got: nil", strm.ErrNotFound)
+ }
+
+ persistedPaymentItem, err := workflowPaymentsDB.Get(paymentID)
+ if err != nil {
+ panic(err)
+ }
+
+ if persistedPaymentItem.Status != expectedStatus {
+ t.Errorf("Expected persistedPaymentItem to have status %s but got: %s",
+ expectedStatus, persistedPaymentItem.Status)
+ }
+
+ if persistedPaymentItem.TxHash != eventTxHash {
+ t.Errorf("Expected persistedPaymentItem to have TxHash %s but got: %s",
+ eventTxHash, persistedPaymentItem.TxHash)
+ }
+ })
+
+ t.Run("ShouldReturnErrorOverflowOnBigXesAmount", func(e *testing.T) {
+
+ var persistedPaymentItem *model.WorkflowPaymentItem
+ defer removePaymentIfExists(workflowPaymentsDB, &persistedPaymentItem)
+
+ paymentID := "5"
+ eventTxHash := "0x8c962ca22918cf37e89a7bef93efe2938320c38ec113321d847d6fc48f2ba2fa"
+ paymentTxHash := ""
+ from := "0xe4f2604bc8300004aa4477af5f2AdBd37765F3F7"
+ to := "0xe902Fb81617079236cB6eF8f34b2A1e759ef676D"
+
+ xesTmp := big.NewInt(1000000000000000000)
+ xesAmount := xesTmp.Mul(xesTmp, big.NewInt(1000000000000000000))
+
+ err = runTest(mockCtrl, workflowPaymentsDB, xesAmount, eventTxHash, paymentID,
+ paymentTxHash, from, to, model.PaymentStatusRedeemed, xesAmount)
+
+ if err != xesOverflowError {
+ if err != nil {
+ t.Errorf("Expected to have %s but got: %s", xesOverflowError, err.Error())
+ }
+ t.Errorf("Expected to have %s but got: nil", xesOverflowError)
+ }
+
+ persistedPaymentItem, err = workflowPaymentsDB.Get(paymentID)
+ if err != nil {
+ panic(err)
+ }
+ })
+
+ t.Run("ShouldReturnErrorOverflowOnNegativeXesAmount", func(e *testing.T) {
+
+ var persistedPaymentItem *model.WorkflowPaymentItem
+ defer removePaymentIfExists(workflowPaymentsDB, &persistedPaymentItem)
+
+ paymentID := "6"
+ eventTxHash := "0x8c962ca22918cf37e89a7bef93efe2938320c38ec113321d847d6fc48f2ba2fa"
+ paymentTxHash := ""
+ from := "0xe4f2604bc8300004aa4477af5f2AdBd37765F3F7"
+ to := "0xe902Fb81617079236cB6eF8f34b2A1e759ef676D"
+
+ xesAmount := big.NewInt(-1)
+
+ err = runTest(mockCtrl, workflowPaymentsDB, xesAmount, eventTxHash, paymentID,
+ paymentTxHash, from, to, model.PaymentStatusRedeemed, xesAmount)
+
+ if err != xesOverflowError {
+ if err != nil {
+ t.Errorf("Expected to have %s but got: %s", xesOverflowError, err.Error())
+ }
+ t.Errorf("Expected to have %s but got: nil", xesOverflowError)
+ }
+
+ persistedPaymentItem, err = workflowPaymentsDB.Get(paymentID)
+ if err != nil {
+ panic(err)
+ }
+ })
+
+}
+
+func runTest(mockCtrl *gomock.Controller, workflowPaymentsDB *storm.WorkflowPaymentsDB,
+ eventXesAmount *big.Int, eventTxHash, paymentID, paymentTxHash, from, to, status string, xesAmount *big.Int) error {
+
+ adapterMock := NewMockadapter(mockCtrl)
+ adapterMock.EXPECT().eventFromLog(gomock.Any(), gomock.Any(), gomock.Eq("Transfer")).Return(nil).Times(1)
+
+ newPaymentItem := &model.WorkflowPaymentItem{
+ ID: paymentID,
+ From: from,
+ To: to,
+ CreatedAt: time.Now(),
+ Status: status,
+ Xes: xesAmount.Uint64(),
+ WorkflowID: "1",
+ }
+
+ if paymentTxHash != "" {
+ newPaymentItem.TxHash = paymentTxHash
+ }
+
+ err := workflowPaymentsDB.Save(newPaymentItem)
+ if err != nil {
+ return err
+ }
+
+ eventXes := xesAmount.Mul(eventXesAmount, big.NewInt(1000000000000000000))
+
+ event := &XesMainTokenTransfer{
+ Value: eventXes,
+ FromAddress: common.HexToAddress(from),
+ ToAddress: common.HexToAddress(to),
+ }
+
+ listener := &PaymentListener{
+ xesAdapter: adapterMock,
+ workflowPaymentsDB: workflowPaymentsDB,
+ }
+
+ ethLog := &types.Log{
+ TxHash: common.HexToHash(eventTxHash),
+ }
+
+ return listener.eventsHandler(ethLog, event)
+}
diff --git a/main/handlers/blockchain/listener.go b/main/handlers/blockchain/signature_listener.go
similarity index 56%
rename from main/handlers/blockchain/listener.go
rename to main/handlers/blockchain/signature_listener.go
index 859e6cbc6..588c58402 100644
--- a/main/handlers/blockchain/listener.go
+++ b/main/handlers/blockchain/signature_listener.go
@@ -3,13 +3,9 @@ package blockchain
import (
"context"
"encoding/hex"
- "errors"
"log"
- "math/big"
"time"
- "git.proxeus.com/core/central/sys/email"
-
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
@@ -18,21 +14,10 @@ import (
"git.proxeus.com/core/central/main/ethglue"
"git.proxeus.com/core/central/sys/db/storm"
- "git.proxeus.com/core/central/sys/model"
+ "git.proxeus.com/core/central/sys/email"
)
type (
- listener struct {
- logs chan types.Log
- ethWebSocketURL string
- ethURL string
- sub ethereum.Subscription
- }
- Paymentlistener struct {
- listener
- workflowPaymentsDB storm.WorkflowPaymentsDBInterface
- xesAdapter adapter
- }
Signaturelistener struct {
listener
signatureRequestsDB storm.SignatureRequestsDB
@@ -45,127 +30,9 @@ type (
}
)
-func NewPaymentListener(xesAdapter adapter, ethWebSocketURL, ethURL string, workflowPaymentsDB storm.WorkflowPaymentsDBInterface) *Paymentlistener {
- me := &Paymentlistener{}
- me.xesAdapter = xesAdapter
- me.ethWebSocketURL = ethWebSocketURL
- me.ethURL = ethURL
- me.workflowPaymentsDB = workflowPaymentsDB
- me.logs = make(chan types.Log, 200)
- return me
-}
-
-func (me *Paymentlistener) Listen(ctx context.Context) {
- var readyCh <-chan struct{}
-
- for {
- readyCh = me.ethConnectWebSocketsAsync(ctx)
- select {
- case <-readyCh:
- log.Println("[paymentlistener] listen on contract started. contract address: ", me.xesAdapter.getContractAddress())
- reconnect := me.listenLoop(ctx)
- if !reconnect {
- log.Printf("[paymentlistener][eventHandler] finished")
- return
- }
- case <-ctx.Done():
- log.Printf("[paymentlistener][eventHandler] done")
- return
- }
- }
- return
-}
-
-func (me *Paymentlistener) listenLoop(ctx context.Context) (shouldReconnect bool) {
- for {
- select {
- case <-ctx.Done():
- return false
- case err, ok := <-me.sub.Err():
- if !ok {
- return true
- }
- log.Println("ERROR sub", err)
- return true
- case vLog, ok := <-me.logs:
- if !ok {
- return true
- }
- event := new(XesMainTokenTransfer)
- err := me.eventsHandler(&vLog, event)
- if err != nil {
- if err == xesOverflowError {
- log.Fatal("[blockchain][listener] ", err.Error())
- }
- log.Println("[blockchain][listener] ", err.Error())
- }
- }
- }
-}
-
-func (me *Paymentlistener) ethConnectWebSocketsAsync(ctx context.Context) <-chan struct{} {
-
- filterAddresses := []common.Address{common.HexToAddress(me.xesAdapter.getContractAddress())}
-
- readyCh := make(chan struct{})
- go func() {
- for {
- select {
- case <-ctx.Done():
- return
- default:
- var err error
- ethwsconn, err := ethglue.DialContext(ctx, me.ethWebSocketURL)
- if err != nil {
- log.Printf("failed to dial for eth events, will retry (%s)\n", err)
- continue
- }
- query := ethereum.FilterQuery{
- Addresses: filterAddresses,
- }
- ctx, cancel := context.WithTimeout(ctx, time.Duration(10*time.Second))
- me.sub, err = ethwsconn.SubscribeFilterLogs(ctx, query, me.logs)
- cancel()
- if err != nil {
- log.Printf("failed to subscribe for eth events, will retry (%s)\n", err)
- time.Sleep(time.Second * 4)
- continue
- }
- // success!
- readyCh <- struct{}{}
- return
- }
- }
- }()
- return readyCh
-}
-
-var xesOverflowError = errors.New("overflow on xes event")
-
-func (me *Paymentlistener) eventsHandler(lg *types.Log, event *XesMainTokenTransfer) error {
- log.Printf("[paymentlistener][eventHandler] txHash: %s, value: %s, %v",
- lg.TxHash.String(), event.Value.String(), lg)
- if err := me.xesAdapter.eventFromLog(event, lg, "Transfer"); err != nil {
- return err
- }
-
- bigXes := event.Value.Div(event.Value, big.NewInt(1000000000000000000)) //to xes-ether
-
- if !bigXes.IsUint64() {
- log.Println(" error overflow on transfer event value:", event.Value)
- return xesOverflowError
- }
-
- item := &model.WorkflowPaymentItem{
- TxHash: lg.TxHash.String(),
- Xes: bigXes.Uint64(),
- From: event.FromAddress.String(),
- To: event.ToAddress.String(),
- }
- return me.workflowPaymentsDB.Add(item)
-}
+func NewSignatureListener(ethWebSocketURL, ethURL, BlockchainContractAddress string, SignatureRequestsDB *storm.SignatureRequestsDB,
+ UserDB storm.UserDBInterface, EmailSender email.EmailSender, ProxeusFSABI abi.ABI, domain string) *Signaturelistener {
-func NewSignatureListener(ethWebSocketURL, ethURL, BlockchainContractAddress string, SignatureRequestsDB *storm.SignatureRequestsDB, UserDB storm.UserDBInterface, EmailSender email.EmailSender, ProxeusFSABI abi.ABI, domain string) *Signaturelistener {
me := &Signaturelistener{}
me.BlockchainContractAddress = BlockchainContractAddress
me.ethWebSocketURL = ethWebSocketURL
diff --git a/main/handlers/payment/handler.go b/main/handlers/payment/handler.go
new file mode 100644
index 000000000..ffb9f40af
--- /dev/null
+++ b/main/handlers/payment/handler.go
@@ -0,0 +1,276 @@
+package payment
+
+import (
+ "errors"
+ "log"
+ "net/http"
+ "strings"
+ "time"
+
+ uuid "github.com/satori/go.uuid"
+
+ "git.proxeus.com/core/central/sys/db/storm"
+
+ strm "github.com/asdine/storm"
+
+ "github.com/labstack/echo"
+
+ "git.proxeus.com/core/central/main/www"
+ "git.proxeus.com/core/central/sys/model"
+)
+
+var errNotAuthorized = errors.New("user not authorized")
+
+type createPaymentRequest struct {
+ WorkflowId string `json:"workflowId"`
+}
+
+//create a payment for a workflow
+func CreateWorkflowPayment(e echo.Context) error {
+ c := e.(*www.Context)
+
+ user, err := getUser(c)
+ if err != nil {
+ return c.NoContent(http.StatusUnauthorized)
+ }
+
+ createPaymentRequest := &createPaymentRequest{}
+ err = c.Bind(&createPaymentRequest)
+ if err != nil {
+ return c.String(http.StatusBadRequest, err.Error())
+ }
+
+ createPaymentRequest.WorkflowId = strings.TrimSpace(createPaymentRequest.WorkflowId)
+
+ if createPaymentRequest.WorkflowId == "" {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ workflow, err := c.System().DB.Workflow.Get(c.Session(false), createPaymentRequest.WorkflowId)
+ if err != nil {
+ return c.String(http.StatusBadRequest, err.Error())
+ }
+
+ id := uuid.NewV4().String()
+
+ payment := &model.WorkflowPaymentItem{
+ ID: id,
+ Xes: workflow.Price,
+ From: user.EthereumAddr,
+ To: workflow.OwnerEthAddress,
+ Status: model.PaymentStatusCreated,
+ CreatedAt: time.Now(),
+ WorkflowID: createPaymentRequest.WorkflowId,
+ }
+
+ err = c.System().DB.WorkflowPaymentsDB.Save(payment)
+ if err != nil {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ return c.JSON(http.StatusOK, payment)
+}
+
+// Gets a payment by Id
+// Payment is only returned if the from address == the user sending the request
+func GetWorkflowPaymentById(e echo.Context) error {
+ c := e.(*www.Context)
+ paymentId := c.Param("paymentId")
+
+ user, err := getUser(c)
+ if err != nil {
+ return c.NoContent(http.StatusUnauthorized)
+ }
+
+ payment, err := c.System().DB.WorkflowPaymentsDB.Get(paymentId)
+ if err != nil {
+ log.Println("[GetWorkflowPaymentById] getUserPaymentById err: ", err.Error())
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ if payment.From != user.EthereumAddr {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ return c.JSON(http.StatusOK, payment)
+}
+
+// Returns payment with the given txHash and status.
+// Payment is only returned if the from address == the user sending the request
+func GetWorkflowPayment(e echo.Context) error {
+ c := e.(*www.Context)
+ txHash := c.QueryParam("txHash")
+ status := c.QueryParam("status")
+
+ user, err := getUser(c)
+ if err != nil {
+ return c.NoContent(http.StatusUnauthorized)
+ }
+
+ payment, err := getWorkflowPayment(c.System().DB.WorkflowPaymentsDB, txHash, user.EthereumAddr, status)
+ if err != nil {
+ if err == strm.ErrNotFound {
+ return c.NoContent(http.StatusNotFound)
+ }
+ log.Println("[GetWorkflowPayment] GetByTxHashAndStatusAndFromEthAddress err: ", err.Error())
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ return c.JSON(http.StatusOK, payment)
+}
+
+var errRequiredParamMissing = errors.New("required parameter missing")
+
+func getWorkflowPayment(workflowPaymentsDB storm.WorkflowPaymentsDBInterface, txHash,
+ ethAddr, status string) (*model.WorkflowPaymentItem, error) {
+
+ if txHash == "" {
+ log.Printf("[GetWorkflowPayment] bad request, either provide paymentId, txHash or workflowId")
+ return nil, errRequiredParamMissing
+ }
+
+ payment, err := workflowPaymentsDB.GetByTxHashAndStatusAndFromEthAddress(txHash, status, ethAddr)
+ if err != nil {
+ log.Println("[GetWorkflowPayment] GetByTxHashAndStatusAndFromEthAddress err: ", err.Error())
+ return nil, err
+ }
+
+ log.Printf("[workflowHandler][GetWorkflowPayment] ID: %s, txHash: %s", payment.ID, payment.TxHash)
+
+ return payment, nil
+}
+
+type updatePaymentPendingRequest struct {
+ TxHash string `json:"txHash"`
+}
+
+var errTxHashEmpty = errors.New("no txHash given")
+
+// Set a workflow payment from status created to status pending
+func UpdateWorkflowPaymentPending(e echo.Context) error {
+ c := e.(*www.Context)
+ paymentId := strings.TrimSpace(c.Param("paymentId"))
+
+ user, err := getUser(c)
+ if err != nil {
+ return c.NoContent(http.StatusUnauthorized)
+ }
+
+ updatePaymentRequest := &updatePaymentPendingRequest{}
+ err = c.Bind(&updatePaymentRequest)
+ if err != nil {
+ log.Printf("[UpdateWorkflowPayment] UpdateWorkflowPayment bind err: %s", err.Error())
+ return err
+ }
+
+ err = updateWorkflowPaymentPending(c.System().DB.WorkflowPaymentsDB, paymentId, updatePaymentRequest.TxHash, user.EthereumAddr)
+ if err != nil {
+ log.Printf("[UpdateWorkflowPayment] err: %s", err.Error())
+ if err == errTxHashEmpty {
+ return c.String(http.StatusBadRequest, err.Error())
+ }
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ return c.NoContent(http.StatusOK)
+}
+
+func updateWorkflowPaymentPending(workflowPaymentsDB storm.WorkflowPaymentsDBInterface, paymentId, txHash, ethAddr string) error {
+ txHash = strings.TrimSpace(txHash)
+ if txHash == "" {
+ return errTxHashEmpty
+ }
+
+ err := workflowPaymentsDB.Update(paymentId, model.PaymentStatusPending, txHash, ethAddr)
+ if err != nil {
+ log.Printf("[UpdateWorkflowPayment] WorkflowPaymentsDB.Update err: %s", err.Error())
+ return err
+ }
+
+ return nil
+}
+
+// Set status of workflow from created to cancelled
+func CancelWorkflowPayment(e echo.Context) error {
+ c := e.(*www.Context)
+ paymentId := strings.TrimSpace(c.Param("paymentId"))
+
+ user, err := getUser(c)
+ if err != nil {
+ return errNotAuthorized
+ }
+
+ err = cancelWorkflowPayment(c.System().DB.WorkflowPaymentsDB, paymentId, user.EthereumAddr)
+ if err != nil {
+ return c.String(http.StatusNotFound, err.Error())
+ }
+
+ return c.NoContent(http.StatusOK)
+}
+
+func cancelWorkflowPayment(workflowPaymentsDB storm.WorkflowPaymentsDBInterface, paymentId, ethAddr string) error {
+ return workflowPaymentsDB.Cancel(paymentId, ethAddr)
+}
+
+// Set the payment status from confirmed to redeemed
+func RedeemPayment(workflowPaymentsDB storm.WorkflowPaymentsDBInterface, workflowId, ethAddr string) error {
+ return workflowPaymentsDB.Redeem(workflowId, ethAddr)
+}
+
+//returns a bool indicating whether a payment is required for the user for a workflow
+func CheckIfWorkflowPaymentRequired(c *www.Context, workflowId string) (bool, error) {
+ sess := c.Session(false)
+
+ workflow, err := c.System().DB.Workflow.Get(sess, workflowId)
+ if err != nil {
+ return true, err
+ }
+
+ _, alreadyStarted, err := c.System().DB.UserData.GetByWorkflow(sess, workflow, false)
+ if err != nil {
+ if err != strm.ErrNotFound {
+ return true, nil
+ }
+ //if workflow not found (strm.ErrNotFound ) still check with isPaymentRequired
+ }
+
+ return isPaymentRequired(alreadyStarted, workflow, c.Session(false).UserID()), nil
+}
+
+func isPaymentRequired(alreadyStarted bool, workflow *model.WorkflowItem, userId string) bool {
+ return !alreadyStarted && workflow.Owner != userId && workflow.Price != 0
+}
+
+// Set Payment for a workflow to status = Deleted. Only for superadmin
+func DeleteWorkflowPayment(e echo.Context) error {
+ c := e.(*www.Context)
+ paymentId := strings.TrimSpace(c.Param("paymentId"))
+
+ if paymentId == "" {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ err := c.System().DB.WorkflowPaymentsDB.Delete(paymentId)
+ if err != nil {
+ return c.String(http.StatusBadRequest, err.Error())
+ }
+
+ return c.NoContent(http.StatusOK)
+}
+
+// List all Payment. Only for superadmin and debugging purposes
+func ListPayments(e echo.Context) error {
+ c := e.(*www.Context)
+
+ payments, err := c.System().DB.WorkflowPaymentsDB.All()
+ if err != nil {
+ return c.NoContent(http.StatusBadRequest)
+ }
+
+ return c.JSON(http.StatusOK, payments)
+}
+
+func getUser(c *www.Context) (*model.User, error) {
+ sess := c.Session(false)
+ return c.System().DB.User.Get(sess, sess.UserID())
+}
diff --git a/main/handlers/payment/handler_test.go b/main/handlers/payment/handler_test.go
new file mode 100644
index 000000000..de84dfb75
--- /dev/null
+++ b/main/handlers/payment/handler_test.go
@@ -0,0 +1,549 @@
+package payment
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/golang/mock/gomock"
+ "github.com/gorilla/sessions"
+ "github.com/labstack/echo"
+ "github.com/stretchr/testify/assert"
+
+ strm "github.com/asdine/storm"
+
+ "git.proxeus.com/core/central/main/www"
+ "git.proxeus.com/core/central/sys"
+ "git.proxeus.com/core/central/sys/db/storm"
+ "git.proxeus.com/core/central/sys/model"
+
+ sysSess "git.proxeus.com/core/central/sys/session"
+)
+
+func setupPaymentRequestTest(httpMethod, targetUrl, body string) (*www.Context, *httptest.ResponseRecorder, *model.User, *model.User) {
+ e := echo.New()
+ req := httptest.NewRequest(httpMethod, targetUrl, strings.NewReader(body))
+ req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
+ rec := httptest.NewRecorder()
+ c := e.NewContext(req, rec)
+
+ sessionStore := sessions.NewCookieStore([]byte("secret_Dummy_1234"), []byte("12345678901234567890123456789012"))
+ c.Set("_session_store", sessionStore)
+ sysSession := &sysSess.Session{}
+ sysSession.SetUserID("1")
+
+ c.Set("sys.session", sysSession)
+ wwwContext := &www.Context{Context: c}
+ wwwContext.SetRequest(req)
+
+ user := &model.User{}
+ user.EthereumAddr = "0x00"
+
+ ownerUser := &model.User{}
+ ownerUser.EthereumAddr = "0x3"
+
+ return wwwContext, rec, user, ownerUser
+}
+
+type paymentResponse struct {
+ Id string `json:id`
+}
+
+func TestCreateWorkflowPayment(t *testing.T) {
+
+ mockCtrl, workflowPaymentsDB := up(t)
+ defer down(mockCtrl, workflowPaymentsDB)
+
+ body := `{"workflowId":"552a2f0e-c6c4-403b-8aaf-2d9ebf55eb8f"}`
+
+ wwwContext, rec, user, _ := setupPaymentRequestTest(http.MethodPost, "/api/admin/payments", body)
+
+ userDBMock := storm.NewMockUserDBInterface(mockCtrl)
+ userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq("1")).Return(user, nil).Times(1)
+
+ workflow := &model.WorkflowItem{Price: 2000000000000000000}
+ workflow.Owner = user.EthereumAddr
+ workflowDBMock := storm.NewMockWorkflowDBInterface(mockCtrl)
+ workflowDBMock.EXPECT().Get(gomock.Any(), gomock.Any()).Return(workflow, nil)
+
+ system := &sys.System{}
+ system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDB, User: userDBMock, Workflow: workflowDBMock}
+ www.SetSystem(system)
+
+ t.Run("ShouldCreatePaymentItem", func(t *testing.T) {
+ if assert.NoError(t, CreateWorkflowPayment(wwwContext)) {
+ assert.Equal(t, http.StatusOK, rec.Code)
+
+ var response = paymentResponse{}
+ err := json.Unmarshal(rec.Body.Bytes(), &response)
+ if err != nil {
+ panic(err)
+ }
+
+ err = workflowPaymentsDB.Remove(&model.WorkflowPaymentItem{ID: response.Id})
+ if err != nil {
+ panic(err)
+ }
+ }
+ })
+}
+
+func TestGetWorkflowPaymentById(t *testing.T) {
+ mockCtrl, workflowPaymentsDB := up(t)
+ defer down(mockCtrl, workflowPaymentsDB)
+
+ t.Run("ShouldReturnPayment", func(t *testing.T) {
+ paymentId := "1"
+
+ wwwContext, rec, user, _ := setupPaymentRequestTest(http.MethodGet,
+ fmt.Sprintf("/api/admin/payments/%s", paymentId), "{}")
+
+ wwwContext.SetParamNames("paymentId")
+ wwwContext.SetParamValues(paymentId)
+
+ userDBMock := storm.NewMockUserDBInterface(mockCtrl)
+ userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq("1")).Return(user, nil).Times(1)
+
+ system := &sys.System{}
+ system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDB, User: userDBMock}
+ www.SetSystem(system)
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, From: user.EthereumAddr})
+ if err != nil {
+ panic(err)
+ }
+
+ if assert.NoError(t, GetWorkflowPaymentById(wwwContext)) {
+ assert.Equal(t, http.StatusOK, rec.Code)
+
+ var response = paymentResponse{}
+ err := json.Unmarshal(rec.Body.Bytes(), &response)
+ if err != nil {
+ panic(err)
+ }
+
+ err = workflowPaymentsDB.Remove(&model.WorkflowPaymentItem{ID: response.Id})
+ if err != nil {
+ panic(err)
+ }
+ }
+ })
+
+ t.Run("ShouldNotReturnPayment", func(t *testing.T) {
+ paymentId := "2"
+
+ wwwContext, rec, user, userOwner := setupPaymentRequestTest(http.MethodGet,
+ fmt.Sprintf("/api/admin/payments/%s", paymentId), "{}")
+
+ wwwContext.SetParamNames("paymentId")
+ wwwContext.SetParamValues(paymentId)
+
+ userDBMock := storm.NewMockUserDBInterface(mockCtrl)
+ userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq("1")).Return(user, nil).Times(1)
+
+ system := &sys.System{}
+ system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDB, User: userDBMock}
+ www.SetSystem(system)
+
+ //here pass "userOwner" instead of "user"
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, From: userOwner.EthereumAddr})
+ if err != nil {
+ panic(err)
+ }
+
+ if assert.NoError(t, GetWorkflowPaymentById(wwwContext)) {
+ assert.Equal(t, http.StatusBadRequest, rec.Code)
+
+ err = workflowPaymentsDB.Remove(&model.WorkflowPaymentItem{ID: paymentId})
+ if err != nil {
+ panic(err)
+ }
+ }
+ })
+}
+
+func TestGetWorkflowPayment(t *testing.T) {
+ mockCtrl, workflowPaymentsDB := up(t)
+ defer down(mockCtrl, workflowPaymentsDB)
+
+ t.Run("ShouldReturnPayment", func(t *testing.T) {
+ paymentId := "3"
+ txHash := "0x3"
+ from := "0x4"
+ status := model.PaymentStatusConfirmed
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, TxHash: txHash, From: from, Status: status})
+ if err != nil {
+ panic(err)
+ }
+
+ payment, err := getWorkflowPayment(workflowPaymentsDB, txHash, from, status)
+
+ assert.Nil(t, err)
+ assert.Equal(t, paymentId, payment.ID)
+
+ err = workflowPaymentsDB.Remove(payment)
+ if err != nil {
+ panic(err)
+ }
+ })
+ t.Run("ShouldNotReturnPaymentIfFromNotMatching", func(t *testing.T) {
+ paymentId := "4"
+ txHash := "0x3"
+ from := "0x4"
+ status := model.PaymentStatusConfirmed
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, TxHash: txHash, From: "0x5", Status: status})
+ if err != nil {
+ panic(err)
+ }
+
+ payment, err := getWorkflowPayment(workflowPaymentsDB, txHash, from, status)
+
+ assert.Equal(t, strm.ErrNotFound, err)
+ assert.Nil(t, payment)
+
+ err = workflowPaymentsDB.Remove(&model.WorkflowPaymentItem{ID: paymentId})
+ if err != nil {
+ panic(err)
+ }
+ })
+}
+
+func TestUpdateWorkflowPaymentPending(t *testing.T) {
+
+ mockCtrl, workflowPaymentsDB := up(t)
+ defer down(mockCtrl, workflowPaymentsDB)
+
+ t.Run("ShouldUpdatePayment", func(t *testing.T) {
+ paymentId := "3"
+ txHash := "0x3"
+ from := "0x4"
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, From: from, Status: model.PaymentStatusCreated})
+ if err != nil {
+ panic(err)
+ }
+
+ assert.NoError(t, updateWorkflowPaymentPending(workflowPaymentsDB, paymentId, txHash, from))
+
+ payment, err := workflowPaymentsDB.Get(paymentId)
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, paymentId, payment.ID)
+ assert.Equal(t, model.PaymentStatusPending, payment.Status)
+ assert.Equal(t, txHash, payment.TxHash)
+
+ err = workflowPaymentsDB.Remove(payment)
+ if err != nil {
+ panic(err)
+ }
+ })
+ t.Run("ShouldReturnErrorOnUpdatePaymentIfFromNotMatching", func(t *testing.T) {
+ paymentId := "4"
+ txHash := "0x4"
+ from := "0x5"
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, From: from, Status: model.PaymentStatusCreated})
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, strm.ErrNotFound, updateWorkflowPaymentPending(workflowPaymentsDB, paymentId, txHash, "0x6"))
+
+ payment, err := workflowPaymentsDB.Get(paymentId)
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, paymentId, payment.ID)
+ assert.Equal(t, model.PaymentStatusCreated, payment.Status)
+ assert.Equal(t, "", payment.TxHash)
+
+ err = workflowPaymentsDB.Remove(payment)
+ if err != nil {
+ panic(err)
+ }
+ })
+}
+
+func TestCancelWorkflowPayment(t *testing.T) {
+ mockCtrl, workflowPaymentsDB := up(t)
+ defer down(mockCtrl, workflowPaymentsDB)
+
+ t.Run("ShouldCancelWorkflowPayment", func(t *testing.T) {
+ paymentId := "4"
+ txHash := "0x4"
+ from := "0x5"
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, TxHash: txHash, From: from,
+ Status: model.PaymentStatusCreated})
+ if err != nil {
+ panic(err)
+ }
+
+ assert.NoError(t, cancelWorkflowPayment(workflowPaymentsDB, paymentId, from))
+
+ payment, err := workflowPaymentsDB.Get(paymentId)
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, model.PaymentStatusCancelled, payment.Status)
+
+ err = workflowPaymentsDB.Remove(payment)
+ if err != nil {
+ panic(err)
+ }
+ })
+
+ t.Run("ShouldNotCancelWorkflowPaymentIfStatusIsNotPending", func(t *testing.T) {
+ paymentId := "5"
+ txHash := "0x5"
+ from := "0x6"
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, TxHash: txHash, From: from,
+ Status: model.PaymentStatusConfirmed})
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Error(t, cancelWorkflowPayment(workflowPaymentsDB, paymentId, from))
+
+ payment, err := workflowPaymentsDB.Get(paymentId)
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, model.PaymentStatusConfirmed, payment.Status)
+
+ err = workflowPaymentsDB.Remove(payment)
+ if err != nil {
+ panic(err)
+ }
+ })
+
+ t.Run("ShouldNotCancelWorkflowPaymentIfFromNotMatching", func(t *testing.T) {
+ paymentId := "6"
+ txHash := "0x6"
+ from := "0x7"
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, TxHash: txHash, From: from,
+ Status: model.PaymentStatusCreated})
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Error(t, cancelWorkflowPayment(workflowPaymentsDB, paymentId, "0x8"))
+
+ payment, err := workflowPaymentsDB.Get(paymentId)
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, model.PaymentStatusCreated, payment.Status)
+
+ err = workflowPaymentsDB.Remove(payment)
+ if err != nil {
+ panic(err)
+ }
+ })
+}
+
+func TestRedeemPayment(t *testing.T) {
+ mockCtrl, workflowPaymentsDB := up(t)
+ defer down(mockCtrl, workflowPaymentsDB)
+
+ t.Run("ShouldRedeemWorkflowPayment", func(t *testing.T) {
+ paymentId := "7"
+ txHash := "0x7"
+ from := "0x8"
+ workflowId := "01-02"
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, TxHash: txHash, From: from,
+ Status: model.PaymentStatusConfirmed, WorkflowID: workflowId})
+ if err != nil {
+ panic(err)
+ }
+
+ assert.NoError(t, RedeemPayment(workflowPaymentsDB, workflowId, from))
+
+ payment, err := workflowPaymentsDB.Get(paymentId)
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, model.PaymentStatusRedeemed, payment.Status)
+
+ err = workflowPaymentsDB.Remove(payment)
+ if err != nil {
+ panic(err)
+ }
+ })
+
+ t.Run("ShouldRedeemNewerPaymentItemIfTwoAvailable", func(t *testing.T) {
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: "8first", TxHash: "0x8", From: "0x9",
+ Status: model.PaymentStatusConfirmed, WorkflowID: "01-03"})
+ if err != nil {
+ panic(err)
+ }
+
+ err = workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: "9second", TxHash: "0x9", From: "0x9",
+ Status: model.PaymentStatusConfirmed, WorkflowID: "01-03"})
+ if err != nil {
+ panic(err)
+ }
+
+ assert.NoError(t, RedeemPayment(workflowPaymentsDB, "01-03", "0x9"))
+
+ firstPayment, err := workflowPaymentsDB.Get("8first")
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, model.PaymentStatusConfirmed, firstPayment.Status)
+
+ secondPayment, err := workflowPaymentsDB.Get("9second")
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, model.PaymentStatusRedeemed, secondPayment.Status)
+
+ err = workflowPaymentsDB.Remove(firstPayment)
+ if err != nil {
+ panic(err)
+ }
+ err = workflowPaymentsDB.Remove(secondPayment)
+ if err != nil {
+ panic(err)
+ }
+ })
+
+ t.Run("ShouldNotRedeemWorkflowPaymentIfStatusIsPending", func(t *testing.T) {
+ paymentId := "9"
+ txHash := "0x9"
+ from := "0x10"
+ workflowId := "01-03"
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, TxHash: txHash, From: from,
+ Status: model.PaymentStatusPending, WorkflowID: workflowId})
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Error(t, RedeemPayment(workflowPaymentsDB, workflowId, from))
+
+ payment, err := workflowPaymentsDB.Get(paymentId)
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, model.PaymentStatusPending, payment.Status)
+
+ err = workflowPaymentsDB.Remove(payment)
+ if err != nil {
+ panic(err)
+ }
+ })
+
+ t.Run("ShouldNotRedeemWorkflowPaymentIfFromNotMatching", func(t *testing.T) {
+ paymentId := "10"
+ txHash := "0x10"
+ from := "0x11"
+ workflowId := "01-04"
+
+ err := workflowPaymentsDB.Save(&model.WorkflowPaymentItem{ID: paymentId, TxHash: txHash, From: from,
+ Status: model.PaymentStatusConfirmed, WorkflowID: workflowId})
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Error(t, RedeemPayment(workflowPaymentsDB, workflowId, "0x12"))
+
+ payment, err := workflowPaymentsDB.Get(paymentId)
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, model.PaymentStatusConfirmed, payment.Status)
+
+ err = workflowPaymentsDB.Remove(payment)
+ if err != nil {
+ panic(err)
+ }
+ })
+}
+
+func TestCheckIfWorkflowPaymentRequired(t *testing.T) {
+ mockCtrl, workflowPaymentsDB := up(t)
+ defer down(mockCtrl, workflowPaymentsDB)
+
+ t.Run("ShouldRequirePaymentIfWorkflowNotForFree", func(t *testing.T) {
+ permissions := &model.Permissions{Owner: "1"}
+ workflow := &model.WorkflowItem{Price: 2, Permissions: *permissions}
+ assert.True(t, isPaymentRequired(false, workflow, "2"))
+ })
+ t.Run("ShouldNotRequirePaymentIfWorkflowNotForFreeButAlreadyStarted", func(t *testing.T) {
+ permissions := &model.Permissions{Owner: "1"}
+ workflow := &model.WorkflowItem{Price: 2, Permissions: *permissions}
+ assert.False(t, isPaymentRequired(true, workflow, "2"))
+ })
+ t.Run("ShouldNotRequirePaymentIfWorkflowIsFree", func(t *testing.T) {
+ permissions := &model.Permissions{Owner: "1"}
+ workflow := &model.WorkflowItem{Price: 0, Permissions: *permissions}
+ assert.False(t, isPaymentRequired(false, workflow, "2"))
+ })
+ t.Run("ShouldNotRequirePaymentForWorkflowOwner", func(t *testing.T) {
+ permissions := &model.Permissions{Owner: "1"}
+ workflow := &model.WorkflowItem{Price: 2, Permissions: *permissions}
+ assert.False(t, isPaymentRequired(false, workflow, "1"))
+ })
+}
+
+var errCleanupTestData = errors.New("db data has not been cleanup up after finishing tests")
+
+func up(t *testing.T) (*gomock.Controller, storm.WorkflowPaymentsDBInterface) {
+ mockCtrl := gomock.NewController(t)
+
+ workflowPaymentsDB, err := storm.NewWorkflowPaymentDB(".test_data")
+ if err != nil {
+ panic(err)
+ }
+ return mockCtrl, workflowPaymentsDB
+}
+
+func down(mockCtrl *gomock.Controller, workflowPaymentsDB storm.WorkflowPaymentsDBInterface) {
+
+ mockCtrl.Finish()
+
+ payments, err := workflowPaymentsDB.All()
+ if err != nil {
+ panic(err)
+ }
+ if len(payments) != 0 {
+ panic(errCleanupTestData)
+ }
+
+ err = os.Remove(filepath.Join(".test_data", storm.WorkflowPaymentDBDir, storm.WorkflowPaymentDB))
+ if err != nil {
+ panic(err.Error())
+ }
+ err = os.Remove(filepath.Join(".test_data", storm.WorkflowPaymentDBDir))
+ if err != nil {
+ panic(err.Error())
+ }
+ err = os.Remove(".test_data")
+ if err != nil {
+ panic(err.Error())
+ }
+
+}
diff --git a/main/handlers/routes.go b/main/handlers/routes.go
index 5d9ac5906..14f01522c 100644
--- a/main/handlers/routes.go
+++ b/main/handlers/routes.go
@@ -3,6 +3,8 @@ package handlers
import (
"strings"
+ "git.proxeus.com/core/central/main/handlers/payment"
+
"git.proxeus.com/core/central/main/handlers/api"
"github.com/labstack/echo"
@@ -12,22 +14,14 @@ import (
"git.proxeus.com/core/central/main/handlers/template_ide"
"git.proxeus.com/core/central/main/handlers/workflow"
"git.proxeus.com/core/central/main/www"
- "git.proxeus.com/core/central/sys"
"git.proxeus.com/core/central/sys/model"
)
-func MainHostedAPI(e *echo.Echo, s *www.Security, system *sys.System) {
- configured, _ := system.Configured()
- var initialHandler *www.InitialHandler
- if !configured {
- initialHandler = www.NewInitialHandler(configured)
- e.Use(initialHandler.Handler)
- }
-
+func MainHostedAPI(e *echo.Echo, s *www.Security, version string) {
const (
- GET = echo.GET
- POST = echo.POST
- //PUT = echo.PUT
+ GET = echo.GET
+ POST = echo.POST
+ PUT = echo.PUT
DELETE = echo.DELETE
)
@@ -80,9 +74,9 @@ func MainHostedAPI(e *echo.Echo, s *www.Security, system *sys.System) {
{POST, USER, "/api/import", api.PostImport},
{GET, ROOT, "/api/init", api.GetInit},
{POST, ROOT, "/api/init", api.PostInit},
- {GET, PUBLIC, "/api/challenge", api.ChallengeHandler},
- {POST, PUBLIC, "/api/change/bcaddress", api.UpdateAddress},
- {POST, PUBLIC, "/api/change/email", api.ChangeEmailRequest},
+ {GET, PUBLIC, "/api/challenge", api.ChallengeHandler}, // Need session
+ {POST, PUBLIC, "/api/change/bcaddress", api.UpdateAddress}, // Need session
+ {POST, PUBLIC, "/api/change/email", api.ChangeEmailRequest}, // Need session
{POST, PUBLIC, "/api/change/email/:token", api.ChangeEmail},
{POST, PUBLIC, "/api/reset/password", api.ResetPasswordRequest},
{POST, PUBLIC, "/api/reset/password/:token", api.ResetPassword},
@@ -90,15 +84,19 @@ func MainHostedAPI(e *echo.Echo, s *www.Security, system *sys.System) {
{POST, PUBLIC, "/api/register/:token", api.Register},
{POST, PUBLIC, "/api/login", api.LoginHandler},
{POST, PUBLIC, "/api/logout", api.LogoutHandler},
- {GET, PUBLIC, "/api/config", api.ConfigHandler},
- {GET, PUBLIC, "/api/me", api.MeHandler},
+ {GET, PUBLIC, "/api/config", api.ConfigHandler(version)},
+ {GET, PUBLIC, "/api/me", api.MeHandler}, // Need session
{POST, USER, "/api/me", api.MeUpdateHandler},
+ // API Key session
+ {GET, PUBLIC, "/api/session/token", api.GetSessionTokenHandler},
+ {DELETE, USER, "/api/session/token", api.DeleteSessionTokenHandler},
+
{POST, USER, "/api/my/profile/photo", api.PutProfilePhotoHandler},
{GET, ROOT, "/api/settings/export", api.ExportSettings},
{GET, USER, "/api/userdata/export", api.ExportUserData},
- {GET, PUBLIC, "/api/document/:ID", api.DocumentHandler},
+ {GET, PUBLIC, "/api/document/:ID", api.DocumentHandler}, // Need session
{GET, PUBLIC, "/api/document/list", workflow.ListPublishedHandler},
{GET, PUBLIC, "/api/document/:ID/allAtOnce/schema", api.WorkflowSchema},
@@ -107,10 +105,10 @@ func MainHostedAPI(e *echo.Echo, s *www.Security, system *sys.System) {
{POST, GUEST, "/api/document/:ID/name", api.DocumentEditHandler},
{POST, GUEST, "/api/document/:ID/data", api.DocumentDataHandler},
{POST, GUEST, "/api/document/:ID/next", api.DocumentNextHandler},
- {GET, PUBLIC, "/api/document/:ID/prev", api.DocumentPrevHandler},
+ {GET, PUBLIC, "/api/document/:ID/prev", api.DocumentPrevHandler}, // Why PUBLIC access for prev when next is GUEST
{GET, GUEST, "/api/document/:ID/file/:inputName", api.DocumentFileGetHandler},
- {POST, PUBLIC, "/api/document/:ID/file/:inputName", api.DocumentFilePostHandler},
- {GET, PUBLIC, "/api/document/:ID/preview/:templateID/:lang/:format", api.DocumentPreviewHandler},
+ {POST, PUBLIC, "/api/document/:ID/file/:inputName", api.DocumentFilePostHandler}, // Should be GUEST
+ {GET, PUBLIC, "/api/document/:ID/preview/:templateID/:lang/:format", api.DocumentPreviewHandler}, // Should be GUEST
{GET, GUEST, "/api/document/:ID/delete", api.DocumentDeleteHandler},
{GET, GUEST, "/api/user/document", api.UserDocumentListHandler},
@@ -151,53 +149,58 @@ func MainHostedAPI(e *echo.Echo, s *www.Security, system *sys.System) {
{GET, CREATOR, "/api/admin/workflow/:ID/delete", workflow.DeleteHandler},
{GET, USER, "/api/workflow/export", workflow.ExportWorkflow},
{GET, USER, "/api/user/workflow/list", workflow.ListPublishedHandler},
- {GET, PUBLIC, "/api/admin/workflow/list", workflow.ListHandler},
- {GET, PUBLIC, "/api/admin/workflow/:ID", workflow.GetHandler},
- {POST, PUBLIC, "/api/admin/workflow/update", workflow.UpdateHandler},
-
- {GET, PUBLIC, "/api/admin/workflow/:ID/payment", workflow.GetWorkflowPayment},
- {POST, PUBLIC, "/api/admin/workflow/:ID/payment/:txHash", workflow.AddWorkflowPayment},
-
- {GET, PUBLIC, "/api/management-list", api.ManagementListHandler},
+ {GET, PUBLIC, "/api/admin/workflow/list", workflow.ListHandler}, // Need session
+ {GET, PUBLIC, "/api/admin/workflow/:ID", workflow.GetHandler}, // Need session
+ {POST, PUBLIC, "/api/admin/workflow/update", workflow.UpdateHandler}, // Need session
+
+ // payment
+ {GET, USER, "/api/admin/payments/check", api.CheckForWorkflowPayment},
+ {POST, USER, "/api/admin/payments", payment.CreateWorkflowPayment},
+ {GET, USER, "/api/admin/payments/:paymentId", payment.GetWorkflowPaymentById},
+ {GET, USER, "/api/admin/payments", payment.GetWorkflowPayment},
+ {PUT, USER, "/api/admin/payments/:paymentId", payment.UpdateWorkflowPaymentPending},
+ {POST, USER, "/api/admin/payments/:paymentId/cancel", payment.CancelWorkflowPayment},
+ {GET, SUPERADMIN, "/api/admin/payments/list", payment.ListPayments},
+ {DELETE, SUPERADMIN, "/api/admin/payments/:paymentId", payment.DeleteWorkflowPayment},
// form builder
- {GET, PUBLIC, "/api/form/component", formbuilder.GetComponentsHandler},
+ {GET, PUBLIC, "/api/form/component", formbuilder.GetComponentsHandler}, // `Need session`
{GET, USER, "/api/form/export", formbuilder.ExportForms},
{GET, CREATOR, "/api/admin/form/:ID/delete", formbuilder.DeleteHandler},
- {GET, PUBLIC, "/api/admin/form/list", formbuilder.ListHandler},
+ {GET, PUBLIC, "/api/admin/form/list", formbuilder.ListHandler}, // Need session
{GET, USER, "/api/admin/:type/list", workflow.ListCustomNodeHandler},
- {GET, PUBLIC, "/api/admin/form/:formID", formbuilder.GetOneFormHandler},
- {POST, PUBLIC, "/api/admin/form/update", formbuilder.UpdateFormHandler},
+ {GET, PUBLIC, "/api/admin/form/:formID", formbuilder.GetOneFormHandler}, // Need session
+ {POST, PUBLIC, "/api/admin/form/update", formbuilder.UpdateFormHandler}, // Need session
- {GET, PUBLIC, "/api/admin/form/component", formbuilder.GetComponentsHandler},
+ {GET, PUBLIC, "/api/admin/form/component", formbuilder.GetComponentsHandler}, // Need session
{POST, SUPERADMIN, "/api/admin/form/component", formbuilder.SetComponentHandler},
{DELETE, SUPERADMIN, "/api/admin/form/component/:id", formbuilder.DeleteComponentHandler},
- {GET, PUBLIC, "/api/admin/form/vars", formbuilder.VarsHandler},
+ {GET, PUBLIC, "/api/admin/form/vars", formbuilder.VarsHandler}, // Need session
- {POST, PUBLIC, "/api/admin/form/test/setFormSrc/:id", formbuilder.SetFormSrcHandler},
- {GET, PUBLIC, "/api/admin/form/test/data/:id", formbuilder.GetDataId},
- {POST, PUBLIC, "/api/admin/form/test/data/:id", formbuilder.TestFormDataHandler},
- {GET, PUBLIC, "/api/admin/form/test/file/:id/:fieldname", formbuilder.GetFileIdFieldName},
+ {POST, PUBLIC, "/api/admin/form/test/setFormSrc/:id", formbuilder.SetFormSrcHandler}, // Need session
+ {GET, PUBLIC, "/api/admin/form/test/data/:id", formbuilder.GetDataId}, // Need session
+ {POST, PUBLIC, "/api/admin/form/test/data/:id", formbuilder.TestFormDataHandler}, // Need session
+ {GET, PUBLIC, "/api/admin/form/test/file/:id/:fieldname", formbuilder.GetFileIdFieldName}, // Need session
{GET, PUBLIC, "/api/admin/form/file/types", formbuilder.GetFileTypes},
- {POST, PUBLIC, "/api/admin/form/test/file/:id/:fieldname", formbuilder.PostFileIdFieldName},
+ {POST, PUBLIC, "/api/admin/form/test/file/:id/:fieldname", formbuilder.PostFileIdFieldName}, // Need session
// template IDE
{GET, CREATOR, "/api/admin/template/:ID/delete", template_ide.DeleteHandler},
{GET, USER, "/api/template/export", template_ide.ExportTemplate},
- {GET, PUBLIC, "/api/admin/template/vars", template_ide.VarsTemplateHandler},
- {GET, PUBLIC, "/api/admin/template/list", template_ide.ListHandler},
- {POST, PUBLIC, "/api/admin/template/update", template_ide.UpdateHandler},
- {GET, PUBLIC, "/api/admin/template/:id", template_ide.OneTmplHandler},
- {GET, PUBLIC, "/api/admin/template/download/:id/:lang", template_ide.DownloadTemplateHandler},
- {POST, PUBLIC, "/api/admin/template/upload/:id/:lang", template_ide.UploadTemplateHandler},
- {GET, PUBLIC, "/api/admin/template/delete/:id/:lang", template_ide.DeleteTemplateHandler},
-
- {GET, PUBLIC, "/api/admin/template/ide/active/:id/:lang", template_ide.IdeSetActiveHandler},
- {POST, PUBLIC, "/api/admin/template/ide/upload/:id/:lang", template_ide.IdePostUploadHandler},
- {GET, PUBLIC, "/api/admin/template/ide/delete/:id/:lang", template_ide.IdeGetDeleteHandler},
- {GET, PUBLIC, "/api/admin/template/ide/download/:id", template_ide.IdeGetDownloadHandler},
+ {GET, PUBLIC, "/api/admin/template/vars", template_ide.VarsTemplateHandler}, // Need session
+ {GET, PUBLIC, "/api/admin/template/list", template_ide.ListHandler}, // Need session
+ {POST, PUBLIC, "/api/admin/template/update", template_ide.UpdateHandler}, // Need session
+ {GET, PUBLIC, "/api/admin/template/:id", template_ide.OneTmplHandler}, // Need session
+ {GET, PUBLIC, "/api/admin/template/download/:id/:lang", template_ide.DownloadTemplateHandler}, // Need session
+ {POST, PUBLIC, "/api/admin/template/upload/:id/:lang", template_ide.UploadTemplateHandler}, // Need session
+ {GET, PUBLIC, "/api/admin/template/delete/:id/:lang", template_ide.DeleteTemplateHandler}, // Need session
+
+ {GET, PUBLIC, "/api/admin/template/ide/active/:id/:lang", template_ide.IdeSetActiveHandler}, // Need session
+ {POST, PUBLIC, "/api/admin/template/ide/upload/:id/:lang", template_ide.IdePostUploadHandler}, // Need session
+ {GET, PUBLIC, "/api/admin/template/ide/delete/:id/:lang", template_ide.IdeGetDeleteHandler}, // Need session
+ {GET, PUBLIC, "/api/admin/template/ide/download/:id", template_ide.IdeGetDownloadHandler}, // Need session
{GET, CREATOR, "/api/admin/template/ide/tmplAssistanceDownload", template_ide.IdeGetTmpAssDownload},
- {GET, PUBLIC, "/api/admin/template/ide/form", template_ide.IdeFormHandler},
+ {GET, PUBLIC, "/api/admin/template/ide/form", template_ide.IdeFormHandler}, // Need session
}
addEndpoint := func(r r, ms ...echo.MiddlewareFunc) {
diff --git a/main/handlers/workflow/handlers.go b/main/handlers/workflow/handlers.go
index 35943ffce..5a0b1c504 100644
--- a/main/handlers/workflow/handlers.go
+++ b/main/handlers/workflow/handlers.go
@@ -3,9 +3,6 @@ package workflow
import (
"log"
"net/http"
- "strings"
-
- "github.com/pkg/errors"
"git.proxeus.com/core/central/sys/workflow"
@@ -158,120 +155,6 @@ func UpdateHandler(e echo.Context) error {
return c.NoContent(http.StatusBadRequest)
}
-// Checks if a workflow payment exist in the db.
-// The payment can be retrieved either by txHash or workflowId/documentId and ethereum address.
-// Getting the payment with txHash is used when metamask notifies the frontend that a payment has been received but the backend has not yet been notified what
-// workflow the payment was for. The backend verifies if it has received the payment.
-// Once the payment process is finished and the backend has been notified what workflow the payment is for, the payment is checked/retrieved in
-// workflowId/documentId and ethereum address.
-func GetWorkflowPayment(e echo.Context) error {
- c := e.(*www.Context)
- txHash := c.QueryParam("txHash")
- workflowId := c.Param("ID")
-
- var (
- workflowPaymentItem *model.WorkflowPaymentItem
- err error
- )
- if txHash == "" {
- sess := c.Session(false)
- user, err := c.System().DB.User.Get(sess, sess.UserID())
- if err != nil {
- return c.NoContent(http.StatusBadRequest)
- }
- workflowPaymentItem, err = c.System().DB.WorkflowPaymentsDB.GetByWorkflowIdAndFromEthAddress(workflowId, user.EthereumAddr)
- if err != nil {
- if err.Error() == "not found" {
- return c.NoContent(http.StatusNotFound)
- }
- return c.NoContent(http.StatusBadRequest)
- }
- err = checkPayment(c, workflowId, workflowPaymentItem)
- if err != nil {
- return c.String(http.StatusBadRequest, err.Error())
- }
- } else {
- workflowPaymentItem, err = c.System().DB.WorkflowPaymentsDB.GetByTxHash(txHash)
- if err != nil {
- return c.NoContent(http.StatusBadRequest)
- }
- }
-
- log.Println("[workflowHandler][GetWorkflowPayment]", workflowPaymentItem.TxHash)
-
- return c.JSON(http.StatusOK, workflowPaymentItem)
-}
-
-// Once the payment has been confirmed this function redeems the payment for a worklflowId.
-// If all parameters in checkPayment function are valid the worfklowId is set to the workflowPaymentItem.
-func AddWorkflowPayment(e echo.Context) error {
- c := e.(*www.Context)
- txHash := c.Param("txHash")
- workflowId := c.Param("ID")
-
- workflowPaymentItem, err := c.System().DB.WorkflowPaymentsDB.GetByTxHash(txHash)
- if err != nil || workflowPaymentItem.WorkflowID != "" {
- return c.NoContent(http.StatusBadRequest)
- }
-
- err = checkPayment(c, workflowId, workflowPaymentItem)
- if err != nil {
- return c.String(http.StatusBadRequest, err.Error())
- }
-
- workflowPaymentItem.WorkflowID = workflowId
-
- err = c.System().DB.WorkflowPaymentsDB.Add(workflowPaymentItem)
- if err != nil {
- return c.NoContent(http.StatusBadRequest)
- }
-
- return c.NoContent(http.StatusOK)
-}
-
-var errPaymentFailed = errors.New("failed to validate payment")
-
-// Verify that a payment can be claimed by user by validating payment parameter against workflow parameters.
-// A payment can only be claimed if all these parameters match: price, payer, receiver
-func checkPayment(c *www.Context, workflowId string, workflowPaymentItem *model.WorkflowPaymentItem) error {
- sess := c.Session(false)
- if sess == nil {
- return errPaymentFailed
- }
- workflow, err := c.System().DB.Workflow.Get(sess, workflowId)
- if err != nil {
- return err
- }
-
- if workflowPaymentItem.Xes != workflow.Price {
- return errPaymentFailed
- }
-
- payer, err := c.System().DB.User.Get(sess, sess.UserID())
- if err != nil || payer == nil {
- return errPaymentFailed
- }
-
- if payer.EthereumAddr == "" {
- return errPaymentFailed
- }
-
- if !strings.EqualFold(workflowPaymentItem.From, payer.EthereumAddr) {
- return errPaymentFailed
- }
-
- workflowOwner, err := c.System().DB.User.Get(sess, workflow.Owner)
- if err != nil {
- return errPaymentFailed
- }
-
- if !strings.EqualFold(workflowPaymentItem.To, workflowOwner.EthereumAddr) {
- return errPaymentFailed
- }
-
- return nil
-}
-
func DeleteHandler(e echo.Context) error {
c := e.(*www.Context)
ID := c.Param("ID")
@@ -295,17 +178,19 @@ func ListHandler(e echo.Context) error {
}
func listHandler(c *www.Context, publishedOnly bool) error {
- contains := c.QueryParam("c")
- a, err := c.Auth()
- if err != nil {
- return c.NoContent(http.StatusUnauthorized)
+ var sess model.Authorization
+ if s := c.Session(false); s != nil {
+ sess = s
}
+ contains := c.QueryParam("c")
settings := helpers.ReadReqSettings(c)
var dat []*model.WorkflowItem
+ var err error
+
if publishedOnly {
- dat, err = c.System().DB.Workflow.ListPublished(a, contains, settings)
+ dat, err = c.System().DB.Workflow.ListPublished(sess, contains, settings)
} else {
- dat, err = c.System().DB.Workflow.List(a, contains, settings)
+ dat, err = c.System().DB.Workflow.List(sess, contains, settings)
}
if err != nil {
diff --git a/main/handlers/workflow/handlers_test.go b/main/handlers/workflow/handlers_test.go
deleted file mode 100644
index 3be3be65a..000000000
--- a/main/handlers/workflow/handlers_test.go
+++ /dev/null
@@ -1,228 +0,0 @@
-package workflow
-
-import (
- "net/http"
- "net/http/httptest"
- "strings"
- "testing"
-
- "github.com/golang/mock/gomock"
-
- "git.proxeus.com/core/central/sys"
- "git.proxeus.com/core/central/sys/db/storm"
- "git.proxeus.com/core/central/sys/model"
-
- "github.com/gorilla/sessions"
- "github.com/labstack/echo"
- "github.com/stretchr/testify/assert"
-
- "git.proxeus.com/core/central/main/www"
- sysSess "git.proxeus.com/core/central/sys/session"
-)
-
-func setupPaymentTest(httpMethod, targetUrl string) (*www.Context, *httptest.ResponseRecorder, *model.User, *model.User) {
- e := echo.New()
- req := httptest.NewRequest(httpMethod, targetUrl, nil)
- rec := httptest.NewRecorder()
- c := e.NewContext(req, rec)
-
- sessionStore := sessions.NewCookieStore([]byte("secret_Dummy_1234"), []byte("12345678901234567890123456789012"))
- c.Set("_session_store", sessionStore)
- sysSession := &sysSess.Session{}
- sysSession.SetUserID("1")
-
- c.Set("sys.session", sysSession)
- wwwContext := &www.Context{Context: c}
- wwwContext.SetRequest(req)
-
- user := &model.User{}
- user.EthereumAddr = "0x00"
-
- ownerUser := &model.User{}
- ownerUser.EthereumAddr = "0x3"
-
- return wwwContext, rec, user, ownerUser
-}
-
-func TestAddWorkflowPayment(t *testing.T) {
- mockCtrl := gomock.NewController(t)
- defer mockCtrl.Finish()
-
- t.Run("AddWorkflowPaymentShouldSucceed", func(t *testing.T) {
- wwwContext, rec, user, ownerUser := setupPaymentTest(http.MethodPost, "/api/admin/workflow/1/payment/0x2222")
-
- userDBMock := storm.NewMockUserDBInterface(mockCtrl)
- userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq("1")).Return(user, nil).Times(1)
- userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq(ownerUser.EthereumAddr)).Return(ownerUser, nil).Times(1)
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2000000000000000000, From: user.EthereumAddr, To: "0x3", TxHash: "0x5"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByTxHash(gomock.Any()).Return(workflowPaymentItem, nil).Times(1)
- workflowPaymentsDBMock.EXPECT().Add(gomock.Any()).Return(nil).Times(1)
-
- workflow := &model.WorkflowItem{Price: 2000000000000000000}
- workflow.Owner = ownerUser.EthereumAddr
- workflowDBMock := storm.NewMockWorkflowDBInterface(mockCtrl)
- workflowDBMock.EXPECT().Get(gomock.Any(), gomock.Any()).Return(workflow, nil)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock, User: userDBMock, Workflow: workflowDBMock}
- www.SetSystem(system)
-
- if assert.NoError(t, AddWorkflowPayment(wwwContext)) {
- assert.Equal(t, http.StatusOK, rec.Code)
- }
- })
-
- t.Run("AddWorkflowPaymentShouldFailIncorrectPayer", func(t *testing.T) {
- wwwContext, rec, user, ownerUser := setupPaymentTest(http.MethodPost, "/api/admin/workflow/1/payment/0x2222")
-
- userDBMock := storm.NewMockUserDBInterface(mockCtrl)
- userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq("1")).Return(user, nil).Times(1)
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2000000000000000000, From: "0xWrong", To: "0x3", TxHash: "0x5"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByTxHash(gomock.Any()).Return(workflowPaymentItem, nil).Times(1)
-
- workflow := &model.WorkflowItem{Price: 2000000000000000000}
- workflow.Owner = ownerUser.EthereumAddr
- workflowDBMock := storm.NewMockWorkflowDBInterface(mockCtrl)
- workflowDBMock.EXPECT().Get(gomock.Any(), gomock.Any()).Return(workflow, nil)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock, User: userDBMock, Workflow: workflowDBMock}
- www.SetSystem(system)
-
- if assert.NoError(t, AddWorkflowPayment(wwwContext)) {
- assert.Equal(t, http.StatusBadRequest, rec.Code)
- responseBody := rec.Body.String()
- assert.Equal(t, errPaymentFailed.Error(), responseBody)
- }
- })
-
- t.Run("AddWorkflowPaymentShouldFailPaymentItemWorkflowIDAlreadySet", func(t *testing.T) {
- wwwContext, rec, user, _ := setupPaymentTest(http.MethodPost, "/api/admin/workflow/1/payment/0x2222")
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2000000000000000000, From: user.EthereumAddr, To: "0x3", WorkflowID: "3", TxHash: "0x5"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByTxHash(gomock.Any()).Return(workflowPaymentItem, nil).Times(1)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock}
- www.SetSystem(system)
-
- if assert.NoError(t, AddWorkflowPayment(wwwContext)) {
- assert.Equal(t, http.StatusBadRequest, rec.Code)
- }
- })
-}
-
-func TestGetWorkflowPayment(t *testing.T) {
-
- mockCtrl := gomock.NewController(t)
- defer mockCtrl.Finish()
-
- t.Run("GetWorkflowPaymentByWorkflowIdAndFromEthAddressShouldSucceed", func(t *testing.T) {
-
- wwwContext, rec, user, ownerUser := setupPaymentTest(http.MethodGet, "/api/admin/workflow/1/payment")
-
- userDBMock := storm.NewMockUserDBInterface(mockCtrl)
- userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq("1")).Return(user, nil).Times(2)
- userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq(ownerUser.EthereumAddr)).Return(ownerUser, nil).Times(1)
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2000000000000000000, From: user.EthereumAddr, To: "0x3", WorkflowID: "3", TxHash: "0x5"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByWorkflowIdAndFromEthAddress(gomock.Eq(""), gomock.Eq(user.EthereumAddr)).Return(workflowPaymentItem, nil).Times(1)
-
- workflow := &model.WorkflowItem{Price: 2000000000000000000}
- workflow.Owner = ownerUser.EthereumAddr
- workflowDBMock := storm.NewMockWorkflowDBInterface(mockCtrl)
- workflowDBMock.EXPECT().Get(gomock.Any(), gomock.Any()).Return(workflow, nil)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock, User: userDBMock, Workflow: workflowDBMock}
- www.SetSystem(system)
-
- if assert.NoError(t, GetWorkflowPayment(wwwContext)) {
- assert.Equal(t, http.StatusOK, rec.Code)
- responseBody := rec.Body.String()
- successResponseJSON := `{"hash":"0x5","workflowID":"3","From":"0x00","To":"0x3","xes":2000000000000000000,"Status":"","createdAt":"0001-01-01T00:00:00Z"}`
- assert.Equal(t, successResponseJSON, strings.Trim(responseBody, "\n"))
- }
- })
-
- t.Run("GetWorkflowPaymentByTxHashShouldSucceed", func(t *testing.T) {
-
- wwwContext, rec, user, _ := setupPaymentTest(http.MethodGet, "/api/admin/workflow/1/payment?txHash=0x2222")
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2000000000000000000, From: user.EthereumAddr, To: "0x3", WorkflowID: "3", TxHash: "0x5"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByTxHash(gomock.Eq("0x2222")).Return(workflowPaymentItem, nil).Times(1)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock}
- www.SetSystem(system)
-
- if assert.NoError(t, GetWorkflowPayment(wwwContext)) {
- assert.Equal(t, http.StatusOK, rec.Code)
- responseBody := rec.Body.String()
- successResponseJSON := `{"hash":"0x5","workflowID":"3","From":"0x00","To":"0x3","xes":2000000000000000000,"Status":"","createdAt":"0001-01-01T00:00:00Z"}`
- assert.Equal(t, successResponseJSON, strings.Trim(responseBody, "\n"))
- }
- })
-
- t.Run("GetWorkflowPaymentShouldFailIncorrectPaymentAmount", func(t *testing.T) {
-
- wwwContext, rec, user, ownerUser := setupPaymentTest(http.MethodGet, "/api/admin/workflow/1/payment")
-
- userDBMock := storm.NewMockUserDBInterface(mockCtrl)
- userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq("1")).Return(user, nil).Times(1)
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2100000000000000000, From: user.EthereumAddr, To: "0x3", WorkflowID: "3", TxHash: "0x5"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByWorkflowIdAndFromEthAddress(gomock.Eq(""), gomock.Eq(user.EthereumAddr)).Return(workflowPaymentItem, nil).Times(1)
-
- workflow := &model.WorkflowItem{Price: 1900000000000000000}
- workflow.Owner = ownerUser.EthereumAddr
- workflowDBMock := storm.NewMockWorkflowDBInterface(mockCtrl)
- workflowDBMock.EXPECT().Get(gomock.Any(), gomock.Any()).Return(workflow, nil)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock, User: userDBMock, Workflow: workflowDBMock}
- www.SetSystem(system)
-
- if assert.NoError(t, GetWorkflowPayment(wwwContext)) {
- assert.Equal(t, http.StatusBadRequest, rec.Code)
- responseBody := rec.Body.String()
- assert.Equal(t, errPaymentFailed.Error(), responseBody)
- }
- })
-
- t.Run("GetWorkflowPaymentShouldFailIncorrectPayee", func(t *testing.T) {
-
- wwwContext, rec, user, ownerUser := setupPaymentTest(http.MethodGet, "/api/admin/workflow/1/payment")
-
- userDBMock := storm.NewMockUserDBInterface(mockCtrl)
- userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq("1")).Return(user, nil).Times(2)
- userDBMock.EXPECT().Get(gomock.Any(), gomock.Eq(ownerUser.EthereumAddr)).Return(ownerUser, nil).Times(1)
-
- workflowPaymentItem := &model.WorkflowPaymentItem{Xes: 2100000000000000000, From: user.EthereumAddr, To: "0xWrong", WorkflowID: "3", TxHash: "0x5"}
- workflowPaymentsDBMock := storm.NewMockWorkflowPaymentsDBInterface(mockCtrl)
- workflowPaymentsDBMock.EXPECT().GetByWorkflowIdAndFromEthAddress(gomock.Eq(""), gomock.Eq(user.EthereumAddr)).Return(workflowPaymentItem, nil).Times(1)
-
- workflow := &model.WorkflowItem{Price: 2100000000000000000}
- workflow.Owner = ownerUser.EthereumAddr
- workflowDBMock := storm.NewMockWorkflowDBInterface(mockCtrl)
- workflowDBMock.EXPECT().Get(gomock.Any(), gomock.Any()).Return(workflow, nil)
-
- system := &sys.System{}
- system.DB = &storm.DBSet{WorkflowPaymentsDB: workflowPaymentsDBMock, User: userDBMock, Workflow: workflowDBMock}
- www.SetSystem(system)
-
- if assert.NoError(t, GetWorkflowPayment(wwwContext)) {
- assert.Equal(t, http.StatusBadRequest, rec.Code)
- responseBody := rec.Body.String()
- assert.Equal(t, errPaymentFailed.Error(), responseBody)
- }
- })
-}
diff --git a/main/main.go b/main/main.go
index abd56c13e..6474f4c56 100644
--- a/main/main.go
+++ b/main/main.go
@@ -2,19 +2,17 @@ package main
import (
"fmt"
- "log"
"net/http"
_ "net/http/pprof"
+ "os"
"path"
"github.com/labstack/echo"
- "github.com/labstack/echo/middleware"
"strings"
cfg "git.proxeus.com/core/central/main/config"
"git.proxeus.com/core/central/main/handlers"
- "git.proxeus.com/core/central/main/handlers/api"
"git.proxeus.com/core/central/main/handlers/assets"
"git.proxeus.com/core/central/main/www"
"git.proxeus.com/core/central/sys"
@@ -25,68 +23,26 @@ import (
// ServerVersion is added to http headers and can be set during making a build
var ServerVersion = "build-unknown"
-func xVersionHeader(next echo.HandlerFunc) echo.HandlerFunc {
- return func(c echo.Context) error {
- c.Response().Header().Set("X-Version", ServerVersion)
- return next(c)
- }
-}
-
var embedded *www.Embedded
func main() {
- e := echo.New()
- //Simple Request Logging
- //e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
- // Format: "[echo] ${time_rfc3339} client=${remote_ip}, method=${method}, uri=${uri}, status=${status}\n",
- //}))
-
- //Request Logging with User Info and Body on Error
- e.Use(middleware.BodyDump(func(e echo.Context, reqBody, resBody []byte) {
- c := www.Context{Context: e}
- //c := e.(*www.Context)
- s := c.Session(false)
- if s == nil {
- return
- }
- if s.ID() != "" {
- id := s.UserID()
- user, err := c.System().DB.User.Get(s, id)
- if err != nil {
- return
- }
- userName := user.Name
- userAddr := user.EthereumAddr
- log.Println("[echo] Method: "+e.Request().Method, "Status:", e.Response().Status, "User: "+userAddr, "("+userName+")", "URI: "+e.Request().RequestURI)
- if len(reqBody) > 0 && c.Response().Status != 200 && c.Response().Status != 404 {
- fmt.Printf("[echo][errorrequest] %s\n", reqBody)
- }
- }
-
- }))
- e.HTTPErrorHandler = www.DefaultHTTPErrorHandler
- e.Use(func(h echo.HandlerFunc) echo.HandlerFunc {
- return func(c echo.Context) error {
- return h(&www.Context{Context: c})
- }
- })
- e.Pre(xVersionHeader)
- c := middleware.DefaultSecureConfig
- c.XFrameOptions = ""
- e.Pre(middleware.SecureWithConfig(c))
-
- e.GET("/static/*", StaticHandler)
-
- www.SetupSession(e)
system, err := sys.NewWithSettings(cfg.Config.Settings)
if err != nil {
panic(err)
}
+
+ if system.TestMode {
+ fmt.Println("#######################################################")
+ fmt.Println("# STARTING PROXEUS IN TEST MODE - NOT FOR PRODUCTION #")
+ fmt.Println("#######################################################")
+ }
+
+ www.SetSystem(system)
+
embedded = &www.Embedded{Asset: assets.Asset}
sys.ReadAllFile = func(path string) ([]byte, error) {
return embedded.Asset(path)
}
- www.SetSystem(system)
go func() { //parse i18n from the UI assets to provide them under the translation section
i18nUIParser := i18n.NewUIParser()
@@ -122,14 +78,22 @@ func main() {
}
}()
- secure := www.NewSecurity()
+ e := www.Setup(ServerVersion)
- // Routes
- e.Pre(middleware.Secure())
+ // Static route
+ e.GET("/static/*", StaticHandler)
- api.ServerVersion = ServerVersion
+ // Initial config middleware
+ configured, err := system.Configured()
+ if err != nil && !os.IsNotExist(err) {
+ panic(err)
+ }
+ if !configured {
+ e.Use(www.NewInitialHandler(configured).Handler)
+ }
- handlers.MainHostedAPI(e, secure, system)
+ // Main routes
+ handlers.MainHostedAPI(e, www.NewSecurity(), ServerVersion)
www.StartServer(e, cfg.Config.ServiceAddress, false)
system.Shutdown()
diff --git a/main/www/apikey.go b/main/www/apikey.go
new file mode 100644
index 000000000..ecf95d9b8
--- /dev/null
+++ b/main/www/apikey.go
@@ -0,0 +1,56 @@
+package www
+
+import (
+ "net/http"
+
+ "github.com/labstack/echo"
+
+ "git.proxeus.com/core/central/sys/session"
+)
+
+// SessionAuthToken create a request session if a valid API Key is found
+func SessionTokenAuth(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(e echo.Context) error {
+ c := e.(*Context)
+
+ if sess := c.Session(false); sess != nil {
+ return next(c)
+ }
+
+ // We first check if we can authenticate with an API key
+ sess, err := sessionFromSessionToken(c)
+ if err != nil {
+ // We had an session token but it not valid
+ return c.NoContent(http.StatusUnauthorized)
+ }
+
+ var removeCookie bool
+ if sess != nil {
+ c.Set("sys.session", sess)
+ removeCookie = true
+ }
+
+ if err = next(c); err != nil {
+ c.Error(err)
+ }
+
+ if removeCookie {
+ c.Response().Header().Del("Set-Cookie")
+ }
+
+ return nil
+ }
+}
+
+func sessionFromSessionToken(c *Context) (*session.Session, error) {
+ token := c.SessionToken()
+ if token == "" {
+ return nil, nil
+ }
+
+ sess, err := c.System().SessionMgmnt.Get(token)
+ if err != nil {
+ return nil, err
+ }
+ return sess, nil
+}
diff --git a/main/www/context.go b/main/www/context.go
index b311577a7..544325051 100644
--- a/main/www/context.go
+++ b/main/www/context.go
@@ -1,7 +1,10 @@
package www
import (
+ "encoding/base64"
+ "errors"
"regexp"
+ "strings"
"github.com/labstack/echo"
@@ -45,27 +48,6 @@ func (me *Context) SessionWithUser(usr *model.User) *session.Session {
return sess
}
-//Auth checks if there is a session available otherwise it retrieves the api key if possible
-func (me *Context) Auth() (model.Authorization, error) {
- sess := me.Session(false)
- if sess == nil {
- u, err := useApiKeyAsUserAuth(me)
- if err != nil {
- return nil, err
- }
- return u, nil
- }
- return sess, nil
-}
-
-func (me *Context) ApiKey() (string, error) {
- _, apiKey := readApiKeyFromHeader(me.Request().Header.Get("Authorization"))
- if len(apiKey) > 0 {
- return apiKey, nil
- }
- return "", echo.ErrNotFound
-}
-
func (me *Context) EndSession() {
_ = delSession(me)
}
@@ -82,33 +64,63 @@ func (me *Context) I18n() *WebI18n {
return me.webI18n
}
-var apiKeyFromHeaderReg = regexp.MustCompile(`\s*(\w+)?\s*([^\s]*)`)
+var errInvalidRole = errors.New("the role of the user did not match")
-//Returns type and key as string.
-func readApiKeyFromHeader(headerValue string) (string, string) {
- subm := apiKeyFromHeaderReg.FindAllStringSubmatch(headerValue, 1)
+func (me *Context) EnsureUserRole(role model.Role) error {
+ sess := me.Session(false)
+ if sess == nil {
+ return errInvalidRole
+ }
+ user, err := me.System().DB.User.Get(sess, sess.UserID())
+ if err != nil {
+ return errInvalidRole
+ }
+ if !user.IsGrantedFor(role) {
+ return errInvalidRole
+ }
+ return nil
+}
+
+// Extract the session token from the header
+func (me *Context) SessionToken() string {
+ return extractSessionToken(me.Request().Header.Get("Authorization"))
+}
+
+var sessionTokenFromHeaderReg = regexp.MustCompile(`^Bearer\s([^\s]+)$`)
+
+func extractSessionToken(headerValue string) string {
+ subm := sessionTokenFromHeaderReg.FindStringSubmatch(headerValue)
l := len(subm)
- if l == 1 {
- l = len(subm[0])
- if l == 3 {
- if len(subm[0][2]) == 0 {
- return "", subm[0][1]
- } else {
- return subm[0][1], subm[0][2]
- }
- }
+ if l != 2 {
+ return ""
}
- return "", ""
+ return subm[1]
}
-func useApiKeyAsUserAuth(c *Context) (model.Authorization, error) {
- apiKey, err := c.ApiKey()
- if err != nil {
- return nil, err
+// Extract the basic authentication from the header
+func (me *Context) BasicAuth() (string, string) {
+ return extractBasicAuth(me.Request().Header.Get("Authorization"))
+}
+
+var basicAuthFromHeaderReg = regexp.MustCompile(`^Basic\s([^\s]+)$`)
+
+func extractBasicAuth(headerValue string) (string, string) {
+ subm := basicAuthFromHeaderReg.FindStringSubmatch(headerValue)
+ l := len(subm)
+ if l != 2 {
+ return "", ""
}
- u, err := c.System().DB.User.APIKey(apiKey)
+
+ b, err := base64.StdEncoding.DecodeString(subm[1])
if err != nil {
- return nil, err
+ return "", ""
}
- return u, nil
+
+ fields := strings.Split(string(b), ":")
+
+ if len(fields) != 2 {
+ return "", ""
+ }
+
+ return strings.TrimSpace(fields[0]), strings.TrimSpace(fields[1])
}
diff --git a/main/www/context_test.go b/main/www/context_test.go
new file mode 100644
index 000000000..a4d8d84cf
--- /dev/null
+++ b/main/www/context_test.go
@@ -0,0 +1,135 @@
+package www
+
+import (
+ "encoding/base64"
+ "testing"
+)
+
+func TestExtractApiKey(t *testing.T) {
+
+ tests := []struct {
+ title string
+ value string
+ expected string
+ }{
+ {
+ "No header",
+ "",
+ "",
+ },
+ {
+ "Authorization header but wrong type",
+ "Basic 1234",
+ "",
+ },
+ {
+ "Authorization header right type, wrong spacing",
+ "Bearer 1234",
+ "",
+ },
+ {
+ "Authorization header right type, wrong spacing2",
+ " Bearer 1234",
+ "",
+ },
+ {
+ "Authorization header right type, wrong spacing3",
+ "Bearer 1234 ",
+ "",
+ },
+ {
+ "Good",
+ "Bearer 1234",
+ "1234",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.title, func(t *testing.T) {
+ result := extractSessionToken(test.value)
+ if result != test.expected {
+ t.Errorf("Expected %s and got %s", test.expected, result)
+ }
+ })
+ }
+
+}
+
+func TestExtractBasicAuth(t *testing.T) {
+ tests := []struct {
+ title string
+ value string
+ user string
+ password string
+ }{
+ {
+ "No header",
+ "",
+ "",
+ "",
+ },
+ {
+ "Authorization header but wrong type",
+ "Bearer 1234",
+ "",
+ "",
+ },
+ {
+ "Authorization header right type, wrong spacing",
+ "Basic " + base64.StdEncoding.EncodeToString([]byte("foo:bar")),
+ "",
+ "",
+ },
+ {
+ "Authorization header right type, wrong spacing2",
+ " Basic " + base64.StdEncoding.EncodeToString([]byte("foo:bar")),
+ "",
+ "",
+ },
+ {
+ "authorization header right type, wrong spacing3",
+ "Basic " + base64.StdEncoding.EncodeToString([]byte("foo:bar")) + " ",
+ "",
+ "",
+ },
+ {
+ "authorization header right type, wrong content",
+ "Basic " + base64.StdEncoding.EncodeToString([]byte("foo:")),
+ "foo",
+ "",
+ },
+ {
+ "authorization header right type, wrong content",
+ "Basic " + base64.StdEncoding.EncodeToString([]byte(":bar")),
+ "",
+ "bar",
+ },
+ {
+ "authorization header right type, wrong content",
+ "Basic " + base64.StdEncoding.EncodeToString([]byte(":")),
+ "",
+ "",
+ },
+ {
+ "authorization header right type, wrong content",
+ "Basic " + base64.StdEncoding.EncodeToString([]byte("")),
+ "",
+ "",
+ },
+ {
+ "Good",
+ "Basic " + base64.StdEncoding.EncodeToString([]byte("foo:bar")),
+ "foo",
+ "bar",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.title, func(t *testing.T) {
+ u, p := extractBasicAuth(test.value)
+ if u != test.user || p != test.password {
+ t.Errorf("Expected %s:%s and got %s:%s", test.user, test.password, u, p)
+ }
+ })
+ }
+}
diff --git a/main/www/default_server.go b/main/www/default_server.go
deleted file mode 100644
index cd99eec1d..000000000
--- a/main/www/default_server.go
+++ /dev/null
@@ -1,83 +0,0 @@
-package www
-
-import (
- "context"
- "fmt"
- "io"
- "log"
- "os"
- "os/signal"
- "path"
- "syscall"
-
- "golang.org/x/crypto/acme/autocert"
-
- "github.com/labstack/echo"
- "github.com/labstack/echo/middleware"
- "github.com/natefinch/lumberjack"
-)
-
-func Setup(logFileLocation string) *echo.Echo {
- e := echo.New()
-
- // logging setup
- {
- e.Debug = true
- var lw io.Writer
- lw = &lumberjack.Logger{
- Filename: logFileLocation,
- MaxSize: 100, // MB
- MaxAge: 120, // days
- }
- // test it
- _, err := lw.Write([]byte("log init\n"))
- if err != nil {
- log.Printf("File logging disabled due to: <%s>\n", err)
- // fallback to std
- lw = os.Stdout
- } else {
- log.Printf("Logging to: %s\n", logFileLocation)
- }
- e.Logger.SetOutput(lw)
- log.SetOutput(lw)
- e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{Output: lw}))
- }
-
- // very important
- e.Use(middleware.Recover())
-
- e.HTTPErrorHandler = DefaultHTTPErrorHandler
- e.HideBanner = true
-
- return e
-}
-
-func StartServer(e *echo.Echo, addr string, autoTLS bool) {
- if autoTLS {
- dirCache := path.Join(os.TempDir(), ".cache")
- e.AutoTLSManager.Cache = autocert.DirCache(dirCache)
- }
- quit := make(chan os.Signal)
-
- // Start server
- go func() {
- if autoTLS {
- fmt.Println("starting https at", addr)
- if err := e.StartAutoTLS(addr); err != nil {
- log.Println(err)
- }
- } else {
- fmt.Println("starting plain http at", addr)
- if err := e.Start(addr); err != nil {
- log.Println(err)
- }
- }
- }()
-
- signal.Notify(quit, os.Interrupt)
- signal.Notify(quit, syscall.SIGTERM)
- <-quit
- if err := e.Shutdown(context.Background()); err != nil {
- log.Fatal(err)
- }
-}
diff --git a/main/www/error.go b/main/www/error.go
index b459891ad..08bb1654e 100644
--- a/main/www/error.go
+++ b/main/www/error.go
@@ -25,22 +25,28 @@ func DefaultHTTPErrorHandler(err error, c echo.Context) {
if _, ok := msg.(string); ok {
msg = echo.Map{"message": msg}
}
- if !c.Response().Committed {
- if c.Request().Header.Get("X-Requested-With") == "XMLHttpRequest" {
- if err = c.JSON(code, msg); err != nil {
- goto ERROR
- }
- } else {
- bts, err = sys.ReadAllFile("frontend.html")
- if err == nil {
- if err = c.HTMLBlob(code, bts); err != nil {
- goto ERROR
- }
- return
- }
+ if c.Response().Committed {
+ log.Println(err)
+ return
+ }
+
+ if c.Request().Header.Get("X-Requested-With") == "XMLHttpRequest" {
+ err := c.JSON(code, msg)
+ if err != nil {
+ log.Println(err)
}
+ return
+ }
+
+ bts, err = sys.ReadAllFile("frontend.html")
+ if err != nil {
+ log.Println(err)
+ return
+ }
+
+ err = c.HTMLBlob(code, bts)
+ if err != nil {
+ log.Println(err)
}
-ERROR:
- log.Println(err)
}
diff --git a/main/www/initial.go b/main/www/initial.go
index 5e102dbfd..8f7015dc2 100644
--- a/main/www/initial.go
+++ b/main/www/initial.go
@@ -22,48 +22,50 @@ func NewInitialHandler(configured bool) *InitialHandler {
func (me *InitialHandler) Handler(next echo.HandlerFunc) echo.HandlerFunc {
return func(e echo.Context) error {
c := e.(*Context)
- if !me.configured {
- sess := c.Session(false)
- if sess == nil || sess.AccessRights() != model.ROOT { //ensure we have a tmp root session to power up
- sess = c.SessionWithUser(&model.User{ID: "XYZ", Role: model.ROOT})
- }
+ if me.configured {
+ return next(c)
+ }
- if me.cleanOnNextCall && c.Request().RequestURI != "/api/import/results" && c.Request().RequestURI != "/api/init" {
- me.configured, _ = c.System().Configured()
- me.cleanOnNextCall = false
- er2 := c.System().SessionMgmnt.Clean()
- if er2 != nil {
- return c.NoContent(http.StatusInternalServerError)
- }
- return next(c)
+ sess := c.Session(false)
+ if sess == nil || sess.AccessRights() != model.ROOT { //ensure we have a tmp root session to power up
+ sess = c.SessionWithUser(&model.User{ID: "XYZ", Role: model.ROOT})
+ }
+
+ if me.cleanOnNextCall && c.Request().RequestURI != "/api/import/results" && c.Request().RequestURI != "/api/init" {
+ me.configured, _ = c.System().Configured()
+ me.cleanOnNextCall = false
+ er2 := c.System().SessionMgmnt.Clean()
+ if er2 != nil {
+ return c.NoContent(http.StatusInternalServerError)
}
- if strings.ToLower(c.Request().Method) == "get" {
- if !strings.HasPrefix(c.Request().RequestURI, "/api/") &&
- !strings.HasPrefix(c.Request().RequestURI, "/static/") &&
- !strings.HasPrefix(c.Request().RequestURI, "/favicon.ico") {
- bts, err := sys.ReadAllFile("initial.html")
- if err != nil {
- return c.NoContent(http.StatusNotFound)
- }
- return c.HTMLBlob(http.StatusOK, bts)
- }
- return next(c)
- } else {
- if strings.HasPrefix(c.Request().RequestURI, "/api/init") || strings.HasPrefix(c.Request().RequestURI, "/api/import") {
- er := next(c)
- me.configured, _ = c.System().Configured()
- if me.configured {
- me.configured = false
- //to let /api/import/results through as all sessions will be deleted afterwards, this makes it possible
- //to view the results before they are gone
- me.cleanOnNextCall = true
- }
- return er
+ return next(c)
+ }
+
+ if strings.ToLower(c.Request().Method) == "get" {
+ if !strings.HasPrefix(c.Request().RequestURI, "/api/") &&
+ !strings.HasPrefix(c.Request().RequestURI, "/static/") &&
+ !strings.HasPrefix(c.Request().RequestURI, "/favicon.ico") {
+ bts, err := sys.ReadAllFile("initial.html")
+ if err != nil {
+ return c.NoContent(http.StatusNotFound)
}
- return c.NoContent(http.StatusBadRequest)
+ return c.HTMLBlob(http.StatusOK, bts)
}
+ return next(c)
+ }
+ if strings.HasPrefix(c.Request().RequestURI, "/api/init") || strings.HasPrefix(c.Request().RequestURI, "/api/import") {
+ er := next(c)
+ me.configured, _ = c.System().Configured()
+ if me.configured {
+ me.configured = false
+ //to let /api/import/results through as all sessions will be deleted afterwards, this makes it possible
+ //to view the results before they are gone
+ me.cleanOnNextCall = true
+ }
+ return er
}
- return next(c)
+
+ return c.NoContent(http.StatusBadRequest)
}
}
diff --git a/main/www/server.go b/main/www/server.go
index 837eb88cd..473f72601 100644
--- a/main/www/server.go
+++ b/main/www/server.go
@@ -1,80 +1,106 @@
package www
import (
- "bytes"
- "io"
- "io/ioutil"
+ "context"
+ "fmt"
+ "log"
"os"
+ "os/signal"
+ "path"
+ "syscall"
- "path/filepath"
-)
+ "golang.org/x/crypto/acme/autocert"
-type MyServer struct {
- quit chan os.Signal
-}
+ "github.com/labstack/echo"
+ "github.com/labstack/echo/middleware"
+)
-func (ms *MyServer) Close() {
- if ms.quit != nil {
- ms.quit <- os.Interrupt
- }
-}
+func Setup(serverVersion string) *echo.Echo {
+ e := echo.New()
+ e.HTTPErrorHandler = DefaultHTTPErrorHandler
-type MyHTMLTemplateLoader struct {
- BaseDir string
- MoreDirs *[]string
-}
+ // Pre routing middleware
+ e.Pre(xVersionHeader(serverVersion))
+ c := middleware.DefaultSecureConfig
+ c.XFrameOptions = ""
+ e.Pre(middleware.SecureWithConfig(c))
+ e.Pre(middleware.Secure())
-// Abs calculates the path to a given template. Whenever a path must be resolved
-// due to an import from another template, the base equals the parent template's path.
-func (htl *MyHTMLTemplateLoader) Abs(base, name string) (absPath string) {
- if filepath.IsAbs(name) {
- return name
- }
+ // Post routing middleware
+ e.Use(func(h echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ return h(&Context{Context: c})
+ }
+ })
+ //Simple Request Logging
+ e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
+ Format: "[echo] ${time_rfc3339} client=${remote_ip}, method=${method}, uri=${uri}, status=${status}\n",
+ }))
- // Our own base dir has always priority; if there's none
- // we use the path provided in base.
- var err error
- if htl.BaseDir == "" {
- if base == "" {
- base, err = os.Getwd()
+ //Request Logging with User Info and Body on Error
+ e.Use(middleware.BodyDump(func(e echo.Context, reqBody, resBody []byte) {
+ c := e.(*Context)
+ s := c.Session(false)
+ if s == nil {
+ return
+ }
+ if s.ID() != "" {
+ id := s.UserID()
+ user, err := c.System().DB.User.Get(s, id)
if err != nil {
- panic(err)
+ return
+ }
+ userName := user.Name
+ userAddr := user.EthereumAddr
+ log.Println("[echo] Method: "+e.Request().Method, "Status:", e.Response().Status, "User: "+userAddr, "("+userName+")", "URI: "+e.Request().RequestURI)
+ if len(reqBody) > 0 && c.Response().Status != 200 && c.Response().Status != 404 {
+ fmt.Printf("[echo][errorrequest] %s\n", reqBody)
}
- absPath = filepath.Join(base, name)
- htl.checkPath(&name, &absPath)
- return
}
- absPath = filepath.Join(filepath.Dir(base), name)
- htl.checkPath(&name, &absPath)
- return
- }
- absPath = filepath.Join(htl.BaseDir, name)
- htl.checkPath(&name, &absPath)
- return
+
+ }))
+
+ e.Use(SessionMiddleware())
+ e.Use(SessionTokenAuth)
+
+ return e
}
-func (htl *MyHTMLTemplateLoader) checkPath(relPath, absPath *string) {
- if htl.MoreDirs != nil && len(*htl.MoreDirs) > 0 {
- var err error
- if _, err = os.Stat(*absPath); err == nil {
- return
- }
- newPath := ""
- for _, dirPath := range *htl.MoreDirs {
- newPath = filepath.Join(dirPath, *relPath)
- if _, err = os.Stat(newPath); err == nil {
- *absPath = newPath
- break
+func StartServer(e *echo.Echo, addr string, autoTLS bool) {
+ if autoTLS {
+ dirCache := path.Join(os.TempDir(), ".cache")
+ e.AutoTLSManager.Cache = autocert.DirCache(dirCache)
+ }
+ quit := make(chan os.Signal)
+
+ // Start server
+ go func() {
+ if autoTLS {
+ fmt.Println("starting https at", addr)
+ if err := e.StartAutoTLS(addr); err != nil {
+ log.Println(err)
+ }
+ } else {
+ fmt.Println("starting plain http at", addr)
+ if err := e.Start(addr); err != nil {
+ log.Println(err)
}
}
+ }()
+
+ signal.Notify(quit, os.Interrupt)
+ signal.Notify(quit, syscall.SIGTERM)
+ <-quit
+ if err := e.Shutdown(context.Background()); err != nil {
+ log.Fatal(err)
}
}
-// Get returns an io.Reader where the template's content can be read from.
-func (htl *MyHTMLTemplateLoader) Get(path string) (io.Reader, error) {
- buf, err := ioutil.ReadFile(path)
- if err != nil {
- return nil, err
+func xVersionHeader(version string) echo.MiddlewareFunc {
+ return func(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ c.Response().Header().Set("X-Version", version)
+ return next(c)
+ }
}
- return bytes.NewReader(buf), nil
}
diff --git a/main/www/session.go b/main/www/session.go
index 031af2af2..7e7c14490 100644
--- a/main/www/session.go
+++ b/main/www/session.go
@@ -14,11 +14,11 @@ import (
sysSess "git.proxeus.com/core/central/sys/session"
)
-func SetupSession(e *echo.Echo) {
+func SessionMiddleware() echo.MiddlewareFunc {
gob.Register(map[string]interface{}{})
gob.Register(map[string]map[string]string{})
sessionStore := sessions.NewCookieStore([]byte("secret_Dummy_1234"), []byte("12345678901234567890123456789012"))
- e.Use(session.Middleware(sessionStore))
+ return session.Middleware(sessionStore)
}
var anonymousUser = &model.User{Role: model.PUBLIC}
@@ -30,17 +30,16 @@ func init() {
}
func getSessionWithUser(c *Context, create bool, usr *model.User) (currentSession *sysSess.Session, err error) {
- var sess *sessions.Session
- var ok bool
if !create || usr == nil {
if csess := c.Get("sys.session"); csess != nil {
+ var ok bool
if currentSession, ok = csess.(*sysSess.Session); ok {
return
}
}
}
- sess, err = session.Get("s", c)
+ sess, err := session.Get("s", c)
if sess == nil || err != nil {
return
}
diff --git a/sys/db/storm/user_data.go b/sys/db/storm/user_data.go
index 31022d653..0ffdc75b8 100644
--- a/sys/db/storm/user_data.go
+++ b/sys/db/storm/user_data.go
@@ -47,8 +47,14 @@ func NewUserDataDB(dir string) (*UserDataDB, error) {
udb.baseFilePath = assetDir
example := &model.UserDataItem{}
- udb.db.Init(example)
- udb.db.ReIndex(example)
+ err = udb.db.Init(example)
+ if err != nil {
+ return nil, err
+ }
+ err = udb.db.ReIndex(example)
+ if err != nil {
+ return nil, err
+ }
var fVersion int
verr := udb.db.Get(usrdVersion, usrdVersion, &fVersion)
if verr == nil && fVersion != example.GetVersion() {
@@ -146,30 +152,21 @@ func (me *UserDataDB) GetAllFileInfosOf(ud *model.UserDataItem) []*file.IO {
return m.GetAllFileInfos(me.baseFilePath)
}
-func (me *UserDataDB) GetByWorkflow(auth model.Authorization, wf *model.WorkflowItem, finished bool) (*model.UserDataItem, error) {
+func (me *UserDataDB) GetByWorkflow(auth model.Authorization, wf *model.WorkflowItem, finished bool) (*model.UserDataItem, bool, error) {
var item model.UserDataItem
matchers := defaultMatcher(auth, "", nil, true)
matchers = append(matchers, q.And(q.Eq("WorkflowID", wf.ID), q.Eq("Finished", finished)))
+ alreadyStarted := false
err := me.db.Select(matchers...).OrderBy("Created").Reverse().First(&item)
- if err == storm.ErrNotFound && !finished {
- item.WorkflowID = wf.ID
- item.Name = wf.Name
- item.Detail = wf.Detail
- er := me.Put(auth, &item)
- if er != nil {
- return nil, er
- } else {
- return &item, nil
- }
- }
if err != nil {
- return nil, err
+ return nil, alreadyStarted, err
}
+ alreadyStarted = true
if !item.Permissions.IsReadGrantedFor(auth) {
- return nil, model.ErrAuthorityMissing
+ return nil, alreadyStarted, model.ErrAuthorityMissing
}
- me.db.Get(usrdHeavyData, item.ID, &item.Data)
- return &item, nil
+ err = me.db.Get(usrdHeavyData, item.ID, &item.Data)
+ return &item, alreadyStarted, err
}
func (me *UserDataDB) GetData(auth model.Authorization, id, dataPath string) (interface{}, error) {
diff --git a/sys/db/storm/user_data_test.go b/sys/db/storm/user_data_test.go
index cf1d2b9e4..6fbcb02d5 100644
--- a/sys/db/storm/user_data_test.go
+++ b/sys/db/storm/user_data_test.go
@@ -40,7 +40,7 @@ func TestPutGetData(t *testing.T) {
t.Error("data is nil")
}
if innerMap, ok := newUsrData.Data["input"].(map[string]interface{}); ok {
- if someInt, ok := innerMap["someInt"].(uint16); ok {
+ if someInt, ok := innerMap["someInt"].(int64); ok {
if someInt != 1234 {
t.Error("someInt missing", someInt)
}
@@ -48,16 +48,16 @@ func TestPutGetData(t *testing.T) {
t.Error("someInt missing")
}
if list, ok := innerMap["list"].([]interface{}); ok {
- if i, ok := list[0].(uint16); ok && i != 1 {
+ if i, ok := list[0].(int64); ok && i != 1 {
t.Error("not 1")
}
- if i, ok := list[1].(uint16); ok && i != 2 {
+ if i, ok := list[1].(int64); ok && i != 2 {
t.Error("not 2")
}
- if i, ok := list[2].(uint16); ok && i != 3 {
+ if i, ok := list[2].(int64); ok && i != 3 {
t.Error("not 3")
}
- if i, ok := list[3].(uint16); ok && i != 4 {
+ if i, ok := list[3].(int64); ok && i != 4 {
t.Error("not 4")
}
if i, ok := list[4].(string); ok && i != "hello" {
diff --git a/sys/db/storm/utils.go b/sys/db/storm/utils.go
index eaaa5d42a..509c93e8e 100644
--- a/sys/db/storm/utils.go
+++ b/sys/db/storm/utils.go
@@ -115,12 +115,16 @@ func defaultMatcher(auth model.Authorization, contains string, params *simpleQue
func publishedMatcher(auth model.Authorization, contains string, params *simpleQuery) []q.Matcher {
matchers := commonMatcher(auth, contains, params)
- matchers = append(matchers, q.And(
- q.Or(
+ var m q.Matcher
+ if auth == nil {
+ m = q.Eq("Published", true)
+ } else {
+ m = q.Or(
q.Eq("Owner", auth.UserID()),
q.Eq("Published", true),
- ),
- ))
+ )
+ }
+ matchers = append(matchers, q.And(m))
return matchers
}
diff --git a/sys/db/storm/workflow.go b/sys/db/storm/workflow.go
index 34b099be6..2f17e90e2 100644
--- a/sys/db/storm/workflow.go
+++ b/sys/db/storm/workflow.go
@@ -19,6 +19,7 @@ type WorkflowDBInterface interface {
List(auth model.Authorization, contains string, options map[string]interface{}) ([]*model.WorkflowItem, error)
GetPublished(auth model.Authorization, id string) (*model.WorkflowItem, error)
Get(auth model.Authorization, id string) (*model.WorkflowItem, error)
+ GetList(auth model.Authorization, id []string) ([]*model.WorkflowItem, error)
Put(auth model.Authorization, item *model.WorkflowItem) error
put(auth model.Authorization, item *model.WorkflowItem, updated bool) error
getDB() *storm.DB
@@ -154,6 +155,19 @@ func (me *WorkflowDB) Get(auth model.Authorization, id string) (*model.WorkflowI
return itemRef, nil
}
+// Retrieve multiple workflows. If one is not found, an error is returned
+func (me *WorkflowDB) GetList(auth model.Authorization, ids []string) ([]*model.WorkflowItem, error) {
+ var workflows []*model.WorkflowItem
+ for _, id := range ids {
+ workflow, err := me.Get(auth, id)
+ if err != nil {
+ return nil, err
+ }
+ workflows = append(workflows, workflow)
+ }
+ return workflows, nil
+}
+
func (me *WorkflowDB) Put(auth model.Authorization, item *model.WorkflowItem) error {
return me.put(auth, item, true)
}
diff --git a/sys/db/storm/workflow_mock.go b/sys/db/storm/workflow_mock.go
index b7021a696..bbaba688c 100644
--- a/sys/db/storm/workflow_mock.go
+++ b/sys/db/storm/workflow_mock.go
@@ -96,6 +96,21 @@ func (mr *MockWorkflowDBInterfaceMockRecorder) Get(auth, id interface{}) *gomock
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockWorkflowDBInterface)(nil).Get), auth, id)
}
+// GetList mocks base method
+func (m *MockWorkflowDBInterface) GetList(auth model.Authorization, ids []string) ([]*model.WorkflowItem, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetList", auth, ids)
+ ret0, _ := ret[0].([]*model.WorkflowItem)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetList indicates an expected call of GetList
+func (mr *MockWorkflowDBInterfaceMockRecorder) GetList(auth, ids interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetList", reflect.TypeOf((*MockWorkflowDBInterface)(nil).GetList), auth, ids)
+}
+
// Put mocks base method
func (m *MockWorkflowDBInterface) Put(auth model.Authorization, item *model.WorkflowItem) error {
m.ctrl.T.Helper()
diff --git a/sys/db/storm/workflow_payments.go b/sys/db/storm/workflow_payments.go
index 8341dae60..23315566f 100644
--- a/sys/db/storm/workflow_payments.go
+++ b/sys/db/storm/workflow_payments.go
@@ -1,7 +1,11 @@
package storm
import (
+ "errors"
+ "log"
"path/filepath"
+ "strings"
+ "time"
"github.com/asdine/storm"
"github.com/asdine/storm/codec/msgpack"
@@ -11,11 +15,18 @@ import (
)
type WorkflowPaymentsDBInterface interface {
- GetByTxHash(txHash string) (*model.WorkflowPaymentItem, error)
- GetByWorkflowId(workflowID string) (*model.WorkflowPaymentItem, error)
- GetByWorkflowIdAndFromEthAddress(workflowID, ethAddr string) (*model.WorkflowPaymentItem, error)
- Add(item *model.WorkflowPaymentItem) error
- Delete(txHash string) error
+ GetByTxHashAndStatusAndFromEthAddress(txHash, status, from string) (*model.WorkflowPaymentItem, error)
+ Get(paymentId string) (*model.WorkflowPaymentItem, error)
+ ConfirmPayment(txHash, from, to string, xes uint64) error
+ GetByWorkflowIdAndFromEthAddress(workflowID, fromEthAddr string, statuses []string) (*model.WorkflowPaymentItem, error)
+ SetAbandonedToTimeoutBeforeTime(beforeTime time.Time) error
+ Save(item *model.WorkflowPaymentItem) error
+ Update(paymentId, status, txHash, from string) error
+ Cancel(paymentId, from string) error
+ Redeem(workflowId, from string) error
+ Delete(paymentId string) error
+ Remove(payment *model.WorkflowPaymentItem) error
+ All() ([]*model.WorkflowPaymentItem, error)
Close() error
}
@@ -23,27 +34,33 @@ type WorkflowPaymentsDB struct {
db *storm.DB
}
-const workflowPaymentVersion = "sig_vers"
-const workflowPaymentDBDir = "workflowpayments"
-const workflowPaymentDB = "workflowpaymentsdb"
+const workflowPaymentVersion = "payment_vers"
+const WorkflowPaymentDBDir = "workflowpayments"
+const WorkflowPaymentDB = "workflowpaymentsdb"
func NewWorkflowPaymentDB(dir string) (*WorkflowPaymentsDB, error) {
var err error
var msgpackDb *storm.DB
- baseDir := filepath.Join(dir, workflowPaymentDBDir)
+ baseDir := filepath.Join(dir, WorkflowPaymentDBDir)
err = ensureDir(baseDir)
if err != nil {
return nil, err
}
- msgpackDb, err = storm.Open(filepath.Join(baseDir, workflowPaymentDB), storm.Codec(msgpack.Codec))
+ msgpackDb, err = storm.Open(filepath.Join(baseDir, WorkflowPaymentDB), storm.Codec(msgpack.Codec))
if err != nil {
return nil, err
}
udb := &WorkflowPaymentsDB{db: msgpackDb}
example := &model.WorkflowPaymentItem{}
- udb.db.Init(example)
- udb.db.ReIndex(example)
+ err = udb.db.Init(example)
+ if err != nil {
+ return nil, err
+ }
+ err = udb.db.ReIndex(example)
+ if err != nil {
+ return nil, err
+ }
err = udb.db.Set(workflowPaymentVersion, workflowPaymentVersion, example.GetVersion())
if err != nil {
@@ -53,72 +70,296 @@ func NewWorkflowPaymentDB(dir string) (*WorkflowPaymentsDB, error) {
return udb, nil
}
-func (me *WorkflowPaymentsDB) GetByTxHash(txHash string) (*model.WorkflowPaymentItem, error) {
- tx, err := me.db.Begin(false)
+func (me *WorkflowPaymentsDB) All() ([]*model.WorkflowPaymentItem, error) {
+ var items []*model.WorkflowPaymentItem
+
+ err := me.db.All(&items)
+ return items, err
+}
+
+func (me *WorkflowPaymentsDB) Get(paymentId string) (*model.WorkflowPaymentItem, error) {
+ var item model.WorkflowPaymentItem
+
+ query := me.db.Select(
+ q.Eq("ID", paymentId),
+ ).OrderBy("CreatedAt").Reverse().Limit(1) //always match newest entry
+
+ err := query.First(&item)
+ return &item, err
+}
+func (me *WorkflowPaymentsDB) GetByTxHashAndStatusAndFromEthAddress(txHash, status,
+ fromEthAddr string) (*model.WorkflowPaymentItem, error) {
+
+ var item model.WorkflowPaymentItem
+
+ query := me.db.Select(
+ q.Eq("TxHash", txHash),
+ q.Eq("Status", status),
+ q.Eq("From", fromEthAddr),
+ ).OrderBy("CreatedAt").Reverse()
+
+ err := query.First(&item)
if err != nil {
return nil, err
}
- var item model.WorkflowPaymentItem
- defer tx.Rollback()
- err = tx.One("TxHash", txHash, &item)
+
+ return &item, nil
+}
+
+func (me *WorkflowPaymentsDB) GetByWorkflowIdAndFromEthAddress(workflowID, fromEthAddr string,
+ statuses []string) (*model.WorkflowPaymentItem, error) {
+
+ var (
+ item model.WorkflowPaymentItem
+ query storm.Query
+ )
+
+ if len(statuses) == 0 {
+ query = me.db.Select(
+ q.Eq("WorkflowID", workflowID),
+ q.Eq("From", fromEthAddr),
+ )
+ } else {
+ query = me.db.Select(
+ q.Eq("WorkflowID", workflowID),
+ q.Eq("From", fromEthAddr),
+ q.In("Status", statuses),
+ )
+ }
+
+ query.OrderBy("CreatedAt").Reverse()
+
+ err := query.First(&item)
if err != nil {
return nil, err
}
return &item, nil
}
-func (me *WorkflowPaymentsDB) GetByWorkflowId(workflowID string) (*model.WorkflowPaymentItem, error) {
- tx, err := me.db.Begin(false)
+func (me *WorkflowPaymentsDB) SetAbandonedToTimeoutBeforeTime(beforeTime time.Time) error {
+ query := me.db.Select(
+ q.Or(
+ q.Eq("Status", model.PaymentStatusCreated),
+ q.Eq("Status", model.PaymentStatusPending),
+ ),
+ q.Lt("CreatedAt", beforeTime),
+ )
+
+ return query.Each(new(model.WorkflowPaymentItem), func(record interface{}) error {
+ u := record.(*model.WorkflowPaymentItem)
+ u.Status = model.PaymentStatusTimeout
+ return me.Save(u)
+ })
+}
+
+func (me *WorkflowPaymentsDB) Save(item *model.WorkflowPaymentItem) error {
+ if item.CreatedAt.IsZero() {
+ item.CreatedAt = time.Now()
+ }
+ return me.db.Save(item)
+}
+
+func (me *WorkflowPaymentsDB) ConfirmPayment(txHash, from, to string, xes uint64) error {
+ tx, err := me.db.Begin(true)
if err != nil {
- return nil, err
+ return err
}
+ defer func() {
+ if err := tx.Rollback(); err != nil && err != storm.ErrNotInTransaction {
+ log.Println("[WorkflowPaymentsDB] Rollback error: ", err.Error())
+ }
+ }()
+
var item model.WorkflowPaymentItem
- defer tx.Rollback()
- err = tx.One("WorkflowID", workflowID, &item)
+
+ // Initially try to get payment by TxHash
+ query := tx.Select(
+ q.Eq("TxHash", txHash),
+ q.Eq("From", from),
+ q.Eq("To", to),
+ q.Eq("Xes", xes),
+ q.In("Status", []string{model.PaymentStatusPending, model.PaymentStatusCreated}),
+ ).OrderBy("CreatedAt").Reverse().Limit(1) //always match newest entry
+
+ err = query.First(&item)
if err != nil {
- return nil, err
+ if err != storm.ErrNotFound {
+ return err
+ }
+
+ // prioritize PaymentStatusPending over PaymentStatusCreated
+ query := tx.Select(
+ q.Eq("From", from),
+ q.Eq("To", to),
+ q.Eq("Xes", xes),
+ q.Eq("Status", model.PaymentStatusPending),
+ ).OrderBy("CreatedAt").Reverse().Limit(1) //always match newest entry
+
+ err = query.First(&item)
+ if err != nil {
+ if err != storm.ErrNotFound {
+ return err
+ }
+
+ query = tx.Select(
+ q.Eq("From", from),
+ q.Eq("To", to),
+ q.Eq("Xes", xes),
+ q.Eq("Status", model.PaymentStatusCreated),
+ ).OrderBy("CreatedAt").Reverse().Limit(1) //always match newest entry
+
+ err = query.First(&item)
+ if err != nil {
+ return err
+ }
+ }
}
- return &item, nil
+
+ item.Status = model.PaymentStatusConfirmed
+ if item.TxHash == "" {
+ item.TxHash = txHash
+ }
+
+ err = tx.Update(&item)
+ if err != nil {
+ log.Println("[ConfirmPayment] tx.Update err: ", err.Error())
+ return err
+ }
+
+ return tx.Commit()
}
-func (me *WorkflowPaymentsDB) GetByWorkflowIdAndFromEthAddress(workflowID, ethAddr string) (*model.WorkflowPaymentItem, error) {
+func (me *WorkflowPaymentsDB) Redeem(workflowId, from string) error {
+ tx, err := me.db.Begin(true)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if err := tx.Rollback(); err != nil && err != storm.ErrNotInTransaction {
+ log.Println("[WorkflowPaymentsDB] Rollback error: ", err.Error())
+ }
+ }()
+
var item model.WorkflowPaymentItem
- query := me.db.Select(q.Eq("WorkflowID", workflowID), q.Eq("From", ethAddr))
+ query := tx.Select(
+ q.Eq("WorkflowID", workflowId),
+ q.Eq("From", from),
+ q.Eq("Status", model.PaymentStatusConfirmed),
+ ).OrderBy("CreatedAt").Reverse().Limit(1) //always match newest entry
- err := query.First(&item)
+ err = query.First(&item)
if err != nil {
- return nil, err
+ return err
}
- return &item, nil
+
+ item.Status = model.PaymentStatusRedeemed
+
+ err = tx.Update(&item)
+ if err != nil {
+ return err
+ }
+ return tx.Commit()
+}
+
+func (me *WorkflowPaymentsDB) Cancel(paymentId, from string) error {
+ tx, err := me.db.Begin(true)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if err := tx.Rollback(); err != nil && err != storm.ErrNotInTransaction {
+ log.Println("[WorkflowPaymentsDB] Rollback error: ", err.Error())
+ }
+ }()
+ var item model.WorkflowPaymentItem
+
+ query := tx.Select(
+ q.Eq("ID", paymentId),
+ q.Eq("From", from),
+ q.Eq("Status", model.PaymentStatusCreated),
+ ).OrderBy("CreatedAt").Reverse() //always match newest entry
+
+ err = query.First(&item)
+ if err != nil {
+ return err
+ }
+
+ item.Status = model.PaymentStatusCancelled
+
+ err = tx.Update(&item)
+ if err != nil {
+ return err
+ }
+ return tx.Commit()
}
-func (me *WorkflowPaymentsDB) Add(item *model.WorkflowPaymentItem) error {
+func (me *WorkflowPaymentsDB) Delete(paymentId string) error {
tx, err := me.db.Begin(true)
if err != nil {
return err
}
- defer tx.Rollback()
- tx.Save(item)
+ defer func() {
+ if err := tx.Rollback(); err != nil && err != storm.ErrNotInTransaction {
+ log.Println("[WorkflowPaymentsDB] Rollback error: ", err.Error())
+ }
+ }()
+ var item model.WorkflowPaymentItem
+
+ err = tx.One("ID", paymentId, &item)
+ if err != nil {
+ return err
+ }
+
+ item.Status = model.PaymentStatusDeleted
+
+ err = tx.Update(&item)
if err != nil {
return err
}
return tx.Commit()
}
-func (me *WorkflowPaymentsDB) Delete(txHash string) error {
+func (me *WorkflowPaymentsDB) Remove(payment *model.WorkflowPaymentItem) error {
+ return me.db.DeleteStruct(payment)
+}
+
+var errNothingToUpdate = errors.New("nothing to update")
+
+func (me *WorkflowPaymentsDB) Update(paymentId, status, txHash, from string) error {
tx, err := me.db.Begin(true)
if err != nil {
return err
}
- defer tx.Rollback()
+ defer func() {
+ if err := tx.Rollback(); err != nil && err != storm.ErrNotInTransaction {
+ log.Println("[WorkflowPaymentsDB] Rollback error: ", err.Error())
+ }
+ }()
var item model.WorkflowPaymentItem
- err = tx.One("TxHash", txHash, &item)
+ query := tx.Select(
+ q.Eq("ID", paymentId),
+ q.Eq("From", from),
+ q.Eq("Status", model.PaymentStatusCreated),
+ ).OrderBy("CreatedAt").Reverse().Limit(1) //always match newest entry
+
+ err = query.First(&item)
if err != nil {
return err
}
- err = tx.DeleteStruct(&item)
+ if strings.TrimSpace(status) == "" && strings.TrimSpace(txHash) == "" {
+ return errNothingToUpdate
+ }
+
+ if strings.TrimSpace(status) != "" {
+ item.Status = status
+ }
+ if strings.TrimSpace(txHash) != "" {
+ item.TxHash = txHash
+ }
+
+ err = tx.Update(&item)
if err != nil {
return err
}
diff --git a/sys/db/storm/workflow_payments_mock.go b/sys/db/storm/workflow_payments_mock.go
index ccdfbc6b1..7c5062ac6 100644
--- a/sys/db/storm/workflow_payments_mock.go
+++ b/sys/db/storm/workflow_payments_mock.go
@@ -35,77 +35,148 @@ func (m *MockWorkflowPaymentsDBInterface) EXPECT() *MockWorkflowPaymentsDBInterf
return m.recorder
}
-// GetByTxHash mocks base method
+// GetByTxHashAndStatusAndFromEthAddress mocks base method
func (m *MockWorkflowPaymentsDBInterface) GetByTxHash(txHash string) (*model.WorkflowPaymentItem, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetByTxHash", txHash)
+ ret := m.ctrl.Call(m, "GetByTxHashAndStatusAndFromEthAddress", txHash)
ret0, _ := ret[0].(*model.WorkflowPaymentItem)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// GetByTxHash indicates an expected call of GetByTxHash
+// GetByTxHashAndStatusAndFromEthAddress indicates an expected call of GetByTxHashAndStatusAndFromEthAddress
func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) GetByTxHash(txHash interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByTxHash", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).GetByTxHash), txHash)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByTxHashAndStatusAndFromEthAddress", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).GetByTxHash), txHash)
}
-// GetByWorkflowId mocks base method
-func (m *MockWorkflowPaymentsDBInterface) GetByWorkflowId(workflowID string) (*model.WorkflowPaymentItem, error) {
+// Get mocks base method
+func (m *MockWorkflowPaymentsDBInterface) Get(paymentId, from string) (*model.WorkflowPaymentItem, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetByWorkflowId", workflowID)
+ ret := m.ctrl.Call(m, "Get", paymentId, from)
ret0, _ := ret[0].(*model.WorkflowPaymentItem)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// GetByWorkflowId indicates an expected call of GetByWorkflowId
-func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) GetByWorkflowId(workflowID interface{}) *gomock.Call {
+// Get indicates an expected call of Get
+func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) Get(paymentId, from interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByWorkflowId", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).GetByWorkflowId), workflowID)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).Get), paymentId, from)
+}
+
+// ConfirmPayment mocks base method
+func (m *MockWorkflowPaymentsDBInterface) ConfirmPayment(txHash, from, to string, xes uint64) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ConfirmPayment", txHash, from, to, xes)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// ConfirmPayment indicates an expected call of ConfirmPayment
+func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) ConfirmPayment(txHash, from, to, xes interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfirmPayment", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).ConfirmPayment), txHash, from, to, xes)
}
// GetByWorkflowIdAndFromEthAddress mocks base method
-func (m *MockWorkflowPaymentsDBInterface) GetByWorkflowIdAndFromEthAddress(workflowID, ethAddr string) (*model.WorkflowPaymentItem, error) {
+func (m *MockWorkflowPaymentsDBInterface) GetByWorkflowIdAndFromEthAddress(workflowID, ethAddr string, statuses []string) (*model.WorkflowPaymentItem, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetByWorkflowIdAndFromEthAddress", workflowID, ethAddr)
+ ret := m.ctrl.Call(m, "GetByWorkflowIdAndFromEthAddress", workflowID, ethAddr, statuses)
ret0, _ := ret[0].(*model.WorkflowPaymentItem)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetByWorkflowIdAndFromEthAddress indicates an expected call of GetByWorkflowIdAndFromEthAddress
-func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) GetByWorkflowIdAndFromEthAddress(workflowID, ethAddr interface{}) *gomock.Call {
+func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) GetByWorkflowIdAndFromEthAddress(workflowID, ethAddr, statuses interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByWorkflowIdAndFromEthAddress", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).GetByWorkflowIdAndFromEthAddress), workflowID, ethAddr, statuses)
+}
+
+// Save mocks base method
+func (m *MockWorkflowPaymentsDBInterface) Save(item *model.WorkflowPaymentItem) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Save", item)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Save indicates an expected call of Save
+func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) Save(item interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).Save), item)
+}
+
+// Update mocks base method
+func (m *MockWorkflowPaymentsDBInterface) Update(paymentId, status, txHash, from string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Update", paymentId, status, txHash, from)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Update indicates an expected call of Update
+func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) Update(paymentId, status, txHash, from interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByWorkflowIdAndFromEthAddress", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).GetByWorkflowIdAndFromEthAddress), workflowID, ethAddr)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).Update), paymentId, status, txHash, from)
}
-// Add mocks base method
-func (m *MockWorkflowPaymentsDBInterface) Add(item *model.WorkflowPaymentItem) error {
+// Cancel mocks base method
+func (m *MockWorkflowPaymentsDBInterface) Cancel(paymentId, from string) error {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "Add", item)
+ ret := m.ctrl.Call(m, "Cancel", paymentId, from)
ret0, _ := ret[0].(error)
return ret0
}
-// Add indicates an expected call of Add
-func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) Add(item interface{}) *gomock.Call {
+// Cancel indicates an expected call of Cancel
+func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) Cancel(paymentId, from interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).Add), item)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cancel", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).Cancel), paymentId, from)
}
// Delete mocks base method
-func (m *MockWorkflowPaymentsDBInterface) Delete(txHash string) error {
+func (m *MockWorkflowPaymentsDBInterface) Delete(paymentId string) error {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "Delete", txHash)
+ ret := m.ctrl.Call(m, "Delete", paymentId)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete
-func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) Delete(txHash interface{}) *gomock.Call {
+func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) Delete(paymentId interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).Delete), paymentId)
+}
+
+// Redeem mocks base method
+func (m *MockWorkflowPaymentsDBInterface) Redeem(workflowId, from string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Redeem", workflowId, from)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Redeem indicates an expected call of Redeem
+func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) Redeem(workflowId, from interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Redeem", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).Redeem), workflowId, from)
+}
+
+// All mocks base method
+func (m *MockWorkflowPaymentsDBInterface) All() ([]*model.WorkflowPaymentItem, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "All")
+ ret0, _ := ret[0].([]*model.WorkflowPaymentItem)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// All indicates an expected call of All
+func (mr *MockWorkflowPaymentsDBInterfaceMockRecorder) All() *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).Delete), txHash)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "All", reflect.TypeOf((*MockWorkflowPaymentsDBInterface)(nil).All))
}
// Close mocks base method
diff --git a/sys/db_test.go b/sys/db_test.go
index 1d4f4f6ff..59d32caf8 100644
--- a/sys/db_test.go
+++ b/sys/db_test.go
@@ -38,7 +38,7 @@ func TestRemWildcards(t *testing.T) {
}
func TestUnifySelector(t *testing.T) {
- var dbPath = "TestWriteReadTypeConversion.db"
+ var dbPath = "TestWriteReadTypeConversion2.db"
db, err := Open(dbPath)
sel, keys := db.unify(`omg.blabla["myemail@gmail.com"].omg.blabla[1]`)
fmt.Println(string(*sel))
@@ -52,7 +52,7 @@ func TestUnifySelector(t *testing.T) {
}
func TestAbsoluteKeyPath(t *testing.T) {
- var dbPath = "TestWriteReadTypeConversion.db"
+ var dbPath = "TestWriteReadTypeConversion3.db"
db, err := Open(dbPath)
var config = map[string]interface{}{"absolute.key.path": true, "sort": map[string]interface{}{"firstObject": "asc"}}
_ = db.Write(map[string]interface{}{
@@ -94,7 +94,7 @@ func TestArray(t *testing.T) {
}
func TestDelete(t *testing.T) {
- var dbPath = "TestWriteReadTypeConversion.db"
+ var dbPath = "TestWriteReadTypeConversion4.db"
db, err := Open(dbPath)
var i int = 2147483647
_ = db.Write(map[string]interface{}{
@@ -122,7 +122,7 @@ func TestDelete(t *testing.T) {
}
func TestWriteReadTypeConversion(t *testing.T) {
- var dbPath = "TestWriteReadTypeConversion.db"
+ var dbPath = "TestWriteReadTypeConversion5.db"
db, err := Open(dbPath)
var i int = 2147483647
var imin int = -2147483648
diff --git a/sys/model/all.go b/sys/model/all.go
index 27f39d100..2fa51f3d8 100644
--- a/sys/model/all.go
+++ b/sys/model/all.go
@@ -12,7 +12,10 @@ const (
PaymentStatusCreated = "created"
PaymentStatusPending = "pending"
PaymentStatusConfirmed = "confirmed"
- PaymentStatusFinished = "finished"
+ PaymentStatusCancelled = "cancelled"
+ PaymentStatusRedeemed = "redeemed"
+ PaymentStatusDeleted = "deleted"
+ PaymentStatusTimeout = "timeout"
)
type (
@@ -111,12 +114,13 @@ type (
WorkflowPaymentItem struct {
//save from who payment
- TxHash string `json:"hash" storm:"id"`
+ ID string `json:"id" storm:"id,unique"`
+ TxHash string `json:"hash" storm:"index,unique"`
WorkflowID string `json:"workflowID" storm:"index"`
- From string `json:"From"`
- To string `json:"To"`
+ From string `json:"from" storm:"index"`
+ To string `json:"to"`
Xes uint64 `json:"xes"`
- Status string `json:"Status"`
+ Status string `json:"status" storm:"index"`
CreatedAt time.Time `json:"createdAt"`
}
@@ -127,6 +131,12 @@ func (me *FormItem) GetVersion() int {
return 0
}
+func (me *FormItem) Clone() FormItem {
+ form := *me
+ form.ID = ""
+ return form
+}
+
func (me *FormComponentItem) GetVersion() int {
return 0
}
@@ -135,6 +145,18 @@ func (me *WorkflowItem) GetVersion() int {
return 0
}
+func (me *WorkflowItem) Clone() WorkflowItem {
+ workflow := *me
+ workflow.ID = "" // without id the repository will create a new one
+ return workflow
+}
+
+func (me *TemplateItem) Clone() TemplateItem {
+ template := *me
+ template.ID = ""
+ return template
+}
+
func (me *TemplateItem) GetVersion() int {
return 1
}
diff --git a/sys/model/settings.go b/sys/model/settings.go
index 35e7d40a0..77d96e70c 100644
--- a/sys/model/settings.go
+++ b/sys/model/settings.go
@@ -15,6 +15,11 @@ type Settings struct {
SparkpostApiKey string `json:"sparkpostApiKey" validate:"required=true" usage:"Sparkpost API key which will be used to send out emails."`
EmailFrom string `json:"emailFrom" validate:"required=true,email=true" usage:"Email that is being used to send out emails."`
LogPath string `json:"logPath" default:"./log" usage:"Location of the log file of this service."`
+ DefaultWorkflowIds string `json:"defaultWorkflowIds" usage:"Workflow IDs to set to clone and add to a new user"`
+ AirdropEnabled string `json:"airdropEnabled" validate:"required=true" default:"false" usage:"Enables/Disables the XES & Ether airdrop feature on ropsten."`
+ AirdropAmountXES string `json:"airdropAmountXES" default:"0" usage:"Amount of XES to airdrop to newly registered users."`
+ AirdropAmountEther string `json:"airdropAmountEther" default:"0" usage:"Amount of Ether to airdrop to newly registered users."`
+ TestMode string `json:"testMode" default:"false" usage:"Run the server in test mode (NOT FOR PRODUCTION)."`
}
func NewDefaultSettings() *Settings {
diff --git a/sys/session/manager.go b/sys/session/manager.go
index a316293f8..e2bface1b 100644
--- a/sys/session/manager.go
+++ b/sys/session/manager.go
@@ -184,7 +184,10 @@ func (me *Manager) Clean() error {
func (me *Manager) Close() (err error) {
me.sessionsDB.IterateMemStorage(func(key string, val interface{}) {
if sess, ok := val.(*Session); ok {
- _ = sess.close()
+ err = sess.close()
+ if err != nil {
+ log.Println(err.Error())
+ }
}
})
me.sessionsDB.Close()
diff --git a/sys/session/manager_test.go b/sys/session/manager_test.go
index 2192cc9b0..eac51e714 100644
--- a/sys/session/manager_test.go
+++ b/sys/session/manager_test.go
@@ -95,7 +95,7 @@ func TestOnCreatedOnLoadOnExpireOnRemove(t *testing.T) {
}
time.Sleep(time.Second * 1)
if exired, exists := myOnExpireMap[s1ID]; !exists || !exired {
- t.Error(s1ID, "not exired")
+ t.Error(s1ID, "not expired")
}
if exired, exists := myOnExpireMap[s2ID]; !exists || !exired {
t.Error(s2ID, "not exired")
@@ -178,8 +178,9 @@ type MySessionObject struct {
closeCalled bool
}
-func (me *MySessionObject) Close() {
+func (me *MySessionObject) Close() error {
me.closeCalled = true
+ return nil
}
type MySessionObject2 struct {
@@ -195,13 +196,13 @@ type MySessionObject3 struct {
closeCalled bool
}
-func (me *MySessionObject3) Close() (string, error) {
+func (me *MySessionObject3) Close() error {
me.closeCalled = true
- return "", nil
+ return nil
}
func TestExtendExpiryAndCloseOnSessionMemStore(t *testing.T) {
- sessDir := "./testSessionDir"
+ sessDir := "./testSessionDir2"
expiry := time.Millisecond * 800
myOnCreatedMap := make(map[string]bool)
myOnLoadMap := make(map[string]bool)
@@ -256,11 +257,11 @@ func TestExtendExpiryAndCloseOnSessionMemStore(t *testing.T) {
if err != nil || s == nil {
t.Error(err, s)
}
- if exired, exists := myOnExpireMap[s1ID]; exists || exired {
+ if expired, exists := myOnExpireMap[s1ID]; exists || expired {
t.Error(s1ID, "should't exire")
}
time.Sleep(time.Second * 1)
- if exired, exists := myOnExpireMap[s1ID]; !exists || !exired {
+ if expired, exists := myOnExpireMap[s1ID]; !exists || !expired {
t.Error(s1ID, "not exired")
}
if !obj1.closeCalled {
@@ -281,7 +282,7 @@ func TestExtendExpiryAndCloseOnSessionMemStore(t *testing.T) {
}
func TestCloseOnSessionMemStoreWhenClosingManager(t *testing.T) {
- sessDir := "./testSessionDir"
+ sessDir := "./testSessionDir3"
expiry := time.Second * 3
sm, err := NewManager(sessDir, expiry)
if err != nil {
diff --git a/sys/session/session.go b/sys/session/session.go
index 941a5f181..7190abd0d 100644
--- a/sys/session/session.go
+++ b/sys/session/session.go
@@ -230,6 +230,9 @@ func (me *Session) close() (err error) {
me.store.IterateMemStorage(func(key string, val interface{}) {
//ensure all changes during runtime are persisted before closing
me.store.UpdatedValueRef(key)
+ if closer, ok := val.(io.Closer); ok {
+ closer.Close()
+ }
})
me.store.Close()
}
diff --git a/sys/system.go b/sys/system.go
index 4b74c3f9c..5f9c32b5a 100644
--- a/sys/system.go
+++ b/sys/system.go
@@ -43,6 +43,7 @@ var (
type (
System struct {
+ TestMode bool
SessionMgmnt *session.Manager
DB *storm.DBSet
DS *eio.DocumentServiceClient
@@ -53,6 +54,7 @@ type (
fallbackSettings *model.Settings
paymentListenerCancelFunc context.CancelFunc
signatureListenerCancelFunc context.CancelFunc
+ tick *time.Ticker
}
sessionNotify struct {
system *System
@@ -93,6 +95,10 @@ func NewWithSettings(settings model.Settings) (*System, error) {
}
me := &System{settingsDB: stngsDB, fallbackSettings: &settings}
+ if strings.ToLower(settings.TestMode) == "true" {
+ me.TestMode = true
+ }
+
err = me.init(me.GetSettings())
if err != nil {
return nil, err
@@ -101,10 +107,9 @@ func NewWithSettings(settings model.Settings) (*System, error) {
}
func (me *System) init(stngs *model.Settings) error {
- log.Println("Init with settings: ", stngs)
- var err error
- var expiry time.Duration
- expiry, err = time.ParseDuration(stngs.SessionExpiry)
+ log.Printf("Init with settings: %#v\n", stngs)
+
+ expiry, err := time.ParseDuration(stngs.SessionExpiry)
if err != nil {
expiry = time.Hour
}
@@ -130,6 +135,10 @@ func (me *System) init(stngs *model.Settings) error {
log.Println("Wrong blockchain network: ", stngs.BlockchainNet)
}
+ cfg.Config.AirdropEnabled = stngs.AirdropEnabled
+ cfg.Config.AirdropAmountEther = stngs.AirdropAmountEther
+ cfg.Config.AirdropAmountXES = stngs.AirdropAmountXES
+
me.closeDBs()
var cacheExpiry time.Duration
cacheExpiry, err = time.ParseDuration(stngs.CacheExpiry)
@@ -201,9 +210,28 @@ func (me *System) init(stngs *model.Settings) error {
me.signatureListenerCancelFunc = cancelSig
go bcListenerSignature.Listen(ctxSig)
+ if me.tick != nil {
+ me.tick.Stop()
+ }
+ me.tick = time.NewTicker(time.Hour * 6)
+ go me.scheduledCleanup(me.tick)
+
return nil
}
+func (me *System) scheduledCleanup(tick *time.Ticker) {
+ for range tick.C {
+ beforeTime := time.Now().AddDate(0, 0, -14)
+ log.Println("[scheduler][workflowpaymentcleanup] Timing out abandoned payments from before ", beforeTime)
+ err := me.DB.WorkflowPaymentsDB.SetAbandonedToTimeoutBeforeTime(beforeTime)
+ if err != nil {
+ log.Println("[scheduler][workflowpaymentcleanup] err: ", err.Error())
+ continue
+ }
+ log.Println("[scheduler][workflowpaymentcleanup] Done")
+ }
+}
+
func (me *System) Configured() (bool, error) {
count, err := me.DB.User.Count()
if err != nil {
diff --git a/sys/system_test.go b/sys/system_test.go
index 0758c946a..2c36cc8f6 100644
--- a/sys/system_test.go
+++ b/sys/system_test.go
@@ -16,7 +16,7 @@ func TestNew(t *testing.T) {
wfItem(m)
}
-func wfItem(a model.PermissionItem) {
+func wfItem(a *model.WorkflowItem) {
bts, err := json.Marshal(a)
log.Println(err, string(bts))
}
diff --git a/sys/workflow/workflow_test.go b/sys/workflow/workflow_test.go
index b461165e4..3917e755e 100644
--- a/sys/workflow/workflow_test.go
+++ b/sys/workflow/workflow_test.go
@@ -67,7 +67,7 @@ func NewUserImpl(n *Node) (NodeIF, error) {
return &UserImpl{}, nil
}
-func (me *FormImpl) Execute(n *Node, data interface{}) (bool, error) {
+func (me *FormImpl) Execute(n *Node) (bool, error) {
if me.n == nil {
me.n = n
}
@@ -75,14 +75,14 @@ func (me *FormImpl) Execute(n *Node, data interface{}) (bool, error) {
if !me.presented {
//present
if testVerbose {
- log.Println("--->WF TEST IMPL [form] Execute present state", n, data)
+ log.Println("--->WF TEST IMPL [form] Execute present state", n)
}
me.presented = true
return false, nil
}
//validate
if testVerbose {
- log.Println("--->WF TEST IMPL [form] Execute validate state", n, data)
+ log.Println("--->WF TEST IMPL [form] Execute validate state", n)
}
return true, nil
}
@@ -101,13 +101,13 @@ func (me *FormImpl) Close() {
me.n = nil
}
-func (me *UserImpl) Execute(n *Node, data interface{}) (bool, error) {
+func (me *UserImpl) Execute(n *Node) (bool, error) {
if me.n == nil {
me.n = n
}
nodeStateMap[n.ID] = true
if testVerbose {
- log.Println("--->WF TEST IMPL [user] Execute", n, data)
+ log.Println("--->WF TEST IMPL [user] Execute", n)
}
return true, nil
}
@@ -126,13 +126,13 @@ func (me *UserImpl) Close() {
me.n = nil
}
-func (me *TemplateImpl) Execute(n *Node, data interface{}) (bool, error) {
+func (me *TemplateImpl) Execute(n *Node) (bool, error) {
if me.n == nil {
me.n = n
}
nodeStateMap[n.ID] = true
if testVerbose {
- log.Println("--->WF TEST IMPL [template] Execute", n, data)
+ log.Println("--->WF TEST IMPL [template] Execute", n)
}
return true, nil
}
diff --git a/test/apikey_test.go b/test/apikey_test.go
new file mode 100644
index 000000000..fdb197ff0
--- /dev/null
+++ b/test/apikey_test.go
@@ -0,0 +1,46 @@
+package test
+
+import (
+ "encoding/base64"
+ "net/http"
+ "testing"
+)
+
+func TestApiKey(t *testing.T) {
+ s := new(t, serverURL)
+ u := registerTestUser(s)
+
+ login(s, u)
+ apiKey, summary := createApiKey(s, u, "test-"+s.id)
+ logout(s)
+
+ token := getSessionToken(s, u.username, apiKey)
+ deleteSessionToken(s, token)
+
+ login(s, u)
+ deleteApiKey(s, u, summary)
+ deleteUser(s, u)
+}
+
+func createApiKey(s *session, u *user, name string) (string, string) {
+ key := s.e.GET("/api/user/create/api/key/{id}").WithPath("id", u.uuid).WithQuery("name", name).Expect().Status(http.StatusOK).Body().Raw()
+
+ summary := key[:4] + "..." + key[len(key)-4:]
+ s.e.GET("/api/me").Expect().Status(http.StatusOK).JSON().Path("$..Key").Array().Contains(summary)
+
+ return key, summary
+}
+
+func getSessionToken(s *session, username, apiKey string) string {
+ b := base64.StdEncoding.EncodeToString([]byte(username + ":" + apiKey))
+ return s.e.GET("/api/session/token").WithHeader("Authorization", "Basic "+b).Expect().Status(http.StatusOK).JSON().Object().Value("token").String().NotEmpty().Raw()
+}
+
+func deleteSessionToken(s *session, token string) {
+ s.e.DELETE("/api/session/token").WithHeader("Authorization", "Bearer "+token).Expect().Status(http.StatusOK).NoContent()
+}
+
+func deleteApiKey(s *session, u *user, summary string) {
+ s.e.DELETE("/api/user/create/api/key/{id}").WithPath("id", u.uuid).WithQuery("hiddenApiKey", summary).Expect().Status(http.StatusOK)
+ s.e.GET("/api/me").Expect().Status(http.StatusOK).JSON().Path("$..Key").Array().NotContains(summary)
+}
diff --git a/test/assets/test_expected.pdf b/test/assets/test_expected.pdf
new file mode 100644
index 000000000..2ac94b36a
Binary files /dev/null and b/test/assets/test_expected.pdf differ
diff --git a/test/assets/test_template.odt b/test/assets/test_template.odt
new file mode 100644
index 000000000..cc232a64b
Binary files /dev/null and b/test/assets/test_template.odt differ
diff --git a/test/bindata.go b/test/bindata.go
new file mode 100644
index 000000000..bbe0896d7
--- /dev/null
+++ b/test/bindata.go
@@ -0,0 +1,262 @@
+// Code generated by go-bindata.
+// sources:
+// test/assets/test_expected.pdf
+// test/assets/test_template.odt
+// DO NOT EDIT!
+
+package test
+
+import (
+ "bytes"
+ "compress/gzip"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+func bindataRead(data []byte, name string) ([]byte, error) {
+ gz, err := gzip.NewReader(bytes.NewBuffer(data))
+ if err != nil {
+ return nil, fmt.Errorf("Read %q: %v", name, err)
+ }
+
+ var buf bytes.Buffer
+ _, err = io.Copy(&buf, gz)
+ clErr := gz.Close()
+
+ if err != nil {
+ return nil, fmt.Errorf("Read %q: %v", name, err)
+ }
+ if clErr != nil {
+ return nil, err
+ }
+
+ return buf.Bytes(), nil
+}
+
+type asset struct {
+ bytes []byte
+ info os.FileInfo
+}
+
+type bindataFileInfo struct {
+ name string
+ size int64
+ mode os.FileMode
+ modTime time.Time
+}
+
+func (fi bindataFileInfo) Name() string {
+ return fi.name
+}
+func (fi bindataFileInfo) Size() int64 {
+ return fi.size
+}
+func (fi bindataFileInfo) Mode() os.FileMode {
+ return fi.mode
+}
+func (fi bindataFileInfo) ModTime() time.Time {
+ return fi.modTime
+}
+func (fi bindataFileInfo) IsDir() bool {
+ return false
+}
+func (fi bindataFileInfo) Sys() interface{} {
+ return nil
+}
+
+var _testAssetsTest_expectedPdf = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x84\x79\x09\x34\x94\x7d\xff\x77\x91\xa5\x11\xca\x96\xdd\xc8\x92\x7d\xf6\xc5\x5a\xf6\x08\xd9\xc9\x3e\x18\x8c\x65\x86\x31\x76\x12\xb2\x93\x10\x65\x8f\xc8\x16\xd9\x25\x6b\x54\xf6\x5d\x59\xb3\xdf\xb2\x86\xc8\x1a\xbd\x47\xdd\xef\xf3\x3c\xff\xbb\xe7\x7d\xff\xd7\x39\xd7\x75\xae\xef\xe7\xfb\xb9\x7e\xdf\xed\x33\x67\xce\xf9\xfd\x04\xb4\x95\x54\xc4\x21\x12\x70\x80\x40\x5b\x6e\x5b\x43\x5b\x4d\x5b\x16\x00\x0a\x04\x03\x09\xd6\x8e\x00\x19\x19\x90\x06\x16\x6f\x4f\x72\x00\xc2\x80\x60\xa0\x2e\x48\x05\xe7\x4c\xc2\x12\x41\x2a\xce\x18\x12\x56\x09\x6b\x43\xb0\xc5\xca\xc9\x01\xdc\x49\x44\x2c\xc6\x05\xe0\x9d\xee\xd2\x23\x4d\xd3\x7a\xf3\xf2\x85\xd9\xd3\x7b\xef\x3f\x29\xf4\x8d\xfb\xe1\x67\xd1\x60\x39\x40\x7d\x4b\x50\xe8\x18\xdb\xc6\x85\xe1\x83\x7d\x73\x6d\x0d\xbe\xe1\x5e\x30\x8f\xd8\x70\xb7\xcc\xc5\xd4\x31\xd5\xb5\x5b\x64\xa6\xd9\x3e\xd0\x7c\x66\xd9\x26\x1a\x87\xa6\xb4\xec\x94\xa7\xa4\x2e\xb5\x97\xf9\x9e\x79\xdf\x83\x22\x43\x27\x36\xcc\x5a\x0c\x72\x5b\xde\xf2\x01\x0d\xbf\x24\x0b\xf2\x2a\x0e\x38\x6d\xae\xe6\x71\xf5\x36\xa5\x6c\x83\xd6\x2a\x63\x6f\x20\xfd\xc7\x62\xb9\xf3\x3c\xcd\xc0\x42\xd5\x25\x19\xad\xf5\xa1\x6c\x2b\xb4\x96\x11\xc2\xa2\x00\x2c\xde\xf6\xef\x8c\xb0\x78\xdb\xb3\x22\x00\xb0\xbf\xab\x81\xc0\x10\xff\xc2\x10\x7f\x54\x88\xfc\x7f\x54\xf8\xb7\x1f\x02\x94\x44\xc3\xc1\xff\x59\xee\x5f\x68\x1f\x12\xc7\xf7\x94\x19\x14\xdf\xdd\x0a\xff\x5b\x44\x31\x1e\x9d\x60\x95\x94\x31\xad\x79\x9b\x74\x21\x2f\x85\x97\x1d\xc9\x71\x63\x6e\x9c\x3c\xf2\x44\x85\xf5\x48\x01\x53\x40\x4d\xae\x7a\x94\x22\x99\xf3\x54\xb2\x2b\xfa\x82\x02\x4e\x78\x63\xef\x59\xbd\x73\x9a\xaa\x4f\x27\xeb\x98\xe0\xd6\x5e\xbc\xeb\xe3\x80\x7a\xd3\xe9\x3d\xf1\xb2\x3c\x71\xdc\xb0\x84\x8b\x17\x8d\x58\x15\x05\xb7\xb8\xd2\xf4\xe6\xfe\x70\x87\x9b\xc2\xc4\xd7\xb5\xc9\x9f\x9e\xb9\x29\x87\x5f\x8f\xf7\x87\x1d\x86\x3a\xc9\x31\x42\x0f\x84\x2f\xbf\x95\x86\x66\x0b\x3e\xee\xaa\x60\x38\x77\xc0\x7d\x2e\x39\x46\xe3\x45\x5e\xff\xde\x54\x43\xca\x6c\xf4\xb9\x56\xa7\xe7\x91\xe5\x91\xa0\xac\xf1\x1d\xee\xfb\x73\x62\xf7\x57\x3a\xcb\xb7\x15\x9f\xd7\x37\x37\xad\x52\x9d\x0b\x5d\x38\x97\xfc\x3d\xbe\x8c\xfd\xa8\x3d\xf1\x95\x21\xf9\x63\x59\x05\xe4\x27\x95\x88\x81\x2c\x49\xdb\x6b\x3f\x3c\x5a\x9f\xa8\x08\x0e\xa1\x5e\xa1\xd6\xbf\x9d\x7b\xcc\x08\x58\x6b\x5f\xe0\x5b\x65\x98\x6c\xf3\x6c\x1d\x65\xbc\xf7\xf9\x81\xaf\xfb\xfc\xa3\x2d\xa7\xa0\xad\x73\x73\xba\x1a\x19\xd4\x33\xa3\x7e\xbb\x0c\x73\x28\x72\x26\xce\xdb\xef\xf4\x56\xbe\x60\x56\xc4\xee\x3b\x5b\xfd\x50\x82\xa6\xb3\x7e\x3e\xe8\x35\xa2\xb6\x04\x00\x3d\x2f\xd3\xae\xb4\x3d\x54\x20\xdb\x61\x95\x88\xa0\xb4\xc9\xec\x4c\x27\x08\x0b\x89\x05\xff\x0c\x0f\xdf\x46\xa3\x81\xf8\x06\xee\x97\x47\xdf\xa3\x14\xfd\x2f\x89\xcc\xb9\xdd\x1f\xaf\x65\xf5\x3c\x4c\x5d\x49\x3d\x69\xfb\x89\x65\xb2\xbb\xf7\x9e\x81\xd4\xe6\x35\xb7\xbf\x2c\x7f\x1e\xff\x9e\x02\x4c\xcd\x1b\xd1\x56\x02\x8e\xcd\x3c\x1c\x7a\x7d\xb1\x3e\xc2\x8b\xa3\x41\xeb\x07\x2b\xa7\x2a\xa3\xff\x86\x89\xe1\x60\xec\x47\x63\x25\x16\xca\xba\x29\x97\x2d\x86\x1f\x3f\x52\xbf\xde\xa0\xe8\x6f\xa9\x37\x3a\x02\x4e\x84\x6c\x51\xcb\x7c\x58\x1e\x53\xaf\x91\x8b\xeb\xb7\xb9\x1a\xd6\x41\x59\x39\xa5\xfc\x44\x76\x14\x2a\xb0\x9e\xdf\x1d\x40\x9a\xd6\x18\xbd\x78\xde\x63\x55\xba\xca\xda\x22\x44\xb6\x15\xd6\xca\xe1\x4a\xa6\xb5\xb2\xcd\xdc\x66\x3a\xa6\xb0\xaf\x15\xb5\x15\x83\x7f\xc6\x78\x24\x87\xd9\x15\x53\xc6\x04\x41\xd0\x17\xbc\x1e\x21\x6f\xc4\x8c\x06\xfe\xe0\xeb\xc8\x3a\xe1\x1a\x9f\xa0\xec\x67\xc8\x26\xc7\x07\x2d\xb8\xb3\xf3\x5c\xdb\xe7\x35\xb1\xf2\x7c\xe8\xa9\x48\x07\xae\x8e\x3e\x0e\xdd\xa4\x6e\xcd\xa3\xa2\xae\xcb\x5a\x9f\xa6\x17\x39\x7d\x94\x23\x23\x81\x6f\xb4\x0b\x38\x0d\x56\xe4\xbc\x7f\xcd\x4c\xc2\x50\xfe\xb4\xfa\x75\xab\x63\x99\xdd\x29\xf0\x65\x6a\x80\xaa\x19\x80\x85\xf5\x69\x00\x4a\x35\x6d\x23\xfd\x24\xa3\xe0\x74\xd3\x7f\xf1\x33\xe1\x29\xd0\xe5\xe9\x89\xf2\x52\xb2\x46\x47\x01\x2b\xcd\x60\x24\x62\x64\xdb\xb2\x2d\xae\xd1\xa5\x9e\x2a\x24\x28\xe8\xb4\x69\x3a\x43\x56\xfa\x4d\xad\x97\x07\x7e\xcd\x81\xe0\xf4\xbd\x16\x7e\x5c\xe3\x9b\x56\x63\xd6\xec\x33\xfc\x81\x76\x6d\xb4\xc4\xbc\xb2\x42\x07\x57\xf0\xca\xd4\x50\xcf\xbc\x5a\xdf\x62\xde\xd3\xe0\xf1\x84\xcb\xa7\xf9\x1c\xb2\xdb\x4d\xe3\x8f\xed\xe1\x11\x23\xcd\x52\x6a\x6d\x98\x66\x41\x43\xb5\x3d\x43\x83\xbd\x6e\x47\xbf\x80\x8c\xc5\x57\x11\x55\x23\xa3\x87\x7f\x7d\xc2\x68\xea\x0a\xc4\xe2\xc3\x1c\xeb\x1a\x33\x12\x6d\x0d\x12\xc3\x04\x4a\x52\xf1\xab\x32\xdf\xca\x96\x04\x3a\x07\x8b\xf9\x1b\xef\xaa\x6d\xa6\xf3\x4f\xe4\xf6\x96\xa5\xd4\x3d\x12\xd8\xc8\x63\xb9\x2b\x15\x1d\x22\x4d\x7e\x81\x87\x10\x1f\x92\xb0\x87\x0c\xaf\x75\x1f\xd3\xee\x8d\xbf\xa6\x30\x85\x1f\x9a\xd5\x29\x77\x10\x68\xfe\x24\xc5\x96\xb2\x47\xaf\x17\x83\xd6\xb3\xc2\x25\xa7\xb5\x25\x1f\x49\x3d\x2d\x67\xba\x6c\x7c\xf3\x51\x8d\x86\x56\xcf\x6c\x85\x78\xc7\x79\x4c\x4a\xe0\x42\xcc\xb9\xa5\x6b\x4b\xc9\xd0\xef\xc5\xbc\x25\xf6\xf6\x2d\x33\xf3\xa4\xfa\x90\xef\x1e\x5c\x26\xe5\x53\xad\x77\x07\x7a\x34\x1d\x7d\x7a\x53\x23\xd3\x1e\xe7\x88\x91\xd0\x82\xbd\x23\x19\x59\x4d\x5f\x5e\xd2\xee\xd1\x8e\xb2\x84\x2d\x3e\x7a\x73\xfd\x59\xa4\xc8\x8a\x56\xf4\xe9\xca\x8a\x34\xc2\xaf\xa4\xc6\x0c\x3f\x98\xcc\x73\xef\x79\x30\x94\xde\xc9\x83\x18\x6d\x59\x8c\x35\x94\xf6\xfb\xf2\x83\x64\xe1\x19\x99\x5a\xde\x25\x78\x7b\x78\x93\x3c\x8f\xaf\xf9\xe4\x53\x12\x93\x41\xfe\xa2\xc1\xf8\x34\x15\xa5\xa3\x42\x65\x58\xe3\x79\x94\x74\x7a\x89\xfe\xda\xca\x8a\xe1\x5d\xc7\x0c\xee\x4c\xc3\xfe\x3e\xad\x8d\x44\x81\x34\x19\x59\x3d\x63\x74\xc6\x96\x5e\x5d\xf6\xf5\x9b\x82\x7d\x7d\x72\x01\x3d\x85\xd5\xd3\x8c\x23\xce\x6d\x57\xa6\x58\x47\xf4\x08\x1b\x1b\x23\x2c\xeb\x53\x5c\x03\x0f\x8a\x0c\x85\x73\x18\x42\x9b\x34\x2b\xfc\x1a\x87\x5d\xb2\x12\x57\x0e\x6d\x9c\x9f\x55\x68\xbb\x15\xb4\xe2\x51\xa2\x6d\xe5\x95\x16\x63\xaa\x11\xc8\x6a\xd8\xa3\xf7\x02\x63\x5f\xfc\x39\x1b\xd4\x5d\xb4\x19\xc3\xd8\xbe\xf8\x3f\x5c\x60\xb5\x4a\x2f\xe4\xb4\xd7\x0a\x30\x7e\x31\xed\xb9\x47\xf1\x02\xa1\x68\xa0\x7a\x6b\x4f\xef\x7d\x5c\xe1\xc4\x88\x5c\x6a\xe9\xc9\x4b\x48\x85\xa6\xe8\x69\xcb\x4a\x72\xfe\xed\xf5\x21\x4a\x81\x0e\xac\x40\x54\x11\x1f\x53\x12\xdc\x06\xc1\x1c\x59\xb5\x24\x3a\x30\x05\x4e\xc2\x71\xa8\x46\xdd\xe8\x98\x9e\xf6\x4c\x08\x71\xf4\x8b\xac\x92\x17\x84\xf5\x3c\xb4\xdc\xa3\x5f\x9b\xe9\xbc\x45\xc1\xf6\xa8\x60\x60\xa3\x7a\x5c\x7e\x99\xde\x61\x9a\x85\xac\xa1\x68\x1a\xf2\x53\xe3\xb9\x02\xfb\x2a\x18\x21\x36\xd1\x9a\x56\x28\x4c\x54\x10\x4d\x0a\x70\xff\x8c\xc5\x1c\x3e\x0f\x38\x5f\x9a\x53\x8c\xcb\xeb\x65\xbf\x14\xb7\x6c\x92\x3a\xff\x65\x4d\xfd\xdb\x1d\xfa\x5a\x5b\x45\xab\x9a\xc5\x8c\xa0\xfc\x16\x98\x9f\x9b\x5d\xf5\xa2\x7f\x2b\x79\x31\xef\x58\xeb\x83\xee\x54\x5c\x57\xd1\xc9\x70\xd0\x40\xc1\x12\xbb\x67\x98\xc3\xe7\x58\xf5\xf3\xf9\xa6\x3b\x94\x85\x6d\xf2\x92\x97\xe9\x2a\xca\x9f\x03\xc4\x9b\x97\x17\xdb\xae\x2c\x56\x0e\xf1\x2c\xaf\x27\x4a\x7b\xd1\x51\x7e\x5a\x0b\xb2\x65\xeb\x49\xcf\xee\x0a\x4c\x62\x93\x9c\xed\x49\xcd\x3e\x3f\xd2\x63\x60\x27\x0c\x44\xbf\x07\x5e\x9c\x88\x00\x56\x33\x00\x8f\x91\xc3\x06\x30\x83\x94\x95\x2a\x9d\xb7\x92\x49\xb4\xa5\x84\x4a\xa6\x9b\xd7\x17\x73\x85\x2f\xb9\xd4\x2d\xd2\xd7\x2e\x13\x2f\x35\xb7\x16\xc4\xc7\xea\xb9\xfb\xf5\x99\x70\x2b\x76\x6a\xb9\xd7\x0f\x73\x91\xec\xe3\x8e\xa4\x82\xc6\x12\xbe\x3e\x85\xf7\xbc\x2c\xd8\x90\x64\x71\xd1\x7b\x07\x1e\xf6\x47\xd6\xba\x33\x14\x63\x29\x38\x7c\x85\x81\xe1\x7f\x3d\xe3\x8c\x87\x13\xee\xdc\xff\xeb\x43\x57\x5b\xf1\xcb\xab\x21\xa3\x66\x9b\x06\x3f\x99\xc7\x1a\xac\x75\x52\x5a\x34\xd7\x39\x5b\xa3\x0a\xab\x3f\x49\x7d\xb9\x6a\xe4\xb4\x70\xc0\x39\x71\x8f\xb9\x9e\xaf\x3d\xf9\x45\x2f\xd1\x7e\x06\x0e\x93\x58\xa3\x0c\xba\xb6\x39\x70\x3f\xb6\xcc\xab\xf1\x73\xe7\x3e\x59\xcd\x33\xec\x84\x9f\x00\x87\x38\xae\xe9\x59\xbe\x7d\x83\x45\xcd\x53\x3e\x8e\x94\x2b\x75\x7c\x70\xf1\x7d\x0a\xa4\x3b\x6e\xe2\x0e\xde\x9d\xdb\xae\x49\xfd\x19\xa3\xf4\x71\xc0\x77\x8f\xaa\xea\x43\x3b\xdb\x06\xa7\x2f\x31\x57\x37\x57\x39\x2e\xc9\x60\x9f\xdb\xb7\xd5\x1f\x91\xc7\xd1\x08\xc1\xe9\x8f\x83\x61\xb7\x23\xaf\x7a\xab\x93\x6d\x11\x8b\x5a\x0a\x62\x3a\x2e\x47\xf2\xf6\xe8\x82\x78\xed\x24\x36\xfd\xbb\x68\xc0\xef\xeb\x28\xb5\x66\x0f\x3a\xee\x5e\xd0\x82\x7d\x71\xa0\xe6\xb3\x0f\x15\xeb\x4d\xf6\xd0\xef\xe5\xf2\x91\x07\x3e\xe4\xed\xde\x5e\xca\xa5\x55\x82\xd9\xe3\xf4\xd2\x3f\xc4\xb1\x59\x09\xec\x98\xc4\x58\x54\xa2\x66\x4c\x85\x98\x2e\xf6\xb1\xd9\xe6\xb6\x47\x7e\x83\xcb\x9a\x2f\xf2\xc9\xe8\x44\x28\x2f\xa7\xf3\x96\x58\x68\xc9\x45\xeb\xc1\x0e\x2d\xec\x0d\xad\xa3\xf8\x1b\xc6\xa9\x37\xd9\x5f\xaf\xfb\x5a\x81\xb7\x94\x78\x4f\x14\xa4\x0f\x93\x38\x0f\xc7\xe4\x17\x74\xaf\x64\xdb\x0d\xf7\x47\x59\x02\xca\xe5\xeb\x10\xac\x29\xdc\x74\x15\xef\x29\xb1\x23\xec\x47\x39\x61\xdf\x58\x1c\x13\x1b\x42\xc9\x3a\x45\x47\xc4\x88\x63\xf9\xcc\x1d\x72\x2a\x04\xeb\xdd\x17\x62\xb2\x71\x83\x9a\xf0\x1c\xe3\xb0\x5c\x39\xe5\xc6\xcd\xc1\x47\x9a\x2d\x5f\xef\xa7\x25\xe7\x56\x6d\x65\xa5\x0f\xd7\xd4\xde\xcd\x0f\xae\x2a\xd4\x98\xbc\xa7\x73\x87\xf9\xb9\x20\xeb\xe8\xa0\xfb\x32\xd9\xd3\xad\x23\x00\x0d\x60\x5d\xb0\x72\x5c\x82\x3a\x8f\x2e\xed\x29\x88\x8e\x8a\xa6\xf7\x30\xc7\xfc\x1c\x46\x88\x5a\xc0\x82\x0a\x29\x35\xb0\x74\xd3\x75\xb6\x9e\x4e\x66\xb3\x85\x91\x2e\x9d\x68\xd5\x1a\x24\x4b\xc7\x5f\xba\xd8\xc2\xf1\x7d\x41\x8f\x74\x53\xaf\x84\x4b\x86\x59\x93\x9b\xbe\x7e\xf2\xb6\x6b\xc8\xe7\x85\xf2\x9d\xfe\x2e\x95\xfd\x54\x0f\x4f\x9f\x48\x66\x6c\xfd\xa4\xc4\xa8\xfe\x68\x4e\xe3\x54\x90\x93\xe5\xf7\x1f\xf3\x2a\x5f\xf0\x36\x87\xdc\xf6\x33\xc7\xa2\x96\x1b\xae\xec\xf3\x79\xcd\xed\x21\x29\xf5\x81\x96\xdf\x06\xc9\xb5\x8e\xa4\x79\x6f\xae\xad\x4f\x5a\xdf\xf4\xcf\xcc\xcc\xbe\x1a\xb2\xfa\xd9\x18\x07\x97\x30\x36\x76\x30\xb6\x7a\xdd\x57\x3d\x50\xc6\xf7\xda\x95\x77\xcc\x39\x39\x43\xde\x94\xed\xf1\x06\x75\xf0\x06\xd7\xd5\xd3\x62\x9a\xfe\xba\xad\x2a\x9a\xb8\xd7\xe0\xaf\xad\xbc\x57\x91\x75\xda\x6e\xbd\xca\xbe\x7c\xa1\x17\x99\x99\x1c\x2f\x3a\x17\x68\x3b\xde\x7d\x5a\x87\x36\x42\xce\x17\x55\xbc\x5f\xf6\x7c\x6c\x11\x9e\x69\xb0\xc6\xf9\x5e\x08\x54\x68\x51\xe9\xeb\x72\x98\x20\x13\x6e\xef\x41\x3e\x1f\x98\x75\xec\x69\x27\xbf\x70\x30\xdd\xc2\xb5\xa6\xb5\x7f\xb2\xda\xda\xfd\xb3\xc1\x67\x38\x4e\xdd\xf1\xfd\x97\xe6\xfd\x10\x8b\xc9\xab\xa7\x5d\xb5\xde\xf5\xf5\x7b\xa8\x27\x72\x07\x2e\x07\xb0\x40\xaf\xf6\x77\x8e\x7b\xe7\x42\xcb\xb0\x7b\xe2\x73\x9a\xdb\xf5\x65\x97\x74\xa7\x0d\x2b\x3b\x17\x8d\x17\x49\x26\xf2\x9e\xc6\x5c\x12\xe1\xf3\x97\xcf\x3b\x8f\x30\x94\x0b\xd1\x73\x8a\x26\x77\x7f\x02\x94\x2b\xac\xd2\x49\x14\x95\x45\x86\x7e\x96\xaf\x34\xb6\x33\x0f\xca\x37\x9a\x8d\x67\x66\xb5\x0c\x52\x14\x75\x6d\x07\xe8\x1b\x2e\xa7\x68\xd7\xc9\x8a\x5d\xa1\xef\xb7\x25\xef\xb7\x71\xd8\x8c\x45\x55\x28\x2d\x15\x3d\xcc\xf3\x79\xac\x10\xfb\x59\xaf\x0f\x6a\x52\x6f\xe7\xe1\xa1\x49\xae\xab\x92\x69\xf0\x17\x5d\xca\xcd\x14\xa8\xa8\x38\xdf\x8b\xad\xa5\x4a\xfb\xfa\x3e\xa1\x26\x47\xc9\xba\x95\xb0\xa0\xe9\x95\x63\x7a\xd9\x9d\xd9\x31\xe1\xac\x2a\x74\x51\xd5\xb1\xd7\xe7\xed\xba\xb6\xd7\xf3\xc5\x33\xae\x4f\x80\x53\x12\xa2\x19\x1c\xa3\x87\x8e\x6b\xcd\x09\xe9\xbe\x0e\xdb\x04\xeb\xe8\x62\xa4\xcf\xa7\x2a\xb1\xe2\xc0\xfb\x44\x0f\xbd\xd2\x19\xb0\x3f\x7b\x97\x55\x88\x57\x7c\xde\xeb\x31\xfe\x28\x9a\x2e\xd1\x4b\xca\xc9\x39\x5c\x6f\xfd\x5f\x75\x5a\x8b\x53\x94\x6d\x5e\x62\xb8\x81\x12\xa6\xb1\xec\x8a\x55\xaf\xa1\x12\xe5\x88\xd5\x77\xab\x6f\x78\x71\x51\x96\x2e\x4e\xb1\xe6\x49\x32\x96\x23\x68\xa6\x6e\x27\xc8\xf4\xeb\x8d\xad\xd9\x68\x50\x69\x89\x69\xe5\xc9\x9b\xe3\xdb\xf4\xf9\x0f\xd2\x6a\x3e\x35\x1c\x39\x2e\x0e\xd5\xd6\x1c\x39\x93\x3f\x14\x58\x0c\xe4\x92\x2d\x9f\x56\xab\x2e\x6d\xdf\xf4\xb6\x8f\x65\x66\x8e\xbe\x0a\xaa\x35\x11\x2f\x23\xad\x10\xbb\x89\x79\xd9\x78\x65\xaf\xb2\xbe\x37\x24\xb7\xd0\x8f\x15\x0d\x76\x3e\x5e\xae\x18\x9d\xd8\xb6\xaa\x67\x28\x07\xf8\xb8\x92\x17\xe1\xb2\xd2\x64\x6e\xf0\x31\xd3\xba\x51\x99\xc1\xdb\x6a\x80\xa1\xa8\xf1\xb3\x59\x3f\x40\x53\xf6\x6c\xb3\xe8\x41\x4e\xf0\x8c\xfe\xd6\x6d\xc0\x74\xb6\x95\xc9\x5b\x6d\xad\x0a\x5d\xf9\x90\xe0\xf8\x99\xbe\xc4\x63\x99\xab\xec\x06\xae\xe4\xb5\x09\x4f\x6c\x64\x3b\x35\xa7\x4a\x67\xf9\x47\x67\xf2\x66\x1d\x29\x9e\x88\x18\xbf\x98\x05\x31\xfb\x4b\x5c\x77\xad\x0d\x0e\x67\x8a\x21\x06\xb1\x70\x03\x0e\x3b\x8c\x5c\x65\xef\x1e\xa2\x28\xb6\x65\xae\xfa\x2b\xbd\xb5\x01\xd0\xed\x80\x4d\x5e\xa7\xd4\x39\x34\xbf\x7a\x66\xe4\x23\x2f\xaf\xf9\x96\x20\xfa\x74\xb6\xcf\xc4\xfb\xe9\x2b\x21\x0d\x53\xe0\x07\x26\xfb\x5d\xa1\xb1\x5c\xa0\x90\xc3\x74\x1a\x57\x71\xf6\xba\x49\x1f\x77\xe7\x14\xe4\xb1\xc6\xd7\x46\xb7\xe9\xed\x42\xa7\x0e\x33\xe1\x78\xc6\x94\x34\xdd\x0e\x97\x4b\xeb\xb3\x6b\x71\xa8\x8c\x40\x75\xe4\xf2\xc2\x9b\x87\x9a\x82\x19\x49\xc6\x5f\xad\xa4\x3f\xac\x3a\xfa\x44\x16\xf6\x60\xb2\xc2\x57\x5c\x7b\xd9\x51\x72\xd3\xc4\x3b\x3f\x16\x8f\x9e\x3c\xb4\xdf\xb3\x5b\x3d\xae\x3d\xaa\xd9\xa8\xb0\x3d\x3f\xd3\x52\xaa\x71\x92\x29\x8b\xe6\xa5\xeb\xda\x08\xa7\xb7\x9f\x6e\xd8\x75\x08\xed\x37\xe2\x15\x3a\xf1\x33\x7a\x69\xec\xc5\x56\x58\x37\xd2\xf5\x96\x5e\xd4\x7f\x1d\xc4\x28\xd6\x43\x62\xe3\x4f\xcd\x82\xba\xbe\x7d\xec\xd5\x7e\x4d\x48\x44\x44\x18\x95\xcc\x2a\x50\x6b\x23\xef\x35\x80\x11\x17\xae\x60\xf1\xa0\x2a\xd1\xda\xf1\x90\x95\x8f\xae\xd3\x28\x58\xbf\x18\x6c\x45\x8e\x52\xc7\x57\xd5\xe1\x4e\xdd\x86\xf7\x3f\x8f\x2d\x7c\xd8\xc1\x8f\x4c\x1d\x94\x71\x30\x72\x7e\x7f\x53\x3e\x98\x31\x79\xbf\xc2\xe3\x4d\x8e\xc4\xf8\x7b\x70\xf3\xb5\x97\x2c\xfa\x61\x50\xd5\x42\x20\xa8\x30\x5b\x76\x85\x38\x84\x34\x24\x37\x61\x3e\xc8\x4b\x6b\xda\x6d\xad\xcb\xf2\x0f\xfc\xe9\x3d\xbd\x9b\xe7\xa5\xd2\xbe\x85\xbc\x94\xf1\xa6\x3c\x2d\x13\xf0\xde\x54\xfa\xde\xe1\x1c\x55\x4a\x5c\x7a\xd3\xf4\x13\xe3\x7b\x61\xed\x22\xfe\x29\x1f\xd1\x77\x03\x70\x89\x21\x9b\x6a\xdd\x42\xe3\xd0\x05\xb7\xa3\x2f\xe8\x55\x17\xca\xe7\x83\xba\x56\x7d\xcf\x37\xc2\xee\xb1\x29\xd9\x78\x69\x93\x1c\x28\x0e\x5c\xcf\x67\x0a\xb5\x14\x92\xad\x0b\xdf\x24\x9c\xb7\x13\x6e\xa3\x58\x16\x69\x89\xa6\xf0\xce\xd4\xfb\x2a\xda\xf2\x82\x4c\x1f\xb0\x45\x6c\xab\x0c\x39\xb7\x03\xb3\x0e\x28\x79\xc3\x79\xba\x52\xae\xd1\xb9\x0c\xbe\xe7\xfb\x48\x75\x4f\xb1\x48\x46\x56\xa6\xa7\x49\xe0\x54\xc3\x9f\x25\x5d\x9b\xc2\xcb\xcf\xaf\x4b\x54\x04\xb0\xf1\xa2\x48\x44\x47\x84\x89\xc6\xa9\xa8\xbb\xfb\x2d\xde\x72\x84\xab\x6a\xf2\xad\x75\xbc\x17\xa5\x66\x72\x85\x4c\xb9\xf4\xd3\x2f\xf7\x87\xf0\x13\xdf\xb2\x29\xa5\xfd\x1c\xb7\xac\x69\xb5\x3e\xce\xba\xd2\x32\xac\x97\xc8\x77\x64\x5f\xe8\xa7\xe5\x33\xb0\x98\xf9\xf1\x65\x4f\xf3\x94\xa6\x05\xbf\x13\x6d\xd9\x1a\xa2\x12\x79\x07\x43\x7b\x02\xb8\xb5\xf7\xd1\xb4\x2c\x12\xa9\x75\x4a\x53\x63\x47\x23\x01\x66\x7e\x1e\xb2\xa9\xd9\xca\xa0\xbf\x6f\xd7\x74\xf7\xd3\x89\xd1\x74\x4f\xa8\x79\xa0\xfb\xea\x56\xf9\xe9\xad\xea\xcd\xe8\x7d\xad\xfd\xe2\x8c\x34\x0e\x53\xdf\x77\xf6\x30\x1e\xcb\x23\xc7\xd0\x57\x01\x9a\xb4\x86\xeb\x37\x88\x8d\xf2\x0d\xc7\x83\x1f\xa9\x4e\x9f\x37\x80\xf5\x24\xd8\xf7\x54\xa8\x7c\x17\xd7\x37\x2f\xf4\x34\x61\xad\x08\x21\x51\x73\x0d\xfb\x95\x82\xbc\xcd\xc6\x42\x35\x13\x51\x9a\x87\xd7\x64\xc9\x3c\xa8\x68\x2e\x19\xc2\xac\xc6\x2d\x27\xb7\x58\x38\xbe\xd6\x60\xa2\x69\x9f\x4c\x0c\x58\xb5\xa1\x9c\x0f\x82\x69\x79\xca\xe7\x36\x90\x97\x7a\x9f\x13\x7c\x43\xf4\x22\xf5\xbb\x42\x2d\xe6\x88\x77\x96\x22\x4e\xcc\x76\x65\xfb\x8e\xdc\xb4\x66\x9c\x14\xb3\x07\xc3\x12\x68\xdf\x86\x8d\x97\x58\x9d\x9a\x34\x5b\x47\xf6\x8b\xe3\xe9\x45\xf9\x1f\xfb\x2d\xad\xaa\x9a\xbe\x8e\x19\xc1\x85\x77\xa6\x34\x5e\x14\x0f\x87\x52\x05\x1c\x54\xbe\x23\xdc\x63\x55\xbc\x73\xbb\x10\xad\x5e\x62\xe9\xbd\x72\x23\x34\x94\x04\xc2\xd6\xd5\xdb\x05\x62\xc7\x34\xea\x9e\xb4\x8d\x36\xd8\xb0\xf0\x19\x97\xe4\x7d\x18\x8a\xea\x22\xae\x12\xea\xda\x92\x9f\xdc\x69\xbb\x9b\x40\x60\x8e\x1e\x13\x34\xaf\xa9\x62\xf0\x32\xb2\xc6\x09\xb0\x8b\xb8\xb5\x89\x26\x1d\x28\x08\x4c\x5b\x44\xae\xa8\xaf\x17\x2a\xbd\x35\x8d\x8c\x4d\x72\x28\x7e\xe7\x25\xba\x6d\x2d\xe0\x98\x84\xee\x61\x2b\x55\x97\xe9\x1f\x0c\x17\x85\xe8\x44\xaa\x0b\xf4\x75\x5f\xab\x6f\x16\x78\x65\xf1\x34\xcb\xf8\x31\xbc\xfd\x85\x4c\xca\x8b\xe0\xa1\xae\x53\x70\x79\xa4\xdd\x71\xfc\x96\x73\xb7\x6c\x64\x4f\xbb\xdf\xf6\xad\xb4\x57\xc5\x4e\x57\xb9\xa7\x12\x7d\x9e\x19\xe1\x55\xc3\xcb\x8a\x5b\xbd\x56\xac\x7e\x4e\x0b\xb4\xd6\x01\x66\xda\xf3\x0f\xe4\x42\x76\xcb\xfc\x34\x58\x19\x90\xf7\xa9\xe9\x08\x72\x11\x94\x7c\xad\xc3\xf2\x13\x0f\x2a\xfb\x0c\x91\x12\xcf\x2a\x3c\x00\x79\xde\xeb\x23\xc2\x4d\x20\x8f\xe1\x23\xef\xa9\x31\x1a\x96\x89\x94\x64\x9f\xdb\x1b\x1f\xc8\xc0\x0a\x3e\xa1\x75\x68\x66\x78\xda\x83\x21\x7d\xed\x82\x74\xbf\x82\x94\x35\xb2\x8d\x5b\xb7\x2f\xd4\x97\xbc\xab\x2c\xba\xe0\x52\x15\xbd\x5c\x76\xe0\xfd\xe2\x24\x69\xd4\xc2\xef\x24\x69\x14\x76\x03\xbf\x31\xf5\x69\x71\x63\xec\x93\xa0\x0c\xb1\x90\x4b\x71\x2c\x6e\x52\xa7\xf7\x87\xcb\x12\xbc\x54\xd5\x6f\xa9\xad\xa7\xae\x67\x38\x65\x26\x47\x31\xeb\x4e\x14\xb9\x1d\x41\x33\xc0\xaf\xe3\xcd\x61\xc0\xdb\xa4\x80\x2c\xef\xbe\xc2\x26\xbf\x43\x17\x9d\x17\xa2\x9b\x5a\x13\x06\xda\x81\xe3\x05\x6b\x29\x53\x6c\x3f\xff\x42\xd9\x1c\x4d\xce\x75\xab\xba\x4d\x53\x4e\x78\x79\x06\xf1\x73\x87\x1b\xba\x41\x9c\xaf\x70\x53\xd4\xb6\x4c\xcb\xca\x02\xd6\x2e\x09\x1b\xdf\xf2\x30\xb1\x3b\xb7\x55\x7e\xb5\xef\x41\x7d\xc4\x8c\x9a\x2b\x6a\x94\xae\xe5\xae\x73\x0b\xeb\x87\x07\x7e\xca\xf2\x53\x82\xf7\xfa\xcb\x7a\xb7\xd2\xe7\x54\x3f\x28\xbf\xc9\xb7\x76\xb0\x72\xdb\x59\x76\x79\x56\x86\x8a\x6a\xbd\x57\x94\xdc\x51\x37\x8a\x71\x96\xc8\x34\x77\x2f\x1f\xdc\x65\x0c\x7b\x93\x7c\x5e\xc5\xc3\x38\xf7\x04\xd7\x4c\xc7\xb1\xec\xb3\x5b\x40\xc7\xd1\xd9\xfd\xc4\x03\x4d\xaf\x3e\x97\x4b\x97\x36\x0b\xfe\x11\x42\x97\x96\xcd\x34\xba\xea\x2a\xc7\xb4\x80\xe4\xaa\xd1\x5e\x3f\xcf\x25\x71\x5f\xb9\x35\xda\x9d\xd0\xcf\xf1\x20\x6b\xcd\xf5\xca\xcb\x2b\x2e\x52\x66\x97\x68\x94\x6f\x44\x4f\xb7\x4c\x1f\xc9\x0a\xd2\xad\x80\x65\x47\x39\x00\x24\xe3\x73\x95\xd9\x86\xe1\xca\xc6\x4e\x5b\xcb\x65\xe7\x0d\x59\x73\xa4\x9c\x1f\x28\x87\x51\x91\xd0\x71\xab\x7e\x26\xe1\x9a\x5b\x99\x73\x54\x52\xce\x61\xd8\x0f\x81\x83\x73\xfe\xc6\xb6\x3f\x32\xe8\x4c\x1f\x8c\xee\xbc\x94\x32\x3b\xb7\xd6\x4b\x58\x75\xe5\x5d\xef\x7c\xe2\xf1\x9a\x7a\xef\x0b\xcf\xe0\x9c\xb5\x71\xc6\x4f\x4b\xf1\xc1\x72\x07\x29\x5f\x34\x67\x82\x82\xa3\x49\x92\xca\x7c\xa4\x62\xe7\xb2\x31\xbf\xb8\x38\xc7\x5d\x89\x12\x47\xb3\x71\xa1\xea\xf7\x0b\x36\x0b\x98\x42\xc9\x4c\x8d\x77\x89\x8e\x52\xec\x71\x08\xe3\xd4\x57\x2c\xa6\x02\xdf\x13\x22\x3d\x11\xe6\xd7\x3d\xde\xa9\xb0\xbf\xc9\x39\x6e\xbd\x7b\xb7\x10\x94\x1e\xf3\xf0\x68\x7e\x21\xbb\xa4\x30\xde\x55\x8c\x49\x4b\xfd\xfb\xd0\xc7\xf5\x62\xb7\xc8\xce\x4d\xc0\x75\xbd\x98\x8a\xa2\xec\xf2\x72\xcc\x47\xec\x15\x09\x9d\x7e\xfe\xa5\x42\x65\xbd\x04\x91\xbc\x44\x47\x21\x99\x6c\x41\x42\xc2\xfc\x73\xa3\x0f\x7c\x02\x6d\x36\x83\xfa\x52\xe9\xfc\xd6\x70\x41\x5a\xf8\x2e\xe2\x96\xac\xa3\x14\xe7\xed\xdb\x8e\x62\xfc\xdf\xfb\x85\xbf\xb2\x72\xc5\x3e\x7c\x20\xe9\xc6\x9f\xae\x51\xcc\xcf\x79\x47\x55\x68\xf9\x84\x2a\x61\x50\x5f\x48\xaa\xb5\x21\x8f\x7d\x89\x98\x62\x94\xd4\x44\xf2\x86\x8a\xb9\x18\x0d\xaf\xb8\x8a\x70\xef\x14\x0a\x7f\x53\x8e\xff\xae\x6a\xb3\xad\x9f\x41\x02\x33\x70\x45\xde\xa2\xfa\xf8\x98\x09\x90\x9f\x63\x03\xb1\xfe\x2a\xcd\x6f\x5e\xc8\x77\xc5\x28\xcf\xcc\xba\xfc\x80\x3f\x25\xfd\xd2\xa8\xce\xd4\xf3\x4e\x57\x11\x86\xd8\x58\x35\x61\x8b\x94\x97\x35\x8f\x1f\x16\xe0\x92\x77\x95\xd0\x19\x31\x0f\x0b\x16\x16\xc2\x0b\x3c\x18\x2f\x0e\x4d\xa8\x36\x4a\xb1\x6a\xaa\x76\x6c\x2b\xa1\x05\x3c\xc2\xe9\xd5\x1b\xb7\x2f\x22\xf6\x1c\x14\x91\x4c\x45\xbd\x42\x06\x86\x51\x5a\xfc\x09\xdb\x6a\xdc\x36\x01\x8e\xfc\x6c\x74\xed\x1d\x29\xec\xba\xfb\xca\xed\xcf\x71\x15\x2c\x3a\x89\xab\xd4\x06\x31\x5d\x2c\x5c\x6a\xdb\x51\xda\xbc\xb9\x0f\xd8\x8b\xf7\x79\x6e\x7d\x60\xc4\x44\xed\x86\xd5\x94\x76\x07\x33\x59\xd5\xa4\x16\x1e\x1c\xf0\xfb\xd8\x0d\x95\xbb\xa2\xdf\xc9\xe8\x8e\x25\xce\xb9\xe9\x5e\x6b\x5f\xd8\x3e\xd0\x14\xcb\x1a\xf4\x4e\x20\xa3\xcd\xe3\xfb\xbe\x73\x8d\x27\xb6\x7d\x28\x97\xfd\xc9\x88\x76\x04\x43\xbe\xe3\x7b\x55\x3a\x8d\x32\x39\x9d\x94\xe1\xba\xd9\x91\x78\xdb\x74\xef\x48\x37\x84\xa6\xa1\xa0\x2e\x6d\x3c\x49\xaa\x5c\x47\x87\x8e\x5b\x66\xec\xd9\x6a\x09\x5b\x76\x1f\xfc\x11\xf6\xaf\x3b\xdb\x43\xe1\x0c\x98\x46\x1e\x35\x3b\xac\x75\x43\xf4\x04\x8f\x4d\x89\x74\xcb\x71\x77\xc2\xf2\xda\x72\x82\x27\x7f\x81\x5c\x09\x53\x7a\x2e\xcd\x88\x5a\xdd\xa1\xd9\x78\x99\xae\x97\x3c\x36\x46\x70\xa8\x30\x4c\x9f\xd4\xc0\x9b\xc9\xa9\xe7\x59\xe5\x29\x4f\xa3\x84\x89\xe9\x83\x67\x84\xf3\x5e\xe2\x3d\x32\x62\x23\x4b\x72\x4d\x7a\x76\x25\x42\x91\x8c\x4e\x3e\xe1\xb6\x72\x84\xc8\xa0\xe2\x2c\xb5\x58\xfe\xb1\x88\xc3\x83\x24\x92\xfa\x80\x8a\xae\xdb\x7c\x5c\x62\xd3\x85\xde\x0b\xd7\xd5\xa2\xcd\x0d\xf0\x28\x3a\xc8\x84\x96\x78\xfa\x4e\x7e\x49\xc9\xb5\x02\xa3\xb8\x9c\x0c\xa7\xd1\x2b\x23\xf2\x47\x7c\xb6\xd9\xd1\xbb\x7b\x57\xbc\x9a\xcc\x2f\x1e\xa1\x05\xd4\x27\x4b\xe2\x84\x16\x72\xeb\x99\x75\xf9\x79\x04\xf1\x11\x3b\xfc\x05\xf9\x3b\xfd\xb8\x9f\x92\x4d\x70\xfb\xc0\xab\xf9\x79\x4b\xb6\x86\x3a\xce\xb1\xbb\x38\xe0\xb2\x89\xa1\xc4\x64\x41\x56\xa5\x9b\x0b\x41\xa7\x42\x64\xbb\x2b\x5f\x32\xfc\xfd\x0d\xf9\x7d\xfe\x29\xa5\x2a\x74\xdc\x7c\xdc\xa8\xea\xb5\x3a\xf0\xa6\x5d\x14\x1a\xf3\x30\xf4\xb5\x81\x9d\xdf\x7d\xfe\xa3\xd7\xfa\xf4\xa3\xb6\xdf\xd5\xdc\x09\x10\x89\x8f\x81\xb4\x71\xd2\x1d\x0b\x42\x2b\x71\x5b\x7d\x4b\xfc\xaa\xaf\x5d\x90\xbc\x77\xbb\xdd\x82\xf9\x7c\xf3\xf6\xdb\xce\x67\x3a\x45\xf1\xa7\x58\x48\xd4\x32\xf9\x9c\xda\x5a\x96\xca\x8a\x06\xc8\xbc\x51\xfb\xea\x2e\x9d\x0f\x93\x79\xef\x23\xd7\xaa\xe7\x6b\x31\x9e\xfa\x7d\x47\x68\x5e\x09\x5b\x8d\xbe\x15\x0f\x6e\xbe\x42\x57\xdc\x90\xc9\x5b\xda\x6b\x27\xf1\xc2\xe9\xeb\x73\xe1\xc7\xa7\xb9\x3c\x6a\xa3\x06\xdf\x9b\xde\xbd\x8e\x53\x93\x55\x7f\xa1\x97\x3f\xea\xf7\xd8\x52\x6f\x8a\x10\xe3\x7f\x4b\xa6\x69\xaa\xa7\x67\x5f\x7c\x5b\xaa\xb0\x61\x75\x5c\xeb\xce\x65\x54\x2f\x56\x35\x42\x4e\x2b\x4b\xf1\xa8\x28\xed\x59\x91\x6d\xc0\xc6\xf0\xe8\x47\xf9\x26\x75\x3e\xdf\x0f\x8c\x53\xa5\x4a\x71\x30\xcc\x7c\x59\x61\xc4\x4e\xd3\xfb\xa9\xdb\xde\x71\x20\x23\x01\x69\xd4\xf3\xa3\x1e\x75\x33\x94\xbc\x6b\xba\x7d\x7c\xb1\x8d\xec\x26\x6d\x37\x7f\x0f\x1f\xf6\x51\x97\x57\x70\x40\x2e\xa4\x2f\xc0\xde\x26\x7a\x6b\x45\xdb\x87\xd0\xe6\xfb\x6a\x58\x26\xc4\xac\x51\xc1\xf7\x85\xd8\xea\x85\xfc\x91\x90\xac\x7e\x0a\xd3\x9f\x98\xc6\x17\xa8\x81\xa6\x41\x0f\xcd\xe8\x71\x40\x5f\x56\x2f\x93\x25\x22\xd1\xb7\x4f\x72\x12\xc2\xed\xe2\x20\xd1\xee\x5c\x9d\x32\xf2\xfa\xab\x70\xda\x36\x7a\x26\x0b\x9f\xba\xd6\x2f\x94\x55\xeb\xac\xac\x38\x65\x72\xc2\x53\x78\x28\xf4\x4a\xd3\x64\x9f\xfd\xcb\x95\xd0\xe5\xc4\x0d\x6f\xcf\x9a\xf9\x5c\xbf\xfb\xfb\x7c\xf5\x99\x15\xc3\xe9\xa7\x4a\x7f\xe9\x5b\xcc\xbc\x2e\x7d\x02\xa4\xdb\x49\x8a\xc5\x06\xbf\x8a\xa3\xce\x6a\xb9\xe0\x9c\x4b\xa5\x15\x9a\x63\x93\x45\x29\x17\xbe\x63\xd4\x1b\xd1\x64\xc7\xbe\xfb\x34\xa9\xd9\x38\xee\x7d\xd2\x2c\x73\x7a\xb3\x5c\x82\x7b\xb9\x86\x76\x8c\xf9\xd1\x6a\x5c\x6f\xc9\x1b\xf2\x9d\x4f\x84\x52\xc6\x80\x2a\x2a\x8f\xed\x93\x6b\xd9\xfd\x77\x8a\x2d\x24\x57\x44\x68\x9c\x3f\x9a\x35\x06\x96\xf2\x07\x8a\xbe\xea\xba\x26\x1d\xab\xd3\x3a\x2c\x93\xc9\xf4\x0a\x4b\xde\x8f\x89\x39\x90\xee\x50\xbc\xea\x57\xb3\xd4\xfe\x92\x23\x33\xdc\x89\xd3\x94\x6f\xb6\x15\xcc\x99\xbd\x03\xcf\xb8\xa6\xe2\x7a\x89\x85\xc1\x85\xf5\x8d\x1b\x35\xa5\xab\xa8\x59\x9c\xc5\x1b\xbf\xf3\x83\xaa\xfb\x76\xad\xe3\x8d\xe7\x5f\x85\x0b\x27\xd4\x67\x2e\x9b\x52\xcf\x68\xec\x6a\x56\x85\x9a\xac\x14\x0d\x8c\x2b\x7d\xb9\x22\x45\xfb\xe8\xce\xe8\x4b\x0d\xbe\x4e\x21\x92\x11\xa9\x21\x73\x4b\x6a\x48\x52\xd3\x68\xf1\xfa\x73\xe4\x95\xa4\xcf\x2b\x6a\xdc\x93\x2b\xd3\x2f\x66\x4c\x23\xbf\xe6\xd7\x5b\xb6\x4b\x53\xe7\x4b\x7b\x0f\xbe\x3b\x5d\xf9\xb6\x36\x3a\x38\xeb\xd2\xbe\xf3\x64\x6b\x57\xe0\xc1\x6d\x07\x1d\x39\x85\x03\xea\xe8\x7b\xf7\xdc\x62\xd7\xf2\x33\x69\xc8\x6f\x6c\x37\xc8\x9d\xbb\x2a\xf4\x89\x26\xf6\xaa\xaf\x84\x86\xf9\x80\x04\x2b\xe8\x19\x7a\xeb\x8d\xf1\x52\x54\x73\x8c\x22\xa8\x79\xa7\xa2\x6c\x01\xeb\xec\x4b\x67\xde\x7f\xb2\x2b\x39\xb7\xbe\xee\xb3\x69\xd4\x67\x1a\x65\xe2\xd7\x47\x9b\x75\x3b\x9d\x31\x0b\x67\x3a\x97\x13\xa8\xdc\x95\x92\x0d\xdb\xcf\x90\x0c\x58\xca\xb7\x8f\xd9\x6e\x56\x5d\x4b\x2c\xbd\x1e\x7a\x90\xb8\xec\x6b\xdd\xce\x58\x79\x27\x22\x9f\x7b\xa7\xee\xe5\xf5\xb8\x74\x26\xad\xac\x77\xef\xd4\x9c\xa5\x22\x6b\xc2\x73\xc3\xd9\x79\x18\x18\xe9\xe7\xae\xf2\x98\x2d\xea\x36\x0d\xec\x52\xbc\x3d\x94\x6c\xf8\xe9\xd0\xe1\x71\x64\xd1\x60\x1f\xde\x4d\xa1\x1a\x1e\x07\xb8\xfe\x30\x67\x4d\x28\xf3\x40\xec\x5e\x98\x50\xbc\x5a\x15\xd7\x23\x3a\xea\x95\xb9\xbb\x46\x8e\x0e\x9f\x92\x80\xed\x6e\x52\x36\x2e\x95\xa5\xea\x77\xde\xdf\xef\xe4\xa3\x5b\x8c\xac\x94\x60\x99\xad\x55\x61\x97\x29\xf9\xb2\x3f\xa7\xcc\x1c\x1e\x6e\x58\x1f\xf6\xf4\x6d\xe3\xf2\xe7\x34\xcb\x11\x5f\xdb\xfe\x00\xbf\xa6\x26\xd5\x29\x59\xe3\x35\x5d\xca\x2b\x94\xe1\x54\xa1\x8c\x21\x9b\x74\x80\x52\x79\xb3\x59\x61\x36\xb0\xee\x12\x65\x85\x2d\xb3\xb9\x75\xe2\x6b\xc5\x98\xe8\xfd\xc8\x2f\x4a\x89\x75\x84\xb4\xc9\x8f\x8a\x76\xf3\x9f\x55\xdf\x6c\xae\xb1\xd4\xd6\x76\x96\xba\xb8\x24\x74\x4f\x3c\x9c\xde\x23\x73\x72\x12\xa5\xb0\x76\x71\xaf\xbe\x18\x3d\x91\x41\x8e\xe5\x6e\xbf\xcf\x55\x42\x8e\x8c\xf3\xb8\x79\xd3\x31\x8a\x8a\x92\x69\xed\xa5\x41\x99\x49\xf8\x4b\xd5\x97\x39\xcc\xa9\x37\xd1\x3a\x61\x2a\x32\xd1\xd7\xed\x8e\x8e\x1d\x9d\xa4\xfb\x53\xed\xfc\xa4\x11\x7a\xfd\x78\xda\xcd\xa1\xa2\xa3\xb1\x8f\xf5\xee\xda\xdd\xf5\xb7\x43\x82\xde\x7e\x0e\x62\x7b\xe7\x17\xa2\x1a\xc7\x43\x35\x33\xaf\xe5\x62\x60\x79\x93\xc3\x82\x73\x7d\x44\xe1\x82\x37\x1b\x58\xf9\xbe\xcb\xb6\x06\xdb\x41\x70\xa1\xfe\x9b\x64\x9e\xa7\x45\x25\x9a\xa3\x64\x19\xd5\x7e\x7a\x9c\x77\xf2\xe8\x2f\x27\x87\x4d\x4c\xe8\x47\xdd\x1e\x50\xbb\x25\x15\xaa\x74\xeb\x8a\xac\xb2\x57\x30\xee\x2f\xb0\xa8\xb0\x46\x1e\x6e\x58\xcd\x46\x3c\x77\x2c\x45\xf0\xc7\x39\x3d\xbb\xa5\x88\x32\x94\xf2\x65\xe1\x81\x8b\x77\x9f\x54\x4f\x89\xed\x27\x2b\x8b\x3a\x88\x9f\xd0\x8b\x5e\xf0\xaf\xfd\x2f\xbb\xe0\xc8\xbf\x77\xbc\x11\x28\x49\xf0\xbf\x40\xd4\xbf\xb7\xc1\xf5\x7d\x5c\xb1\x20\x15\x02\x9e\xa4\x84\x75\xb7\x21\xe2\x5c\x49\x04\xe2\x2f\x53\x0b\xe3\x82\x05\x29\xc8\x9f\x5d\xa2\x1a\x38\x6b\x2c\x11\x43\xc2\x11\xf0\x7a\x58\x22\xce\x4e\x5c\x81\xe0\x6c\x0b\x00\xa9\x38\x63\xec\xdd\x81\x70\xc0\x2f\xba\x82\x02\xc1\xdb\x54\x1c\x01\x87\x01\xc5\x61\x60\x18\x10\x02\x83\xc3\x80\x10\x30\x18\x65\x0e\x52\x23\x61\x9c\x71\x36\xf2\x78\x7b\x67\x2c\x10\x0c\x00\xc9\xbb\xdb\x60\xf1\x24\x20\x5a\x12\x02\x00\x9d\x85\x3c\x33\xc4\xa1\x10\x24\x00\xa4\x88\x71\xbd\x85\xc5\xd9\x3b\x90\x7e\x7d\x08\x00\xe9\x91\xb0\x2e\x86\x40\x34\xf8\x77\x00\x15\x9c\x33\x16\x0a\x44\x00\xc1\x40\x5d\x80\x9c\xdc\xbf\x2a\x41\xff\xb1\xa1\x0f\x45\xc3\xfe\x97\x03\x0b\xf3\xf8\x7e\xfc\x03\xe0\xe5\x0f\x9b\x32\x14\xed\x1b\x18\xbe\xd5\xd5\x9a\x48\xc8\xb8\x23\x72\x7b\xe0\x46\xe1\x06\x39\x30\x26\x39\xf7\x9a\xbc\xcc\xc1\x5e\x28\x66\x52\x50\xd6\xe1\x5e\x9b\xff\x0c\xfa\x1e\xbf\xe6\xa8\xa7\x49\xdd\xe2\x47\x89\x1c\x2a\x55\xd5\xbc\xbc\x50\x8f\xf1\x97\xe7\xa4\x5a\x72\xe1\x6a\x30\x91\x24\xd7\x5a\xc0\x8c\xf5\x52\x18\xff\xf4\xe4\x66\x71\x48\xdd\xf0\x82\xae\x01\xbf\xcf\x4e\xe3\xc7\xf1\x0c\x6e\x09\x22\xdb\xec\x43\x7e\x90\x61\xb0\x81\x6f\x4e\x78\xdc\x8b\x7d\x8f\x66\xb6\x1d\x0a\x98\x83\x2a\x5b\xa1\x93\x08\x66\xd7\x3d\xf3\xb5\xcf\x12\x8c\x7f\x59\xca\x73\xa8\x36\x69\x32\x6e\xfa\xdb\x75\xef\x19\xda\x81\x4b\x6b\xf0\xd2\x6b\x2c\x17\x8b\xe7\xc8\xc4\x46\x19\x1f\x45\x95\xa7\x1a\x8e\x3a\x69\xde\xec\x0e\x6c\x84\xc7\x25\x63\x98\xb2\xa6\x7c\x71\xab\x79\x31\xcc\xf9\x13\xee\x61\x66\x2c\x41\xee\x8b\xee\xdc\x07\x4c\x6c\x08\x0a\xe2\x07\xb2\x27\x3a\x33\x94\x72\xaa\xb4\x99\x33\xed\x3b\x82\x5b\x8a\x8b\xf0\x2d\x01\xc7\xae\x96\xb8\xf8\x6d\x53\xcd\xf4\x48\x77\x93\xc5\xf4\xb9\x6f\xb5\xe2\x6d\x84\x07\xe9\x81\xf3\xd9\xd6\x95\x9f\x54\x43\x9e\x04\xa5\x3c\x80\x3f\x0e\x9b\x39\x86\x46\x5f\x94\xf0\x6f\x3a\xff\x91\x3f\x32\xe7\xbf\xc8\x47\xf2\xbf\x28\x05\xa4\xe7\x61\x4d\x3a\x33\xf4\x89\x1e\xd8\x5f\xa8\x02\xc6\x1d\xfb\xcb\xf3\xff\x17\x0c\x8e\xe8\x4e\x52\x74\xc0\x10\xcf\x74\xa0\x81\xf9\xfb\x1d\x02\x03\x80\x8c\x70\xb6\x24\x07\x77\x53\x14\x0a\x05\x44\x22\x91\x40\x38\x1c\x06\x84\xa1\x25\x81\x30\x18\x0c\x08\x45\x80\x81\x68\x18\x0c\x88\x40\x20\x81\x50\x14\x0a\x88\x00\x83\x7f\xdf\x08\xe4\x2f\xff\xd9\xbb\x39\xe0\x1f\x0a\x06\xa2\x7e\xe9\x04\xa4\x4f\x30\xc0\xe3\xce\xc6\x0f\x44\xff\x53\x38\x10\xf0\xbf\x2b\x53\x81\x00\x25\xff\xf0\x43\xfe\xc3\x4f\xc0\x9f\x89\xf2\xf7\x9a\xda\x44\x82\x8d\x1e\x96\x64\x0a\xd2\x56\x52\x01\xe9\x63\xbd\x49\xe6\xff\xe3\xb3\x7f\xf4\x4b\x1b\x63\x7f\xf6\x20\x9e\xc9\x1c\xfe\xeb\xa8\x49\x17\xeb\x4e\xf0\x20\xda\x60\xdd\x81\xbf\x42\xe8\x82\x34\xb1\xb6\x38\xcc\xd9\xcf\xe7\x2c\x00\x42\x12\x21\x01\x45\x21\x10\x92\x60\x04\x02\x02\x41\x43\x80\x68\x38\x44\x02\x8d\x84\xc0\x21\x28\x18\x14\x8a\x86\x21\xcc\x41\xaa\x44\x82\x87\xab\x8c\x0c\x48\x0f\xa4\x4f\xc4\xe0\xdd\x5d\xcf\x16\xb7\xf1\x01\x29\xea\x81\x94\xb0\x9e\x38\x1b\xac\xae\xaa\x02\x48\x0d\x48\x22\x7a\x60\xe5\xe4\x40\x8a\x04\x3c\x09\x8b\x27\xb9\x03\xa1\x67\xb1\xfe\x23\x51\xf8\x7f\x49\xd4\x1d\xf0\x47\x76\x80\x7f\xa7\x07\xfc\x3b\xbf\xb3\x94\xce\x5a\x7e\x1b\x67\xeb\x6e\x0a\xfc\xc5\x3a\x33\x15\x09\x1e\x67\x5d\xfa\xcf\x5e\x40\xff\x11\x43\x11\x43\xc2\x38\x13\xec\x7f\xc7\xfa\xdd\x0e\x00\xe8\x8e\x2b\x16\x2f\x6f\x73\xa6\x15\xd3\xdf\x6b\x81\x8c\xef\x9a\x00\xf1\x1e\xce\xce\xbf\x1f\x60\xf3\x33\xb1\xe0\xed\x85\xb0\x78\x71\x03\x3d\xe1\xff\xd1\x6c\xd8\xbf\x03\x28\x12\xb1\x18\x12\x81\x28\xa3\xa2\xac\xa2\x02\x06\x23\x50\x60\x30\x0a\x0a\x06\x23\x25\xc1\x60\x14\x1c\x0c\x46\x22\xce\x6c\xb9\x5f\xd3\xb3\xf5\xb0\xc1\xfe\x5f\x1e\x5c\xf1\x37\x07\x09\xfd\x9b\x8f\x00\x83\xe1\x2a\x60\x30\x12\xf9\xf7\x7d\xe6\x83\xfd\xc6\xa1\x60\x30\x18\x86\x04\x83\xa1\xca\x60\x30\x0c\x2c\x07\xf8\x1d\x12\x47\xc0\x2b\x61\x48\x58\x21\x25\x29\x28\x18\x22\x09\x96\x84\xa0\x21\x48\x18\x1c\x8a\x34\xb9\x2e\xfc\x1f\x99\x7a\x13\xb1\x76\x00\x30\x10\x02\x07\x80\xff\x75\x01\x91\x08\x04\x0c\x01\xb4\x03\xfe\x8d\x21\x25\x25\xd1\xc0\xdf\x1e\x3c\xf0\x5f\x3c\x88\xe4\x1f\x18\x14\x8a\xf8\x27\x86\x82\x20\x51\x7f\xf2\xe0\x7f\xf0\x90\x90\x3f\xd7\x43\x42\xe0\xe0\x3f\x30\x18\x1c\xfa\x07\x86\x94\x84\xff\x81\x49\x9e\xc9\xe4\x9f\x18\x1c\xf6\x47\x7e\x50\x24\xf2\x0f\x0c\x86\xfc\x0f\x1e\x89\x88\xc1\x39\x63\x89\x67\xa3\xd4\xc3\xf9\x62\x81\x10\x38\x48\x97\x40\x20\x01\x7f\x89\x48\x17\x00\x52\xc3\xdb\x11\x80\xbf\x06\x7e\x66\x28\x01\x4d\x81\x32\x50\xa4\x82\x22\x0a\xa5\x08\x81\x28\xa8\x20\x20\x28\x28\x04\x06\x56\x41\xca\xc3\x95\xd1\x92\x48\xa8\x0a\x0c\xac\x04\x96\x03\xfc\xef\x94\x33\xe5\x2a\x11\x6c\x14\x1d\xb0\x36\x4e\xee\x1e\x2e\x40\x10\x1a\xa1\xa0\xa8\x20\x0f\x55\x42\x41\x51\x28\x79\xb4\x3c\x4c\x19\x8e\x52\x02\x4b\xa2\xa0\x2a\x28\x25\x38\x5a\x12\x0c\x03\xfc\xfa\x27\xc1\x10\x49\xbf\x46\x8a\x42\xc0\x60\x00\x01\x01\xe5\x3b\x2a\x80\xff\x13\x00\x00\xff\xff\x25\xb2\x5e\xd5\x50\x1f\x00\x00")
+
+func testAssetsTest_expectedPdfBytes() ([]byte, error) {
+ return bindataRead(
+ _testAssetsTest_expectedPdf,
+ "test/assets/test_expected.pdf",
+ )
+}
+
+func testAssetsTest_expectedPdf() (*asset, error) {
+ bytes, err := testAssetsTest_expectedPdfBytes()
+ if err != nil {
+ return nil, err
+ }
+
+ info := bindataFileInfo{name: "test/assets/test_expected.pdf", size: 8016, mode: os.FileMode(420), modTime: time.Unix(1568824473, 0)}
+ a := &asset{bytes: bytes, info: info}
+ return a, nil
+}
+
+var _testAssetsTest_templateOdt = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcc\x79\x77\x50\x93\xdb\xd7\xee\x8b\xf4\x8e\x34\x01\xa9\x0a\xd2\xa4\x17\xe9\xd2\x11\xa9\x0a\x08\x86\x5e\x42\x87\x84\x24\x74\xa4\x57\x81\xd0\x11\x10\xa4\x07\x04\x29\x52\x0f\x20\x20\x4d\x90\xaa\xa1\x0a\x02\x02\xd2\x09\xbd\x48\xbf\xe3\xef\xce\xb9\x47\xcf\xd5\xf3\x9d\xdf\x7f\xdf\x9a\x79\x67\x92\x99\x3c\xcf\xde\xd9\x7b\xbf\x6b\x3d\xfb\x59\xba\x1a\xd8\x38\xd4\x00\x40\x00\x00\x83\x4f\x84\x75\xcc\xde\x09\x93\x70\x02\x00\xf0\xfd\x21\x00\x00\xc0\xc5\xc1\x05\x8c\xf0\x86\x82\x2d\xa1\x50\x67\x07\x6b\x4b\x84\x03\xc4\x55\xc0\xc3\xd5\x86\x1f\x62\x09\x77\x80\xf3\x43\xa0\x60\x57\x1b\x88\xb5\xbb\x0b\xd8\x15\xc1\x8f\x00\x7b\x21\xfe\x2f\x19\x01\x01\xc1\x7f\xc8\x80\x1f\xe2\x3f\x64\x60\x84\x25\xbf\x97\x8b\x73\xac\x9e\x56\xdc\x73\x41\xea\x37\x47\x86\xca\x35\x86\x95\xc7\x04\x16\xc4\x13\xc6\x3a\xa6\x2e\x85\x5a\x85\xcd\x2a\xc8\x73\xc5\xcf\x94\xd7\xe9\xed\x3e\x5d\x9c\x66\x0c\x0d\xb8\xc2\xb8\xe6\x03\x97\x07\x04\x21\x2f\x30\xf3\x73\x98\xcf\xce\x66\x6d\x88\x87\xa5\x68\x62\xec\x0d\x17\x79\x37\xbe\x82\x12\x8d\xad\xf1\xcd\xa6\x6c\x5f\xf2\xd1\x67\x8a\x2c\x71\x89\xd8\x34\x19\xe6\x83\x39\xaf\x02\xf0\xfa\xc4\xb5\x4b\xa4\x40\xd7\xd9\x32\xd0\xd2\x8f\xa7\x1e\x86\x9e\xb4\x90\x9f\x48\x7c\x40\xa9\x5e\x7d\x27\x54\x92\x60\x91\x7a\x5c\x41\x79\x52\x31\xf3\x8a\xae\xb3\xd0\x53\xe8\xa9\xe3\xd8\x7b\x24\x8f\x47\x38\x12\xa4\xed\x4e\xec\xf8\x3a\x67\xcb\x44\xaa\x4e\xac\x0c\xd7\xbc\x6c\x32\xda\x41\xf5\x66\xdb\x55\x51\xc1\x48\x29\x6a\x3c\x36\xd0\x1f\x90\x58\xfd\x80\x4d\x77\x5a\x4a\xc5\x0f\x46\xa6\xc5\xa0\xf5\x14\x09\x84\xd0\xa2\xa9\x15\xbb\x3d\xa8\x62\xc3\xed\xa0\xf5\xea\xd2\xa7\x58\xb6\xa5\xad\xd2\x20\x35\xb3\x5a\x4d\xcf\x1e\x10\x8d\xb5\x95\x6b\xe0\xe3\xee\xed\xe9\xfd\x9c\xa7\xd0\x7b\x7e\x99\xc7\x5e\x4d\xc4\xd6\x8d\x5a\xdc\xb7\xb0\x06\x45\x64\xe4\x70\x1b\xf3\xb9\x70\x8a\x0c\x1f\xce\x47\x1c\xf3\xf7\xdb\x27\xa2\x23\x71\x1e\x27\xaa\x1d\x6f\xd3\xda\xe7\x6a\x95\xb3\x16\xb8\x5f\xdf\x8a\x8e\xa6\xbf\x34\x38\xff\xac\x12\x47\xe8\xfd\x82\xcf\xb5\x97\xc5\xe0\x4c\x00\x92\x53\x3a\xaf\xb3\x41\xff\xe2\x0f\x31\x2e\xa8\x95\x4b\x09\xcb\xae\xc5\x17\x07\x12\x24\x9a\x95\x0c\x1a\xef\xe7\x6b\x73\xb6\x7a\x0c\xbe\x4c\x2c\x3a\x15\xfd\x64\x22\x74\x58\xf6\xb1\x35\xc8\x38\xb4\xfe\x53\xc2\x02\xd7\x52\xa8\x45\x82\x2d\xb7\x86\xd5\xb5\xf8\x4e\xf5\xcc\xa2\x08\x3f\xdf\x48\x45\x55\x80\x20\xdd\xd7\xab\xfe\xf9\xc6\xf8\xde\xd4\xe3\x67\x39\xd5\x89\x10\xd5\xcf\x55\x4a\xd8\x72\xe3\xd7\xc0\x1a\x43\x2e\xc3\x04\xbd\x22\xa5\x10\xf9\x27\x74\x27\xdf\x62\xea\x0c\xfb\xa1\x5d\x24\xa5\xa4\x4e\x3a\x1a\x6c\x19\xc4\xcd\xe2\x56\x6d\xf3\xf4\x0b\x9c\xdd\x4b\xb4\xfd\x20\xa3\x89\x5e\x17\x87\x80\x44\xda\xc6\x54\x37\x3b\x98\x12\x89\xac\x3e\xcd\x4a\xc7\x55\xc5\xa3\x72\xa7\x03\x72\x5d\x0d\x7c\x82\x4a\xe6\x2a\xf1\xcf\x58\x00\xb0\x81\x0d\x00\xbf\x3f\x11\x24\x00\x00\xc0\xc1\x08\x84\x83\xab\x1d\xfc\xfb\xa9\xa8\x05\x19\x1b\x32\xa8\x53\xf8\xcf\x9a\xcf\xcd\x4d\xe7\xbc\xa3\x59\x2c\x96\x0c\x64\xb3\x0c\x69\xc7\x8a\xb1\xdb\x1b\xeb\xa7\xfe\x90\x5b\xe4\xa4\xed\x19\xf7\xb6\x75\x76\x0d\x2f\x26\xda\xa2\x87\xe4\xa0\xa0\xbf\xb0\xa4\xa9\xa4\xa5\x82\x5f\x76\x3c\x7b\x29\x4a\xca\x07\xcf\x9a\x63\xed\x39\x7a\x35\x22\x09\x7d\x75\x85\xc0\xfa\x65\xf7\x9d\xc2\x6e\x85\xbd\x6f\x55\xfb\xab\x87\x15\x4e\x8c\xb1\x36\x38\xa3\x74\xf9\xb7\xb1\x30\x48\x3e\x09\xb5\xb8\x89\x9b\x3e\xa6\x53\x8e\xa1\x3e\xec\xab\x61\xfd\x2b\x3c\x44\xbc\x2e\x06\xd1\x38\xce\xa3\x2a\x06\x9a\x0a\x15\xe6\xa5\x2d\xfe\x8d\x0c\xc9\x7c\x15\xfd\x29\x23\xc3\xcf\xd1\x9d\x0d\x35\x20\xfe\x21\xdb\x6a\xf2\xea\x38\x96\x0f\x31\x7d\x45\x86\x86\x86\x8b\x50\xcf\x73\xae\x1b\x2d\x32\x2b\x60\x01\x43\x5a\x66\x39\x69\x49\x49\x01\x81\x80\x56\x19\x26\x7f\x4c\x53\xe5\x28\x7f\x47\xfb\xa3\xcb\x56\xe1\x81\xc7\xab\x1e\x2e\x11\x07\x87\x90\x0a\x43\xdf\xc7\x17\x92\xa9\x64\x94\x90\xe7\xf9\xa3\xe0\x8a\x0d\x94\xb2\x46\xb9\x9c\x46\x5a\xb3\x51\x59\x0d\xa6\xf9\xb2\x75\x2f\xeb\x45\x05\x3d\x05\xb6\xa6\xc6\xba\xb4\x6b\x02\x9a\x29\xcc\xc7\xa4\x51\xb6\x81\xbf\x95\xe6\x75\xeb\x84\x78\x0c\x63\xc4\x67\x4e\xfa\xa6\xf7\x8f\xda\x8a\x96\x53\xa2\xf7\xac\xda\x5b\x83\x19\x75\xea\x04\x9a\x60\x06\xc7\x69\x3a\x5e\x01\xc1\xd5\xcc\x9d\xd4\x56\x1f\xd2\x08\x64\xf1\x9a\x72\x2d\x22\x8b\xe5\x57\xd0\x65\x22\xf4\x5f\x87\x3f\xa2\xdd\x43\x44\x88\x8f\xe8\x3a\xf8\x57\x6f\xe6\x6e\xcc\xcc\xc7\x51\x25\x30\x6b\x30\x75\x30\xce\xd7\xd7\x4d\x32\x9b\xcf\x31\x3d\x75\xaf\x9e\x18\x9a\x2f\xf3\xb2\x7e\x39\x46\xbf\xea\x82\x51\x3a\xbe\xdd\xca\xe9\x19\x3e\xd3\xf5\x78\x91\xa7\x75\xc2\x54\xfe\xd8\x48\xb2\xb9\x9f\x77\xb4\x37\x37\xb8\x43\x89\x23\x52\x56\xa2\x32\x64\x64\x46\x3b\xd4\xeb\x6c\xf1\x10\x14\x6f\xaa\xb6\x2c\xf8\x09\xb7\x75\xd8\xaf\x2a\x80\xf5\x16\x76\x62\x0d\xbd\x13\x95\x68\x39\xc5\xf5\xda\xae\x2b\x22\xe3\xf2\x29\x1c\x3e\x54\x68\xde\x55\xc9\x67\x67\x1a\x3e\x4c\x6e\x3e\x82\x9a\xad\xb5\x1a\x7a\xca\x64\x03\xc8\x5a\xf3\xa5\x1a\xad\xbe\x43\x05\x85\xaf\xac\xae\x0c\x8d\x6f\x2b\xf7\x33\x90\xf6\xb1\x14\xe4\x16\x9b\x52\xc9\x77\xd5\x88\x08\xf7\xad\xaf\xf2\xc1\xb8\xa6\x60\x3b\x62\x09\x98\xc6\xe6\xf9\x77\x18\x72\x5d\x86\x99\xbb\x6e\x02\x02\x36\xaa\x12\x3c\xd8\x17\xec\x21\x9e\x51\x7c\xdc\x17\xf6\x59\xc5\x74\xb0\xfb\x14\xb3\x34\xd6\xc5\xd1\xf6\x52\xe2\x69\xab\x5a\x2d\x72\xc9\x89\x19\x63\x2b\xed\x87\xf5\x9d\x3e\x42\x39\x78\xc7\xe6\x31\xf7\x38\x85\xe7\xab\x12\x96\x21\x10\x4e\x71\x16\xba\x1a\x6e\xac\xa7\x58\xea\x0a\xfe\x39\x88\x41\x52\xd1\xae\xb3\x39\x3e\xaa\x57\x55\x4f\xbc\xb7\xee\x69\xb6\x89\x92\xde\x7a\x02\xfd\x64\x6b\x2b\x9d\xc6\x27\x53\x88\x48\x89\xb4\x08\xe6\x09\xd7\x72\x94\x09\xcd\x18\x52\x5f\xa3\xf5\xec\x40\xe1\xd0\x04\xa4\x08\x4d\x94\x0b\x06\x87\xb0\x0f\xeb\xf3\xbd\xd9\x78\x1d\x77\xe1\x6b\x89\xd2\x12\x8d\x82\x62\xfb\xf5\x24\xb2\xe1\x4c\xc1\x71\xa9\xbf\xa1\xf0\x3c\xd0\x2a\x58\xc7\xd8\x42\xfb\x31\x3a\x53\xf4\x1a\x03\xf3\x59\x6c\x9d\x62\xbc\x61\x9b\x6a\xaa\xa8\x72\xf5\xfd\x5e\x20\x3a\x07\xa5\x95\xdc\x5d\xba\xa4\x0d\x52\x70\x37\xb0\x13\xdd\xdd\xde\xcc\x4b\x79\x61\xe3\x2d\xc3\x52\x37\xf2\xd1\x71\xe7\xde\x8b\x00\xcf\xbb\x2a\x29\x50\xc2\x2d\xbb\x5b\x36\x92\x43\x58\x3a\x38\x19\xfe\x37\x92\xad\x3e\xec\xf1\x66\xfb\x78\x9d\x40\x5b\x44\xac\x68\x13\x2f\xe6\x52\xc4\x92\x35\xf5\x22\x9b\x84\x61\xe9\x9f\x06\xb6\x71\x45\x45\x69\x88\x96\xfd\x32\x65\x2d\x28\xa1\xef\x73\x17\x1f\x90\xbf\x54\xc8\xc6\x13\x73\xba\xb0\x8a\xae\xbc\x68\x47\x93\x96\x4b\x18\xbb\xa5\x55\x80\xd2\x29\x37\xdf\x3e\x8f\xe2\x7d\x7e\xf4\x7e\x97\xe8\xa2\x8d\x8d\x18\x4f\x76\x89\xe1\x46\xad\xa9\xe0\xd5\x94\x2f\xca\xc6\xd7\xae\x31\x56\xca\x17\xde\x23\x0e\x75\xfe\x0c\x14\xce\xf4\xb5\x1f\x93\x8e\x55\x0b\x6f\xb2\xb9\xa6\xba\xda\xa8\x38\x56\x5e\xb4\xc8\x34\x15\x9b\x50\x6f\xe3\xa4\x95\xd5\xc6\x57\x42\xdd\xcc\x9e\x81\xd2\xba\xe4\x46\x0d\x42\xec\xe3\x6e\x3f\xe1\x24\xe8\xd8\x77\xc4\x4e\x52\x7e\xc3\x7d\x70\x24\x30\x26\x33\x37\xe2\x6c\xad\x78\x58\xc0\xfc\xe1\x95\x6f\x2d\xa7\x52\x5c\x06\x0a\x14\x8a\x8a\xbf\x89\xd8\xdc\xc6\x62\x0a\x43\x2e\x4b\xea\x14\x67\xcd\xe9\x4c\x7d\x94\xdb\x71\xeb\x1c\x7a\x94\xef\x92\x2e\x82\x63\xb8\xec\x6c\x36\xc9\x35\xcf\x1c\x49\xc7\x5d\x0a\xa3\x7f\x85\x3f\xe2\x76\xf2\xa5\x42\xd2\xee\x76\x4a\x64\x67\xf9\x8b\x6f\x12\x95\x18\xda\xce\xfb\xe4\x5e\xf8\x37\x13\xb8\xc9\x48\xc5\x38\xba\x33\x7a\x2a\xb6\x04\x4d\x0d\xa5\x23\xf0\xdd\x8d\xd7\x56\xad\x19\x88\xeb\x5d\x3e\x83\x9c\x77\xa9\x4c\x76\x75\x6c\xb3\x18\xf9\xee\x91\x4e\x70\xae\x16\x7b\x8c\xa7\xeb\xba\x67\x5f\x18\xf1\x78\x68\x36\x97\xf8\xf3\xdc\x08\xf6\x0a\x50\x94\xcf\x9e\x44\x89\x8e\x1d\xf7\x45\xdc\x31\xc7\x16\x7b\x7d\x23\xcd\x3d\x7d\xf4\x63\xa8\xfe\x2e\xad\x0e\xaa\x2c\xec\xf0\x4d\xa1\xb5\x22\xaf\xca\x91\x86\x92\x8d\x71\x23\x1f\x9f\x1a\x45\xb9\xed\xb9\x8d\xa1\x9a\x96\xb4\xca\x97\x3b\x5e\x21\x33\x77\x0b\x56\xd4\x64\x8b\x6a\xd5\xb6\xb1\x49\xfc\x5f\x0c\x23\x95\xfb\x27\x5a\xce\xcb\x19\x06\xb6\x7c\x1c\x49\xfd\xa8\x4e\xfa\x8e\xb8\xc7\xc5\x83\x31\xcd\xfc\xc8\x87\x88\xe6\x68\xc3\xca\x39\x1d\xa1\x0b\x08\x8b\x08\xd5\x62\x76\xfe\x08\x75\x4a\x66\x5b\x97\x7e\xb3\x58\x28\xd2\xc2\xce\x37\x69\xb6\xfb\xdc\x79\xc5\x6f\xad\x48\x1f\x32\xfe\xbe\x95\xd6\xdf\x8d\xc2\x9f\xd1\xa7\x38\x85\x0a\x8f\x8e\xc8\xdc\x7c\x50\x08\x47\x60\xf5\xc6\x46\xd0\x47\xc5\x73\xbc\xc0\x30\x84\x1f\xfe\x82\x0d\xbb\x7e\x71\x52\x31\xbc\x68\xbf\x5f\x31\x36\x59\xfb\x51\xe1\xd9\x2e\xc1\xcd\xaf\xcf\x32\x48\xb1\xcd\x51\xfa\x70\x81\xcf\x05\xda\xb9\x1b\xab\x42\x05\x8e\x01\xc2\x83\x56\xca\xb6\x0b\xb5\xce\xc6\x24\x0a\xd3\x65\x64\x47\x92\x9a\xe0\xec\x0f\x9d\xfa\x8f\x50\xd8\x37\xd9\xe9\x9d\x4d\x0c\x83\x77\x85\xaf\x2d\x87\x58\x66\x57\xd4\xbd\xcb\xf8\xe3\x4b\x61\xa5\x11\xea\x6b\xea\x52\xa6\xaa\x87\xf0\xb2\x75\x19\xe9\x50\xce\xc5\x01\xb6\x61\x9f\xdc\x29\x8e\x9e\xfd\xc8\x74\xd5\x6c\xa4\x65\x3b\x50\xf5\x7c\xb5\x07\x3c\xdf\xcc\x50\xf7\xcd\xaa\xf9\xda\x99\x1d\xcf\xd7\xa7\x8b\x51\x0b\xc8\x9b\xc7\x51\x8c\x3a\x48\x52\xa3\x3c\xa2\xd6\x9c\x2b\x52\xdc\xc8\x7c\xee\x05\x7b\x15\x05\x72\xbb\x59\x40\x42\xd1\xeb\xf1\x81\x6f\x3c\x09\xab\x3a\xcd\xf9\x50\xfc\xdd\x2e\x11\x92\x5b\xd4\xa8\x0c\xe8\x90\xa8\x37\x92\xcd\xec\x19\x4d\x58\xde\x68\xdc\x9d\x3a\xa4\x38\xff\x10\x4f\xca\x5e\xe6\x2c\x28\x9e\xdc\x85\xbc\xe7\x8f\x44\x92\x20\x8d\x0c\x8b\xaa\x90\x65\x43\x5c\xf8\x18\xbd\x8c\x41\x34\x13\x4d\xc5\x17\x0f\x72\x9b\xa9\x69\xfc\x84\xb0\x4f\xa3\x53\xe5\xa8\x05\x70\x79\xce\x8d\x51\x2a\x17\xa5\x62\x2e\x3d\x13\x19\x34\x7a\x48\xbf\x72\x84\xfb\x71\xd0\x5c\x69\x98\x2c\x57\x0b\x17\xe3\xd3\x67\x99\xeb\xcf\xa1\x75\xce\x7f\xc8\xb9\x61\x48\x27\x4d\x78\xda\xbf\xec\x4c\x2b\x8d\xe7\xd9\x05\x50\x7b\xbd\xcb\xcf\x74\x20\x39\x33\xa0\x8c\xb5\x9f\xed\xd9\x19\x3d\x08\x1a\x34\x7c\x60\x00\x7f\xc2\x49\x82\x4d\xf5\x6d\xe2\xa4\xe5\x8c\xa0\x3a\xff\x3a\x11\x5f\xdf\x1b\xa8\x2c\xf9\x41\xb2\x92\xf7\x88\x93\xf4\x37\x43\x75\x8d\x92\xb9\xdb\x6e\xed\x2f\xdd\xf9\xe0\x7f\xf4\x87\xc6\x50\x05\x0f\x6e\x73\x62\xc5\x1f\xf5\xa0\xa5\xbb\xdc\x36\x6d\x35\xe8\x2e\x1b\x12\xb3\x58\xe8\x52\x66\xfd\x78\x4d\x66\x0e\x68\x0e\x30\x01\xdf\x8b\xd9\x2b\x92\x0c\xbe\x24\x3c\x00\x88\xe0\xfb\xb3\x98\xfd\xa9\x95\x7e\x2c\x66\x8c\x00\x00\x28\x41\x5c\x6d\x1d\xec\xdc\x61\xff\x51\x49\x70\x61\x01\x4b\x6b\x6b\xb0\x33\x18\x66\x89\x80\xc0\x04\x7e\x8f\x64\xf8\x05\x12\x8e\xb0\x44\xb8\xc3\xad\x2c\xff\x5b\x1c\x14\x02\x75\x87\xba\x80\x5d\xdd\xff\x01\xc7\xf2\x0b\x9c\x83\x8b\xa5\x1d\x18\x2e\xa0\xe8\x80\x70\xb1\x84\xc2\xff\x01\x4c\xf7\x0b\xf0\xf7\xf1\xfe\x79\xaa\xbf\x42\xd9\x3a\x43\x2c\x11\xe0\x7f\x42\xfd\x6a\x49\xa1\x30\x88\x1d\x0c\x0c\xff\x1f\x96\xe6\x57\xe3\x21\x20\x10\xe7\xff\x7e\x41\xbf\xa3\xa0\x96\xae\x60\x67\x81\x7f\xd6\x31\x2e\x96\xae\x0e\xb6\x60\x38\x82\x1f\x66\x63\x3b\x90\x34\xe0\x1a\x22\x48\x11\xba\x25\x13\x06\x1e\x9c\x90\x17\xc0\xc5\x85\x71\xf5\x36\x38\x66\xbf\x15\x0b\x37\xc2\x15\xd8\xcd\xa0\x9c\x69\x41\x26\x3d\x7a\x50\x50\xb2\x96\x3d\xb2\x50\xae\x2a\x32\x8b\xac\x79\x8e\xb1\x64\xec\x89\xae\x2a\x6e\xd3\x17\xb5\xe6\xe6\xe8\xbd\x67\x5f\xbb\xca\x2b\xd5\xc4\xdf\x50\x38\x25\x35\x39\x51\xc7\xcb\x92\xc1\xb4\xe3\x18\x52\x98\xcd\xd3\xe8\xe9\xca\x23\x69\x9e\x1a\xc6\x77\x87\xc4\x79\xa8\x3a\x8f\xcb\x2b\x63\x4f\x46\xe7\xcd\x8d\xb6\x51\x09\xb7\x7b\x24\xef\x6a\x90\x04\x84\xcc\xb1\x8a\x0e\x07\x5f\xfd\x18\x24\x07\xe7\x09\x6d\x0d\x1f\x20\xd6\x73\xe2\xbd\x8f\x58\x65\xa3\xa7\xa5\x5e\xe6\x54\x11\x59\xd6\x3d\x6c\x8e\xa8\x63\x4d\x08\x92\x8f\x25\xd4\xec\x0d\x45\x84\x30\x36\x6a\x6b\x74\x89\xa4\xee\x3d\x90\x6c\xcb\xc5\xcc\x77\x5b\x34\x0f\x87\xe4\xc9\x58\x6c\xf3\x22\x87\x26\x66\x22\x49\x50\x8b\x66\x5f\x9a\x2b\x37\x4d\x42\x66\xfd\x52\xec\x7d\x36\xad\x7d\xdc\x87\xbb\x76\x66\x6f\x26\x5b\x64\x0a\x6f\xea\xac\x35\x49\x7b\x5f\x2e\x5f\xf0\x0f\x5d\xd2\xcf\x57\xd4\x78\x28\x3b\xcb\x0f\xe6\xdb\x9d\xc4\x2f\xd7\xec\xb2\x18\xa8\x55\x7c\x7f\x73\x6a\x8e\xec\xd1\xb8\x58\x00\x10\xf2\x8f\x32\x90\xe8\xbb\x0c\x44\x78\x3b\x83\xff\x23\x02\x31\x20\xe3\xb8\x29\x71\x5a\xff\x59\x73\xcb\xe2\x95\x96\xf7\x89\x1b\xe9\xfd\x13\x03\x05\xe4\xb8\xb4\x7c\xf1\x88\xa0\xac\x7d\xcb\x6c\x22\x50\xde\x6d\x57\xae\x87\xac\x2f\x5f\x67\x99\xb7\x46\x50\x73\x3f\xa8\xb2\xe1\x4d\xaf\x69\x8a\x76\x27\x62\x17\x1b\x9a\x6b\xf0\x5b\x6e\x7f\x3e\x73\xe1\x75\x7b\x75\x1e\x97\x58\x5d\xc2\x97\x28\x11\xcc\x78\x5b\x50\xc3\x5b\x75\xe2\x82\xfb\x1b\x66\x61\xb6\xbd\xfb\x4b\xb7\x67\x80\x77\xdf\x5e\x66\x81\xd7\xf3\x9d\x1a\x7e\x21\xbd\x5c\x7d\xce\xa7\x8b\x57\x6e\x89\x34\xb8\x44\x66\xee\x15\xe5\xe8\x96\x4c\xaa\x5e\xad\x10\x74\x52\x7d\x66\xb2\x91\x6f\xc0\x08\xc9\x5b\xf2\xb4\x58\xde\x50\x49\xfd\x98\x7a\x09\x95\xb8\x4d\xee\xd4\xdd\x84\x3c\x7c\x10\x4e\x4d\x64\x4e\x20\xe7\xb7\x6c\xcb\x48\x6c\x80\x1c\xbc\x27\xfe\x5c\xcb\xa6\x18\x29\xb6\xb0\x18\xba\xc2\x7e\x05\x49\xd1\x1a\x48\x8e\x76\xeb\xd3\x8f\xbf\x2f\x8b\x5b\x6b\x61\x8c\x64\x4f\x30\x0e\xbf\x7f\xbb\x9e\x67\xda\xf1\x6e\xe6\x90\x71\xb6\xaf\x53\xbc\xa3\x2a\x8c\xbf\xb7\xa1\x5a\xac\xe2\x6e\xbd\x72\x49\xda\xf6\xaa\xd0\x09\xa0\xbc\x37\x1e\x71\x54\x66\x77\x85\x89\xff\x4a\x56\x67\xc2\x92\xdb\x19\x77\x1e\xb7\x9c\xd4\x72\x67\x4b\xac\xc5\xa0\x13\x1e\x9d\xd2\x48\x59\xf3\x37\x23\x4b\x05\x51\xc2\x07\x22\x2e\x53\x49\x5f\xae\x97\x7d\x40\x9a\x91\xd6\x5b\xc2\x44\x02\x25\x9b\xdf\x3e\xb9\xe6\xc0\xc7\xbc\x37\x77\x63\x4f\x81\x22\x4c\x65\x3d\x2f\xd9\x49\x2c\x7c\xae\x3b\x40\x39\x83\x96\x47\xf0\x2e\xec\xf3\x2b\x7e\x4f\xa3\x7e\xd2\x67\x83\x8a\x49\x6b\x76\x9b\x43\x29\x16\x69\xe7\x9f\xf2\x1e\x5e\xcb\x6c\x8a\xc3\x29\xff\x43\xb7\x0c\x8e\x4b\x59\xad\x37\x2c\x78\xda\x88\xb8\xd5\xef\x41\xe3\x95\xbb\x72\x3a\x3c\xfe\xe9\x6c\x75\xfd\x15\xfe\xc3\x67\xee\xe3\x42\x0c\x5f\x9f\x29\xf8\x79\xc8\x92\x75\xc0\x0b\x42\x9f\x64\x6c\x52\x65\x4c\x3e\xe2\x6f\x1b\xf1\x9e\x08\xb7\x7a\xc5\xcc\xca\xd1\xf1\x30\x3c\x54\x61\x66\xc7\x71\x7d\x60\x93\x46\xe4\x91\x7d\xf9\xa7\x8c\xfa\x08\x77\x43\x16\x03\xbe\x62\x21\x09\x8d\x9b\x9a\xaf\x5f\xb0\xe7\xdb\xa4\x2b\x9b\x76\xa4\x97\x2e\x72\x39\xa4\xed\x09\x2e\x82\x71\x6b\xe5\x7a\x94\x7b\x5b\xf3\x44\x8c\x62\x64\x08\x5b\x7a\xfa\xb4\xe6\x5c\x2b\xf1\xb6\xfd\xca\x59\x64\xf0\xf9\x4a\xc5\xef\x9a\x1d\x56\x57\x2f\x80\xef\xce\x3d\xb1\xee\xf5\xc3\x8e\x99\xb2\x6d\x39\x0f\x80\x7c\x34\xc1\xdd\xfa\x4c\x94\x0e\x37\x62\x82\x27\xcf\x0b\x76\x70\xa7\x6c\xcd\x88\x1f\x7f\x22\x13\xc8\x98\x05\x8d\xeb\x6d\xb7\x68\xab\x87\xd3\x0e\x45\xbe\x67\x37\x0a\x2c\x43\x56\xb0\xef\xef\x53\x9f\xd1\x18\x98\x5c\x7c\x11\x12\xc3\x6e\xcb\xe3\xd4\xdb\x0b\xf7\xed\x67\x55\x8d\x57\xda\x95\x2f\x20\xc9\xe3\xf5\x21\xdc\x97\xa7\x9a\xef\x12\xd1\xec\x2b\x72\x27\xa7\xb3\xa3\x63\xf5\x8a\x32\x8a\xcb\xbf\xc1\x13\xa6\xb2\x89\x4b\x68\x35\x16\x2a\x20\x48\xe6\xf7\x02\x3c\xf5\xcc\x62\x8f\x61\xb8\x51\xd7\xaf\x14\x9a\x55\xd0\xbe\xe8\xe4\x7c\x22\xff\xb0\x8d\x63\x68\x74\x3e\x7b\xc5\x4e\xf3\x16\x86\x61\xfb\x9c\xbb\x4e\x36\x69\xb7\xc1\xf6\xe6\xad\xda\xd0\x71\xc7\x38\x54\xef\x20\xf2\xce\xa9\xbe\x1d\x38\x1c\xae\xaf\xd7\xe4\xeb\x9e\x58\xa6\xee\x1b\x54\x12\x5c\x10\xb4\x50\xdb\xda\x41\xc4\x20\x47\xfb\xf4\xad\xab\x61\x13\x5b\xca\x97\x6c\xc1\x7d\xea\x5e\xcb\x6b\x3b\xd3\x27\x9b\x03\xb2\x8d\xc4\xb4\x3e\x07\xa1\xb8\x41\xe8\xd4\xdd\xca\xc9\xaf\xdc\xfa\x8f\x06\xeb\xf1\x29\x68\x76\xef\xa7\xbb\xb6\x79\x15\x07\x4c\x71\x57\xc9\xd3\xdf\x0d\xb5\x28\xeb\xe4\x91\x91\xc3\xdd\x81\x65\xa5\xe6\x77\x0e\x07\x16\x5b\xbc\x26\xb7\x14\xeb\xc8\x7d\xa5\x4b\xa4\xe1\x69\x04\x55\x90\x83\x4f\xa3\xc4\xd6\xd7\x76\x6e\xec\x45\x7b\xf0\xc8\xda\x85\x8d\xdd\x31\x5c\xa3\x5f\x8f\xd4\x24\x16\xc6\xbd\x4b\xa6\x4c\x63\x59\x2d\xd0\x73\x35\xc5\xec\x50\xff\x1e\x6b\x88\x27\xb9\x8b\x12\xfa\x8c\xad\x84\x66\x5c\x8a\xb5\xf0\x5e\xb8\x77\xc7\x53\x7b\x03\xd6\xa2\xb7\x69\xaf\xa9\xe8\xda\x7a\xc9\xd9\x6e\xbc\x5c\x03\x93\x59\xd2\x8f\x69\x99\xb2\x98\xfa\x45\x67\x71\x0c\x52\x4c\x94\x29\xc0\xb6\xef\xaf\x8d\x64\xd4\x95\x81\xc7\xdf\x65\xa5\x9b\x68\x90\xb4\x08\x33\x49\xe5\x7f\xb6\x0d\xa5\x14\xdb\xa4\x17\x1b\x8d\x66\xb3\x26\x31\x4d\x1c\x92\xd7\x79\x94\x22\x6c\x4d\xfd\xfa\x49\xec\xa1\x60\x68\x3a\xfd\x3d\xcb\xdc\x47\x57\xd0\xf3\xf5\xcb\xb4\x78\xaf\x2c\x22\x28\xfd\xdb\x83\xe9\xb7\x54\xdd\xc3\x2b\x27\xd9\xc4\x41\xa1\xa0\xc2\x9b\x9c\x3b\xbb\x2d\x8e\x96\x88\x49\xa6\x03\xf8\x1c\xd7\x18\x89\xda\xb7\xd5\x71\x78\x98\x0f\x2e\xea\x9b\x84\x73\x0e\x76\x33\x9f\xe1\xc2\xae\x3c\x44\x83\xaf\x47\x17\xae\x71\xb2\x38\x9e\x43\x6c\x52\x31\xb1\x8c\x08\x14\x47\xb7\x75\xbd\xc1\xf9\xda\x10\x38\x62\xbd\xcb\x6d\x24\x76\xa0\xc5\x3a\x86\xa9\x98\xd7\xa7\x49\x4b\x8f\x0c\xa7\x06\xd3\x77\x76\xd0\x39\xde\xcf\x4e\x4c\xac\x69\x19\xe8\xc7\x94\xd8\xbe\x2c\xb9\x1e\xd4\xe8\x0e\xe0\x00\x44\x9d\x47\x3e\xbb\x4d\x7e\x64\x22\x57\xf8\x02\x1d\x38\x2a\x8d\x1b\x74\x1a\x7b\x47\xec\xb8\x4d\x1b\x39\x86\x2a\xc3\x2d\xb6\x52\x16\xf9\x9a\x2f\xd7\xd1\x53\x3a\x82\x1b\x8d\xe3\x82\xeb\x38\xfc\x57\x2d\x73\xe5\xc8\x1a\x47\xa4\x75\x06\x4a\x52\xbb\x02\x17\x69\x6a\x87\x33\x0f\x9b\xc5\xef\xbf\xce\x96\x1c\xe0\xf6\xea\x0e\x2b\xc7\xcb\x72\xac\x54\x96\x9f\xd1\xd5\x57\xcf\xea\xae\x30\x60\x24\xf2\xa7\xe3\x8a\x48\x50\xf8\xe8\xdf\xfe\x07\x11\xcd\xac\xb5\xc1\x0a\x31\x60\xea\x39\x4e\x7d\x4a\x84\x41\xe2\x95\x70\x29\xdd\x20\xbe\xe5\x1b\x41\xcc\xac\x42\x09\x0a\xe6\xcf\x2d\xcf\x19\x5c\x3c\x8f\x79\xb0\x64\xb0\x5b\x20\x41\x67\x5f\x31\xdd\xee\xa6\x97\x56\xfe\x28\x7b\xee\x26\xc9\x59\xcb\xd3\x80\xd9\x9d\x0d\x51\xdb\xb1\x5a\x7e\xc3\x1b\x58\xa6\x78\xac\xa5\xfb\x3e\xfe\xf5\xc2\xeb\xde\x77\x5a\x16\xee\xaf\xfa\x8a\x1b\xad\x25\xa5\xdc\x12\x2b\x6a\xa3\xa6\x64\xbe\x28\xbb\xca\xc4\x5f\xe5\x42\xd1\xaf\x1a\x98\x1e\x0b\xe4\xe1\x23\x12\xc7\x41\x83\xca\x77\x85\xc0\x4b\x93\xef\x59\x02\x24\xe8\x3c\xca\x9b\xee\x6b\x89\x67\xef\x99\x1e\xa7\xdb\xf6\x1d\xbb\x37\xc1\xef\xf8\x3a\x3e\xdb\x58\xc9\xda\xd1\x73\x81\x8e\x44\x97\x32\x0d\x80\x86\x77\x9f\x1c\x7f\x2a\x7a\x2e\xbd\xaa\xd4\x27\x84\x51\x19\xd7\xaf\x65\x31\x95\x34\xe7\x5e\x78\x8d\x0e\xe4\xa8\xca\x5b\xf9\x4a\x33\xd4\xce\xd9\x8a\x12\x6d\x6d\x5c\xbb\xc5\xbb\xb9\x69\xe9\xda\x95\x3e\xa0\x10\xf4\xbe\x45\xd7\x67\x35\xaa\x35\x3b\x0e\xed\x3b\x21\xea\x17\x7b\x7a\x8d\x81\x7c\xf4\x86\x1d\xb5\xaf\x80\x8a\x21\x5c\x4f\xf4\x78\x01\x36\x62\x5a\xda\xaf\xb4\xc9\x69\xa3\xed\x3b\x2b\xe8\x09\xf0\x04\xb5\x54\x82\xa7\x5a\xce\x32\x52\x5d\xca\x6d\x9f\x4b\xc3\xe1\xd0\xc4\x27\x4f\xd5\xf9\x3d\x0b\x99\x75\xea\x77\xa2\x64\xcd\x45\x50\x76\x6f\x97\xae\x91\xa5\x41\xcf\x5e\x64\xe1\x90\xda\xa6\x4a\x09\x1f\xdf\x4d\x65\xdf\x13\xf7\x7f\x41\xce\x37\xed\x23\x24\xdd\xe7\x1f\x84\x8d\xd9\xb9\xbe\x44\x27\xb4\xcb\xbf\x34\x71\xa7\x37\xe2\xb2\xf9\xfe\x9d\xcd\x1d\x90\x58\x3d\xbe\x5d\xee\x37\x5f\x25\x42\xbc\x18\xea\x56\x0b\xc7\x4f\xd7\x03\xf7\x01\x89\x49\xd7\xc9\x35\xe1\x11\x48\x30\x41\x06\x3c\xfa\x8d\x6b\xa2\xc7\x1a\x85\x4f\x76\x27\x87\x90\xb4\x77\x64\xe1\x4b\x63\x67\x72\x37\x6a\x4c\x20\xc9\xcb\x1a\xbb\x13\x3c\x5f\xcd\x8d\xc0\xae\x5e\xf1\x10\x49\xe7\xbe\x63\x2c\xbb\xb9\xcd\x91\x1a\xa7\x09\x1d\x5f\x68\xcc\x24\xde\x08\x77\xbd\xdd\x95\xaa\x46\x2c\x34\xbe\xcd\x13\xec\x02\xf2\x9e\x69\xd2\x78\x39\xd6\xa3\x3b\xf2\x27\x81\x6f\x1a\xb0\xd0\xf8\x5f\xe7\xf0\xee\xb1\xc4\x77\x5f\x67\x7d\x22\x2f\x7e\x47\x1e\xef\x7a\xf5\xb7\x4b\x82\x04\xb9\xf7\x11\x51\x6e\x61\x1c\x1b\x51\x8f\xb5\x0f\x6b\x9b\xd5\x9c\x08\x73\x65\xf9\xbf\x66\xf2\x9e\xfb\x92\xbc\xee\x64\x26\xc8\x88\xe8\xbe\xa6\xec\xf4\x8a\x1c\x67\x20\xe7\x39\x97\x71\xc1\x46\x5f\xc8\xdd\x22\x49\x95\x95\x22\x42\xbe\xa1\x27\xc8\xdc\xf3\x2c\x08\x9b\xed\xa7\xe4\x20\x9b\xcf\xf2\x9c\x66\x37\xd1\x8f\x05\x51\xb6\xbb\x24\x23\x32\xe3\x19\xbc\xc3\x03\x28\x5f\x7b\x7a\xa5\x6e\xa2\x55\xdd\x1c\x09\xc3\x54\x55\x23\x9e\xfe\xfa\xbb\x3e\x85\x47\x66\x8d\xb1\x35\x9b\x41\xc6\xe6\xd6\x21\x9a\x12\x6d\x12\xc1\xd2\xcd\x18\xb6\xc9\x15\x9c\x8f\x98\x00\x7e\xdd\x14\x75\xc1\x76\x51\x89\xb2\xeb\xbd\x41\x36\x27\xeb\x9d\x06\x55\x31\x0b\x9d\x33\xec\x74\xc1\x4d\xda\x20\x97\x54\xf2\x24\x66\x10\x0d\x90\x43\xd4\xc0\x7a\xb2\xc7\x76\xdd\x91\x3f\x59\x5e\x19\xb7\xe3\x7a\x74\xd4\xd4\xd3\xb6\xca\x77\x2b\xce\xba\x46\x21\x28\xe6\xba\x62\x82\xd1\x72\x5f\x84\x10\xf3\x16\xee\xfb\xc9\xe5\x13\xd1\xb1\xcb\x58\xb7\x09\x8a\x0a\xd5\x28\x69\x76\x69\x89\xf9\x45\xa5\x33\x54\xf5\xdb\x65\xaa\x83\xcd\xaf\x3b\x65\x5b\xe3\x0b\x7e\xb1\xb5\x31\x22\x32\xdc\xb8\xa0\xa0\x0b\x39\x34\xea\xb3\x64\x92\xc2\xd9\x95\xe9\x19\x38\x99\x84\x89\x56\x38\x87\x94\x47\x78\x70\xc2\xf4\x35\xe6\x54\x85\xf9\x5e\x85\xcf\x5f\xd5\xde\xc4\x24\x75\x0f\x32\x1d\x8d\x10\x84\x2b\x8a\xd3\xf9\x0f\x24\x75\xbe\x27\x20\xef\x3d\x08\x99\xa1\xd3\x18\x4f\xfa\xa2\x39\xc8\xed\x20\xc0\xab\x7b\x58\x01\x2a\x41\x09\x67\xf8\x38\xbc\xc3\x15\x15\x7f\xf6\xfa\x68\xd6\x33\xce\x5e\xcb\xcf\x7a\x8f\x73\x7b\xf6\xa0\x1b\x36\xeb\x6d\x6c\x51\x90\x5d\xd2\x1f\xd7\x13\xf1\xf6\xba\xcc\xb5\x11\x83\x55\xef\xc7\x20\xc5\x28\x1b\xde\xfc\x51\x5b\xfc\xda\x82\xe8\x0a\xad\x96\x01\x33\x49\xdf\x69\x4b\x56\x39\x86\x4b\xd7\xd3\xe9\x56\xac\xef\xaa\xc3\x7f\xfe\x50\x4e\x8e\x00\x00\x8e\xf8\xfe\x49\x75\x10\x03\x00\x60\x0d\x71\x45\x80\x5d\x11\xdf\x65\x47\xb3\xd1\x03\xc8\x94\x38\x85\xff\x6c\x05\x85\xa8\x05\x44\x98\x6f\x8a\x8f\x91\xcf\xc3\x3c\xdc\x0a\x9b\xd2\x22\x4d\xd6\xe2\x09\x0f\x5d\x54\x62\xd4\x83\x02\xd5\x87\xa9\xcd\x79\x97\xcd\x6a\xf7\xf3\xc0\x5d\xb0\x64\x9f\x3e\x0d\xca\xdd\x96\xa6\xd3\x4d\x2f\xcf\x02\xd8\xe5\x5c\xbc\x08\x44\xa7\x20\x2f\xb8\xe2\x0c\x2a\x7c\x20\xdb\x6e\x9d\xa0\xfb\xa2\xec\xec\x6e\x3d\x4b\x48\xe1\xa5\xe5\x79\x47\xf3\x3d\x3d\x86\x24\xae\xab\x6e\xe9\x54\xe5\x21\x46\x50\xd2\x56\xd9\x0f\x4b\x3c\x25\x85\x8a\x91\x29\xad\xc3\x8d\x91\x14\x94\xb8\x73\x07\x9b\xca\x3d\x24\x03\xa8\x61\x69\xfd\x8a\x73\x55\xce\x24\xf5\x76\xc6\x96\x92\x12\x75\x31\xc8\x6b\x78\x72\x2e\x8e\x13\xe3\x06\xf7\x4b\x7f\x67\x31\x32\xe9\x2c\xf3\x11\x82\xad\xcd\xcd\xf8\x14\xa9\xe2\x3c\x6c\x94\xd2\xfe\xf8\x42\xc7\xad\x99\x04\x46\xfc\x64\xde\xd8\xe3\x28\xb4\x02\x87\x80\x93\x24\x94\xc7\xa6\x1c\x6f\x5b\x44\x3f\xda\x18\x7a\x63\x57\xa1\x2f\x90\xa0\x87\x39\x71\xf5\x03\x4f\x6a\xcd\x07\xde\xd6\x64\x19\x4a\xa3\x07\x34\x6e\x99\x54\x34\xab\x12\xc2\xde\x2e\xe4\x6c\x6b\x79\x37\x7d\x11\x07\x48\x37\xd9\x47\x53\x16\x4d\x8d\x92\x63\x4d\x41\x8c\x02\x1f\x6f\x12\x60\xe2\xba\xf1\xaf\xa5\x77\x8e\x8e\x86\x90\x17\xf5\x58\x80\xfc\x62\x1e\x6b\x92\xb9\x40\x85\xe1\x1c\x29\x4f\x13\x3b\xd2\x95\xe8\xc5\xfd\xde\xe7\xb2\x26\x32\x5c\xb5\x7d\xc4\x27\x55\xfd\x81\xa3\x6c\x87\xca\xf7\xf5\xd9\xed\xe2\x51\x00\xea\x87\xdb\x2c\xff\x4e\x03\xdf\xc4\x14\xd7\x30\xd1\xf1\x86\x69\xe4\xfd\x81\x83\x42\x46\xd7\x4b\x79\x55\x5a\xfb\x9c\x38\x90\xa8\xc0\x1e\xe7\x8d\x6e\xe6\x67\xab\x44\x85\x71\x31\x32\xc4\x01\xaf\x70\x89\x37\x1d\xfb\x14\xcd\x09\x37\x99\x12\x0b\x28\x1e\x36\x7d\xf1\x30\x42\xf2\x6b\x11\x6f\x9e\x08\xa7\x08\x4a\xe9\xbf\x6d\xa7\x96\x32\xcc\xe6\x5c\x18\x5d\x7e\xff\x38\x51\xdb\xe0\x08\x86\xf4\x39\x00\x8b\xc8\x35\xb2\x9c\x76\xc3\x2d\x92\x2f\x65\x65\xea\x63\x46\xdc\xf9\x39\x5e\xc5\x7e\x60\xea\x2a\xf4\x0f\x4c\x24\x81\xe1\x52\x91\xac\x69\x53\x3f\x50\x3b\x7d\x95\x1e\xf5\xce\xda\xe1\xba\x73\x40\x24\xe3\xbb\x07\x42\x54\x9a\xec\x0e\xd9\x5a\x90\x2a\xf6\xa2\x70\xde\xfb\xef\x9d\xf1\x12\xe9\xae\xaa\xc6\xb0\x85\xb5\xab\x47\x6a\x0f\x76\xa3\xe5\xb8\x68\x0b\xae\x6c\xe6\xbf\x09\x9c\xfb\xc8\xeb\x72\xab\xc5\x7a\x44\x34\xc1\xe3\x36\x42\xcb\x2c\x5b\x38\x7a\x86\x83\x13\xfd\xda\x2d\x92\xfa\x4d\xbf\x49\x1b\xa6\xfa\xe3\x62\xdd\x10\x5e\x5e\xc5\xcb\x18\x4e\x8b\x8e\xba\x93\x81\xd9\x2c\xbb\xb6\x7e\xf3\x7a\x48\xe0\x49\xcb\x83\x1e\x97\x82\x6d\x5a\x82\x87\x8c\x49\xac\x6c\x42\x7a\xeb\xa7\x8d\xd8\x9f\xeb\x0c\x2b\x62\xf7\x52\x2e\x75\xdf\x89\xb6\xcd\xe0\xc1\x69\x4e\x98\xe3\x21\x6e\x92\xe0\xee\x94\x4b\x2a\xfe\xd0\x93\x06\xec\x92\x3b\x7e\x5b\xfd\xa8\x36\x3c\xfa\x39\xa8\xbd\xae\x7e\x49\xde\xfd\xe3\x15\xdc\x56\x6a\xd4\x2b\x59\x38\x04\xdb\x71\xb5\x72\x8b\x19\x8a\x83\xc5\x0d\xeb\x1f\xa2\xc0\x1d\x15\x32\xc9\xf7\xbd\xfd\xb2\xd9\xdd\x4c\x81\xcc\x8a\xb9\x0b\x23\xd6\xd0\x10\x36\xb9\xca\x75\x51\xba\x20\xc5\xa3\xb5\x6b\x2c\x21\x8b\x36\x22\xe2\x78\x88\x79\xd2\xde\x4a\x29\xd5\x53\x1a\xe1\x4b\xe5\x7a\x3d\x03\x72\xdf\x3f\x28\x91\xea\xf3\x1a\x13\x69\xad\x69\x75\x9c\x06\xe9\x41\xb1\x03\xa3\x91\x74\x39\x0b\x66\x48\xa4\xbc\x35\x62\xd0\x50\xa9\xea\x2d\xa5\x1d\xe5\xb5\xa6\x47\xc0\xf5\xa5\x06\xc8\x35\x05\x2a\xb6\x37\xe9\x54\xb7\xa9\x5a\xc5\x9c\x5d\xfb\x72\x02\xdb\x74\x0a\x50\xca\xde\x9b\x4e\xac\xc1\x3b\x58\xf5\xf4\x32\xb0\x67\xd5\x47\x5b\xb4\xfe\x6a\xf3\x8c\xb8\xb2\x31\x84\x95\x1d\x47\x44\xc7\x9f\x2e\x15\xe5\x68\xdc\xad\x68\xce\xaf\x7d\x99\xcb\x0b\x7c\xa6\xd3\x64\x78\x35\x03\xf3\x45\xce\x9b\xf2\xf6\x0e\x84\xb8\xda\xf7\xdd\x3b\x03\x60\x8f\x7e\x6f\xcc\x52\xcd\xe8\xd4\xb9\xab\x76\xb9\xa5\x9c\xfe\x32\x86\x31\xbc\x96\xe9\xed\x5a\x9c\xce\xce\x19\xdf\xc3\x37\x7e\xeb\xd4\x02\x90\xfb\x3b\x81\x87\x9e\x28\xaf\xd3\x57\xb8\x5a\x75\x66\xc3\x20\xaf\x13\x7f\xb7\x36\x36\x86\x4c\xbc\xd8\x2f\xea\xec\x6a\xf5\x82\xfe\x63\x47\x87\x9f\x4a\x7b\xee\x72\xcc\x15\xbd\xdc\xf5\x1b\xae\x36\x26\x34\xc0\xed\xe8\x96\x4f\x66\x30\xab\x64\xb7\xd8\x26\x25\x36\x5f\xde\xf6\xa9\x9c\x97\x7a\x33\x4b\xc7\x9b\x52\x5e\x71\xbb\x63\xe7\x96\xf5\x94\xc7\x95\x19\xdb\xae\xaf\x47\x8f\x6e\xb6\x8e\xcc\xde\xba\x33\x75\x7c\xaf\xfa\xe1\x98\xde\x13\x89\x63\x95\xb2\x8f\x18\xa1\x95\xfe\x83\x54\x2e\x8c\x0b\x61\x32\xd3\x58\xc1\x47\x81\xf2\xe2\x50\x24\x86\x83\x5b\x66\xbb\xc5\x2f\xf7\x0d\xd7\x41\x01\xe1\xcb\xb7\x67\x92\x64\x6b\xee\x65\x6d\x3b\x76\xd5\xce\xea\xd8\xb0\xf3\x83\xf5\xa8\x2b\x52\xa5\x3c\x88\x47\x50\x4c\xc9\x65\x76\xc1\xc1\x9d\xef\xe9\x85\xf5\x55\x89\xc9\x2d\x1c\x00\x78\x4a\xf9\x77\x3b\x00\xcb\xd5\xbd\x0e\x74\x05\x00\xbe\x3f\xdf\x6f\xa0\xfa\xf6\xee\x2e\x56\xae\x96\x0e\xce\x70\x01\xc4\x9f\x1f\xf9\xa1\xae\x76\x51\xba\xda\x6a\xa4\x44\x0c\xdf\xef\x3d\xa4\xea\xf7\x94\x1f\x02\x00\x50\x0b\x00\x58\x00\x01\x36\x00\x00\xed\xe7\xdd\xab\xdf\xb1\xba\x9a\xfa\x2a\x62\x62\x62\xd2\xd2\xd2\x0a\x0a\x0a\x9a\x9a\x9a\x7a\x7a\x7a\x20\x10\xc8\xca\xca\xca\xd9\xd9\x19\x06\x83\xf9\xf9\xf9\x85\x84\x84\x44\x47\x47\x27\x25\x25\x65\x66\x66\xe6\xe7\xe7\x97\x96\x96\x56\x57\x57\xbf\x79\xf3\xa6\xbd\xbd\xbd\xb7\xb7\xf7\xe3\xc7\x8f\x53\x53\x53\x5f\xbf\x7e\x5d\x5d\x5d\xdd\xdb\xdb\xbb\xb8\xb8\x00\x00\xe0\xf2\xf2\xd2\xc5\x2b\xf1\x0c\x00\xb0\xda\xd4\x95\x15\xf4\xbd\x26\x31\xa3\xd3\xc8\xe7\x82\x14\x78\x1f\x6c\xa2\xb0\x42\x02\xee\x71\x69\x9c\x5d\x08\x80\x12\xef\xa7\xd4\x5d\x6b\x7e\x35\xb1\x16\x83\x53\xf7\x65\xc9\x5e\x34\x63\x0b\xdd\x62\x6d\x5a\xfc\xf2\xaf\xb8\x2c\xe6\x88\x5d\xf5\xce\xec\x4e\x5a\x30\xf1\x0f\x10\x58\xf5\xc2\xd8\x19\x40\x9c\xea\xdf\x71\x3c\x2b\xaf\xe3\x5b\x3a\xd5\x47\xa5\xed\x26\xa2\x28\xd7\x6b\xcc\x98\x87\x17\x32\xa9\xaa\x3c\x8e\xc5\x3d\x5e\x4f\x81\xa2\xbd\x5b\x48\xc7\xd3\x04\xc0\x6b\x64\x67\x6a\xe7\x91\x96\x72\x45\x8d\x7e\xb7\xe7\x5e\x28\x9a\x1b\xaf\x2f\xa0\xc7\xd7\x25\xcc\xef\xaf\xa0\xa1\x98\x5e\x77\xe6\x31\x78\x49\xf6\x90\x35\xea\xe8\xc8\xa2\xb5\x7c\xab\x77\xe1\xf4\x70\xbd\xbb\xfe\x13\x5b\xf6\xf3\x01\x35\xad\xf3\xad\x51\x77\xf3\x8b\x0c\x76\xf7\x80\x4c\x9d\x66\x96\xb6\x34\xbd\x5e\x87\x32\x8e\x72\xb1\x94\x7c\x98\x13\xb7\x4b\x03\x33\x5b\xc3\xc8\x9c\x64\xa6\x4e\xf3\xa0\x03\x7d\x3b\x05\x5a\xe6\x70\x21\x2e\xb1\xe9\x58\x5c\xa0\x6c\xfa\x84\xf1\x84\x9b\xcc\x14\x69\x2c\x91\x7d\xe2\xbd\x7f\x52\xf8\xc2\x91\x60\x41\x22\x75\xea\xa6\x8e\x9a\x66\xb8\x3f\xe3\x89\x24\xd9\xdc\xca\x3e\xa1\xef\xb4\x19\xe1\xc4\xe4\xb9\x06\x81\xff\xa6\x79\xd5\x30\x59\x2e\xea\x7f\x6f\x5c\xcc\x8e\xdc\xc5\xd2\x92\xca\xbb\xbd\x05\x51\x79\xf9\xbd\x98\xa9\xab\x68\x2b\x97\x2b\x5a\x04\xff\xbe\xdc\xd1\x00\x00\xa0\xa5\xa2\xaf\xc0\xa7\xae\xad\x2a\xf0\xff\xcc\x0a\x2f\x17\xe7\xb2\x24\x05\xd7\x0e\x56\x0a\x95\x23\xc9\xb0\xae\x3a\xe1\x9a\xc7\x06\x5c\xda\xb4\x09\x3e\xc1\xfb\xc0\x08\x33\x23\x3b\x1d\x05\xc9\x03\xd8\xa9\x15\xaf\xb6\x89\x41\x82\x15\xb7\xb4\xc5\xbb\xad\xcb\xe3\xb4\x81\xcd\x0d\x67\xf7\xe0\xb4\x98\xee\xf0\x27\xdb\x9e\x8f\xb0\xea\x5a\x44\x26\xee\x6c\x9c\x6e\x1b\x04\x54\x4d\x7b\xd4\xba\x11\xc5\x86\x2b\xc3\x40\xfa\x8b\xd2\x9a\xf5\x2e\x1d\x30\xe5\x55\x03\x61\xf5\x3c\x58\x3c\x7b\x2d\x1a\xff\xed\x60\xfd\x27\xac\xc4\xb3\x29\x01\xb5\xc2\x29\x57\x5b\x8b\x81\x72\xfb\xf5\x39\x19\x37\x6b\x01\xce\xf8\x43\x7a\x69\x95\xc2\x53\x1e\x9c\x65\xe0\x23\x09\x07\xb8\x70\x7c\xb2\x86\xd5\x2e\xf4\x81\x43\x53\xc9\xe6\x4d\x7b\xeb\x5c\x7e\xcc\x0e\xec\x4e\x4e\x75\xad\xbb\x2e\xc2\x39\xcd\x6b\x65\x9d\xf9\x0a\x42\x75\x14\x41\xf8\x41\x90\x26\x58\x0c\x63\x7f\x2f\xb3\x72\xc1\x1a\xcf\x4f\x4e\xaa\xc8\x2a\x24\x8a\x83\x99\x31\x7e\x1e\x1c\x89\x9f\x81\xad\x4b\xb2\xa8\xf2\x01\x47\x79\x11\x34\xdc\xd7\xe6\x1b\x37\xb9\x2f\x89\x64\x2b\xa8\xf5\x12\x7e\x99\xec\x56\xd9\x5e\x3d\x87\xc9\xac\x14\x2c\x59\xa7\x8f\xcb\xf2\x57\xfc\x03\xd1\xcc\xa7\x83\xe4\xa7\x35\xcb\x36\x4e\xcf\x08\xbf\x5c\x18\xfd\xdc\x21\xd4\xc4\xa2\xff\xee\x96\xc2\x70\x22\xfe\x06\x4b\x24\x5d\x91\x0f\x85\x52\x73\xb1\x73\x67\x6b\x51\xcb\x39\xee\xf7\x04\x70\x7e\x23\xf8\x39\x3f\x16\x00\x70\xe0\x7c\x4f\x00\x58\x57\xa8\x81\xdf\x77\x4f\x7f\x8e\x3f\x7b\xa9\x7f\xa2\xfe\xdc\xa8\x1f\xdb\x65\x3f\xa3\xb4\x7e\x68\x9a\xfe\x1d\xf5\xa3\x2f\x49\xf2\x13\xca\xfc\xca\xcf\x8d\xb5\xbf\xcf\xf2\xef\x26\xdb\x5f\x71\x9b\xf0\x7f\x72\x31\x7f\xcf\xc4\xf0\x13\x93\xed\x2f\x98\x7e\x74\x35\xff\x2d\xcf\x8b\x5f\xf0\xfc\xe8\x72\xfe\x9e\x87\xe5\x27\x9e\xb1\x5f\xf0\xfc\x7f\xae\xe7\xef\xc9\xe8\x7e\x22\xa3\x22\xfa\x27\x17\xf4\xdf\xb2\xa8\xff\x82\xe5\x2f\x57\xf4\xdf\x6e\x59\xc0\x2f\x58\x7e\x76\x49\xff\xed\x7c\x1a\x7e\xc1\xf4\x97\x6b\xfa\x6f\x37\x6c\xeb\x37\x2c\x7f\xba\xa8\x3f\x1f\xe2\x1f\x2d\xc2\x9f\x0f\x31\x27\xf1\xcf\xae\xea\xdf\x91\x3f\xca\x7c\xa2\x9f\x90\xb6\x24\x3f\x1a\x8a\x7f\xc7\xfd\x58\xbf\x89\x7f\xc2\x7d\xa6\xfe\xe9\x4a\xf0\xf7\xbf\xfc\xf7\xf2\xfe\x57\x48\xd3\xff\xbe\xd8\xff\x7d\xf8\x1f\xb3\x07\xcd\x4f\x2c\x7d\xd7\x7f\x93\xa2\x75\x35\x70\xf1\xbe\xff\xe0\x2a\x70\x15\x00\xe3\x00\x80\x0c\xd3\xf7\x6f\xff\x27\x00\x00\xff\xff\xae\xb0\x15\x1b\xb7\x21\x00\x00")
+
+func testAssetsTest_templateOdtBytes() ([]byte, error) {
+ return bindataRead(
+ _testAssetsTest_templateOdt,
+ "test/assets/test_template.odt",
+ )
+}
+
+func testAssetsTest_templateOdt() (*asset, error) {
+ bytes, err := testAssetsTest_templateOdtBytes()
+ if err != nil {
+ return nil, err
+ }
+
+ info := bindataFileInfo{name: "test/assets/test_template.odt", size: 8631, mode: os.FileMode(420), modTime: time.Unix(1568821589, 0)}
+ a := &asset{bytes: bytes, info: info}
+ return a, nil
+}
+
+// Asset loads and returns the asset for the given name.
+// It returns an error if the asset could not be found or
+// could not be loaded.
+func Asset(name string) ([]byte, error) {
+ cannonicalName := strings.Replace(name, "\\", "/", -1)
+ if f, ok := _bindata[cannonicalName]; ok {
+ a, err := f()
+ if err != nil {
+ return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
+ }
+ return a.bytes, nil
+ }
+ return nil, fmt.Errorf("Asset %s not found", name)
+}
+
+// MustAsset is like Asset but panics when Asset would return an error.
+// It simplifies safe initialization of global variables.
+func MustAsset(name string) []byte {
+ a, err := Asset(name)
+ if err != nil {
+ panic("asset: Asset(" + name + "): " + err.Error())
+ }
+
+ return a
+}
+
+// AssetInfo loads and returns the asset info for the given name.
+// It returns an error if the asset could not be found or
+// could not be loaded.
+func AssetInfo(name string) (os.FileInfo, error) {
+ cannonicalName := strings.Replace(name, "\\", "/", -1)
+ if f, ok := _bindata[cannonicalName]; ok {
+ a, err := f()
+ if err != nil {
+ return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
+ }
+ return a.info, nil
+ }
+ return nil, fmt.Errorf("AssetInfo %s not found", name)
+}
+
+// AssetNames returns the names of the assets.
+func AssetNames() []string {
+ names := make([]string, 0, len(_bindata))
+ for name := range _bindata {
+ names = append(names, name)
+ }
+ return names
+}
+
+// _bindata is a table, holding each asset generator, mapped to its name.
+var _bindata = map[string]func() (*asset, error){
+ "test/assets/test_expected.pdf": testAssetsTest_expectedPdf,
+ "test/assets/test_template.odt": testAssetsTest_templateOdt,
+}
+
+// AssetDir returns the file names below a certain
+// directory embedded in the file by go-bindata.
+// For example if you run go-bindata on data/... and data contains the
+// following hierarchy:
+// data/
+// foo.txt
+// img/
+// a.png
+// b.png
+// then AssetDir("data") would return []string{"foo.txt", "img"}
+// AssetDir("data/img") would return []string{"a.png", "b.png"}
+// AssetDir("foo.txt") and AssetDir("notexist") would return an error
+// AssetDir("") will return []string{"data"}.
+func AssetDir(name string) ([]string, error) {
+ node := _bintree
+ if len(name) != 0 {
+ cannonicalName := strings.Replace(name, "\\", "/", -1)
+ pathList := strings.Split(cannonicalName, "/")
+ for _, p := range pathList {
+ node = node.Children[p]
+ if node == nil {
+ return nil, fmt.Errorf("Asset %s not found", name)
+ }
+ }
+ }
+ if node.Func != nil {
+ return nil, fmt.Errorf("Asset %s not found", name)
+ }
+ rv := make([]string, 0, len(node.Children))
+ for childName := range node.Children {
+ rv = append(rv, childName)
+ }
+ return rv, nil
+}
+
+type bintree struct {
+ Func func() (*asset, error)
+ Children map[string]*bintree
+}
+var _bintree = &bintree{nil, map[string]*bintree{
+ "test": &bintree{nil, map[string]*bintree{
+ "assets": &bintree{nil, map[string]*bintree{
+ "test_expected.pdf": &bintree{testAssetsTest_expectedPdf, map[string]*bintree{}},
+ "test_template.odt": &bintree{testAssetsTest_templateOdt, map[string]*bintree{}},
+ }},
+ }},
+}}
+
+// RestoreAsset restores an asset under the given directory
+func RestoreAsset(dir, name string) error {
+ data, err := Asset(name)
+ if err != nil {
+ return err
+ }
+ info, err := AssetInfo(name)
+ if err != nil {
+ return err
+ }
+ err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
+ if err != nil {
+ return err
+ }
+ err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
+ if err != nil {
+ return err
+ }
+ err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// RestoreAssets restores an asset under the given directory recursively
+func RestoreAssets(dir, name string) error {
+ children, err := AssetDir(name)
+ // File
+ if err != nil {
+ return RestoreAsset(dir, name)
+ }
+ // Dir
+ for _, child := range children {
+ err = RestoreAssets(dir, filepath.Join(name, child))
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func _filePath(dir, name string) string {
+ cannonicalName := strings.Replace(name, "\\", "/", -1)
+ return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
+}
+
diff --git a/test/common_test.go b/test/common_test.go
new file mode 100644
index 000000000..4eb9a9ecd
--- /dev/null
+++ b/test/common_test.go
@@ -0,0 +1,25 @@
+package test
+
+import (
+ "encoding/json"
+ "regexp"
+)
+
+func toMap(i interface{}) map[string]interface{} {
+ var r map[string]interface{}
+ j, _ := json.Marshal(i)
+ json.Unmarshal(j, &r)
+ return r
+}
+
+func removeUpdatedField(i map[string]interface{}) map[string]interface{} {
+ delete(i, "updated")
+ return i
+}
+
+// Removes the variable data in the PDF like for example the creation date or the checksum
+var cleanPDFRegexp = regexp.MustCompile(`(?s)<<\/Creator.+?>>|<<\/Size.+?>>`)
+
+func cleanPDF(src []byte) []byte {
+ return cleanPDFRegexp.ReplaceAll(src, []byte{})
+}
diff --git a/test/form_test.go b/test/form_test.go
new file mode 100644
index 000000000..b2f698d85
--- /dev/null
+++ b/test/form_test.go
@@ -0,0 +1,115 @@
+package test
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+)
+
+type form struct {
+ permissions
+ ID string `json:"id" storm:"id"`
+ Name string `json:"name" storm:"index"`
+ Detail string `json:"detail"`
+ Updated time.Time `json:"updated" storm:"index"`
+ Created time.Time `json:"created" storm:"index"`
+ Data map[string]interface{} `json:"data"`
+}
+
+func TestForm(t *testing.T) {
+ s := new(t, serverURL)
+ u := registerTestUser(s)
+ login(s, u)
+ f1 := createSimpleForm(s, u, "form1-"+s.id, "test-"+s.id)
+ f2 := createForm(s, u, "form2-"+s.id)
+
+ deleteForm(s, f1.ID, false)
+ deleteForm(s, f2.ID, true)
+}
+
+func createForm(s *session, u *user, name string) *form {
+ now := time.Now()
+ f := &form{
+ permissions: permissions{Owner: u.uuid},
+ Name: name,
+ Created: now,
+ Updated: now,
+ }
+
+ s.e.POST("/api/admin/form/update").WithJSON(f).Expect().Status(http.StatusOK)
+
+ l := s.e.GET("/api/admin/form/list").Expect().Status(http.StatusOK).JSON()
+
+ l.Path("$..name").Array().Contains(f.Name)
+
+ for _, e := range l.Array().Iter() {
+ if e.Object().Value("name").String().Raw() == f.Name {
+ f.ID = e.Object().Value("id").String().Raw()
+ break
+ }
+ }
+
+ return f
+}
+
+func createSimpleForm(s *session, u *user, name, fieldName string) *form {
+ f := createForm(s, u, name)
+ f.Data = simpleFormData(fieldName)
+ return updateForm(s, f)
+}
+
+func simpleFormData(fieldName string) map[string]interface{} {
+ j := fmt.Sprintf(`{
+ "formSrc": {
+ "components": {
+ "5zvr98w21yynozx60nhmc": {
+ "_compId": "HC2",
+ "_order": 0,
+ "autocomplete": "on",
+ "help": "test-help",
+ "label": "test-label",
+ "name": "%s",
+ "placeholder": "test-placeholder",
+ "validate": {
+ "required": true
+ }
+ }
+ },
+ "v": 2
+ }
+ }`, fieldName)
+
+ var result map[string]interface{}
+
+ err := json.Unmarshal([]byte(j), &result)
+ if err != nil {
+ return nil
+ }
+
+ return result
+}
+
+func updateForm(s *session, f *form) *form {
+ s.e.POST("/api/admin/form/update").WithQuery("id", f.ID).WithJSON(f).Expect().Status(http.StatusOK)
+
+ expected := removeUpdatedField(toMap(f))
+ s.e.GET("/api/admin/form/{id}").WithPath("id", f.ID).Expect().Status(http.StatusOK).
+ JSON().Object().ContainsMap(expected)
+
+ return f
+}
+
+func deleteForm(s *session, id string, expectEmptyList bool) {
+
+ s.e.GET(fmt.Sprintf("/api/admin/form/%s/delete", id)).Expect().Status(http.StatusOK)
+ l := s.e.GET("/api/admin/form/list").Expect()
+
+ if expectEmptyList {
+ l.Status(http.StatusNotFound)
+ } else {
+ l.Status(http.StatusOK).
+ JSON().Path("$..name").Array().NotContains(id)
+ }
+}
diff --git a/test/permissions_test.go b/test/permissions_test.go
new file mode 100644
index 000000000..9dfaa3fc2
--- /dev/null
+++ b/test/permissions_test.go
@@ -0,0 +1,32 @@
+package test
+
+type role int
+
+type permission []byte
+
+type groupAndOthers struct {
+ //Allowed to modify: @Owner only!
+ Group role `json:"group,omitempty"`
+ //Rights pattern: group others
+ // -- --
+ //default value is: ----
+ //example for group and others with read perm: r-r-
+ //Allowed to modify: @Owner only!
+ Rights permission `json:"rights,omitempty"`
+}
+
+type permissions struct {
+ //Allowed to modify: @Owner only!
+ Owner string `json:"owner,omitempty"`
+ //Grant can be modified by the owner only and is an optional field to whitelist user', s directly
+ //Allowed to modify: everyone with write rights!
+ Grant map[string]permission `json:"grant,omitempty"`
+
+ GroupAndOthers groupAndOthers `json:"groupAndOthers,omitempty"`
+ //Accessible by everyone who has the ID
+ //Allowed to modify: everyone with write rights!
+ PublicByID permission `json:"publicByID,omitempty"`
+
+ //Execute only! If read or write not set.
+ Published bool `json:"published"`
+}
diff --git a/test/template_test.go b/test/template_test.go
new file mode 100644
index 000000000..30070b734
--- /dev/null
+++ b/test/template_test.go
@@ -0,0 +1,106 @@
+package test
+
+import (
+ "fmt"
+ "net/http"
+ "path/filepath"
+ "strconv"
+ "testing"
+ "time"
+)
+
+type template struct {
+ permissions
+ ID string `json:"id" storm:"id"`
+ Name string `json:"name" storm:"index"`
+ Detail string `json:"detail"`
+ Updated time.Time `json:"updated" storm:"index"`
+ Created time.Time `json:"created" storm:"index"`
+ Data map[string]interface{} `json:"data"`
+}
+
+func TestTemplate(t *testing.T) {
+ s := new(t, serverURL)
+ u := registerTestUser(s)
+ login(s, u)
+ t1 := createSimpleTemplate(s, u, "template1-"+s.id, "test/assets/test_template.odt")
+ t2 := createTemplate(s, u, "template2-"+s.id)
+
+ deleteTemplate(s, t1.ID, false)
+ deleteTemplate(s, t2.ID, true)
+}
+
+func createSimpleTemplate(s *session, u *user, name, path string) *template {
+
+ fileContent, err := Asset(path)
+ if err != nil {
+ s.t.Errorf("Cannot upload asset %s", err)
+ }
+
+ t := createTemplate(s, u, name)
+ uploadTemplateFile(s, t, "en", fileContent, "application/vnd.oasis.opendocument.text", filepath.Base(name))
+
+ return t
+}
+
+func createTemplate(s *session, u *user, name string) *template {
+ now := time.Now()
+
+ t := &template{
+ permissions: permissions{Owner: u.uuid},
+ Name: name,
+ Created: now,
+ Updated: now,
+ }
+
+ s.e.POST("/api/admin/template/update").WithJSON(t).Expect().Status(http.StatusOK)
+
+ l := s.e.GET("/api/admin/template/list").Expect().Status(http.StatusOK).JSON()
+
+ l.Path("$..name").Array().Contains(t.Name)
+
+ for _, e := range l.Array().Iter() {
+ if e.Object().Value("name").String().Raw() == t.Name {
+ t.ID = e.Object().Value("id").String().Raw()
+ break
+ }
+ }
+
+ return t
+}
+
+func updateTemplate(s *session, t *template) *template {
+ s.e.POST("/api/admin/template/update").WithQuery("id", t.ID).WithJSON(t).Expect().Status(http.StatusOK)
+
+ expected := removeUpdatedField(toMap(t))
+ s.e.GET("/api/admin/template/{id}").WithPath("id", t.ID).Expect().Status(http.StatusOK).
+ JSON().Object().ContainsMap(expected)
+
+ return t
+}
+
+func uploadTemplateFile(s *session, t *template, lang string, b []byte, contentType string, fileName string) {
+ s.e.POST("/api/admin/template/upload/{id}/{lang}").WithPath("id", t.ID).WithPath("lang", lang).WithBytes(b).
+ WithHeader("Content-Type", contentType).
+ WithHeader("File-Name", fileName).
+ WithHeader("Content-Length", strconv.Itoa(len(b))).
+ Expect().Status(http.StatusOK)
+
+ r := s.e.GET("/api/admin/template/{id}").WithPath("id", t.ID).
+ Expect().Status(http.StatusOK).JSON()
+ r.Path("$.data.en.name").Equal(fileName)
+ r.Path("$.data.en.contentType").Equal(contentType)
+
+}
+
+func deleteTemplate(s *session, id string, expectEmptyList bool) {
+ s.e.GET(fmt.Sprintf("/api/admin/template/%s/delete", id)).Expect().Status(http.StatusOK)
+ l := s.e.GET("/api/admin/template/list").Expect()
+
+ if expectEmptyList {
+ l.Status(http.StatusNotFound)
+ } else {
+ l.Status(http.StatusOK).
+ JSON().Path("$..name").Array().NotContains(id)
+ }
+}
diff --git a/test/unattended_workflow_test.go b/test/unattended_workflow_test.go
new file mode 100644
index 000000000..cd8739ac3
--- /dev/null
+++ b/test/unattended_workflow_test.go
@@ -0,0 +1,82 @@
+package test
+
+import (
+ "bytes"
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ w "git.proxeus.com/core/central/sys/workflow"
+)
+
+func TestUnattendedWorkflow(t *testing.T) {
+ s := new(t, serverURL)
+ u := registerTestUser(s)
+
+ login(s, u)
+ apiKey, summary := createApiKey(s, u, "test-"+s.id)
+ w := createWorkflow(s, u, "workflow-"+s.id)
+ f := createSimpleForm(s, u, "form-"+s.id, "test_name")
+ tpl := createSimpleTemplate(s, u, "template-"+s.id, "test/assets/test_template.odt")
+ w.Data = simpleWorkflowData(s.id, f.ID, tpl.ID)
+ updateWorkflow(s, w)
+ logout(s)
+
+ token := getSessionToken(s, u.username, apiKey)
+ id := listFirstDocument(s, token)
+ schema := getDocumentSchema(s, token, id)
+
+ data := map[string]interface{}{}
+ i := 0
+ for k, _ := range schema {
+ data[k] = fmt.Sprintf("value-%d", i)
+ i++
+ }
+
+ r := executeAllAtOnce(s, token, id, data)
+
+ expected, err := Asset("test/assets/test_expected.pdf")
+ if err != nil {
+ s.t.Errorf("Cannot upload asset %s", err)
+ }
+
+ if bytes.Compare(cleanPDF(r), cleanPDF(expected)) != 0 {
+ t.Errorf("Wrong pdf result")
+ }
+
+ login(s, u)
+ deleteWorkflow(s, w.ID, true)
+ deleteApiKey(s, u, summary)
+ deleteUser(s, u)
+}
+
+type workflowItem struct {
+ permissions
+ ID string `json:"id" storm:"id"`
+ Name string `json:"name" storm:"index"`
+ Detail string `json:"detail"`
+ Updated time.Time `json:"updated" storm:"index"`
+ Created time.Time `json:"created" storm:"index"`
+ Price uint64 `json:"price" storm:"index"`
+
+ Data *w.Workflow `json:"data"`
+ OwnerEthAddress string `json:"ownerEthAddress"` //only used in frontend
+ Deactivated bool `json:"deactivated"`
+}
+
+func listFirstDocument(s *session, token string) string {
+ return s.e.GET("/api/document/list").WithHeader("Authorization", "Bearer "+token).Expect().Status(http.StatusOK).JSON().Array().First().Object().Value("id").String().Raw()
+}
+
+func getDocumentSchema(s *session, token, id string) map[string]interface{} {
+ schema := s.e.GET("/api/document/{id}/allAtOnce/schema").WithPath("id", id).WithHeader("Authorization", "Bearer "+token).Expect().Status(http.StatusOK).JSON().Object().Path("$.workflow.data").Object()
+ schema.ContainsKey("test_name")
+ return schema.Raw()
+}
+
+func executeAllAtOnce(s *session, token, id string, data map[string]interface{}) []byte {
+ r := s.e.POST("/api/document/{id}/allAtOnce").WithPath("id", id).WithHeader("Authorization", "Bearer "+token).WithJSON(data).Expect().ContentType("application/pdf").Body().Raw()
+
+ return []byte(r)
+}
diff --git a/test/user_test.go b/test/user_test.go
new file mode 100644
index 000000000..a8acec538
--- /dev/null
+++ b/test/user_test.go
@@ -0,0 +1,114 @@
+package test
+
+import (
+ "fmt"
+ "net/http"
+ "os"
+ "testing"
+
+ "git.proxeus.com/core/central/main/handlers/api"
+ uuid "github.com/satori/go.uuid"
+ "gopkg.in/gavv/httpexpect.v2"
+)
+
+var serverURL string
+
+type user struct {
+ uuid string
+ username string
+ password string
+}
+
+type session struct {
+ id string
+ t *testing.T
+ e *httpexpect.Expect
+}
+
+func init() {
+ serverURL = os.Getenv("PROXEUS_URL")
+}
+
+func new(t *testing.T, serverURL string) *session {
+ return &session{
+ id: uuid.NewV4().String(),
+ t: t,
+ e: httpexpect.New(t, serverURL),
+ }
+}
+
+func TestUser(t *testing.T) {
+ s := new(t, serverURL)
+ u := registerTestUser(s)
+ login(s, u)
+ logout(s)
+ login(s, u)
+ deleteUser(s, u)
+}
+
+func registerTestUser(s *session) *user {
+ // Register test user
+ u := &user{
+ username: fmt.Sprintf("test%s@example.com", s.id),
+ password: s.id,
+ }
+
+ s.t.Logf("Starting test %s", s.id)
+ s.t.Logf("User %s %s", u.username, u.password)
+
+ tr := &api.TokenRequest{
+ Email: u.username,
+ }
+
+ r := s.e.POST("/api/register").WithJSON(tr).Expect()
+
+ r.Status(http.StatusOK)
+ r.Header("X-Test-Token").NotEmpty() // This is only true in TESTMODE
+ registrationToken := r.Header("X-Test-Token").Raw()
+
+ p := &struct {
+ Password string `json:"password"`
+ }{
+ Password: u.password,
+ }
+
+ r = s.e.POST("/api/register/" + registrationToken).WithJSON(p).
+ Expect().
+ Status(http.StatusOK)
+
+ return u
+}
+
+func login(s *session, u *user) {
+ l := &struct {
+ Email string `json:"email" form:"email"`
+ Password string `json:"password" form:"password"`
+ }{
+ Email: u.username,
+ Password: u.password,
+ }
+ s.e.POST("/api/login").WithJSON(l).Expect().Status(http.StatusOK)
+
+ me := s.e.GET("/api/me").Expect().Status(http.StatusOK).JSON().Object()
+ me.ValueEqual("email", u.username)
+
+ u.uuid = me.Value("id").String().Raw()
+}
+
+func logout(s *session) {
+ s.e.POST("/api/logout").Expect().Status(http.StatusOK)
+ s.e.GET("/api/me").Expect().Status(http.StatusNotFound)
+}
+
+func deleteUser(s *session, u *user) {
+ s.e.POST("/api/user/delete").Expect().Status(http.StatusOK)
+
+ l := &struct {
+ Email string `json:"email" form:"email"`
+ Password string `json:"password" form:"password"`
+ }{
+ Email: u.username,
+ Password: u.password,
+ }
+ s.e.POST("/api/login").WithJSON(l).Expect().Status(http.StatusBadRequest)
+}
diff --git a/test/workflow_test.go b/test/workflow_test.go
new file mode 100644
index 000000000..18f802366
--- /dev/null
+++ b/test/workflow_test.go
@@ -0,0 +1,131 @@
+package test
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+)
+
+type workflow struct {
+ permissions
+ ID string `json:"id" storm:"id"`
+ Name string `json:"name" storm:"index"`
+ Detail string `json:"detail"`
+ Updated time.Time `json:"updated" storm:"index"`
+ Created time.Time `json:"created" storm:"index"`
+ Data map[string]interface{} `json:"data"`
+}
+
+func TestWorkflow(t *testing.T) {
+ s := new(t, serverURL)
+ u := registerTestUser(s)
+ login(s, u)
+ w1 := createWorkflow(s, u, "workflow1-"+s.id)
+ w2 := createWorkflow(s, u, "workflow2-"+s.id)
+
+ f := createForm(s, u, "form-"+s.id)
+ tpl := createTemplate(s, u, "template-"+s.id)
+ w1.Data = simpleWorkflowData(s.id, f.ID, tpl.ID)
+ updateWorkflow(s, w1)
+
+ deleteWorkflow(s, w1.ID, false)
+ deleteWorkflow(s, w2.ID, true)
+}
+
+func createWorkflow(s *session, u *user, name string) *workflow {
+ now := time.Now()
+ f := &workflow{
+ permissions: permissions{Owner: u.uuid},
+ Name: name,
+ Created: now,
+ Updated: now,
+ }
+
+ s.e.POST("/api/admin/workflow/update").WithJSON(f).Expect().Status(http.StatusOK)
+
+ l := s.e.GET("/api/admin/workflow/list").Expect().Status(http.StatusOK).JSON()
+
+ l.Path("$..name").Array().Contains(f.Name)
+
+ for _, e := range l.Array().Iter() {
+ if e.Object().Value("name").String().Raw() == f.Name {
+ f.ID = e.Object().Value("id").String().Raw()
+ break
+ }
+ }
+
+ return f
+}
+
+func simpleWorkflowData(id string, formId, templateId string) map[string]interface{} {
+ j := fmt.Sprintf(`{
+ "flow": {
+ "start": {
+ "node": "%s",
+ "p": {
+ "x": -438,
+ "y": -100
+ }
+ },
+ "nodes": {
+ "%s": {
+ "id": "%s",
+ "name": "test",
+ "type": "form",
+ "conns": [
+ {
+ "id": "%s"
+ }
+ ],
+ "p": {
+ "x": -225,
+ "y": -102
+ }
+ },
+ "%s": {
+ "id": "%s",
+ "name": "test",
+ "type": "template",
+ "p": {
+ "x": -18,
+ "y": -131
+ }
+ }
+ }
+ }
+ }`, formId, formId, formId, templateId, templateId, templateId)
+
+ var result map[string]interface{}
+
+ err := json.Unmarshal([]byte(j), &result)
+ if err != nil {
+ return nil
+ }
+
+ return result
+}
+
+func updateWorkflow(s *session, f *workflow) *workflow {
+ s.e.POST("/api/admin/workflow/update").WithQuery("id", f.ID).WithJSON(f).Expect().Status(http.StatusOK)
+
+ expected := removeUpdatedField(toMap(f))
+ s.e.GET("/api/admin/workflow/{id}").WithPath("id", f.ID).Expect().Status(http.StatusOK).
+ JSON().Object().ContainsMap(expected)
+
+ return f
+}
+
+func deleteWorkflow(s *session, id string, expectEmptyList bool) {
+
+ s.e.GET(fmt.Sprintf("/api/admin/workflow/%s/delete", id)).Expect().Status(http.StatusOK)
+ l := s.e.GET("/api/admin/workflow/list").Expect()
+
+ if expectEmptyList {
+ l.Status(http.StatusNotFound)
+ } else {
+ l.Status(http.StatusOK).
+ JSON().Path("$..name").Array().NotContains(id)
+ }
+}
diff --git a/ui/core/public/static/proxeus_blue.jpg b/ui/core/public/static/proxeus_blue.jpg
new file mode 100644
index 000000000..8b4ae9a66
Binary files /dev/null and b/ui/core/public/static/proxeus_blue.jpg differ
diff --git a/ui/core/public/static/proxeus_white.jpg b/ui/core/public/static/proxeus_white.jpg
new file mode 100644
index 000000000..ed5d123c3
Binary files /dev/null and b/ui/core/public/static/proxeus_white.jpg differ
diff --git a/ui/core/src/FrontendApp.vue b/ui/core/src/FrontendApp.vue
index c75206197..171d96ac2 100644
--- a/ui/core/src/FrontendApp.vue
+++ b/ui/core/src/FrontendApp.vue
@@ -11,7 +11,7 @@
+
+ {{$t('Default workflow ids explanation','Comma separated ids of workflows you want your new users to inherit (if any)')}}
+
{{$t('Blockchain settings')}}
@@ -38,6 +42,12 @@
{{$t('Infura API Key explanation','API Key to access Infura node.')}}{{$t('Blockchain contract address explanation','Set the ethereum contract address which will be used to register files and verify them.')}}
+
+ {{$t('Airdrop Enable Explanation','Enables/Disables the XES & Ether airdrop feature for new users on ropsten. The Amount and Wallet to be used is configured in the platform configuration.')}}
+
+ {{$t('Airdrop Amount XES Explanation','Set the amount of XES to be airdropped to newly registered users.')}}
+
+ {{$t('Airdrop Amount Ether Explanation','Set the amount of Ether to be airdropped to newly registered users.')}}