├── .github └── workflows │ ├── api-prod.yaml │ ├── api-staging.yaml │ ├── client-prod.yaml │ └── client-staging.yaml ├── .gitignore ├── README.md ├── api ├── .gitignore ├── CHANGELOG.md ├── DEPLOYMENT.md ├── LICENSE ├── README.md ├── now.json ├── package.json ├── src │ ├── api │ │ └── handler.ts │ ├── data │ │ └── config.template.json │ ├── index.ts │ ├── initServices.ts │ ├── models │ │ ├── api │ │ │ ├── IDataResponse.ts │ │ │ ├── IIPFSRetrieveRequest.ts │ │ │ ├── IIPFSRetrieveResponse.ts │ │ │ ├── IIPFSStoreRequest.ts │ │ │ ├── IIPFSStoreResponse.ts │ │ │ └── IResponse.ts │ │ ├── app │ │ │ ├── IHttpRequest.ts │ │ │ ├── IHttpResponse.ts │ │ │ └── IRoute.ts │ │ ├── configuration │ │ │ ├── IAWSDynamoDbConfiguration.ts │ │ │ ├── IConfiguration.ts │ │ │ ├── IIPFSConfiguration.ts │ │ │ └── INodeConfiguration.ts │ │ ├── db │ │ │ └── IState.ts │ │ └── tangle │ │ │ └── IPayload.ts │ ├── routes.ts │ ├── routes │ │ ├── init.ts │ │ ├── ipfsRetrieve.ts │ │ └── ipfsStore.ts │ ├── services │ │ ├── amazonDynamoDbService.ts │ │ ├── messageCacheService.ts │ │ └── stateService.ts │ └── utils │ │ ├── amazonDynamoDbHelper.ts │ │ ├── apiHelper.ts │ │ ├── iotaC2Helper.ts │ │ ├── textHelper.ts │ │ └── validationHelper.ts ├── tsconfig.json ├── tslint.json ├── types │ └── ipfs-http-client │ │ └── index.d.ts └── yarn.lock └── client ├── .env ├── .gitignore ├── CHANGELOG.md ├── DEPLOYMENT.md ├── LICENSE ├── README.md ├── config-overrides.js ├── package.json ├── public ├── assets │ └── iota-icons.ttf ├── data │ └── config.template.json ├── favicon.ico ├── favicon │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── mstile-150x150.png │ └── site.webmanifest └── index.html ├── src ├── app │ ├── App.tsx │ ├── AppState.ts │ └── routes │ │ ├── RetrieveFile.tsx │ │ ├── RetrieveFileParams.tsx │ │ ├── RetrieveFileState.ts │ │ ├── UploadFile.tsx │ │ └── UploadFileState.ts ├── assets │ └── logo.svg ├── factories │ └── serviceFactory.ts ├── index.scss ├── index.tsx ├── models │ ├── api │ │ ├── IIPFSRetrieveRequest.ts │ │ ├── IIPFSRetrieveResponse.ts │ │ ├── IIPFSStoreRequest.ts │ │ ├── IIPFSStoreResponse.ts │ │ └── IResponse.ts │ └── config │ │ ├── IConfiguration.ts │ │ └── ITangleExplorerConfiguration.ts ├── react-app-env.d.ts └── services │ ├── apiClient.ts │ ├── configurationService.ts │ ├── ipfsService.ts │ └── tangleExplorerService.ts ├── tsconfig.json └── yarn.lock /.github/workflows/api-prod.yaml: -------------------------------------------------------------------------------- 1 | name: API Build and Deploy Production 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | paths: 7 | - "api/**" 8 | - ".github/workflows/api-prod.yaml" 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | env: 15 | # Build specific env vars 16 | CONFIG_ID: prod 17 | CONFIG_NODE1_PROVIDER: ${{ secrets.PROD_CONFIG_NODE1_PROVIDER }} 18 | CONFIG_IPFS_NODE_PROVIDER: ${{ secrets.PROD_CONFIG_IPFS_NODE_PROVIDER }} 19 | CONFIG_IPFS_NODE_TOKEN: ${{ secrets.PROD_CONFIG_IPFS_NODE_TOKEN }} 20 | CONFIG_DB_AWS_REGION: ${{ secrets.PROD_CONFIG_DB_AWS_REGION }} 21 | CONFIG_DB_AWS_ACCESS_KEY_ID: ${{ secrets.PROD_CONFIG_DB_AWS_ACCESS_KEY_ID }} 22 | CONFIG_DB_AWS_SECRET_ACCESS_KEY: ${{ secrets.PROD_CONFIG_DB_AWS_SECRET_ACCESS_KEY }} 23 | CONFIG_DB_TABLE_PREFIX: ${{ secrets.PROD_CONFIG_DB_TABLE_PREFIX }} 24 | CONFIG_ALLOWED_DOMAIN: ${{ secrets.PROD_CONFIG_ALLOWED_DOMAIN }} 25 | CONFIG_MAX_BYTES: ${{ secrets.PROD_CONFIG_MAX_BYTES }} 26 | VERCEL_DOMAIN: ${{ secrets.VERCEL_API_PROD_DOMAIN }} 27 | 28 | # Global Vercel env vars 29 | VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} 30 | VERCEL_PROJECT_ID: ${{ secrets.VERCEL_API_PROJECT_ID }} 31 | VERCEL_PROJECT_NAME: ${{ secrets.VERCEL_API_PROJECT_NAME }} 32 | 33 | steps: 34 | - uses: actions/checkout@v2 35 | - name: API Config 36 | run: | 37 | cd api/src/data/ 38 | mv config.template.json config.$CONFIG_ID.json 39 | sed -i 's/NODE1-PROVIDER/${{ env.CONFIG_NODE1_PROVIDER }}/g' config.$CONFIG_ID.json 40 | sed -i 's/IPFS-NODE-PROVIDER/${{ env.CONFIG_IPFS_NODE_PROVIDER }}/g' config.$CONFIG_ID.json 41 | sed -i 's/IPFS-NODE-TOKEN/${{ env.CONFIG_IPFS_NODE_TOKEN }}/g' config.$CONFIG_ID.json 42 | sed -i 's/DB-AWS-REGION/${{ env.CONFIG_DB_AWS_REGION }}/g' config.$CONFIG_ID.json 43 | sed -i 's/DB-AWS-ACCESS-KEY-ID/${{ env.CONFIG_DB_AWS_ACCESS_KEY_ID }}/g' config.$CONFIG_ID.json 44 | sed -i 's/DB-AWS-SECRET-ACCESS-KEY/${{ env.CONFIG_DB_AWS_SECRET_ACCESS_KEY }}/g' config.$CONFIG_ID.json 45 | sed -i 's/DB-TABLE-PREFIX/${{ env.CONFIG_DB_TABLE_PREFIX }}/g' config.$CONFIG_ID.json 46 | sed -i 's/ALLOWED-DOMAIN/${{ env.CONFIG_ALLOWED_DOMAIN }}/g' config.$CONFIG_ID.json 47 | sed -i 's/MAX-BYTES/${{ env.CONFIG_MAX_BYTES }}/g' config.$CONFIG_ID.json 48 | - name: Use Node.js 12.x 49 | uses: actions/setup-node@v1 50 | with: 51 | node-version: "12" 52 | - name: API Deploy 53 | run: | 54 | cd api 55 | npx vercel --token ${{ secrets.VERCEL_TOKEN }} --scope ${{ secrets.VERCEL_SCOPE }} --force -m githubCommitSha=${{ github.sha }} --env CONFIG_ID=$CONFIG_ID 56 | VERCEL_DEPLOYMENT_URL=$(npx vercel ls --token ${{ secrets.VERCEL_TOKEN }} --scope ${{ secrets.VERCEL_SCOPE }} -m githubCommitSha=${{ github.sha }} | grep $VERCEL_PROJECT_NAME | awk {'print $2'}) 57 | npx vercel alias --token ${{ secrets.VERCEL_TOKEN }} --scope ${{ secrets.VERCEL_SCOPE }} $VERCEL_DEPLOYMENT_URL $VERCEL_DOMAIN 58 | -------------------------------------------------------------------------------- /.github/workflows/api-staging.yaml: -------------------------------------------------------------------------------- 1 | name: API Build and Deploy Staging 2 | 3 | on: 4 | push: 5 | branches: [develop] 6 | paths: 7 | - "api/**" 8 | - ".github/workflows/api-staging.yaml" 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | env: 15 | # Build specific env vars 16 | CONFIG_ID: staging 17 | CONFIG_NODE1_PROVIDER: ${{ secrets.STAGING_CONFIG_NODE1_PROVIDER }} 18 | CONFIG_IPFS_NODE_PROVIDER: ${{ secrets.STAGING_CONFIG_IPFS_NODE_PROVIDER }} 19 | CONFIG_IPFS_NODE_TOKEN: ${{ secrets.STAGING_CONFIG_IPFS_NODE_TOKEN }} 20 | CONFIG_DB_AWS_REGION: ${{ secrets.STAGING_CONFIG_DB_AWS_REGION }} 21 | CONFIG_DB_AWS_ACCESS_KEY_ID: ${{ secrets.STAGING_CONFIG_DB_AWS_ACCESS_KEY_ID }} 22 | CONFIG_DB_AWS_SECRET_ACCESS_KEY: ${{ secrets.STAGING_CONFIG_DB_AWS_SECRET_ACCESS_KEY }} 23 | CONFIG_DB_TABLE_PREFIX: ${{ secrets.STAGING_CONFIG_DB_TABLE_PREFIX }} 24 | CONFIG_ALLOWED_DOMAIN: ${{ secrets.STAGING_CONFIG_ALLOWED_DOMAIN }} 25 | CONFIG_MAX_BYTES: ${{ secrets.STAGING_CONFIG_MAX_BYTES }} 26 | VERCEL_DOMAIN: ${{ secrets.VERCEL_API_STAGING_DOMAIN }} 27 | 28 | # Global Vercel env vars 29 | VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} 30 | VERCEL_PROJECT_ID: ${{ secrets.VERCEL_API_PROJECT_ID }} 31 | VERCEL_PROJECT_NAME: ${{ secrets.VERCEL_API_PROJECT_NAME }} 32 | 33 | steps: 34 | - uses: actions/checkout@v2 35 | - name: API Config 36 | run: | 37 | cd api/src/data/ 38 | mv config.template.json config.$CONFIG_ID.json 39 | sed -i 's/NODE1-PROVIDER/${{ env.CONFIG_NODE1_PROVIDER }}/g' config.$CONFIG_ID.json 40 | sed -i 's/IPFS-NODE-PROVIDER/${{ env.CONFIG_IPFS_NODE_PROVIDER }}/g' config.$CONFIG_ID.json 41 | sed -i 's/IPFS-NODE-TOKEN/${{ env.CONFIG_IPFS_NODE_TOKEN }}/g' config.$CONFIG_ID.json 42 | sed -i 's/DB-AWS-REGION/${{ env.CONFIG_DB_AWS_REGION }}/g' config.$CONFIG_ID.json 43 | sed -i 's/DB-AWS-ACCESS-KEY-ID/${{ env.CONFIG_DB_AWS_ACCESS_KEY_ID }}/g' config.$CONFIG_ID.json 44 | sed -i 's/DB-AWS-SECRET-ACCESS-KEY/${{ env.CONFIG_DB_AWS_SECRET_ACCESS_KEY }}/g' config.$CONFIG_ID.json 45 | sed -i 's/DB-TABLE-PREFIX/${{ env.CONFIG_DB_TABLE_PREFIX }}/g' config.$CONFIG_ID.json 46 | sed -i 's/ALLOWED-DOMAIN/${{ env.CONFIG_ALLOWED_DOMAIN }}/g' config.$CONFIG_ID.json 47 | sed -i 's/MAX-BYTES/${{ env.CONFIG_MAX_BYTES }}/g' config.$CONFIG_ID.json 48 | - name: Use Node.js 12.x 49 | uses: actions/setup-node@v1 50 | with: 51 | node-version: "12" 52 | - name: API Deploy 53 | run: | 54 | cd api 55 | npx vercel --token ${{ secrets.VERCEL_TOKEN }} --scope ${{ secrets.VERCEL_SCOPE }} --force -m githubCommitSha=${{ github.sha }} --env CONFIG_ID=$CONFIG_ID 56 | VERCEL_DEPLOYMENT_URL=$(npx vercel ls --token ${{ secrets.VERCEL_TOKEN }} --scope ${{ secrets.VERCEL_SCOPE }} -m githubCommitSha=${{ github.sha }} | grep $VERCEL_PROJECT_NAME | awk {'print $2'}) 57 | npx vercel alias --token ${{ secrets.VERCEL_TOKEN }} --scope ${{ secrets.VERCEL_SCOPE }} $VERCEL_DEPLOYMENT_URL $VERCEL_DOMAIN 58 | -------------------------------------------------------------------------------- /.github/workflows/client-prod.yaml: -------------------------------------------------------------------------------- 1 | name: Client Build and Deploy Production 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | paths: 7 | - "client/**" 8 | - ".github/workflows/client-prod.yaml" 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | env: 14 | # Build specific env vars 15 | CONFIG_ID: prod 16 | CONFIG_API_ENDPOINT: ${{ secrets.PROD_CONFIG_API_ENDPOINT }} 17 | CONFIG_GOOGLE_ANALYTICS_ID: ${{ secrets.PROD_CONFIG_GOOGLE_ANALYTICS_ID }} 18 | TANGLE_EXPLORER_MESSAGES: ${{ secrets.PROD_TANGLE_EXPLORER_MESSAGES }} 19 | CONFIG_MAX_BYTES: ${{ secrets.PROD_CONFIG_MAX_BYTES }} 20 | VERCEL_DOMAIN: ${{ secrets.VERCEL_CLIENT_PROD_DOMAIN }} 21 | 22 | # Global Vercel env vars 23 | VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} 24 | VERCEL_PROJECT_ID: ${{ secrets.VERCEL_CLIENT_PROJECT_ID }} 25 | VERCEL_PROJECT_NAME: ${{ secrets.VERCEL_CLIENT_PROJECT_NAME }} 26 | 27 | steps: 28 | - uses: actions/checkout@v2 29 | - name: Client Config 30 | run: | 31 | cd client/public/data/ 32 | mv config.template.json config.$CONFIG_ID.json 33 | sed -i 's/API-ENDPOINT/${{ env.CONFIG_API_ENDPOINT }}/g' config.$CONFIG_ID.json 34 | sed -i 's/GOOGLE-ANALYTICS-ID/${{ env.CONFIG_GOOGLE_ANALYTICS_ID }}/g' config.$CONFIG_ID.json 35 | sed -i 's/TANGLE-EXPLORER-MESSAGES/${{ env.TANGLE_EXPLORER_MESSAGES }}/g' config.$CONFIG_ID.json 36 | sed -i 's/MAX-BYTES/${{ env.CONFIG_MAX_BYTES }}/g' config.$CONFIG_ID.json 37 | - name: Use Node.js 12.x 38 | uses: actions/setup-node@v1 39 | with: 40 | node-version: "12" 41 | - name: Client Deploy 42 | run: | 43 | cd client 44 | npx vercel --token ${{ secrets.VERCEL_TOKEN }} --scope ${{ secrets.VERCEL_SCOPE }} --force -m githubCommitSha=${{ github.sha }} --build-env REACT_APP_CONFIG_ID=$CONFIG_ID 45 | VERCEL_DEPLOYMENT_URL=$(npx vercel ls --token ${{ secrets.VERCEL_TOKEN }} --scope ${{ secrets.VERCEL_SCOPE }} -m githubCommitSha=${{ github.sha }} | grep $VERCEL_PROJECT_NAME | awk {'print $2'}) 46 | npx vercel alias --token ${{ secrets.VERCEL_TOKEN }} --scope ${{ secrets.VERCEL_SCOPE }} $VERCEL_DEPLOYMENT_URL $VERCEL_DOMAIN 47 | -------------------------------------------------------------------------------- /.github/workflows/client-staging.yaml: -------------------------------------------------------------------------------- 1 | name: Client Build and Deploy Staging 2 | 3 | on: 4 | push: 5 | branches: [develop] 6 | paths: 7 | - "client/**" 8 | - ".github/workflows/client-staging.yaml" 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | env: 15 | # Build specific env vars 16 | CONFIG_ID: staging 17 | CONFIG_API_ENDPOINT: ${{ secrets.STAGING_CONFIG_API_ENDPOINT }} 18 | CONFIG_GOOGLE_ANALYTICS_ID: ${{ secrets.STAGING_CONFIG_GOOGLE_ANALYTICS_ID }} 19 | TANGLE_EXPLORER_MESSAGES: ${{ secrets.STAGING_TANGLE_EXPLORER_MESSAGES }} 20 | CONFIG_MAX_BYTES: ${{ secrets.STAGING_CONFIG_MAX_BYTES }} 21 | VERCEL_DOMAIN: ${{ secrets.VERCEL_CLIENT_STAGING_DOMAIN }} 22 | 23 | # Global Vercel env vars 24 | VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} 25 | VERCEL_PROJECT_ID: ${{ secrets.VERCEL_CLIENT_PROJECT_ID }} 26 | VERCEL_PROJECT_NAME: ${{ secrets.VERCEL_CLIENT_PROJECT_NAME }} 27 | 28 | steps: 29 | - uses: actions/checkout@v2 30 | - name: Client Config 31 | run: | 32 | cd client/public/data/ 33 | mv config.template.json config.$CONFIG_ID.json 34 | sed -i 's/API-ENDPOINT/${{ env.CONFIG_API_ENDPOINT }}/g' config.$CONFIG_ID.json 35 | sed -i 's/GOOGLE-ANALYTICS-ID/${{ env.CONFIG_GOOGLE_ANALYTICS_ID }}/g' config.$CONFIG_ID.json 36 | sed -i 's/TANGLE-EXPLORER-MESSAGES/${{ env.TANGLE_EXPLORER_MESSAGES }}/g' config.$CONFIG_ID.json 37 | sed -i 's/MAX-BYTES/${{ env.CONFIG_MAX_BYTES }}/g' config.$CONFIG_ID.json 38 | - name: Use Node.js 12.x 39 | uses: actions/setup-node@v1 40 | with: 41 | node-version: "12" 42 | - name: Client Deploy 43 | run: | 44 | cd client 45 | npx vercel --token ${{ secrets.VERCEL_TOKEN }} --scope ${{ secrets.VERCEL_SCOPE }} --force -m githubCommitSha=${{ github.sha }} --build-env REACT_APP_CONFIG_ID=$CONFIG_ID 46 | VERCEL_DEPLOYMENT_URL=$(npx vercel ls --token ${{ secrets.VERCEL_TOKEN }} --scope ${{ secrets.VERCEL_SCOPE }} -m githubCommitSha=${{ github.sha }} | grep $VERCEL_PROJECT_NAME | awk {'print $2'}) 47 | npx vercel alias --token ${{ secrets.VERCEL_TOKEN }} --scope ${{ secrets.VERCEL_SCOPE }} $VERCEL_DEPLOYMENT_URL $VERCEL_DOMAIN 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .history 2 | node_modules 3 | 4 | /api/src/data/config.dev.json 5 | /api/src/data/config.local.json 6 | /api/src/data/config.staging.json 7 | /api/src/data/config.prod.json 8 | /api/dist 9 | 10 | /client/public/data/config.dev.json 11 | /client/public/data/config.local.json 12 | /client/public/data/config.staging.json 13 | /client/public/data/config.prod.json 14 | /client/build 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IOTA IPFS 2 | 3 | This package is an implementation of the Tangle and Data Storage Blueprint [https://docs.iota.org/docs/blueprints/0.1/tangle-data-storage/overview](https://docs.iota.org/docs/blueprints/0.1/tangle-data-storage/overview) 4 | 5 | The IOTA IPFS Demonstration is composed of 2 packages `api` and `client`. 6 | 7 | ## API 8 | 9 | The API facilitates all the services required by `client` web UI. It is a set of services running on NodeJS connecting to the Tangle and IPFS. 10 | 11 | See [./api/README.md](./api/README.md) for more details. 12 | 13 | ## Client 14 | 15 | The client package provides a React Web UI to provide facilities to upload and authenticate files using IOTA and IPFS. 16 | 17 | See [./client/README.md](./client/README.md) for more details. 18 | 19 | # Deployed 20 | 21 | A demonstration version of the application is currently deployed at the following urls 22 | 23 | * client - 24 | * api - 25 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | .vercel -------------------------------------------------------------------------------- /api/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.2.2 4 | 5 | * Fixed ipfs-http-client incompatability 6 | * Dependency Update 7 | 8 | ## v1.2.1 9 | 10 | * File size limit 0.5Mb 11 | 12 | ## v1.2.0 13 | 14 | * Added sha3 hash algorithm 15 | 16 | ## v1.1.1 17 | 18 | * Added transaction/bundle caching layer 19 | * Added address indexing state 20 | * Increased storage size to 5Mb 21 | 22 | ## v1.0.0 23 | 24 | * Initial Release 25 | -------------------------------------------------------------------------------- /api/DEPLOYMENT.md: -------------------------------------------------------------------------------- 1 | # Deployment 2 | 3 | ## Configuration 4 | 5 | To configure the `api` you should copy `./src/data/config.template.json` to `./src/data/config.local.json` and modify the content. 6 | 7 | ```js 8 | { 9 | "node": { 10 | "provider": "https://MYNODE/", /* A node to perform Tangle operations */ 11 | }, 12 | "ipfs": { /* IPFS Node with storage support */ 13 | "provider": "MYIPFSNODE_PROVIDER", /* e.g. https://ipfs.mydomain.com:443/api/v0/ */ 14 | "token": "MYIPFSNODE_TOKEN" /* Optional token passed in Authorization header */ 15 | }, 16 | "dynamoDbConnection": { 17 | "region": "DB-AWS-REGION", /* AWS Region e.g. eu-central-1 */ 18 | "accessKeyId": "DB-AWS-ACCESS-KEY-ID", /* AWS Access Key e.g. AKIAI57SG4YC2ZUCSABC */ 19 | "secretAccessKey": "DB-AWS-SECRET-ACCESS-KEY", /* AWS Secret e.g. MUo72/UQWgL97QArGt9HVUA */ 20 | "dbTablePrefix": "DB-TABLE-PREFIX" /* Prefix for database table names e.g. certification-dev- */ 21 | }, 22 | "allowedDomains": [ /* A list of domains for the cors allow-origin */ 23 | "www.mydomain.com" /* Include http://localhost:3000 for local testing */ 24 | ] 25 | } 26 | ``` 27 | 28 | ## Build 29 | 30 | ```shell 31 | npm run build 32 | ``` 33 | 34 | ## Deploy 35 | 36 | The `api` package is setup to be deployed to zeit/now, you should modify the config in `./now.json` to suit your own requirements and then execute the following. 37 | 38 | If you want to use a different name for the config file you can specify an environment variable of CONFIG_ID, e.g. set CONFIG_ID to `dev` will load `config.dev.json` instead. 39 | 40 | ```shell 41 | now 42 | ``` -------------------------------------------------------------------------------- /api/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 IOTA Stiftung 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | # IOTA IPFS Api 2 | 3 | This package facilitates all the services required by the `client` web UIs. 4 | 5 | It is a set of services running on NodeJS connecting to the Tangle and IPFS. 6 | 7 | ## Pre-requisites 8 | 9 | ```shell 10 | npm install 11 | ``` 12 | 13 | ## Development 14 | 15 | This will run the api at 16 | 17 | ```shell 18 | npm run start-dev 19 | ``` 20 | 21 | ## Deployment 22 | 23 | See [./DEPLOYMENT.md](./DEPLOYMENT.md) for more details. 24 | -------------------------------------------------------------------------------- /api/now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "src/api/handler.ts", 6 | "use": "@now/node" 7 | } 8 | ], 9 | "routes": [ 10 | { 11 | "src": "(.*)", 12 | "dest": "/src/api/handler.ts" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iota-poc-ipfs-api", 3 | "description": "API for IOTA IPFS", 4 | "version": "1.2.2", 5 | "author": "Martyn Janes ", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/iotaledger/poc-ipfs.git/tree/master/api" 9 | }, 10 | "license": "MIT", 11 | "private": true, 12 | "scripts": { 13 | "build-clean": "rimraf ./dist/*", 14 | "build-compile": "tsc", 15 | "build-watch": "tsc --watch", 16 | "build-lint": "tslint -p ./tsconfig.json -c ./tslint.json -t verbose", 17 | "build-config": "copyfiles -u 2 ./src/data/config*.json ./dist/data/", 18 | "build": "npm-run-all build-clean build-compile build-lint build-config", 19 | "serve-mon": "nodemon ./dist/index", 20 | "start-dev": "run-p serve-mon build-watch", 21 | "start": "node ./dist/index" 22 | }, 23 | "dependencies": { 24 | "@iota/client": "0.5.1", 25 | "@iota/iota.js": "1.5.3", 26 | "aws-sdk": "2.784.0", 27 | "axios": "0.21.1", 28 | "body-parser": "1.19.0", 29 | "cors": "2.8.5", 30 | "express": "4.17.1", 31 | "ipfs-http-client": "46.0.1", 32 | "sha3": "2.1.3" 33 | }, 34 | "devDependencies": { 35 | "@types/cors": "2.8.8", 36 | "@types/express": "4.17.8", 37 | "@types/node": "14.14.6", 38 | "copyfiles": "2.4.0", 39 | "nodemon": "2.0.6", 40 | "npm-run-all": "4.1.5", 41 | "rimraf": "3.0.2", 42 | "tslint": "6.1.3", 43 | "tslint-eslint-rules": "5.4.0", 44 | "tslint-microsoft-contrib": "6.2.0", 45 | "typescript": "4.0.5" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /api/src/api/handler.ts: -------------------------------------------------------------------------------- 1 | import { IHttpRequest } from "../models/app/IHttpRequest"; 2 | import { IHttpResponse } from "../models/app/IHttpResponse"; 3 | import { IConfiguration } from "../models/configuration/IConfiguration"; 4 | import { routes } from "../routes"; 5 | import { cors, executeRoute, findRoute } from "../utils/apiHelper"; 6 | 7 | /** 8 | * Handle an incoming REST request. 9 | * @param req Request The request. 10 | * @param res Response The response. 11 | * @returns Nothing 12 | */ 13 | export default async function handler(req: IHttpRequest, res: IHttpResponse): Promise { 14 | try { 15 | // tslint:disable-next-line: non-literal-require 16 | const config: IConfiguration = require(`../data/config.${process.env.CONFIG_ID}.json`); 17 | 18 | cors( 19 | req, 20 | res, 21 | process.env.CORS_ALLOW_ORIGINS || config.allowedDomains, 22 | process.env.CORS_ALLOW_METHODS, 23 | process.env.CORS_ALLOW_HEADERS 24 | ); 25 | 26 | if (req.method === "OPTIONS") { 27 | res.status(200).send("OK"); 28 | } else if (!req.url || !req.method) { 29 | res.status(400).send(`Bad request ${JSON.stringify(req)}`); 30 | } else { 31 | const found = findRoute(routes, req.url.split("?")[0], req.method.toLowerCase()); 32 | 33 | if (!found) { 34 | res.status(404).send("Not found"); 35 | } else { 36 | await executeRoute(req, res, config, found?.route, found?.params); 37 | } 38 | } 39 | } catch (err) { 40 | console.error(err); 41 | res.status(400).send("Bad request"); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /api/src/data/config.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": { 3 | "provider": "NODE1-PROVIDER" 4 | }, 5 | "ipfs": { 6 | "provider": "IPFS-NODE-PROVIDER", 7 | "token": "IPFS-NODE-TOKEN" 8 | }, 9 | "dynamoDbConnection": { 10 | "region": "DB-AWS-REGION", 11 | "accessKeyId": "DB-AWS-ACCESS-KEY-ID", 12 | "secretAccessKey": "DB-AWS-SECRET-ACCESS-KEY", 13 | "dbTablePrefix": "DB-TABLE-PREFIX" 14 | }, 15 | "allowedDomains": [ 16 | "ALLOWED-DOMAIN" 17 | ], 18 | "maxBytes": MAX-BYTES 19 | } -------------------------------------------------------------------------------- /api/src/index.ts: -------------------------------------------------------------------------------- 1 | import bodyParser from "body-parser"; 2 | import express, { Application } from "express"; 3 | import { IConfiguration } from "./models/configuration/IConfiguration"; 4 | import { routes } from "./routes"; 5 | import { cors, executeRoute } from "./utils/apiHelper"; 6 | 7 | const configId = process.env.CONFIG_ID || "local"; 8 | // tslint:disable-next-line: no-var-requires non-literal-require 9 | const config: IConfiguration = require(`./data/config.${configId}.json`); 10 | 11 | const app: Application = express(); 12 | 13 | app.use(bodyParser.json({ limit: "10mb" })); 14 | app.use(bodyParser.urlencoded({ limit: "10mb", extended: true })); 15 | 16 | app.use((req, res, next) => { 17 | cors( 18 | req, 19 | res, 20 | config.allowedDomains ? config.allowedDomains.join(",") : undefined, 21 | "GET, POST, OPTIONS, PUT, PATCH, DELETE", 22 | "Content-Type, Authorization"); 23 | next(); 24 | }); 25 | 26 | for (const route of routes) { 27 | app[route.method](route.path, async (req, res) => { 28 | await executeRoute( 29 | req, 30 | res, 31 | config, 32 | route, 33 | req.params); 34 | }); 35 | } 36 | 37 | const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 4000; 38 | app.listen(port, () => { 39 | console.log(`Started API Server on port ${port}`); 40 | console.log(`Running Config '${configId}'`); 41 | }); 42 | 43 | app.on("error", error => { 44 | console.error("Error while initializing service:", error); 45 | }); 46 | -------------------------------------------------------------------------------- /api/src/initServices.ts: -------------------------------------------------------------------------------- 1 | import { IConfiguration } from "./models/configuration/IConfiguration"; 2 | 3 | /** 4 | * Initialise all the services. 5 | * @param config The configuration to initialisation the service with. 6 | */ 7 | export async function initServices(config: IConfiguration): Promise { 8 | } 9 | -------------------------------------------------------------------------------- /api/src/models/api/IDataResponse.ts: -------------------------------------------------------------------------------- 1 | export interface IDataResponse { 2 | /** 3 | * Success 4 | */ 5 | success: boolean; 6 | /** 7 | * The content type of the data response. 8 | */ 9 | contentType?: string; 10 | /** 11 | * Show the content inline. 12 | */ 13 | inline?: boolean; 14 | /** 15 | * The buffer of data to return. 16 | */ 17 | data: Buffer; 18 | /** 19 | * The filename for an attachment. 20 | */ 21 | filename?: string; 22 | } 23 | -------------------------------------------------------------------------------- /api/src/models/api/IIPFSRetrieveRequest.ts: -------------------------------------------------------------------------------- 1 | export interface IIPFSRetrieveRequest { 2 | /** 3 | * The messageId of the file to retrieve. 4 | */ 5 | messageId: string; 6 | } 7 | -------------------------------------------------------------------------------- /api/src/models/api/IIPFSRetrieveResponse.ts: -------------------------------------------------------------------------------- 1 | import { IResponse } from "./IResponse"; 2 | 3 | export interface IIPFSRetrieveResponse extends IResponse { 4 | 5 | /** 6 | * The filename of the file to stored. 7 | */ 8 | name?: string; 9 | 10 | /** 11 | * The description of the file to store. 12 | */ 13 | description?: string; 14 | 15 | /** 16 | * The size of the file to store. 17 | */ 18 | size?: number; 19 | 20 | /** 21 | * The modified date of the file to store. 22 | */ 23 | modified?: string; 24 | 25 | /** 26 | * The hash algorithm of the file to store. 27 | */ 28 | algorithm?: string; 29 | 30 | /** 31 | * The hash of the file to store. 32 | */ 33 | hash?: string; 34 | 35 | /** 36 | * The hash for the ipfs item. 37 | */ 38 | ipfs?: string; 39 | } 40 | -------------------------------------------------------------------------------- /api/src/models/api/IIPFSStoreRequest.ts: -------------------------------------------------------------------------------- 1 | export interface IIPFSStoreRequest { 2 | /** 3 | * The filename of the file to store. 4 | */ 5 | name: string; 6 | 7 | /** 8 | * The description of the file to store. 9 | */ 10 | description: string; 11 | 12 | /** 13 | * The size of the file to store. 14 | */ 15 | size: number; 16 | 17 | /** 18 | * The modified date of the file to store. 19 | */ 20 | modified: string; 21 | 22 | /** 23 | * The hash algorithm of the file to store. 24 | */ 25 | algorithm: string; 26 | 27 | /** 28 | * The hash of the file to store. 29 | */ 30 | hash: string; 31 | 32 | /** 33 | * The data of the file to store in base64. 34 | */ 35 | data: string; 36 | } 37 | -------------------------------------------------------------------------------- /api/src/models/api/IIPFSStoreResponse.ts: -------------------------------------------------------------------------------- 1 | import { IResponse } from "./IResponse"; 2 | 3 | export interface IIPFSStoreResponse extends IResponse { 4 | /** 5 | * The hash for the messages. 6 | */ 7 | messageId?: string; 8 | 9 | /** 10 | * The hash for the ipfs item. 11 | */ 12 | ipfs?: string; 13 | } 14 | -------------------------------------------------------------------------------- /api/src/models/api/IResponse.ts: -------------------------------------------------------------------------------- 1 | export interface IResponse { 2 | /** 3 | * What the request successful. 4 | */ 5 | success: boolean; 6 | 7 | /** 8 | * A message for the response. 9 | */ 10 | message: string; 11 | } 12 | -------------------------------------------------------------------------------- /api/src/models/app/IHttpRequest.ts: -------------------------------------------------------------------------------- 1 | import { IncomingHttpHeaders} from "http"; 2 | 3 | export interface IHttpRequest { 4 | /** 5 | * The request method. 6 | */ 7 | method?: string; 8 | 9 | /** 10 | * The request url. 11 | */ 12 | url?: string; 13 | 14 | /** 15 | * The request body. 16 | */ 17 | body: any; 18 | 19 | /** 20 | * The query parameters. 21 | */ 22 | query: { [key: string]: any }; 23 | 24 | /** 25 | * Incoming Http Headers. 26 | */ 27 | headers: IncomingHttpHeaders; 28 | } 29 | -------------------------------------------------------------------------------- /api/src/models/app/IHttpResponse.ts: -------------------------------------------------------------------------------- 1 | export interface IHttpResponse { 2 | /** 3 | * Set a header on the response. 4 | * @param name The name of the header. 5 | * @param value The value of the header. 6 | */ 7 | setHeader(name: string, value: number | string | string[]): void; 8 | 9 | /** 10 | * Set a status code on the response. 11 | * @param statusCode The status code to send. 12 | * @returns The response. 13 | */ 14 | status(statusCode: number): IHttpResponse; 15 | 16 | /** 17 | * Send data in the response. 18 | * @param body The data to send. 19 | * @returns The response. 20 | */ 21 | send(body: unknown): IHttpResponse; 22 | 23 | /** 24 | * End the response. 25 | */ 26 | end(): void; 27 | } 28 | -------------------------------------------------------------------------------- /api/src/models/app/IRoute.ts: -------------------------------------------------------------------------------- 1 | import { IConfiguration } from "../configuration/IConfiguration"; 2 | 3 | export interface IRoute { 4 | /** 5 | * The path to use for routing. 6 | */ 7 | path: string; 8 | /** 9 | * The http method. 10 | */ 11 | method: "get" | "post" | "put" | "delete"; 12 | /** 13 | * Folder within the routes folder. 14 | */ 15 | folder?: string; 16 | /** 17 | * The name of the function to load. 18 | */ 19 | func?: string; 20 | /** 21 | * The body is separate data so don't merge with params. 22 | */ 23 | dataBody?: boolean; 24 | /** 25 | * The response is data so look for contentType, filename and data. 26 | */ 27 | dataResponse?: boolean; 28 | /** 29 | * Perform inline function instead of module load. 30 | * @param config The confiuration. 31 | * @param params The request parameters. 32 | * @param body The request body. 33 | * @param header The headers in the http request. 34 | */ 35 | inline?( 36 | config: IConfiguration, 37 | params: unknown, 38 | body?: unknown, 39 | headers?: { [id: string]: unknown }): Promise; 40 | } 41 | -------------------------------------------------------------------------------- /api/src/models/configuration/IAWSDynamoDbConfiguration.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Definition of AWS DB configuration. 3 | */ 4 | export interface IAWSDynamoDbConfiguration { 5 | /** 6 | * The region for the AWS connection. 7 | */ 8 | region: string; 9 | /** 10 | * The AWS access key. 11 | */ 12 | accessKeyId: string; 13 | /** 14 | * The AWS secret access key. 15 | */ 16 | secretAccessKey: string; 17 | /** 18 | * Prefix for all tables. 19 | */ 20 | dbTablePrefix: string; 21 | } 22 | -------------------------------------------------------------------------------- /api/src/models/configuration/IConfiguration.ts: -------------------------------------------------------------------------------- 1 | import { IAWSDynamoDbConfiguration } from "./IAWSDynamoDbConfiguration"; 2 | import { IIPFSConfiguration } from "./IIPFSConfiguration"; 3 | import { INodeConfiguration } from "./INodeConfiguration"; 4 | 5 | /** 6 | * Definition of configuration file. 7 | */ 8 | export interface IConfiguration { 9 | /** 10 | * The provider to use for IOTA communication. 11 | */ 12 | node: INodeConfiguration; 13 | 14 | /** 15 | * The IPFS configuration. 16 | */ 17 | ipfs: IIPFSConfiguration; 18 | 19 | /** 20 | * The dynamic db connection. 21 | */ 22 | dynamoDbConnection: IAWSDynamoDbConfiguration; 23 | 24 | /** 25 | * A list of domains allowed to access the api. 26 | */ 27 | allowedDomains: string[]; 28 | 29 | /** 30 | * The maximum number of bytes allowed. 31 | */ 32 | maxBytes?: number; 33 | } 34 | -------------------------------------------------------------------------------- /api/src/models/configuration/IIPFSConfiguration.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration for the IPFS Node. 3 | */ 4 | export interface IIPFSConfiguration { 5 | /** 6 | * The provider for the ipfs node. 7 | */ 8 | provider: string; 9 | /** 10 | * The token for the ipfs node. 11 | */ 12 | token: string; 13 | } 14 | -------------------------------------------------------------------------------- /api/src/models/configuration/INodeConfiguration.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Definition of node configuration file. 3 | */ 4 | export interface INodeConfiguration { 5 | /** 6 | * The provider to use for IOTA communication. 7 | */ 8 | provider: string; 9 | } 10 | -------------------------------------------------------------------------------- /api/src/models/db/IState.ts: -------------------------------------------------------------------------------- 1 | export interface IState { 2 | /** 3 | * The id for the state. 4 | */ 5 | id: string; 6 | /** 7 | * The seed to use for address generation. 8 | */ 9 | seed: string; 10 | /** 11 | * The last address idnex used. 12 | */ 13 | addressIndex: number; 14 | } 15 | -------------------------------------------------------------------------------- /api/src/models/tangle/IPayload.ts: -------------------------------------------------------------------------------- 1 | export interface IPayload { 2 | /** 3 | * The filename of the file to store. 4 | */ 5 | name: string; 6 | 7 | /** 8 | * The description of the file to store. 9 | */ 10 | description: string; 11 | 12 | /** 13 | * The size of the file to store. 14 | */ 15 | size: number; 16 | 17 | /** 18 | * The modified date of the file to store. 19 | */ 20 | modified: string; 21 | 22 | /** 23 | * The hash algorithm of the file to store. 24 | */ 25 | algorithm: string; 26 | 27 | /** 28 | * The hash of the file to store. 29 | */ 30 | hash: string; 31 | 32 | /** 33 | * The IPFS hash of the file. 34 | */ 35 | ipfs: string; 36 | } 37 | -------------------------------------------------------------------------------- /api/src/routes.ts: -------------------------------------------------------------------------------- 1 | import { IRoute } from "./models/app/IRoute"; 2 | 3 | export const routes: IRoute[] = [ 4 | { 5 | path: "/", 6 | method: "get", 7 | inline: async () => { 8 | // tslint:disable-next-line: no-require-imports 9 | const packageJson = require("../package.json"); 10 | return { 11 | name: packageJson.name, 12 | version: packageJson.version 13 | }; 14 | } 15 | }, 16 | { path: "/init", method: "get", func: "init" }, 17 | { path: "/ipfs", method: "post", func: "ipfsStore" }, 18 | { path: "/ipfs", method: "get", func: "ipfsRetrieve" } 19 | ]; 20 | -------------------------------------------------------------------------------- /api/src/routes/init.ts: -------------------------------------------------------------------------------- 1 | import { IConfiguration } from "../models/configuration/IConfiguration"; 2 | import { MessageCacheService } from "../services/messageCacheService"; 3 | import { StateService } from "../services/stateService"; 4 | 5 | /** 6 | * Initialise the database. 7 | * @param config The configuration. 8 | * @returns The response. 9 | */ 10 | export async function init(config: IConfiguration): Promise { 11 | let log = "Initializing\n"; 12 | 13 | try { 14 | log += await new StateService(config.dynamoDbConnection).createTable(); 15 | log += await new MessageCacheService(config.dynamoDbConnection, config.node.provider).createTable(); 16 | 17 | } catch (err) { 18 | log += `Failed\n${err.toString()}\n`; 19 | } 20 | 21 | if (log.indexOf("Failed") < 0) { 22 | log += "Initialization Succeeded"; 23 | } else { 24 | log += "Initialization Failed"; 25 | } 26 | 27 | return log.split("\n"); 28 | } 29 | -------------------------------------------------------------------------------- /api/src/routes/ipfsRetrieve.ts: -------------------------------------------------------------------------------- 1 | import { IIPFSRetrieveRequest } from "../models/api/IIPFSRetrieveRequest"; 2 | import { IIPFSRetrieveResponse } from "../models/api/IIPFSRetrieveResponse"; 3 | import { IConfiguration } from "../models/configuration/IConfiguration"; 4 | import { MessageCacheService } from "../services/messageCacheService"; 5 | import { IotaC2Helper } from "../utils/iotaC2Helper"; 6 | import { ValidationHelper } from "../utils/validationHelper"; 7 | 8 | /** 9 | * Ipfs retrieve command. 10 | * @param config The configuration. 11 | * @param request the request. 12 | * @returns The response. 13 | */ 14 | export async function ipfsRetrieve( 15 | config: IConfiguration, 16 | request: IIPFSRetrieveRequest): Promise { 17 | try { 18 | ValidationHelper.string(request.messageId, "messageId"); 19 | ValidationHelper.isMessageId(request.messageId); 20 | 21 | const messageCacheService = new MessageCacheService( 22 | config.dynamoDbConnection, 23 | config.node.provider); 24 | const message = await messageCacheService.get(request.messageId); 25 | 26 | if (!message) { 27 | throw new Error(`Unable to locate the specified message: '${request.messageId}'.`); 28 | } 29 | 30 | const payload = await IotaC2Helper.messageToPayload(message); 31 | 32 | return { 33 | success: true, 34 | message: "OK", 35 | ...payload 36 | }; 37 | } catch (err) { 38 | return { 39 | success: false, 40 | message: err.toString() 41 | }; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /api/src/routes/ipfsStore.ts: -------------------------------------------------------------------------------- 1 | import { ClientBuilder } from "@iota/client"; 2 | import { Blake2b, Converter } from "@iota/iota.js"; 3 | import crypto from "crypto"; 4 | import ipfsClient from "ipfs-http-client"; 5 | import { SHA3 } from "sha3"; 6 | import { IIPFSStoreRequest } from "../models/api/IIPFSStoreRequest"; 7 | import { IIPFSStoreResponse } from "../models/api/IIPFSStoreResponse"; 8 | import { IConfiguration } from "../models/configuration/IConfiguration"; 9 | import { IPayload } from "../models/tangle/IPayload"; 10 | import { MessageCacheService } from "../services/messageCacheService"; 11 | import { StateService } from "../services/stateService"; 12 | import { IotaC2Helper } from "../utils/iotaC2Helper"; 13 | import { ValidationHelper } from "../utils/validationHelper"; 14 | 15 | /** 16 | * Ipfs store command. 17 | * @param config The configuration. 18 | * @param request the request. 19 | * @returns The response. 20 | */ 21 | export async function ipfsStore(config: IConfiguration, request: IIPFSStoreRequest): Promise { 22 | try { 23 | ValidationHelper.string(request.name, "name"); 24 | ValidationHelper.string(request.description, "description"); 25 | ValidationHelper.number(request.size, "size"); 26 | ValidationHelper.string(request.modified, "modified"); 27 | ValidationHelper.string(request.algorithm, "algorithm"); 28 | ValidationHelper.string(request.hash, "hash"); 29 | ValidationHelper.string(request.data, "data"); 30 | 31 | const BYTES_PER_MEGABYTE = 1048576; 32 | const maxSize = config.maxBytes ?? BYTES_PER_MEGABYTE / 2; 33 | 34 | const buffer = Buffer.from(request.data, "base64"); 35 | 36 | if (buffer.length >= maxSize) { 37 | const size = maxSize >= BYTES_PER_MEGABYTE 38 | ? `${(maxSize / BYTES_PER_MEGABYTE).toFixed(1)} Mb` 39 | : `${(maxSize / 1024)} Kb`; 40 | throw new Error( 41 | `The file is too large for this demonstration, it should be less than ${size}.` 42 | ); 43 | } 44 | 45 | if (buffer.length === 0) { 46 | throw new Error(`The file must be greater than 0 bytes in length.`); 47 | } 48 | 49 | let hex; 50 | 51 | if (request.algorithm === "sha256") { 52 | const hashAlgo = crypto.createHash(request.algorithm); 53 | hashAlgo.update(buffer); 54 | hex = hashAlgo.digest("hex"); 55 | } else if (request.algorithm === "sha3") { 56 | const hashAlgo = new SHA3(256); 57 | hashAlgo.update(buffer); 58 | hex = hashAlgo.digest("hex"); 59 | } 60 | 61 | if (hex !== request.hash) { 62 | throw new Error( 63 | `The hash for the file is incorrect '${request.hash}' was sent but it has been calculated as '${hex}'`); 64 | } 65 | 66 | const parts = /(https?):\/\/(.*):(\d*)(.*)/.exec(config.ipfs.provider); 67 | 68 | if (parts.length !== 5) { 69 | throw new Error(`The IPFS Provider is not formatted correctly, it should be in the format https://ipfs.mydomain.com:443/api/v0/`); 70 | } 71 | 72 | const ipfsConfig = { 73 | protocol: parts[1], 74 | host: parts[2], 75 | port: parts[3], 76 | "api-path": parts[4], 77 | headers: undefined 78 | }; 79 | 80 | if (config.ipfs.token) { 81 | ipfsConfig.headers = { 82 | Authorization: `Basic ${config.ipfs.token}` 83 | }; 84 | } 85 | 86 | const ipfs = ipfsClient(ipfsConfig); 87 | 88 | const addStart = Date.now(); 89 | console.log(`Adding file ${request.name} to IPFS of length ${request.size}`); 90 | const addResponse = await ipfs.add(buffer); 91 | 92 | const ipfsHash = addResponse.path; 93 | console.log(`Adding file ${request.name} complete in ${Date.now() - addStart}ms`); 94 | 95 | const tanglePayload: IPayload = { 96 | name: request.name, 97 | description: request.description, 98 | size: request.size, 99 | modified: request.modified, 100 | algorithm: request.algorithm, 101 | hash: request.hash, 102 | ipfs: ipfsHash 103 | }; 104 | 105 | const json = JSON.stringify(tanglePayload); 106 | 107 | const stateService = new StateService(config.dynamoDbConnection); 108 | 109 | const currentState = await stateService.get("default-c2"); 110 | if (!currentState) { 111 | currentState.seed = IotaC2Helper.generateHash(), 112 | currentState.id = "default-c2", 113 | currentState.addressIndex = 0; 114 | } else { 115 | currentState.addressIndex++; 116 | } 117 | 118 | await stateService.set(currentState); 119 | 120 | // Chrysalis client instance 121 | const client = new ClientBuilder().node(config.node.provider).build(); 122 | 123 | const addresses = client.getAddresses(currentState.seed); 124 | 125 | const address = await addresses 126 | .accountIndex(0) 127 | .range(currentState.addressIndex, currentState.addressIndex + 1) 128 | .get(); 129 | 130 | const hashedAddress = Blake2b.sum256(Converter.utf8ToBytes(address[0].toString())); 131 | 132 | const message = await client 133 | .message() 134 | .index(Converter.bytesToHex(hashedAddress)) 135 | .seed(currentState.seed) 136 | .accountIndex(0) 137 | .data(new TextEncoder().encode(json)) 138 | .submit(); 139 | 140 | const messageCacheService = new MessageCacheService( 141 | config.dynamoDbConnection, 142 | config.node.provider); 143 | 144 | await messageCacheService.set({ messageId: message.messageId, message: message.message }); 145 | 146 | return { 147 | success: true, 148 | message: "OK", 149 | messageId: message.messageId, 150 | ipfs: tanglePayload.ipfs 151 | }; 152 | } catch (err) { 153 | return { 154 | success: false, 155 | message: err.toString() 156 | }; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /api/src/services/amazonDynamoDbService.ts: -------------------------------------------------------------------------------- 1 | import { IAWSDynamoDbConfiguration } from "../models/configuration/IAWSDynamoDbConfiguration"; 2 | import { AmazonDynamoDbHelper } from "../utils/amazonDynamoDbHelper"; 3 | 4 | /** 5 | * Service to handle db requests. 6 | */ 7 | export abstract class AmazonDynamoDbService { 8 | /** 9 | * The name of the database table. 10 | */ 11 | protected _fullTableName: string; 12 | 13 | /** 14 | * Configuration to connection to AWS. 15 | */ 16 | protected readonly _config: IAWSDynamoDbConfiguration; 17 | 18 | /** 19 | * The id field name. 20 | */ 21 | protected readonly _idName: string; 22 | 23 | constructor(config: IAWSDynamoDbConfiguration, tableName: string, idName: string) { 24 | this._config = config; 25 | this._fullTableName = `${this._config.dbTablePrefix}${tableName}`; 26 | this._idName = idName; 27 | } 28 | 29 | /** 30 | * Create the table for the items. 31 | * @returns Log of the table creation. 32 | */ 33 | public async createTable(): Promise { 34 | let log = `Creating table '${this._fullTableName}'\n`; 35 | 36 | try { 37 | const dbConnection = AmazonDynamoDbHelper.createConnection(this._config); 38 | 39 | const tableParams = { 40 | AttributeDefinitions: [ 41 | { 42 | AttributeName: this._idName, 43 | AttributeType: "S" 44 | } 45 | ], 46 | KeySchema: [ 47 | { 48 | AttributeName: this._idName, 49 | KeyType: "HASH" 50 | } 51 | ], 52 | ProvisionedThroughput: { 53 | ReadCapacityUnits: 1, 54 | WriteCapacityUnits: 1 55 | }, 56 | TableName: this._fullTableName 57 | }; 58 | 59 | await dbConnection.createTable(tableParams).promise(); 60 | 61 | log += `Waiting for '${this._fullTableName}'\n`; 62 | 63 | await dbConnection.waitFor("tableExists", { 64 | TableName: this._fullTableName 65 | }).promise(); 66 | 67 | log += `Table '${this._fullTableName}' Created Successfully\n`; 68 | } catch (err) { 69 | if (err.code === "ResourceInUseException") { 70 | log += `Table '${this._fullTableName}' Already Exists\n`; 71 | } else { 72 | log += `Table '${this._fullTableName}' Creation Failed\n${err.toString()}\n`; 73 | } 74 | } 75 | 76 | return log; 77 | } 78 | 79 | /** 80 | * Get the item. 81 | * @param id The id of the item to get. 82 | * @returns The object if it can be found or undefined. 83 | */ 84 | public async get(id: string): Promise { 85 | try { 86 | const docClient = AmazonDynamoDbHelper.createDocClient(this._config); 87 | 88 | const key = {}; 89 | key[this._idName] = id; 90 | 91 | const response = await docClient.get({ 92 | TableName: this._fullTableName, 93 | Key: key 94 | }).promise(); 95 | 96 | return response.Item; 97 | } catch (err) { 98 | } 99 | } 100 | 101 | /** 102 | * Set the item. 103 | * @param item The item to set. 104 | */ 105 | public async set(item: T): Promise { 106 | const docClient = AmazonDynamoDbHelper.createDocClient(this._config); 107 | 108 | await docClient.put({ 109 | TableName: this._fullTableName, 110 | Item: item 111 | }).promise(); 112 | } 113 | 114 | /** 115 | * Delete the item. 116 | * @param itemKey The key of the item to remove. 117 | */ 118 | public async remove(itemKey: string): Promise { 119 | const docClient = AmazonDynamoDbHelper.createDocClient(this._config); 120 | 121 | const key = {}; 122 | key[this._idName] = itemKey; 123 | 124 | await docClient.delete({ 125 | TableName: this._fullTableName, 126 | Key: key 127 | }).promise(); 128 | } 129 | 130 | /** 131 | * Get all the items. 132 | * @returns All the items for the table. 133 | */ 134 | public async getAll(): Promise { 135 | try { 136 | const docClient = AmazonDynamoDbHelper.createDocClient(this._config); 137 | 138 | const response = await docClient.scan({ 139 | TableName: this._fullTableName 140 | }).promise(); 141 | 142 | return response.Items; 143 | } catch (err) { 144 | return []; 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /api/src/services/messageCacheService.ts: -------------------------------------------------------------------------------- 1 | import { Client, ClientBuilder } from "@iota/client"; 2 | import { MessageWrapper } from "@iota/client/lib/types"; 3 | import { IAWSDynamoDbConfiguration } from "../models/configuration/IAWSDynamoDbConfiguration"; 4 | import { AmazonDynamoDbService } from "./amazonDynamoDbService"; 5 | 6 | /** 7 | * Service to handle the message cache. 8 | */ 9 | export class MessageCacheService extends AmazonDynamoDbService { 10 | /** 11 | * The name of the database table. 12 | */ 13 | public static readonly TABLE_NAME: string = "messageCache"; 14 | 15 | /** 16 | * Configuration to connection to tangle. 17 | */ 18 | private readonly _provider: string; 19 | 20 | /** 21 | * Client-instance 22 | */ 23 | private readonly _client: Client; 24 | 25 | /** 26 | * Create a new instance of MessageCacheService. 27 | * @param config The configuration to use. 28 | * @param provider The tangle node. 29 | */ 30 | constructor(config: IAWSDynamoDbConfiguration, provider: string) { 31 | super(config, MessageCacheService.TABLE_NAME, "messageId"); 32 | this._provider = provider; 33 | try { 34 | this._client = new ClientBuilder() 35 | .node(this._provider) 36 | .build(); 37 | } catch (err) { 38 | } 39 | } 40 | 41 | /** 42 | * Get the message with the given id. 43 | * @param messageId The message id. 44 | * @returns The message if it can be found. 45 | */ 46 | public async get(messageId: string): Promise { 47 | if (await this.isNodeHealthy()) { 48 | try { 49 | return this._client.getMessage().data(messageId); 50 | } catch (err) { 51 | } 52 | } 53 | 54 | return super.get(messageId); 55 | } 56 | 57 | /** 58 | * Helper function to check if the node is healthy 59 | * @returns a boolean-value to check if the node is healthy or not 60 | */ 61 | public async isNodeHealthy(): Promise { 62 | return this._client && this._client.getInfo() 63 | .then(info => { return info.nodeinfo.isHealthy; }) 64 | .catch(err => { return false; }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /api/src/services/stateService.ts: -------------------------------------------------------------------------------- 1 | import { IAWSDynamoDbConfiguration } from "../models/configuration/IAWSDynamoDbConfiguration"; 2 | import { IState } from "../models/db/IState"; 3 | import { AmazonDynamoDbService } from "./amazonDynamoDbService"; 4 | 5 | /** 6 | * Service to handle the state. 7 | */ 8 | export class StateService extends AmazonDynamoDbService { 9 | /** 10 | * The name of the database table. 11 | */ 12 | public static readonly TABLE_NAME: string = "state"; 13 | 14 | constructor(config: IAWSDynamoDbConfiguration) { 15 | super(config, StateService.TABLE_NAME, "id"); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /api/src/utils/amazonDynamoDbHelper.ts: -------------------------------------------------------------------------------- 1 | import * as aws from "aws-sdk"; 2 | import { IAWSDynamoDbConfiguration } from "../models/configuration/IAWSDynamoDbConfiguration"; 3 | 4 | /** 5 | * Class to helper with database. 6 | */ 7 | export class AmazonDynamoDbHelper { 8 | /** 9 | * Create and set the configuration for db. 10 | * @param config The configuration to use for connection. 11 | */ 12 | public static createAndSetConfig(config: IAWSDynamoDbConfiguration): void { 13 | const awsConfig = new aws.Config({ 14 | accessKeyId: config.accessKeyId, 15 | secretAccessKey: config.secretAccessKey, 16 | region: config.region 17 | }); 18 | 19 | aws.config.update(awsConfig); 20 | } 21 | 22 | /** 23 | * Create a new DB connection. 24 | * @param config The configuration for the connection. 25 | * @returns The dynamo db connection. 26 | */ 27 | public static createConnection(config: IAWSDynamoDbConfiguration): aws.DynamoDB { 28 | AmazonDynamoDbHelper.createAndSetConfig(config); 29 | 30 | return new aws.DynamoDB({ apiVersion: "2012-10-08" }); 31 | } 32 | 33 | /** 34 | * Create a doc client connection. 35 | * @param config The configuration to use for connection. 36 | * @returns The dynamo db document client. 37 | */ 38 | public static createDocClient(config: IAWSDynamoDbConfiguration): aws.DynamoDB.DocumentClient { 39 | AmazonDynamoDbHelper.createAndSetConfig(config); 40 | 41 | return new aws.DynamoDB.DocumentClient({ 42 | apiVersion: "2012-10-08", 43 | convertEmptyValues: true 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /api/src/utils/apiHelper.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import { inspect } from "util"; 3 | import { initServices } from "../initServices"; 4 | import { IDataResponse } from "../models/api/IDataResponse"; 5 | import { IHttpRequest } from "../models/app/IHttpRequest"; 6 | import { IHttpResponse } from "../models/app/IHttpResponse"; 7 | import { IRoute } from "../models/app/IRoute"; 8 | import { IConfiguration } from "../models/configuration/IConfiguration"; 9 | 10 | /** 11 | * Find a route to match 12 | * @param findRoutes The routes to match against. 13 | * @param url The url to find. 14 | * @param method The method to find. 15 | * @returns The matching route if there is one and any extracted params. 16 | */ 17 | export function findRoute(findRoutes: IRoute[], url: string, method: string): { 18 | /** 19 | * The matching route. 20 | */ 21 | route: IRoute; 22 | /** 23 | * Matching parameters for the route. 24 | */ 25 | params: { [id: string]: string }, 26 | } | undefined { 27 | const urlParts = url.replace(/\/$/, "").split("/"); 28 | 29 | for (const route of findRoutes) { 30 | if (route.method === method) { 31 | const routeParts = route.path.split("/"); 32 | const params: { [id: string]: string } = {}; 33 | 34 | let i; 35 | for (i = 0; i < urlParts.length && i < routeParts.length; i++) { 36 | if (routeParts[i] === urlParts[i]) { 37 | // This segment matches OK 38 | } else if (routeParts[i].startsWith(":") && 39 | (i < urlParts.length || routeParts[i].endsWith("?")) 40 | ) { 41 | // Its a param match in the url 42 | // or an undefined parameter past the end of the match 43 | if (i < urlParts.length) { 44 | params[routeParts[i].substr(1).replace("?", "")] = urlParts[i]; 45 | } 46 | } else { 47 | break; 48 | } 49 | } 50 | 51 | if (i === urlParts.length) { 52 | return { 53 | route, 54 | params 55 | }; 56 | } 57 | } 58 | } 59 | return undefined; 60 | } 61 | 62 | /** 63 | * Execute the route which matches the path. 64 | * @param req The request. 65 | * @param res The response. 66 | * @param config The configuration. 67 | * @param route The route. 68 | * @param pathParams The params extracted from the url. 69 | * @param logHook Optional hook for logging errors. 70 | */ 71 | export async function executeRoute( 72 | req: IHttpRequest, 73 | res: IHttpResponse, 74 | config: IConfiguration, 75 | route: IRoute, 76 | pathParams: { [id: string]: string }, 77 | logHook?: (message: string, statusCode: number, params: unknown) => Promise): Promise { 78 | 79 | let response; 80 | const start = Date.now(); 81 | let filteredParams; 82 | let status = 400; 83 | 84 | try { 85 | let params; 86 | let body; 87 | if (route.dataBody) { 88 | body = req.body; 89 | params = { ...pathParams, ...req.query }; 90 | } else { 91 | params = { ...pathParams, ...req.query, ...req.body }; 92 | } 93 | 94 | filteredParams = logParams(params); 95 | 96 | console.log(`===> ${route.method.toUpperCase()} ${route.path}`); 97 | console.log(inspect(filteredParams, false, null, false)); 98 | 99 | if (route.func) { 100 | let modulePath; 101 | if (route.folder) { 102 | modulePath = join(__dirname, "..", "routes", route.folder, route.func); 103 | } else { 104 | modulePath = join(__dirname, "..", "routes", route.func); 105 | } 106 | let mod; 107 | try { 108 | // tslint:disable-next-line: non-literal-require 109 | mod = require(modulePath); 110 | } catch (err) { 111 | console.error(err); 112 | } 113 | if (mod) { 114 | if (mod[route.func]) { 115 | await initServices(config); 116 | response = await mod[route.func](config, params, body, req.headers || {}); 117 | status = 200; 118 | } else { 119 | status = 400; 120 | response = { 121 | success: false, 122 | message: `Route '${route.path}' module '${ 123 | modulePath}' does not contain a method '${route.func}'` 124 | }; 125 | } 126 | } else { 127 | status = 400; 128 | response = { success: false, message: `Route '${route.path}' module '${modulePath}' failed to load` }; 129 | } 130 | } else if (route.inline !== undefined) { 131 | await initServices(config); 132 | response = await route.inline(config, params, body, req.headers || {}); 133 | status = 200; 134 | } else { 135 | status = 400; 136 | response = { success: false, message: `Route ${route.path} has no func or inline property set` }; 137 | } 138 | } catch (err) { 139 | status = err.httpCode || 400; 140 | response = { success: false, message: err.message }; 141 | if (logHook) { 142 | await logHook(err.message, status, filteredParams); 143 | } 144 | } 145 | 146 | console.log(`<=== duration: ${Date.now() - start}ms`); 147 | console.log(inspect(response, false, null, false)); 148 | 149 | if (route.dataResponse) { 150 | const dataResponse = response as IDataResponse; 151 | if (!dataResponse.success) { 152 | status = 400; 153 | } 154 | if (dataResponse.contentType) { 155 | res.setHeader("Content-Type", dataResponse.contentType); 156 | } 157 | let filename = ""; 158 | if (dataResponse.filename) { 159 | filename = `; filename="${dataResponse.filename}"`; 160 | } 161 | res.setHeader( 162 | "Content-Disposition", `${dataResponse.inline ? "inline" : "attachment"}${filename}`); 163 | 164 | res.setHeader( 165 | "Content-Length", dataResponse.data.length); 166 | 167 | res.status(status).send(dataResponse.data); 168 | } else { 169 | res.setHeader("Content-Type", "application/json"); 170 | res.status(status).send(JSON.stringify(response)); 171 | } 172 | res.end(); 173 | } 174 | 175 | /** 176 | * Convert the params to logable 177 | * @param obj The object to convert. 178 | * @returns The converted object. 179 | */ 180 | function logParams(obj: { [id: string]: any }): { [id: string]: string } { 181 | const newobj: { [id: string]: any } = {}; 182 | for (const key in obj) { 183 | if (key.indexOf("pass") >= 0) { 184 | newobj[key] = "*************"; 185 | } else if (key.indexOf("base64") >= 0 || key.indexOf("binaryData") >= 0) { 186 | newobj[key] = ""; 187 | } else { 188 | if (obj[key] !== undefined && obj[key] !== null) { 189 | if (obj[key].constructor.name === "Object") { 190 | newobj[key] = logParams(obj[key]); 191 | } else if (Array.isArray(obj[key])) { 192 | newobj[key] = obj[key].map(logParams); 193 | } else { 194 | newobj[key] = obj[key]; 195 | } 196 | } else { 197 | newobj[key] = obj[key]; 198 | } 199 | } 200 | } 201 | return newobj; 202 | } 203 | 204 | /** 205 | * Handle cors. 206 | * @param req Request The request. 207 | * @param res Response The response. 208 | * @param allowOrigins The allowed origins. 209 | * @param allowMethods The allowed methods. 210 | * @param allowHeaders The allowed headers. 211 | */ 212 | export function cors( 213 | req: IHttpRequest, 214 | res: IHttpResponse, 215 | allowOrigins: string | string[] | undefined, 216 | allowMethods: string | undefined, 217 | allowHeaders: string | undefined): void { 218 | 219 | if (!allowOrigins || allowOrigins === "*") { 220 | res.setHeader("Access-Control-Allow-Origin", "*"); 221 | } else if (allowOrigins) { 222 | const requestOrigin = req.headers.origin; 223 | const origins = Array.isArray(allowOrigins) ? allowOrigins : allowOrigins.split(";"); 224 | let isAllowed; 225 | for (const origin of origins) { 226 | if (requestOrigin === origin || origin === "*") { 227 | isAllowed = origin; 228 | break; 229 | } 230 | } 231 | if (isAllowed) { 232 | res.setHeader("Access-Control-Allow-Origin", isAllowed); 233 | } 234 | } 235 | 236 | if (req.method === "OPTIONS") { 237 | res.setHeader( 238 | "Access-Control-Allow-Methods", 239 | allowMethods || "GET, POST, OPTIONS, PUT, PATCH, DELETE" 240 | ); 241 | 242 | res.setHeader( 243 | "Access-Control-Allow-Headers", 244 | allowHeaders || 245 | "X-Requested-With, Access-Control-Allow-Origin, X-HTTP-Method-Override, Content-Type, Authorization, Accept" 246 | ); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /api/src/utils/iotaC2Helper.ts: -------------------------------------------------------------------------------- 1 | import { MessageWrapper } from "@iota/client/lib/types"; 2 | import { Converter } from "@iota/iota.js"; 3 | import * as crypto from "crypto"; 4 | import { IPayload } from "../models/tangle/IPayload"; 5 | 6 | /** 7 | * Chrysalis client helper functions 8 | */ 9 | export class IotaC2Helper { 10 | 11 | /** 12 | * Generate a random hash in Base32-encoding. 13 | * @returns The hash. 14 | */ 15 | public static generateHash(): string { 16 | return crypto.createHash("sha256").update(crypto.randomBytes(256)).digest("hex"); 17 | } 18 | 19 | /** 20 | * Convert message object to payload 21 | * @param message The message object to convert. 22 | * @returns The payload. 23 | */ 24 | public static async messageToPayload(message: MessageWrapper): Promise { 25 | // Need the any casts in this function because the iota.rs binding definitions are incorrect. 26 | if (message.message.payload.type as any !== 2) { 27 | throw new Error(`Invalid messageId: ${message.messageId}. Message has no Indexation Payload containing data.`); 28 | } 29 | 30 | const payload: IPayload = JSON.parse(Converter.hexToUtf8((message.message.payload as any).data)); 31 | 32 | if (payload) { 33 | return payload; 34 | } else { 35 | throw new Error(`Error converting Message: ${ 36 | message.messageId} Indexation Payload data to a valid payload`); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /api/src/utils/textHelper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper functions for use with text. 3 | */ 4 | export class TextHelper { 5 | /** 6 | * Encode Non ASCII characters to escaped characters. 7 | * @param value The value to encode. 8 | * @returns The encoded value. 9 | */ 10 | public static encodeNonASCII(value: string): string | undefined { 11 | return value ? 12 | value.replace(/[\u007F-\uFFFF]/g, chr => `\\u${(`0000${chr.charCodeAt(0).toString(16)}`).substr(-4)}`) 13 | : undefined; 14 | } 15 | 16 | /** 17 | * Decode escaped Non ASCII characters. 18 | * @param value The value to decode. 19 | * @returns The decoded value. 20 | */ 21 | public static decodeNonASCII(value: string): string | undefined { 22 | return value ? 23 | value.replace(/\\u([\d\w]{4})/gi, (match, grp) => String.fromCharCode(parseInt(grp, 16))) 24 | : undefined; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /api/src/utils/validationHelper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper functions for validating input. 3 | */ 4 | export class ValidationHelper { 5 | /** 6 | * Does the string have some content. 7 | * @param str The string to validate. 8 | * @param name The parameter name. 9 | */ 10 | public static string(str: string, name: string): void { 11 | if (str === undefined || str === null || str.trim().length === 0) { 12 | throw new Error(`The parameter '${name}' has an invalid value.`); 13 | } 14 | } 15 | 16 | /** 17 | * Does the number have a value. 18 | * @param num The number to validate. 19 | * @param name The parameter name. 20 | */ 21 | public static number(num: number, name: string): void { 22 | if (num === undefined || num === null || typeof num !== "number") { 23 | throw new Error(`The parameter '${name}' has an invalid value.`); 24 | } 25 | } 26 | 27 | /** 28 | * Is the value of one the specified items. 29 | * @param val The value to validate. 30 | * @param options The possible options. 31 | * @param name The parameter name. 32 | */ 33 | public static oneOf(val: any, options: any[], name: string): void { 34 | if (options.indexOf(val) < 0) { 35 | throw new Error(`The parameter '${name}' has an invalid value.`); 36 | } 37 | } 38 | 39 | /** 40 | * Is the given string a valid messageId 41 | * @param str The string to validate. 42 | */ 43 | public static isMessageId(str: string): void { 44 | if (!new RegExp(`^[0-9a-f]{${str.length}}$`).test(str) || str.length !== 64) { 45 | throw new Error(`The messageId is invalid`); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "es2015", 7 | "es2015.iterable" 8 | ], 9 | "esModuleInterop": true, 10 | "module": "commonjs", 11 | "moduleResolution": "node", 12 | "noUnusedLocals": true, 13 | "typeRoots": [ 14 | "node_modules/@types", 15 | "types" 16 | ], 17 | "outDir": "dist", 18 | "resolveJsonModule": true 19 | }, 20 | "include": [ 21 | "src" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /api/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:recommended", 4 | "tslint-microsoft-contrib/recommended", 5 | "tslint-eslint-rules" 6 | ], 7 | "linterOptions": { 8 | "exclude": [ 9 | "node_modules", 10 | "**/*.d.ts", 11 | "**/*.json" 12 | ] 13 | }, 14 | "rules": { 15 | "insecure-random": true, 16 | "no-banned-terms": true, 17 | "no-cookies": true, 18 | "no-delete-expression": true, 19 | "no-disable-auto-sanitization": true, 20 | "no-document-domain": true, 21 | "no-document-write": true, 22 | "no-eval": true, 23 | "no-exec-script": true, 24 | "no-http-string": [ 25 | false 26 | ], 27 | "no-inner-html": true, 28 | "no-octal-literal": true, 29 | "no-reserved-keywords": false, 30 | "no-string-based-set-immediate": true, 31 | "no-string-based-set-interval": true, 32 | "no-string-based-set-timeout": true, 33 | "non-literal-require": true, 34 | "possible-timing-attack": true, 35 | "react-anchor-blank-noopener": true, 36 | "react-iframe-missing-sandbox": true, 37 | "react-no-dangerous-html": true, 38 | "await-promise": [ 39 | true, 40 | "Bluebird" 41 | ], 42 | "forin": false, 43 | "jquery-deferred-must-complete": true, 44 | "label-position": true, 45 | "match-default-export-name": false, 46 | "mocha-avoid-only": true, 47 | "mocha-no-side-effect-code": false, 48 | "no-any": false, 49 | "no-arg": true, 50 | "no-backbone-get-set-outside-model": false, 51 | "no-bitwise": false, 52 | "no-conditional-assignment": true, 53 | "no-console": [ 54 | false, 55 | "debug", 56 | "info", 57 | "log", 58 | "time", 59 | "timeEnd", 60 | "trace" 61 | ], 62 | "no-constant-condition": true, 63 | "no-control-regex": false, 64 | "no-debugger": true, 65 | "no-duplicate-case": false, 66 | "no-duplicate-switch-case": true, 67 | "no-duplicate-super": true, 68 | "no-duplicate-variable": true, 69 | "no-empty": false, 70 | "no-floating-promises": true, 71 | "no-for-in-array": true, 72 | "no-import-side-effect": false, 73 | "no-increment-decrement": false, 74 | "no-invalid-regexp": true, 75 | "no-invalid-template-strings": true, 76 | "no-invalid-this": true, 77 | "no-jquery-raw-elements": true, 78 | "no-misused-new": true, 79 | "no-non-null-assertion": true, 80 | "no-reference-import": true, 81 | "no-regex-spaces": true, 82 | "no-sparse-arrays": true, 83 | "no-stateless-class": false, 84 | "no-string-literal": true, 85 | "no-string-throw": true, 86 | "no-unnecessary-callback-wrapper": true, 87 | "no-unnecessary-initializer": true, 88 | "no-unnecessary-override": true, 89 | "no-unsafe-any": false, 90 | "no-unsafe-finally": true, 91 | "no-unused-expression": true, 92 | "no-with-statement": true, 93 | "promise-function-async": true, 94 | "promise-must-complete": true, 95 | "radix": true, 96 | "react-this-binding-issue": false, 97 | "react-unused-props-and-state": true, 98 | "restrict-plus-operands": true, 99 | "strict-boolean-expressions": false, 100 | "switch-default": false, 101 | "triple-equals": [ 102 | true, 103 | "allow-null-check" 104 | ], 105 | "use-isnan": true, 106 | "use-named-parameter": true, 107 | "adjacent-overload-signatures": true, 108 | "array-type": [ 109 | true, 110 | "array" 111 | ], 112 | "arrow-parens": [ 113 | true, 114 | "ban-single-arg-parens" 115 | ], 116 | "callable-types": true, 117 | "chai-prefer-contains-to-index-of": true, 118 | "chai-vague-errors": false, 119 | "class-name": true, 120 | "comment-format": [ 121 | true 122 | ], 123 | "completed-docs": true, 124 | "export-name": false, 125 | "function-name": [ 126 | true, 127 | { 128 | "method-regex": "^[a-z][\\w\\d]+$", 129 | "private-method-regex": "^[a-z][\\w\\d]+$", 130 | "protected-method-regex": "^[a-z][\\w\\d]+$", 131 | "static-method-regex": "^[a-z][\\w\\d]+$", 132 | "function-regex": "^[a-z][\\w\\d]+$" 133 | } 134 | ], 135 | "import-name": false, 136 | "interface-name": [ 137 | true 138 | ], 139 | "jsdoc-format": true, 140 | "max-classes-per-file": [ 141 | true, 142 | 3 143 | ], 144 | "max-file-line-count": [ 145 | true 146 | ], 147 | "max-func-body-length": [ 148 | false 149 | ], 150 | "max-line-length": [ 151 | true, 152 | { 153 | "limit": 120, 154 | "ignore-pattern": "^import" 155 | } 156 | ], 157 | "member-access": true, 158 | "member-ordering": [ 159 | true, 160 | { 161 | "order": "fields-first" 162 | } 163 | ], 164 | "mocha-unneeded-done": true, 165 | "newline-per-chained-call": false, 166 | "new-parens": true, 167 | "no-construct": true, 168 | "no-default-export": false, 169 | "no-empty-interface": true, 170 | "no-for-in": false, 171 | "no-function-expression": true, 172 | "no-inferrable-types": false, 173 | "no-multiline-string": false, 174 | "no-null-keyword": false, 175 | "no-parameter-properties": true, 176 | "no-relative-imports": false, 177 | "no-require-imports": true, 178 | "no-redundant-jsdoc": false, 179 | "no-shadowed-variable": true, 180 | "no-suspicious-comment": true, 181 | "no-typeof-undefined": false, 182 | "no-unnecessary-field-initialization": true, 183 | "no-unnecessary-local-variable": true, 184 | "no-unnecessary-qualifier": true, 185 | "no-unsupported-browser-code": true, 186 | "no-useless-files": true, 187 | "no-var-keyword": true, 188 | "no-var-requires": true, 189 | "no-void-expression": false, 190 | "object-literal-sort-keys": false, 191 | "one-variable-per-declaration": [ 192 | true 193 | ], 194 | "only-arrow-functions": [ 195 | false 196 | ], 197 | "ordered-imports": [ 198 | true 199 | ], 200 | "prefer-array-literal": false, 201 | "prefer-const": true, 202 | "prefer-for-of": false, 203 | "prefer-method-signature": true, 204 | "prefer-template": true, 205 | "return-undefined": false, 206 | "typedef": [ 207 | true, 208 | "call-signature", 209 | "parameter", 210 | "property-declaration", 211 | "member-variable-declaration" 212 | ], 213 | "underscore-consistent-invocation": true, 214 | "unified-signatures": true, 215 | "variable-name": [ 216 | true, 217 | "allow-leading-underscore" 218 | ], 219 | "align": [ 220 | true, 221 | "parameters", 222 | "arguments", 223 | "statements" 224 | ], 225 | "curly": true, 226 | "eofline": true, 227 | "import-spacing": true, 228 | "indent": [ 229 | true, 230 | "spaces" 231 | ], 232 | "linebreak-style": [ 233 | true 234 | ], 235 | "newline-before-return": false, 236 | "no-consecutive-blank-lines": true, 237 | "no-empty-line-after-opening-brace": false, 238 | "no-single-line-block-comment": false, 239 | "no-trailing-whitespace": true, 240 | "no-unnecessary-semicolons": true, 241 | "object-literal-key-quotes": [ 242 | true, 243 | "as-needed" 244 | ], 245 | "one-line": [ 246 | true, 247 | "check-open-brace", 248 | "check-catch", 249 | "check-else", 250 | "check-whitespace" 251 | ], 252 | "quotemark": [ 253 | true, 254 | "double" 255 | ], 256 | "react-tsx-curly-spacing": false, 257 | "semicolon": [ 258 | true, 259 | "always" 260 | ], 261 | "trailing-comma": [ 262 | true, 263 | { 264 | "singleline": "never", 265 | "multiline": "never" 266 | } 267 | ], 268 | "typedef-whitespace": [ 269 | true, 270 | { 271 | "call-signature": "nospace", 272 | "index-signature": "nospace", 273 | "parameter": "nospace", 274 | "property-declaration": "nospace", 275 | "variable-declaration": "nospace" 276 | }, 277 | { 278 | "call-signature": "onespace", 279 | "index-signature": "onespace", 280 | "parameter": "onespace", 281 | "property-declaration": "onespace", 282 | "variable-declaration": "onespace" 283 | } 284 | ], 285 | "whitespace": [ 286 | true, 287 | "check-branch", 288 | "check-decl", 289 | "check-operator", 290 | "check-separator", 291 | "check-type" 292 | ], 293 | "ban": false, 294 | "ban-types": true, 295 | "cyclomatic-complexity": [ 296 | false 297 | ], 298 | "file-header": [ 299 | false 300 | ], 301 | "import-blacklist": false, 302 | "interface-over-type-literal": false, 303 | "no-angle-bracket-type-assertion": false, 304 | "no-inferred-empty-object-type": false, 305 | "no-internal-module": false, 306 | "no-magic-numbers": false, 307 | "no-mergeable-namespace": false, 308 | "no-namespace": false, 309 | "no-reference": true, 310 | "no-unexternalized-strings": false, 311 | "object-literal-shorthand": false, 312 | "prefer-type-cast": false, 313 | "space-before-function-paren": false, 314 | "prefer-switch": false, 315 | "prefer-function-over-method": false, 316 | "prefer-conditional-expression": false, 317 | "strict-type-predicates": false, 318 | "no-submodule-imports": false, 319 | "no-object-literal-type-assertion": false, 320 | "no-unbound-method": false, 321 | "no-unnecessary-class": false, 322 | "no-implicit-dependencies": false, 323 | "missing-optional-annotation": false, 324 | "no-duplicate-parameter-names": false, 325 | "no-empty-interfaces": false, 326 | "no-missing-visibility-modifiers": false, 327 | "no-multiple-var-decl": false, 328 | "no-var-self": false, 329 | "valid-typeof": false, 330 | "no-switch-case-fall-through": false, 331 | "typeof-compare": false, 332 | "no-dynamic-delete": false, 333 | "binary-expression-operand-order": false, 334 | "valid-jsdoc": [ 335 | true, 336 | { 337 | "requireParamType": false, 338 | "prefer": { 339 | "return": "returns" 340 | }, 341 | "requireReturn": false, 342 | "requireReturnType": false, 343 | "requireParamDescription": true, 344 | "requireReturnDescription": true 345 | } 346 | ], 347 | "file-name-casing": false, 348 | "no-unused-variable": false, 349 | "jsx-no-multiline-js": false, 350 | "jsx-no-lambda": false, 351 | "prefer-readonly": true, 352 | "react-a11y-anchors": false 353 | } 354 | } -------------------------------------------------------------------------------- /api/types/ipfs-http-client/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "ipfs-http-client" { 2 | export namespace IPFS { 3 | interface IPFSConfig { 4 | protocol: string; 5 | host: string; 6 | port: string; 7 | "api-path": string; 8 | headers: { [id: string]: string } | undefined; 9 | } 10 | 11 | interface IPSFCID { 12 | version: number; 13 | codec: string; 14 | multihash: Buffer; 15 | multibaseName: string; 16 | } 17 | 18 | export type FileContent = Object | Blob | string; 19 | 20 | interface IPFSFile { 21 | path: string; 22 | cid: IPSFCID; 23 | size: number; 24 | } 25 | 26 | interface IPFSClient { 27 | add(buffer: Buffer): Promise; 28 | } 29 | } 30 | 31 | var ipfsClient: (config: IPFS.IPFSConfig) => IPFS.IPFSClient; 32 | export default ipfsClient; 33 | } 34 | -------------------------------------------------------------------------------- /client/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | .vercel -------------------------------------------------------------------------------- /client/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.2.2 4 | 5 | * Dependency Update 6 | 7 | ## v1.2.1 8 | 9 | * Added Blueprint and GitHub links 10 | * File size limit 0.5Mb 11 | 12 | ## v1.2.0 13 | 14 | * Added sha3 hash algorithm 15 | 16 | ## v1.1.1 17 | 18 | * Increased storage size to 5Mb 19 | 20 | ## v1.0.0 21 | 22 | * Initial Release 23 | -------------------------------------------------------------------------------- /client/DEPLOYMENT.md: -------------------------------------------------------------------------------- 1 | # Deployment 2 | 3 | ## Configuration 4 | 5 | You should copy `./public/data/config.template.json` to `./public/data/config.local.json` and modify it to point the `apiEndpoint` to wherever you have deployed the `api` package. 6 | 7 | ```js 8 | { 9 | "apiEndpoint": "API-ENDPOINT", /* The url of the api endpoint e.g. https://api.my-domain.com */ 10 | "ipfsGateway": "https://ipfs.io/ipfs/:hash", /* Url of an IPFS gateway for viewing files */ 11 | "tangleExplorer": { 12 | "messages": "https://explorer.iota.org/chrysalis/message/:messageId" 13 | }, 14 | "googleAnalyticsId": "GOOGLE-ANALYTICS-ID" /* Optional, google analytics id */ 15 | } 16 | ``` 17 | 18 | ## Build 19 | 20 | ```shell 21 | npm run build 22 | ``` 23 | 24 | ## Deploy 25 | 26 | The app is configured to use zeit/now for hosting, you can configure `./now.json` to suit your own setup. 27 | 28 | If you want to use a different name for the config file you can specify an environment variable of CONFIG_ID, e.g. set CONFIG_ID to `dev` will load `config.dev.json` instead. 29 | 30 | After modifying the configuration files you can deploy using the folllowing commands: 31 | 32 | ```shell 33 | now 34 | ``` 35 | -------------------------------------------------------------------------------- /client/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 IOTA Stiftung 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # IOTA IPFS Client 2 | 3 | This package provides a React Web UI to provide facilities to upload and authenticate files using IOTA and IPFS. 4 | 5 | ## Pre-requisites 6 | 7 | ```shell 8 | npm install 9 | ``` 10 | 11 | ## Development 12 | 13 | This will run the Web UI at 14 | 15 | ```shell 16 | npm run start 17 | ``` 18 | 19 | ## Deployment 20 | 21 | See [./DEPLOYMENT.md](./DEPLOYMENT.md) for more details. 22 | 23 | ## Demonstration 24 | 25 | There is a demonstration available to view at 26 | -------------------------------------------------------------------------------- /client/config-overrides.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = function override(config, env) { 4 | console.log("⚠️ Re-wiring webpack.config using react-app-rewired and config-overrides.js ⚠️"); 5 | console.log("⚠️ Overriding react-router-dom location to point to a single package, ⚠️"); 6 | console.log("⚠️ to allow for withRouter usage in components from iota-react-components. ⚠️"); 7 | console.log() 8 | 9 | config.resolve = config.resolve || {}; 10 | config.resolve.alias = config.resolve.alias || {}; 11 | config.resolve.alias['react-router-dom'] = path.join(__dirname, './node_modules/react-router-dom/'); 12 | 13 | return config; 14 | } -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iota-poc-ipfs-client", 3 | "description": "Client Side UI for IOTA IPFS", 4 | "version": "1.2.2", 5 | "author": "Martyn Janes ", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/iotaledger/poc-ipfs.git/tree/master/client" 9 | }, 10 | "license": "MIT", 11 | "dependencies": { 12 | "axios": "^0.21.1", 13 | "classnames": "^2.2.6", 14 | "iota-css-theme": "iotaledger/iota-css-theme#db6e7a1c6f85936ec07717dffd771da0e6f9fb2a", 15 | "iota-react-components": "iotaledger/iota-react-components#6480568197e123d88c1ab7a8ef2fe804e2cfec08", 16 | "react": "^17.0.2", 17 | "react-dom": "^17.0.2", 18 | "react-router-dom": "^5.2.0", 19 | "react-scripts": "^4.0.3", 20 | "sass": "^1.34.0", 21 | "sha3": "^2.1.3" 22 | }, 23 | "engines": { 24 | "node": ">=12.x.x" 25 | }, 26 | "scripts": { 27 | "start": "cross-env PORT=3000 react-app-rewired start", 28 | "build": "react-app-rewired build", 29 | "test": "react-app-rewired test", 30 | "eject": "react-app-rewired eject" 31 | }, 32 | "browserslist": [ 33 | ">0.2%", 34 | "not dead", 35 | "not ie <= 11", 36 | "not op_mini all" 37 | ], 38 | "devDependencies": { 39 | "@types/jest": "^26.0.15", 40 | "@types/node": "^14.14.6", 41 | "@types/react": "^16.9.55", 42 | "@types/react-dom": "^16.9.9", 43 | "@types/react-router-dom": "^5.1.6", 44 | "cross-env": "^7.0.2", 45 | "react-app-rewired": "^2.1.8", 46 | "typescript": "^4.0.5" 47 | }, 48 | "eslintConfig": { 49 | "extends": [ 50 | "react-app" 51 | ] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /client/public/assets/iota-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger-archive/poc-ipfs/0f5aee9ee146630e96f507bd800abad0acd611f9/client/public/assets/iota-icons.ttf -------------------------------------------------------------------------------- /client/public/data/config.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiEndpoint": "API-ENDPOINT", 3 | "ipfsGateway": "https://ipfs.io/ipfs/:hash", 4 | "tangleExplorer": { 5 | "messages": "TANGLE-EXPLORER-MESSAGES" 6 | }, 7 | "googleAnalyticsId": "GOOGLE-ANALYTICS-ID", 8 | "maxBytes": MAX-BYTES 9 | } -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger-archive/poc-ipfs/0f5aee9ee146630e96f507bd800abad0acd611f9/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger-archive/poc-ipfs/0f5aee9ee146630e96f507bd800abad0acd611f9/client/public/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /client/public/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger-archive/poc-ipfs/0f5aee9ee146630e96f507bd800abad0acd611f9/client/public/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /client/public/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger-archive/poc-ipfs/0f5aee9ee146630e96f507bd800abad0acd611f9/client/public/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /client/public/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #ffffff 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /client/public/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger-archive/poc-ipfs/0f5aee9ee146630e96f507bd800abad0acd611f9/client/public/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /client/public/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger-archive/poc-ipfs/0f5aee9ee146630e96f507bd800abad0acd611f9/client/public/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /client/public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger-archive/poc-ipfs/0f5aee9ee146630e96f507bd800abad0acd611f9/client/public/favicon/favicon.ico -------------------------------------------------------------------------------- /client/public/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotaledger-archive/poc-ipfs/0f5aee9ee146630e96f507bd800abad0acd611f9/client/public/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /client/public/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "IOTA IPFS", 3 | "short_name": "IOTA IPFS", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | IOTA IPFS 20 | 21 | 22 | 23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /client/src/app/App.tsx: -------------------------------------------------------------------------------- 1 | import "iota-css-theme"; 2 | import { Footer, FoundationDataHelper, GoogleAnalytics, Header, LayoutAppSingle, SideMenu, StatusMessage } from "iota-react-components"; 3 | import React, { Component, ReactNode } from "react"; 4 | import { Link, Route, RouteComponentProps, Switch, withRouter } from "react-router-dom"; 5 | import logo from "../assets/logo.svg"; 6 | import { ServiceFactory } from "../factories/serviceFactory"; 7 | import { IConfiguration } from "../models/config/IConfiguration"; 8 | import { ConfigurationService } from "../services/configurationService"; 9 | import { IpfsService } from "../services/ipfsService"; 10 | import { TangleExplorerService } from "../services/tangleExplorerService"; 11 | import { AppState } from "./AppState"; 12 | import RetrieveFile from "./routes/RetrieveFile"; 13 | import { RetrieveFileParams } from "./routes/RetrieveFileParams"; 14 | import UploadFile from "./routes/UploadFile"; 15 | 16 | /** 17 | * Main application class. 18 | */ 19 | class App extends Component { 20 | /** 21 | * The configuration for the app. 22 | */ 23 | private _configuration?: IConfiguration; 24 | 25 | /** 26 | * Create a new instance of App. 27 | * @param props The props. 28 | */ 29 | constructor(props: any) { 30 | super(props); 31 | 32 | this.state = { 33 | isBusy: true, 34 | status: "Loading Configuration...", 35 | statusColor: "info", 36 | isSideMenuOpen: false 37 | }; 38 | } 39 | 40 | /** 41 | * The component mounted. 42 | */ 43 | public async componentDidMount(): Promise { 44 | try { 45 | this.setState({ foundationData: await FoundationDataHelper.loadData() }); 46 | 47 | const configService = new ConfigurationService(); 48 | const configId = process.env.REACT_APP_CONFIG_ID || "local"; 49 | const config = await configService.load(`/data/config.${configId}.json`); 50 | 51 | ServiceFactory.register("configuration", () => configService); 52 | ServiceFactory.register("ipfs", () => new IpfsService(config.ipfsGateway)); 53 | ServiceFactory.register("tangleExplorer", () => new TangleExplorerService(config.tangleExplorer)); 54 | 55 | this._configuration = config; 56 | 57 | this.setState({ 58 | isBusy: false, 59 | status: "", 60 | statusColor: "success" 61 | }); 62 | } catch (err) { 63 | this.setState({ 64 | isBusy: false, 65 | status: err.message, 66 | statusColor: "danger" 67 | }); 68 | } 69 | } 70 | 71 | /** 72 | * Render the component. 73 | * @returns The node to render. 74 | */ 75 | public render(): ReactNode { 76 | return ( 77 | 78 |
this.setState({ isSideMenuOpen: !this.state.isSideMenuOpen })} 84 | hamburgerMediaQuery="tablet-up-hidden" 85 | /> 86 | 90 | this.setState({ isSideMenuOpen: false })} 93 | history={this.props.history} 94 | items={[ 95 | { 96 | name: "IOTA IPFS", 97 | isExpanded: true, 98 | items: [ 99 | { 100 | items: [ 101 | { 102 | name: "Upload File", 103 | link: "/" 104 | }, 105 | { 106 | name: "Retrieve File", 107 | link: "/retrieve" 108 | } 109 | ] 110 | } 111 | ] 112 | } 113 | ]} 114 | selectedItemLink={this.props.location.pathname} 115 | mediaQuery="tablet-up-hidden" 116 | /> 117 |
118 | 119 | 124 | {!this.state.status && ( 125 | 126 | ()} /> 127 | ) => 131 | ()} 132 | /> 133 | 134 | )} 135 | 136 |
137 |