diff --git a/.gitignore b/.gitignore index 1f7a204..0c31d6e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ build/ node_modules/ .env .vscode/settings.json +org1.example.com + diff --git a/README.md b/README.md index ef8b007..d497bd3 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,136 @@ -# Kinto-storage-service +
-## Run + logo +

Kinto storage service

+ +
+ UADE Informatics Engineering thesis project - 2022 +
-`npm run start` -TODO: Add watch mode for dev +

+ Kinto storage service, handles strorage and authentication operations. +

+ +

+ Documentation + · + Report Bug + · + Request Feature +

+
-### first version of ipfs connection +
-start ipfs node in your computer with -`jsipfs daemon` +# :notebook_with_decorative_cover: Table of Contents -make sure it's reachable (`localhost:5001/webui`) and, if not, configure CORS properly (there are instructions there) +- [About the Project](#star2-about-the-project) + - [Tech Stack](#space_invader-tech-stack) + - [Features](#dart-features) +- [Setup](#Setup) + - [Prerequisites](#bangbang-prerequisites) +- [Usage](#eyes-usage) +- [License](#warning-license) +- [Contact](#handshake-contact) +- [Acknowledgements](#gem-acknowledgements) -have fun +## :star2: About the Project -## Docs +This service manages storage operatiuons, hyperledger blockchain connection, auuthentication and interactions, and IPFS network operations. -TODO: Add Swagger support +### :space_invader: Tech Stack -## Reference + -https://www.toptal.com/express-js/nodejs-typescript-rest-api-pt-1 +### :dart: Features + +- Authenticates users with hyperledger blockchain +- Stoores and retreives files from IPFS network +- Submit operations into hyperledger blockchain + +## :toolbox: Setup + +### :bangbang: Prerequisites + +- **Node** This project uses node and npm as package manager, make sure it is installed. + +```bash + node -v + npm -v +``` + +- **ipfs** This project uses IPFS to store files connect to an IPFS network or create your own by running the following command + +```bash +jsipfs daemon +``` + +### :key: Environment Variables + +To run this project, you will need to add the following environment variables to your .env file, here it is an example of a localhost configuiration + +```bash +PORT=8081 + +ENV=localhost +CCP_PATH=./org1.example.com/connection-org1.json +WALLET_PATH=./wallet +ADMIN_WALLET=admin +ADMIN_WALLET_SECRET=adminpw +CA_ORG_ID=ca.org1.example.com + +IPFS_API=http://127.0.0.1:5001/api/v0 +``` + +## :gear: Usage + +Clone the project + +```bash + git clone https://github.com/K-nto/Kinto-network-status-service.git +``` + +Go to the project directory + +```bash + cd Kinto-network-status-service +``` + +Install dependencies. + +```bash + npm install +``` + +Start the service. + +```bash + npm run start +``` + +## :warning: License + +Distributed under the no License. See LICENSE.txt for more information. + + + +## :handshake: Contact + +Federico Javier Parodi - Fedejp - [Linkedin](https://www.linkedin.com/in/fedejp) - [Github](https://github.com/Fedejp) + +Carlos Santiago Yanzon - Bizk - [Linkedin](https://www.linkedin.com/in/carlos-santiago-yanzon/) - [Github](https://github.com/bizk) + +Project Link: [https://github.com/K-nto](https://github.com/K-nto) + +## :gem: Acknowledgements + +We thank and aknowledge the authors of these resources for their work. + +- [Awesome README](https://github.com/matiassingers/awesome-readme) +- [Emoji Cheat Sheet](https://github.com/ikatyang/emoji-cheat-sheet/blob/master/README.md#travel--places) +- [Readme Template](https://github.com/othneildrew/Best-README-Template) diff --git a/resources/Kintoisologo.png b/resources/Kintoisologo.png new file mode 100644 index 0000000..83cb207 Binary files /dev/null and b/resources/Kintoisologo.png differ diff --git a/src/common/authorization.service.ts b/src/common/authorization.service.ts new file mode 100644 index 0000000..b5c4459 --- /dev/null +++ b/src/common/authorization.service.ts @@ -0,0 +1,18 @@ +import {testUsers} from '../users/testUsers'; + +/** Returns true if the request is originated by a valid public/private key pair + * Probably better in the HL Authenticator class + */ +export const authorized = (userId: string, authToken: string = '') => { + // @TODO: authorize for realsies + // call HL authenticator here or sth + return ( + authToken && + testUsers.find(user => user.address === userId) && + userId == authToken + ); +}; + +export const getUserInfo = (walletAddress: string) => { + return testUsers.find(user => user.address === walletAddress); +}; diff --git a/src/files/files.controller.ts b/src/files/files.controller.ts index 04de24f..bf63501 100644 --- a/src/files/files.controller.ts +++ b/src/files/files.controller.ts @@ -2,9 +2,10 @@ import fileUpload from 'express-fileupload'; import {StorageOperationController} from '../hyperledger/StorageOperationController'; import ipfsService from '../ipfs/ipfs.service'; import {KFSEntry} from './files.interfaces'; +import {IEncryptedFile} from './interfaces/IEncryptedFile.interface'; class FilesController { - async createFile(userId: string, file: fileUpload.UploadedFile) { + async createFile(userId: string, file: IEncryptedFile) { console.log( `[INFO] files.controller - createFile: Create file for user with id ${userId}` ); diff --git a/src/files/files.routes.config.ts b/src/files/files.routes.config.ts index 7a63c4a..f247694 100644 --- a/src/files/files.routes.config.ts +++ b/src/files/files.routes.config.ts @@ -1,8 +1,10 @@ import {Application, Request, Response, NextFunction} from 'express'; import fileUpload from 'express-fileupload'; +import {authorized} from '../common/authorization.service'; import {CommonRoutesConfig} from '../common/common.routes.config'; import {FILES, USERS} from '../common/common.routes.consts'; import filesController from './files.controller'; +import {IEncryptedFile} from './interfaces/IEncryptedFile.interface'; export class FilesRoutes extends CommonRoutesConfig { constructor(app: Application) { @@ -14,12 +16,20 @@ export class FilesRoutes extends CommonRoutesConfig { .route(`/${USERS}/:userId/${FILES}`) .all((req: Request, res: Response, next: NextFunction) => { // Middleware executed on every route. @TODO: Validation @TODO: User authentication @TODO: Register on Hyperledger - console.debug('[Hyperledeger] Authenticated user: ', req.params.userId); + if (!authorized(req.params.userId, req.headers.authorization)) { + console.debug( + '[Authorization] Failed authentication for user: ', + req.params.userId + ); + res.status(403).send('Invalid credentials'); + return; + } console.debug( - '[Hyperledeger] Generated transaction: ', - Math.random().toString(16).substr(16) + '[Authorization] Authenticated user: ', + req.params.userId ); + next(); }) .get((req: Request, res: Response) => @@ -31,11 +41,18 @@ export class FilesRoutes extends CommonRoutesConfig { ) ) .post((req: Request, res: Response) => { - const file = req.files?.file; + const file: IEncryptedFile = {...req.body}; - if (!file) res.status(400).send('Missing file'); + if ( + !file || + !file.address || + !file.fileData || + !file.name || + !file.type + ) + return res.status(400).send('Missing file'); - filesController + return filesController .createFile(req.params.userId, file) .then(result => res.status(201).send(result)) .catch(error => @@ -85,7 +102,7 @@ export class FilesRoutes extends CommonRoutesConfig { const userId = req.body.userId; if (!userId) res.status(400).send('Missing userId'); - filesController + return filesController .deleteFile(userId, fileName) .then(message => res.status(200).send(message)) .catch(error => diff --git a/src/files/interfaces/IEncryptedFile.interface.ts b/src/files/interfaces/IEncryptedFile.interface.ts new file mode 100644 index 0000000..f6a70a5 --- /dev/null +++ b/src/files/interfaces/IEncryptedFile.interface.ts @@ -0,0 +1,6 @@ +export interface IEncryptedFile { + name: string; + type: string; + address: string; + fileData: string; +} diff --git a/src/hyperledger/HyperledgerController.ts b/src/hyperledger/HyperledgerController.ts index 5980ce8..fcb518a 100644 --- a/src/hyperledger/HyperledgerController.ts +++ b/src/hyperledger/HyperledgerController.ts @@ -78,7 +78,7 @@ export class HyperledgerController { ); const resultBytes = transactionArgs.length > 0 - ? await contract.evaluateTransaction(transaction, ...transactionArgs) + ? await contract.submitTransaction(transaction, ...transactionArgs) : await contract.evaluateTransaction(transaction); // eslint-disable-next-line node/no-unsupported-features/node-builtins diff --git a/src/hyperledger/StorageOperationController.ts b/src/hyperledger/StorageOperationController.ts index 824da37..7b4b3a2 100644 --- a/src/hyperledger/StorageOperationController.ts +++ b/src/hyperledger/StorageOperationController.ts @@ -54,7 +54,7 @@ export class StorageOperationController { 'kinto', 'createFileOperation', fileHash, - walletAddress, + walletAddress.toString(), operation ); console.log('[DEBUG] createFileOperation:', transactionResultPayload); diff --git a/src/ipfs/ipfs.service.ts b/src/ipfs/ipfs.service.ts index fc1e26c..2dc3ebf 100644 --- a/src/ipfs/ipfs.service.ts +++ b/src/ipfs/ipfs.service.ts @@ -3,9 +3,10 @@ import {MFSEntry} from 'ipfs-core-types/src/files'; import {create, IPFSHTTPClient} from 'ipfs-http-client'; import all from 'it-all'; import {KFSEntry} from '../files/files.interfaces'; -/** - * @TODO: Better error handling - */ +import {IEncryptedFile} from '../files/interfaces/IEncryptedFile.interface'; + +const IPFS_DEFAULT_URL = 'http://127.0.0.1:5001/api/v0'; + class IPFSService { private static ipfsHttpClient: IPFSHTTPClient; /** @@ -17,7 +18,7 @@ class IPFSService { // @TBD: Network url if (!IPFSService.ipfsHttpClient) IPFSService.ipfsHttpClient = create({ - url: 'http://127.0.0.1:5002/api/v0', + url: process.env.IPFS_API ?? IPFS_DEFAULT_URL, }); } @@ -33,14 +34,17 @@ class IPFSService { */ public async createFile( userId: string, - file: UploadedFile + file: IEncryptedFile ): Promise { const filePath = `/${file.name}`; - console.log('[DEBUG] IPFSService - createFile', userId, file); - - await IPFSService.ipfsHttpClient.files.write(filePath, file.data, { - create: true, - }); + console.log('[DEBUG] IPFSService - createFile', userId, file.name); + await IPFSService.ipfsHttpClient.files.write( + filePath, + JSON.stringify(file), + { + create: true, + } + ); return await this.listFiles(filePath); } @@ -70,8 +74,7 @@ class IPFSService { for await (const chunk of IPFSService.ipfsHttpClient.cat(cid)) { fileChunks.push(chunk); } - return Buffer.concat(fileChunks); - // return Buffer.from(fileChunks.toString()) + return Buffer.concat(fileChunks).toString().split('}')[0].concat('}'); } /** * Overwrites a file that already exists (AKA, has the same name) in the root dir diff --git a/src/nodes/nodes.routes.config.ts b/src/nodes/nodes.routes.config.ts deleted file mode 100644 index 06fea7c..0000000 --- a/src/nodes/nodes.routes.config.ts +++ /dev/null @@ -1,41 +0,0 @@ -import {Application, Request, Response, NextFunction} from 'express'; -import {CommonRoutesConfig} from '../common/common.routes.config'; -import {NODES, USERS} from '../common/common.routes.consts'; - -export class NodesRoutes extends CommonRoutesConfig { - constructor(app: Application) { - super(app, 'NodesRoutes'); - } - configureRoutes() { - this.app - .route(`/${USERS}/:userId/${NODES}/`) - .all((req: Request, res: Response, next: NextFunction) => { - // Middleware executed on every route. @TODO: Validation @TODO: Authentication @TODO: Hyperledger (some) - next(); - }) - .get((req: Request, res: Response) => { - res.status(200).send(`TODO: GET node List for ${req.params.userId}`); - }) - .post((req: Request, res: Response) => { - res.status(200).send(`TODO: POST new node for ${req.params.userId}`); - }); - - this.app - .route(`/${USERS}/:userId/${NODES}/:nodeId`) - .all((req: Request, res: Response, next: NextFunction) => { - // Middleware executed on every route. @TODO: Validation @TODO: Authentication @TODO: Hyperledger (some) - next(); - }) - .get((req: Request, res: Response) => { - res.status(200).send(`TODO: GET node info for ${req.params.nodeId}`); - }) - .patch((req: Request, res: Response) => { - res.status(200).send(`TODO: PATCH node info for ${req.params.nodeId}`); - }) - .delete((req: Request, res: Response) => { - res.status(200).send(`TODO: DELETE node info for ${req.params.nodeId}`); - }); - - return this.app; - } -} diff --git a/src/service.ts b/src/service.ts index 1f9eb41..579b21e 100644 --- a/src/service.ts +++ b/src/service.ts @@ -2,7 +2,6 @@ import debug from 'debug'; import {CommonRoutesConfig} from './common/common.routes.config'; import {UsersRoutes} from './users/users.routes.config'; import {FilesRoutes} from './files/files.routes.config'; -import {NodesRoutes} from './nodes/nodes.routes.config'; import express from 'express'; import * as http from 'http'; import {HyperledgerController} from './hyperledger/HyperledgerController'; @@ -24,7 +23,6 @@ app.use(cors()); routes.push(new UsersRoutes(app)); routes.push(new FilesRoutes(app)); -routes.push(new NodesRoutes(app)); const runningMessage = `Server running at http://localhost:${port}`; app.get('/', (req: express.Request, res: express.Response) => { diff --git a/src/users/testUsers.ts b/src/users/testUsers.ts new file mode 100644 index 0000000..1236c7e --- /dev/null +++ b/src/users/testUsers.ts @@ -0,0 +1,20 @@ +export const testUsers = [ + { + address: 'asd123', + name: 'Jazmin', + availableSpace: 0, + usedSpace: 0, + }, + { + address: 'qwe456', + name: 'Santi', + availableSpace: 25, + usedSpace: 10, + }, + { + address: 'zxc789', + name: 'Fede', + availableSpace: 25, + usedSpace: 0, + }, +]; diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index a29386d..04a2482 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -1,4 +1,5 @@ import {HyperledgerController} from './../hyperledger/HyperledgerController'; +import {testUsers} from './testUsers'; //TODO improve error messages export class UsersController { diff --git a/src/users/users.routes.config.ts b/src/users/users.routes.config.ts index ca9e6e2..40cc50c 100644 --- a/src/users/users.routes.config.ts +++ b/src/users/users.routes.config.ts @@ -1,6 +1,7 @@ import {CommonRoutesConfig} from '../common/common.routes.config'; import {Application, Request, Response, NextFunction} from 'express'; import {USERS} from '../common/common.routes.consts'; +import {authorized, getUserInfo} from '../common/authorization.service'; export class UsersRoutes extends CommonRoutesConfig { constructor(app: Application) { @@ -21,10 +22,24 @@ export class UsersRoutes extends CommonRoutesConfig { .route(`/${USERS}/:userId`) .all((req: Request, res: Response, next: NextFunction) => { // Middleware executed on every route. @TODO: Validation @TODO: Authentication + if (!authorized(req.params.userId, req.headers.authorization)) { + console.debug( + '[Authorization] Failed authentication for user: ', + req.params.userId + ); + + res.status(403).send('Invalid credentials'); + return; + } + console.debug( + '[Authorization] Authenticated user: ', + req.params.userId + ); + next(); }) .get((req: Request, res: Response) => { - res.status(200).send(`TODO: GET user info for ${req.params.userId}`); + res.status(200).send(getUserInfo(req.params.userId)); }) .patch((req: Request, res: Response) => { res diff --git a/wallet/wallet/admin.id b/wallet/wallet/admin.id new file mode 100644 index 0000000..446f041 --- /dev/null +++ b/wallet/wallet/admin.id @@ -0,0 +1 @@ +{"credentials":{"certificate":"-----BEGIN CERTIFICATE-----\nMIIB8zCCAZmgAwIBAgIURzJBHOXU//r+5Gqifz98ahPVzzAwCgYIKoZIzj0EAwIw\ncDELMAkGA1UEBhMCVVMxFzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMQ8wDQYDVQQH\nEwZEdXJoYW0xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMTE2Nh\nLm9yZzEuZXhhbXBsZS5jb20wHhcNMjIwODEwMjMwOTAwWhcNMjMwODEwMjMxNzAw\nWjAhMQ8wDQYDVQQLEwZjbGllbnQxDjAMBgNVBAMTBWFkbWluMFkwEwYHKoZIzj0C\nAQYIKoZIzj0DAQcDQgAEMHQJOWoxKwXdx76xzWI6wC+r+Zzx0EdxFX8TGCTNekYi\neOzLcIx1M9EsbKkUotJm15gaPr6biZVpj8QKuL48WaNgMF4wDgYDVR0PAQH/BAQD\nAgeAMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFKAUdu8PDLO6fKqHKp6d31Wtjt4t\nMB8GA1UdIwQYMBaAFIPR5D/XLQpbe2TWdTU3g3TiGvp6MAoGCCqGSM49BAMCA0gA\nMEUCIQD0V4BSCd/chpGnS2mmoMhktzgO4fDy7XYhes1xMKyDvwIgaL+KZQj0kP+F\nq0Bi29kbjDTBySzeRC1IYPBYTZMQP6c=\n-----END CERTIFICATE-----\n","privateKey":"-----BEGIN PRIVATE KEY-----\r\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg4C3thP8zVapM4xFV\r\nymNSj5CwTRIsP/rGiJD1bIz3LXahRANCAAQwdAk5ajErBd3HvrHNYjrAL6v5nPHQ\r\nR3EVfxMYJM16RiJ47MtwjHUz0SxsqRSi0mbXmBo+vpuJlWmPxAq4vjxZ\r\n-----END PRIVATE KEY-----\r\n"},"mspId":"Org1MSP","type":"X.509","version":1} \ No newline at end of file diff --git a/wallet/wallet/appUser.id b/wallet/wallet/appUser.id new file mode 100644 index 0000000..a2f2210 --- /dev/null +++ b/wallet/wallet/appUser.id @@ -0,0 +1 @@ +{"credentials":{"certificate":"-----BEGIN CERTIFICATE-----\nMIIChDCCAiqgAwIBAgIUCrqo4udDJhYEpxyLDEJd/bSqgSwwCgYIKoZIzj0EAwIw\ncDELMAkGA1UEBhMCVVMxFzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMQ8wDQYDVQQH\nEwZEdXJoYW0xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMTE2Nh\nLm9yZzEuZXhhbXBsZS5jb20wHhcNMjIwODEwMjMwOTAwWhcNMjMwODEwMjMxNzAw\nWjBEMTAwCwYDVQQLEwRvcmcxMA0GA1UECxMGY2xpZW50MBIGA1UECxMLZGVwYXJ0\nbWVudDExEDAOBgNVBAMTB2FwcFVzZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC\nAATDE0yYvGlUb+EHxZcgTnNWUAyIN8O5ryxL71+uOdGeinifsNI7N3DrbV7lA/rd\nD7O28bbj6Z6Uj6QUjmiU/kk7o4HNMIHKMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMB\nAf8EAjAAMB0GA1UdDgQWBBSOAXUCGhPZTwloPbXu/o61LwunNzAfBgNVHSMEGDAW\ngBSD0eQ/1y0KW3tk1nU1N4N04hr6ejBqBggqAwQFBgcIAQReeyJhdHRycyI6eyJo\nZi5BZmZpbGlhdGlvbiI6Im9yZzEuZGVwYXJ0bWVudDEiLCJoZi5FbnJvbGxtZW50\nSUQiOiJhcHBVc2VyIiwiaGYuVHlwZSI6ImNsaWVudCJ9fTAKBggqhkjOPQQDAgNI\nADBFAiEAjLjw0Lm9CX8KpSn1FNmYURJ2ylfbzWedWcYIubn5QH4CIASbrdwffY4H\n1XIOw2c40CqlCkNEQPPsajyb6gtL2nTg\n-----END CERTIFICATE-----\n","privateKey":"-----BEGIN PRIVATE KEY-----\r\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg0CNIr4cG0hdixxZJ\r\nnQTL4HwrnBSsLpvqQAH4r3s8FrChRANCAATDE0yYvGlUb+EHxZcgTnNWUAyIN8O5\r\nryxL71+uOdGeinifsNI7N3DrbV7lA/rdD7O28bbj6Z6Uj6QUjmiU/kk7\r\n-----END PRIVATE KEY-----\r\n"},"mspId":"Org1MSP","type":"X.509","version":1} \ No newline at end of file