diff --git a/package.json b/package.json index e4998ad7..c4b60e52 100644 --- a/package.json +++ b/package.json @@ -52,11 +52,13 @@ "dependencies": { "@babel/runtime": "^7.16.7", "@verida/did-client": "^2.0.0-rc3", + "@verida/did-document": "^2.0.0-rc3", "@verida/encryption-utils": "^2.0.0-rc1", "aws-serverless-express": "^3.4.0", "cors": "^2.8.5", "did-resolver": "^3.1.0", "dotenv": "^8.2.0", + "ethers": "^5.7.2", "express": "^4.17.1", "express-basic-auth": "git+https://github.com/Mozzler/express-basic-auth.git", "jsonwebtoken": "^8.5.1", diff --git a/sample.env b/sample.env index e799a137..0462b00a 100644 --- a/sample.env +++ b/sample.env @@ -1,4 +1,3 @@ -HASH_KEY=this_is_not_the_prod_hash_key DID_RPC_URL= DID_NETWORK=testnet DID_CACHE_DURATION=3600 @@ -14,15 +13,20 @@ ACCESS_TOKEN_EXPIRY=300 REFRESH_JWT_SIGN_PK=insert-random-refresh-symmetric-key # 30 Days REFRESH_TOKEN_EXPIRY=2592000 -DB_REFRESH_TOKENS="verida_refresh_tokens" -DB_DB_INFO="verida_db_info" +DB_REFRESH_TOKENS=verida_refresh_tokens +DB_DB_INFO=verida_db_info # How often garbage collection runs (1=100%, 0.5 = 50%) GC_PERCENT=0.1 # Verida Private Key as hex string (used to sign responses). Including leading 0x. VDA_PRIVATE_KEY= +# Default maximum number of Megabytes for a storage context +DEFAULT_USER_CONTEXT_LIMIT_MB=10 +# Maximum number of users supported by this node +MAX_USERS=10000 // alpha numeric only DB_PUBLIC_USER=784c2n780c9cn0789 DB_PUBLIC_PASS=784c2n780c9cn0789 +DB_DIDS=verida_dids PORT=5151 diff --git a/src/components/db.js b/src/components/db.js index 566ffdf6..60e821a0 100644 --- a/src/components/db.js +++ b/src/components/db.js @@ -29,6 +29,14 @@ class Db { return env.DB_PROTOCOL + "://" + env.DB_HOST + ":" + env.DB_PORT; } + // Total number of users in the system + async totalUsers() { + const couch = db.getCouch() + const usersDb = couch.db.use('_users') + const info = await usersDb.info() + return info.doc_count + } + } const db = new Db() diff --git a/src/components/dbManager.js b/src/components/dbManager.js index f8003771..bf8ab705 100644 --- a/src/components/dbManager.js +++ b/src/components/dbManager.js @@ -2,6 +2,8 @@ import Utils from './utils.js'; import _ from 'lodash'; import Db from "./db.js" import EncryptionUtils from "@verida/encryption-utils" +import dotenv from 'dotenv'; +dotenv.config(); class DbManager { diff --git a/src/components/userManager.js b/src/components/userManager.js index 1b9326cb..9b56bebb 100644 --- a/src/components/userManager.js +++ b/src/components/userManager.js @@ -1,7 +1,10 @@ import crypto from 'crypto'; -//import jwt from 'jsonwebtoken'; import Db from './db.js' -//import AuthManager from './authManager.js'; +import Utils from './utils.js' +import DbManager from './dbManager.js'; + +import dotenv from 'dotenv'; +dotenv.config(); class UserManager { @@ -27,16 +30,26 @@ class UserManager { } async create(username, signature) { + const maxUsers = parseInt(process.env.MAX_USERS) + const currentUsers = await Db.totalUsers() + + if (currentUsers >= maxUsers) { + throw new Error('Maximum user limit reached') + } + const couch = Db.getCouch() const password = crypto.createHash('sha256').update(signature).digest("hex") + const storageLimit = process.env.DEFAULT_USER_CONTEXT_LIMIT_MB*1048576 + // Create CouchDB database user matching username and password let userData = { _id: `org.couchdb.user:${username}`, name: username, password: password, type: "user", - roles: [] + roles: [], + storageLimit }; let usersDb = couch.db.use('_users'); @@ -53,8 +66,6 @@ class UserManager { } } - - /** * Ensure we have a public user in the database for accessing public data */ @@ -102,6 +113,29 @@ class UserManager { } } + async getUsage(did, contextName) { + const username = Utils.generateUsername(did, contextName); + const user = await this.getByUsername(username); + const databases = await DbManager.getUserDatabases(did, contextName) + + const result = { + databases: 0, + bytes: 0, + storageLimit: user.storageLimit + } + + for (let d in databases) { + const database = databases[d] + const dbInfo = await DbManager.getUserDatabase(did, contextName, database.databaseName) + result.databases++ + result.bytes += dbInfo.info.sizes.file + } + + const usage = result.bytes / parseInt(result.storageLimit) + result.usagePercent = Number(usage.toFixed(4)) + return result + } + } let userManager = new UserManager(); diff --git a/src/controllers/auth.js b/src/controllers/auth.js index 50477f36..7c93503b 100644 --- a/src/controllers/auth.js +++ b/src/controllers/auth.js @@ -51,13 +51,20 @@ class AuthController { // Create the user if they don't exist if (!user) { - const response = await UserManager.create(username, signature); - if (!response || !response.id) { + try { + const response = await UserManager.create(username, signature); + if (!response || !response.id) { + return res.status(500).send({ + status: "fail", + data: { + "auth": "User does not exist and unable to create" + } + }) + } + } catch (err) { return res.status(500).send({ - status: "fail", - data: { - "auth": "User does not exist and unable to create" - } + status: 'fail', + message: err.message }) } } @@ -84,6 +91,15 @@ class AuthController { async connect(req, res) { const refreshToken = req.body.refreshToken; const contextName = req.body.contextName; + const did = req.body.did + + const userUsage = await UserManager.getUsage(did, contextName) + if (userUsage.usagePercent >= 100) { + return res.status(400).send({ + status: "fail", + message: 'Storage limit reached' + }); + } const accessToken = await AuthManager.generateAccessToken(refreshToken, contextName); @@ -92,7 +108,6 @@ class AuthController { status: "success", accessToken, host: Db.buildHost() // required to know the CouchDB host - // username: removed, don't think it is needed }); } else { diff --git a/src/controllers/system.js b/src/controllers/system.js new file mode 100644 index 00000000..bb33542c --- /dev/null +++ b/src/controllers/system.js @@ -0,0 +1,28 @@ +import db from '../components/db' +import Utils from '../components/utils' +import packageJson from '../../package.json' + +import dotenv from 'dotenv'; +dotenv.config(); + +class SystemController { + + async status(req, res) { + const currentUsers = await db.totalUsers() + + const results = { + maxUsers: parseInt(process.env.MAX_USERS), + currentUsers, + version: packageJson.version + } + + return Utils.signedResponse({ + status: "success", + results + }, res); + } + +} + +const systemController = new SystemController(); +export default systemController; \ No newline at end of file diff --git a/src/controllers/user.js b/src/controllers/user.js index f3c334a4..71ffd82a 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -1,4 +1,5 @@ import DbManager from '../components/dbManager.js'; +import UserManager from '../components/userManager.js'; import Utils from '../components/utils.js'; import Db from '../components/db.js' @@ -37,6 +38,14 @@ class UserController { }); } + const userUsage = await UserManager.getUsage(did, contextName) + if (userUsage.usagePercent >= 100) { + return res.status(400).send({ + status: "fail", + message: 'Storage limit reached' + }); + } + const databaseHash = Utils.generateDatabaseName(did, contextName, databaseName) let success; @@ -148,6 +157,14 @@ class UserController { const databaseHash = Utils.generateDatabaseName(did, contextName, databaseName) try { + const userUsage = await UserManager.getUsage(did, contextName) + if (userUsage.usagePercent >= 100) { + return res.status(400).send({ + status: "fail", + message: 'Storage limit reached' + }); + } + let success = await DbManager.updateDatabase(username, databaseHash, contextName, options); if (success) { await DbManager.saveUserDatabase(did, contextName, databaseName, databaseHash, options.permissions) @@ -245,19 +262,7 @@ class UserController { } try { - const databases = await DbManager.getUserDatabases(did, contextName) - - const result = { - databases: 0, - bytes: 0 - } - - for (let d in databases) { - const database = databases[d] - const dbInfo = await DbManager.getUserDatabase(did, contextName, database.databaseName) - result.databases++ - result.bytes += dbInfo.info.sizes.file - } + const result = await UserManager.getUsage(did, contextName) return Utils.signedResponse({ status: "success", diff --git a/src/routes/index.js b/src/routes/private.js similarity index 100% rename from src/routes/index.js rename to src/routes/private.js diff --git a/src/routes/public.js b/src/routes/public.js new file mode 100644 index 00000000..18d372d9 --- /dev/null +++ b/src/routes/public.js @@ -0,0 +1,18 @@ +import express from 'express'; +import UserController from '../controllers/user.js'; +import AuthController from '../controllers/auth.js'; +import SystemController from '../controllers/system.js'; + +const router = express.Router(); + +// Specify public endpoints +router.get('/auth/public', UserController.getPublic); +router.get('/status', SystemController.status); +router.post('/auth/generateAuthJwt', AuthController.generateAuthJwt); +router.post('/auth/authenticate', AuthController.authenticate); +router.post('/auth/connect', AuthController.connect); +router.post('/auth/regenerateRefreshToken', AuthController.regenerateRefreshToken); +router.post('/auth/invalidateDeviceId', AuthController.invalidateDeviceId); +router.post('/auth/isTokenValid', AuthController.isTokenValid); + +export default router; \ No newline at end of file diff --git a/src/server-app.js b/src/server-app.js index e53616b1..ea5229aa 100644 --- a/src/server-app.js +++ b/src/server-app.js @@ -3,12 +3,14 @@ import cors from 'cors'; import dotenv from 'dotenv'; import basicAuth from 'express-basic-auth'; -import router from './routes/index.js'; +import privateRoutes from './routes/private.js'; +import publicRoutes from './routes/public.js'; +import didStorageRoutes from './services/didStorage/routes' + import requestValidator from './middleware/requestValidator.js'; import userManager from './components/userManager.js'; -import UserController from './controllers/user.js'; -import AuthController from './controllers/auth.js'; import AuthManager from './components/authManager.js'; +import didUtils from './services/didStorage/utils' dotenv.config(); @@ -19,24 +21,18 @@ let corsConfig = { //origin: process.env.CORS_HOST }; + // Parse incoming requests data app.use(cors(corsConfig)); app.use(express.urlencoded({ extended: false })); app.use(express.json()); - -// Specify public endpoints -app.get('/auth/public', UserController.getPublic); -app.post('/auth/generateAuthJwt', AuthController.generateAuthJwt); -app.post('/auth/authenticate', AuthController.authenticate); -app.post('/auth/connect', AuthController.connect); -app.post('/auth/regenerateRefreshToken', AuthController.regenerateRefreshToken); -app.post('/auth/invalidateDeviceId', AuthController.invalidateDeviceId); -app.post('/auth/isTokenValid', AuthController.isTokenValid); - +app.use(didStorageRoutes); +app.use(publicRoutes); app.use(requestValidator); -app.use(router); +app.use(privateRoutes); AuthManager.initDb(); userManager.ensureDefaultDatabases(); +didUtils.createDb() export default app; diff --git a/src/services/didStorage/controller.js b/src/services/didStorage/controller.js new file mode 100644 index 00000000..6547cbc6 --- /dev/null +++ b/src/services/didStorage/controller.js @@ -0,0 +1,200 @@ +import { DIDDocument } from "@verida/did-document" +import { now } from "lodash" +import Utils from './utils' + +class DidStorage { + + async create(req, res) { + // Verify request parameters + if (!req.params.did) { + return Utils.error(res, `No DID specified`) + } + + if (!req.body.document) { + return Utils.error(res, `No document specified`) + } + + const did = req.params.did.toLowerCase() + const didDocument = new DIDDocument(req.body.document) + const jsonDoc = didDocument.export() + + try { + Utils.verifyDocument(did, didDocument, { + versionId: 0 + }) + } catch (err) { + console.error(err.message) + return Utils.error(res, `Invalid DID Document: ${err.message}`) + } + + // @todo Ensure there is currently no entry for the given DID in the DID Registry + // OR + // there is currently an entry and it references this storage node endpoint + + // Save the DID document + const didDb = Utils.getDidDocumentDb() + + // Create CouchDB database user matching username and password + const documentData = { + _id: `${jsonDoc.id}-0`, + ...jsonDoc + }; + + try { + await didDb.insert(documentData); + return Utils.success(res, {}); + } catch (err) { + if (err.error == 'conflict') { + return Utils.error(res, `DID Document already exists. Use PUT request to update.`) + } + + return Utils.error(res, `Unknown error: ${err.message}`) + } + } + + async update(req, res) { + // Verify request parameters + if (!req.params.did) { + return Utils.error(res, `No DID specified`) + } + + if (!req.body.document) { + return Utils.error(res, `No document specified`) + } + + const did = req.params.did.toLowerCase() + const didDocument = new DIDDocument(req.body.document) + const jsonDoc = didDocument.export() + + let existingDoc + try { + existingDoc = await Utils.getDidDocument(did) + + if (!existingDoc) { + return Utils.error(res, `DID Document not found`) + } + + const nextVersionId = existingDoc.versionId + 1 + Utils.verifyDocument(did, didDocument, { + created: existingDoc.created, + versionId: nextVersionId + }) + + if (existingDoc.updated > jsonDoc.updated) { + return Utils.error(res, `updated must be after the current document`) + } + } catch (err) { + return Utils.error(res, `Invalid DID Document: ${err.message}`) + } + + // @ todo Ensure there is currently no entry for the given DID in the DID Registry + // OR + // there is currently an entry and it references this storage node endpoint + + const didDb = Utils.getDidDocumentDb() + + // Create CouchDB database user matching username and password + const documentData = { + _id: `${jsonDoc.id}-${jsonDoc.versionId}`, + ...jsonDoc + }; + + try { + await didDb.insert(documentData); + return Utils.success(res, {}); + } catch (err) { + /*if (err.error == 'conflict') { + return Utils.error(res, `DID Document already exists. Use PUT request to update.`) + }*/ + + return Utils.error(res, `Unknown error: ${err.message}`) + } + } + + async delete(req, res) { + if (!req.params.did) { + return Utils.error(res, `No DID specified`) + } + + if (!req.headers.signature) { + return Utils.error(res, `No signature specified`) + } + + const did = req.params.did.toLowerCase() + const signature = req.headers.signature + const versionResponse = await Utils.getDidDocument(did, true, false) + + + if (!versionResponse || !versionResponse.versions || versionResponse.versions.length === 0) { + return Utils.error(res, `DID Document not found`) + } + + // Verify signature with the most recent DID document + const didDocument = new DIDDocument(versionResponse.versions[0]) + let validSig = false + const nowInMinutes = Math.round((new Date()).getTime() / 1000 / 60) + for (let i=-1; i<=1; i++) { + const proofString = `Delete DID Document ${did} at ${nowInMinutes+i}` + if (didDocument.verifySig(proofString, signature)) { + validSig = true + break + } + } + + if (!validSig) { + return Utils.error(res, `Invalid signature`) + } + + const didDocuments = versionResponse.versions + const didDb = Utils.getDidDocumentDb() + const docs = [] + + didDocuments.forEach(doc => { + docs.push({ + _id: doc._id, + _rev: doc._rev, + _deleted: true + }) + }) + + const deleteResponse = await didDb.bulk({ + docs + }) + + return Utils.success(res, { + did, + revisions: docs.length + }); + } + + async get(req, res) { + const did = req.params.did.toLowerCase() + const allVersions = req.query.allVersions && req.query.allVersions === 'true' + + const result = await Utils.getDidDocument(did, allVersions) + if (!result) { + return Utils.error(res, `DID Document not found.`, 404) + } + + return Utils.success(res, result) + } + + async migrate(req, res) { + return Utils.error(res, `Not implemented (yet)`, 404) + /* + const did = req.params.did + + // @todo: Verify signature + + return res.status(200).send({ + status: "success-migrate", + data: { + "did": did + } + });*/ + } + +} + +const didStorage = new DidStorage(); +export default didStorage; \ No newline at end of file diff --git a/src/services/didStorage/routes.js b/src/services/didStorage/routes.js new file mode 100644 index 00000000..6414da7f --- /dev/null +++ b/src/services/didStorage/routes.js @@ -0,0 +1,13 @@ +import express from 'express'; +import DidStorageController from './controller.js'; + +const router = express.Router(); + +// Specify public endpoints +router.post('/did/:did/migrate', DidStorageController.migrate); +router.post('/did/:did', DidStorageController.create); +router.put('/did/:did', DidStorageController.update); +router.delete('/did/:did', DidStorageController.delete); +router.get('/did/:did', DidStorageController.get); + +export default router; \ No newline at end of file diff --git a/src/services/didStorage/utils.js b/src/services/didStorage/utils.js new file mode 100644 index 00000000..2db1ecfa --- /dev/null +++ b/src/services/didStorage/utils.js @@ -0,0 +1,157 @@ +import Db from "../../components/db.js" +import dotenv from 'dotenv'; +dotenv.config(); + +class Utils { + + // @todo + verifyDocument(did, document, expectedValues = {}) { + const doc = document.export() + if (doc.id != did) { + throw new Error(`DID must match ID in the DID Document`) + } + + for (let key in expectedValues) { + if (doc[key] != expectedValues[key]) { + throw new Error(`Incorrect value for ${key} (Expected ${expectedValues[key]})`) + } + } + + const requiredFields = ['versionId', 'created', 'updated', 'proof'] + requiredFields.forEach(field => { + if (!doc.hasOwnProperty(field)) { + throw new Error(`Missing required field (${field})`) + } + }) + + if (typeof(doc.versionId) !== 'number') { + throw new Error(`versionId must be a number`) + } + + // ie: 2020-12-20T19:17:47Z + // @see https://www.w3.org/TR/did-core/#did-document-metadata + if (!Date.parse(doc.created) || document.buildTimestamp(new Date(Date.parse(doc.created))) != doc.created) { + console.log(new Date(Date.parse(doc.created))) + console.log(doc.created) + throw new Error(`created must be a valid timestamp`) + } + + // ie: 2020-12-20T19:17:47Z + // @see https://www.w3.org/TR/did-core/#did-document-metadata + if (!Date.parse(doc.updated) || document.buildTimestamp(new Date(Date.parse(doc.updated))) != doc.updated) { + throw new Error(`created must be a valid timestamp`) + } + + if (doc.deactivated && typeof(doc.deactivated) !== 'boolean') { + throw new Error(`deactivated must be a valid boolean value`) + } + + if (!document.verifyProof()) { + throw new Error(`Invalid proof`) + } + + return true + } + + async error(res, message, httpStatus=400) { + return res.status(httpStatus).send({ + status: "fail", + message + }) + } + + async success(res, data) { + return res.status(200).send({ + status: "success", + data + }) + } + + getDb() { + return Db.getCouch() + } + + async createDb() { + try { + const couch = this.getDb() + await couch.db.create(process.env.DB_DIDS) + const dbDids = couch.db.use(process.env.DB_DIDS) + await dbDids.createIndex({ + index: { + fields: ['id', 'versionId'] + }, + name: 'did' + }) + } catch (err) { + if (err.message == "The database could not be created, the file already exists.") { + console.log("DID database not created -- already existed"); + } else { + throw err; + } + } + } + + getDidDocumentDb() { + return this.getDb().use(process.env.DB_DIDS); + } + + async getDidDocument(did, allVersions=false, stripCouchMetadata=true) { + const db = this.getDidDocumentDb() + + const query = { + selector: { + id: did + }, + sort: [ + {'versionId': 'desc'} + ], + limit: 1 + } + + const result = await db.find(query) + + if (result.docs.length === 0) { + return + } + + const latestDoc = result.docs[0] + let resultDocs = [latestDoc] + + if (allVersions) { + const latestVersion = latestDoc.versionId + const keys = [] + for (let i = 0; i<=latestVersion; i++) { + keys.push(`${did}-${i}`) + } + + // Fetch all the versions + const allDocs = await db.fetch({ keys }) + resultDocs = allDocs.rows + } + + const docs = resultDocs.map(item => { + if (item.doc) { + item = item.doc + } + + if (stripCouchMetadata) { + delete item['_id'] + delete item['_rev'] + } + + return item + }) + + if (!allVersions) { + return docs[0] + } + + return { + versions: docs + } + } + +} + +const utils = new Utils(); +export default utils; \ No newline at end of file diff --git a/test/did-storage.js b/test/did-storage.js new file mode 100644 index 00000000..1bd22423 --- /dev/null +++ b/test/did-storage.js @@ -0,0 +1,257 @@ +import Axios from 'axios' +import assert from 'assert'; +import { ethers } from 'ethers' +import { DIDDocument } from '@verida/did-document' + +import dotenv from 'dotenv'; +dotenv.config(); + +import Utils from '../src/services/didStorage/utils' +import TestUtils from './utils' +import CONFIG from './config' +const { SERVER_URL } = CONFIG + +const DID_URL = `${SERVER_URL}/did` + +const wallet = ethers.Wallet.createRandom() + +let DID_ADDRESS, DID, DID_PK, DID_PRIVATE_KEY + +DID_ADDRESS = wallet.address +DID = `did:vda:testnet:${DID_ADDRESS}` +DID_PK = wallet.publicKey +DID_PRIVATE_KEY = wallet.privateKey + +let masterDidDoc + +describe("DID Storage Tests", function() { + this.beforeAll(async () => { + console.log('Executing with:') + console.log(`DID: ${DID}`) + console.log(`DID_PUB_KEY: ${DID_PK}`) + console.log(`DID_PRIVATE_KEY: ${DID_PRIVATE_KEY}`) + + // clear DID database + const couch = Utils.getDb() + await couch.db.destroy(process.env.DB_DIDS) + await Utils.createDb() + }) + + describe("Create", () => { + it("Success", async () => { + try { + const doc = new DIDDocument(DID, DID_PK) + doc.signProof(wallet.privateKey) + + const createResult = await Axios.post(`${DID_URL}/${DID}`, { + document: doc.export() + }); + + masterDidDoc = doc.export() + + assert.equal(createResult.data.status, 'success', 'Success response') + } catch (err) { + console.error(err.response.data) + assert.fail(err.response.data.message) + } + }) + + it("Fail - Duplicate DID Document", async () => { + const doc = new DIDDocument(DID, DID_PK) + doc.signProof(wallet.privateKey) + + try { + await Axios.post(`${DID_URL}/${DID}`, { + document: doc.export() + }); + + assert.fail('DID Document was created a second time') + } catch (err) { + assert.equal(err.response.data.status, 'fail', 'DID Document create failed') + assert.ok(err.response.data.message.match('DID Document already exists'), 'Rejected because DID Document already exists') + } + }) + }) + + describe("Update", () => { + it("Fail - Not next versionId", async () => { + const doc = new DIDDocument(DID, DID_PK) + doc.setAttributes({ + created: masterDidDoc.created + }) + + doc.signProof(wallet.privateKey) + + try { + await Axios.put(`${DID_URL}/${DID}`, { + document: doc.export() + }); + + assert.fail('DID Document was updated with invalid versionId') + } catch (err) { + assert.equal(err.response.data.status, 'fail', 'DID Document create failed') + assert.equal(err.response.data.message, 'Invalid DID Document: Incorrect value for versionId (Expected 1)', 'Rejected because incorrect version') + } + }) + + it("Fail - Invalid DID", async () => { + try { + const doc = new DIDDocument(DID, DID_PK) + doc.setAttributes({ + created: masterDidDoc.created + }) + doc.signProof(wallet.privateKey) + + await Axios.put(`${DID_URL}/abc123`, { + document: doc.export() + }); + + assert.fail(`DID Document was found, when it shouldn't have`) + } catch (err) { + assert.equal(err.response.data.status, 'fail', 'Get DID Document failed') + assert.ok(err.response.data.message.match('DID Document not found'), `Rejected because DID Document doesn't exists`) + } + }) + + it("Success", async () => { + const basicDoc = new DIDDocument(DID, DID_PK) + + const document = basicDoc.export() + basicDoc.setAttributes({ + created: masterDidDoc.created, + versionId: document.versionId + 1 + }) + + basicDoc.signProof(wallet.privateKey) + + try { + const createResult = await Axios.put(`${DID_URL}/${DID}`, { + document: basicDoc.export() + }); + + assert.equal(createResult.data.status, 'success', 'Success response') + } catch (err) { + console.error(err.response.data) + assert.fail('Error updating') + } + }) + }) + + describe("Get", () => { + it("Success - Latest", async () => { + const getResult = await Axios.get(`${DID_URL}/${DID}`); + + assert.ok(getResult.data.status, 'Success response') + assert.equal(getResult.data.data.id, DID.toLowerCase(), 'DID mathces') + + // @tgodo: re-build document and compare it matches + //console.log(getResult.data) + }) + + it("Fail - Invalid DID", async () => { + try { + const getResult = await Axios.get(`${DID_URL}/abc123`); + + assert.fail(`DID Document was found, when it shouldn't have`) + } catch (err) { + assert.equal(err.response.data.status, 'fail', 'Get DID Document failed') + assert.ok(err.response.data.message.match('DID Document not found'), `Rejected because DID Document doesn't exists`) + } + }) + + it("Success - All versions", async () => { + const getResult = await Axios.get(`${DID_URL}/${DID}?allVersions=true`); + + assert.ok(getResult.data.status, 'success', 'Success response') + assert.equal(getResult.data.data.versions.length, 2, 'Two versions returned') + assert.equal(getResult.data.data.versions[0].versionId, 0, 'First doc is version 0') + assert.equal(getResult.data.data.versions[1].versionId, 1, 'Second doc is version 1') + }) + + // Get by versionId + // Get by versionTime + }) + + describe("Delete", () => { + let signature + this.beforeAll(() => { + const nowInMinutes = Math.round((new Date()).getTime() / 1000 / 60) + const proofString = `Delete DID Document ${DID.toLowerCase()} at ${nowInMinutes}` + signature = TestUtils.signString(proofString, wallet.privateKey) + }) + + it("Fail - Invalid DID", async () => { + try { + await Axios.delete(`${DID_URL}/abc123`, { + headers: { + signature + } + }); + + assert.fail(`DID Document was found, when it shouldn't have`) + } catch (err) { + assert.equal(err.response.data.status, 'fail', 'Get DID Document failed') + assert.ok(err.response.data.message.match('DID Document not found'), `Rejected because DID Document doesn't exists`) + } + }) + + it("Success", async () => { + const deleteResult = await Axios.delete(`${DID_URL}/${DID}`, { + headers: { + signature + } + }); + + assert.ok(deleteResult.data.status, 'success', 'Success response') + assert.equal(deleteResult.data.data.revisions, 2, 'Two versions deleted') + }) + + it("Fail - Deleted", async () => { + try { + await Axios.delete(`${DID_URL}/${DID}`, { + headers: { + signature + } + }); + + assert.fail(`DID Document was deleted, when it shouldn't have`) + } catch (err) { + assert.equal(err.response.data.status, 'fail', 'Get DID Document failed') + assert.ok(err.response.data.message.match('DID Document not found'), `Rejected because DID Document doesn't exists`) + } + }) + }) + + describe("Create again - After deletion", () => { + it("Success", async () => { + const doc = new DIDDocument(DID, DID_PK) + doc.signProof(wallet.privateKey) + + const createResult = await Axios.post(`${DID_URL}/${DID}`, { + document: doc.export() + }); + + assert.equal(createResult.data.status, 'success', 'Success response') + }) + + it("Success - Document exists", async () => { + const getResult = await Axios.get(`${DID_URL}/${DID}`); + + assert.ok(getResult.data.status, 'success', 'Success response') + + // @tgodo: re-build document and compare it matches + //console.log(getResult.data) + }) + }) + + /*describe("Migrate", () => { + it("Success", async () => { + console.log(`${DID_URL}/${DID}/migrate`) + const createResult = await Axios.post(`${DID_URL}/${DID}/migrate`, { + hello: 'world' + }); + + console.log(createResult.data) + }) + })*/ +}) \ No newline at end of file diff --git a/test/server.js b/test/server.js index e5404b77..4aadd7e1 100644 --- a/test/server.js +++ b/test/server.js @@ -257,6 +257,8 @@ describe("Server tests", function() { const result = response.data.result assert.equal(result.databases, 2, 'Expected number of databases') assert.ok(result.bytes > 0, 'More than 0 bytes used') + assert.ok(result.usagePercent > 0, 'More than 0 percentage usage') + assert.equal(result.storageLimit, process.env.DEFAULT_USER_CONTEXT_LIMIT_MB*1048576, 'Storage limit is 100Mb') }) // @todo: updates @@ -312,7 +314,15 @@ describe("Server tests", function() { assert.ok(response.data.results.indexOf('DeleteAll_3') >= 0, 'Deleted correct databases (DeleteAll_3)') assert.ok(TestUtils.verifySignature(response), 'Have a valid signature in response') }) + }) + describe("Server info", () => { + it("Status", async () => { + const response = await Axios.get(`${SERVER_URL}/status`); + assert.equal(response.data.results.maxUsers, process.env.MAX_USERS, 'Correct maximum number of users') + assert.ok(response.data.results.currentUsers > 2, 'At least two users') + assert.ok(response.data.results.version && response.data.results.version.length, 'Version specified') + }) }) }) \ No newline at end of file diff --git a/test/utils.js b/test/utils.js index 039aa6ee..5f45f116 100644 --- a/test/utils.js +++ b/test/utils.js @@ -2,7 +2,7 @@ import Axios from 'axios' import PouchDb from 'pouchdb' import { AutoAccount } from "@verida/account-node" import { Network } from "@verida/client-ts" -import EncryptionUtils from '@verida/encryption-utils'; +import EncryptionUtils from '@verida/encryption-utils' import { ethers } from 'ethers' import CONFIG from './config.js' @@ -75,6 +75,14 @@ class Utils { return response } + signString(str, privateKey) { + if (privateKey == 'string') { + privateKey = new Uint8Array(Buffer.from(privateKey.substr(2),'hex')) + } + + return EncryptionUtils.signData(str, privateKey) + } + verifySignature(response) { if (!response.data.signature) { return false @@ -84,7 +92,6 @@ class Utils { delete response.data['signature'] return EncryptionUtils.verifySig(response.data, signature, VDA_PUBLIC_KEY) } - } const utils = new Utils() diff --git a/yarn.lock b/yarn.lock index 1441fd42..7ab4fa8d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5069,11 +5069,6 @@ schema-utils@^2.6.5: ajv "^6.12.4" ajv-keywords "^3.5.2" -scrypt-js@2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-2.0.4.tgz#32f8c5149f0797672e551c07e230f834b6af5f16" - integrity sha512-4KsaGcPnuhtCZQCxFxN3GVYIhKFPTdLd8PLC552XwbMndtD0cjRFAhDuuydXQ0h08ZfPgzqe6EKHozpuH74iDw== - scrypt-js@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.1.tgz#d314a57c2aef69d1ad98a138a21fe9eafa9ee312"