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 @@
-
- - -
-
- - - - - - - - - - - - - - - - - - - - - -
IDOwnerConsignment IDTimestampSignatoryAction
{{item.id}}{{item.owner}}{{item.consignmentID}}{{item.timestamp}}{{item.signatory}} - -
-
-
-
- - - - - diff --git a/ui/core/src/views/Template.vue b/ui/core/src/views/Template.vue index 2f186a5e4..72bda0bdd 100644 --- a/ui/core/src/views/Template.vue +++ b/ui/core/src/views/Template.vue @@ -498,7 +498,6 @@ export default { clearInterval(this.interval) if (file && file.name) { - console.log('updateFileName' + file.name) libreHub.updateFileName(file.name) } this.interval = setInterval(() => { diff --git a/ui/core/src/views/Workflow.vue b/ui/core/src/views/Workflow.vue index b9cc50ddb..d2ed24f74 100644 --- a/ui/core/src/views/Workflow.vue +++ b/ui/core/src/views/Workflow.vue @@ -427,8 +427,6 @@ export default { } } } - console.log('elements') - console.log(elements) if (elements.length) { this.showPublishResponseDialog(null, elements) } @@ -1188,9 +1186,8 @@ function condition(){ '
' }, nodeIconMap: { - ibmsender: 'fcn-ibmsender node-icon mdi mdi-send', - mailsender: 'fcn-ibmsender node-icon mdi mdi-send', - priceretriever: 'fcn-ibmsender node-icon mdi mdi-send', + mailsender: 'fcn-externalnode node-icon mdi mdi-send', + priceretriever: 'fcn-externalnode node-icon mdi mdi-send', condition: 'fcn-condition node-icon mdi mdi-circle-outline', user: 'fcn-usr node-icon mdi mdi-account', form: 'fcn-form node-icon mdi mdi-view-quilt', @@ -1372,59 +1369,6 @@ function condition(){ 'dblclick': _.onDblClick } }, - ibmsender: { - connections: { - from: [ - { - node: { - color: { - background: '#8688ff', - highlight: { background: '#5f5ff0' }, - hover: { background: '#5f5ff0' } - }, - borderWidthSelected: 3 - }, - edge: { - color: { - color: '#8688ff', - highlight: '#a8a5ff', - hover: '#a8a5ff' - } - } - }], - to: Infinity, - space: 1.1 - }, - font: { - color: '#343434', - size: 15, - mod: 'bold', - bold: { - color: '#343434', - size: 14, // px - face: 'arial', - vadjust: 0, - mod: 'bold' - } - }, - icon: { - face: 'Material Design Icons', - code: '\uf48a', - color: '#5353c0' - }, - events: { - 'hoverIn': function () { - }, - 'hoverOut': function () { - }, - 'remove': function () { - }, - 'click': function () { - }, - 'dblclick': function () { - } - } - }, mailsender: { connections: { from: [ diff --git a/ui/core/src/views/appDependentComponents/SettingsInner.vue b/ui/core/src/views/appDependentComponents/SettingsInner.vue index 69cd02b08..973a7cf3f 100644 --- a/ui/core/src/views/appDependentComponents/SettingsInner.vue +++ b/ui/core/src/views/appDependentComponents/SettingsInner.vue @@ -30,6 +30,10 @@ {{$t('Document Service URL explanation','Set the Document Service URL which will be used to render documents.')}} {{$t('Platform Domain explanation','Set the Domain this Platform instance is identifying as (used for example for sending links to this instance)')}} +
+ + {{$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.')}}
{{$t('Email settings')}} @@ -226,7 +236,17 @@ export default { configured: false, initialized: false, importResultsAvailable: false, - results: null + results: null, + airdropoptions: [ + { + label: 'Airdrop enabled on Ropsten', + value: 'true' + }, + { + label: 'Airdrop disabled', + value: 'false' + } + ] } } } diff --git a/ui/core/src/views/appDependentComponents/Sidebar.vue b/ui/core/src/views/appDependentComponents/Sidebar.vue index 911eec431..f4fc6f284 100644 --- a/ui/core/src/views/appDependentComponents/Sidebar.vue +++ b/ui/core/src/views/appDependentComponents/Sidebar.vue @@ -3,20 +3,11 @@ @@ -96,8 +80,7 @@ export default { props: ['user', 'toggled'], data () { return { - documents: [], - sigcount: 0 + documents: [] } }, watch: { @@ -111,6 +94,9 @@ export default { }, userCanAccessBackend () { return this.app.userIsCreatorOrHigher() + }, + signatureRequestCount () { + return this.$store.getters.signatureRequestCount } }, created () { @@ -131,21 +117,22 @@ export default { getSigningRequests () { axios.get('/api/user/document/signingRequests').then(async response => { if (response.data) { - this.sigcount = 0 + let sigCount = 0 this.documents = response.data for (let i = 0, len = this.documents.length; i < len; i++) { - this.getSigners(this.documents[i].hash).then(response => { - if (!response.includes(this.me.etherPK)) { - if (!this.documents[i].rejected) { - this.sigcount++ - } + let response2 = await this.getSigners(this.documents[i].hash) + console.log(response2) + if (!response2.includes(this.me.etherPK)) { + if (!this.documents[i].rejected) { + sigCount++ } - }) + } } + console.log(sigCount) + this.$store.dispatch('UPDATE_SIGNERS_COUNT', { sigCount: sigCount }) } }, (err) => { console.log(err) - this.sigcount = 0 }) } } diff --git a/ui/core/src/views/appDependentComponents/i18n/Translation.vue b/ui/core/src/views/appDependentComponents/i18n/Translation.vue index 4bcd9fdad..74ce4018c 100644 --- a/ui/core/src/views/appDependentComponents/i18n/Translation.vue +++ b/ui/core/src/views/appDependentComponents/i18n/Translation.vue @@ -53,7 +53,6 @@ export default { if (this.translations[this.lk]) { t[this.lk] = this.translations[this.lk] if (this.lk.length <= 4) { - console.log('updateLangLabel....4') this.app.updateLangLabel() this.$root.$emit('translations-updated') } diff --git a/ui/core/src/views/appDependentComponents/permDialog/UserSelector.vue b/ui/core/src/views/appDependentComponents/permDialog/UserSelector.vue index e6a062de1..56c5e08cb 100644 --- a/ui/core/src/views/appDependentComponents/permDialog/UserSelector.vue +++ b/ui/core/src/views/appDependentComponents/permDialog/UserSelector.vue @@ -78,7 +78,6 @@ export default { 'vueTagsInputTarget': 'vueTagsInputSearch' }, mounted () { - console.log(this.$refs.main) if (this.$refs.main) { let inputs = this.$refs.main.getElementsByClassName('ti-new-tag-input') if (inputs && inputs.length === 1) { @@ -125,7 +124,6 @@ export default { return false }, updateVueTagsInput (newTags) { - console.log('updateVueTagsInput') this.addresses = [] this.toBeGranted = newTags this.vueTagsInputTarget = '' diff --git a/ui/wallet/src/ProxeusEthereum/MetamaskWallet.js b/ui/wallet/src/ProxeusEthereum/MetamaskWallet.js index 66289e4af..87ddd7337 100644 --- a/ui/wallet/src/ProxeusEthereum/MetamaskWallet.js +++ b/ui/wallet/src/ProxeusEthereum/MetamaskWallet.js @@ -13,9 +13,10 @@ class MetamaskWallet { return this.web3.eth.personal.sign(message, address) } - async transferXES (to, amount) { + // optional callback parameter + async transferXES (to, amount, callback) { return this.xesTokenContract.methods.transfer(to, amount) - .send({ from: this.getCurrentAddress() }) + .send({ from: this.getCurrentAddress() }, callback) } getCurrentAddress () { diff --git a/ui/wallet/src/ProxeusEthereum/ProxeusWallet.js b/ui/wallet/src/ProxeusEthereum/ProxeusWallet.js index c58004fb9..1f5a4de57 100644 --- a/ui/wallet/src/ProxeusEthereum/ProxeusWallet.js +++ b/ui/wallet/src/ProxeusEthereum/ProxeusWallet.js @@ -16,9 +16,9 @@ class ProxeusWallet { this.web3.eth.accounts.wallet[this.web3.eth.defaultAccount].privateKey).signature } - async transferXES (to, amount) { + async transferXES (to, amount, callback) { return this.xesTokenContract.methods.transfer(to, amount) - .send({ from: this.web3.eth.defaultAccount }) + .send({ from: this.web3.eth.defaultAccount }, callback) } getCurrentAddress () { diff --git a/ui/wallet/src/ProxeusEthereum/WalletInterface.js b/ui/wallet/src/ProxeusEthereum/WalletInterface.js index 956838c4b..b37914620 100644 --- a/ui/wallet/src/ProxeusEthereum/WalletInterface.js +++ b/ui/wallet/src/ProxeusEthereum/WalletInterface.js @@ -435,8 +435,9 @@ class WalletInterface { return this.formatBalance(decimalsToKeep, await this.web3.eth.getBalance(this.getCurrentAddress())) } - transferXES (to, amount) { - return this.wallet.transferXES(to, amount) + // optional callback parameter + transferXES (to, amount, callback) { + return this.wallet.transferXES(to, amount, callback) } async getXESBalance (decimalsToKeep, address) {
- - logo - - - - - + +
- - logo - - - - - + +