diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..26c0388 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# Contributing + +Welcome! We are happy to have your help with this project. + +Note that all contributions will be under our +[license, modeled after SQLite](https://github.com/trentlarson/endorser-ch/blob/master/LICENSE). diff --git a/README.md b/README.md index f2ff62f..8ae5555 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # Image Server +Remaining: +- parameterize bucket for test server +- try American Cloud +- dockerize + +- pretty-up the client, show thumbnail ## setup @@ -28,8 +34,10 @@ node server.js ## test -``` -curl -X POST -F "image=@./test.png" http://localhost:3000/image +```shell +# run this first command in a directory where `npm install did-jwt` has been run +CODE='OWNER_DID="did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F"; OWNER_PRIVATE_KEY_HEX="2b6472c026ec2aa2c4235c994a63868fc9212d18b58f6cbfe861b52e71330f5b"; didJwt = require("did-jwt"); didJwt.createJWT({ exp: Math.floor(Date.now() / 1000) + 60, iat: Math.floor(Date.now() / 1000), iss: OWNER_DID }, { issuer: OWNER_DID, signer: didJwt.SimpleSigner(OWNER_PRIVATE_KEY_HEX) }).then(console.log)' +JWT=`node -e "$CODE"`; curl -X POST -H "Authorization: Bearer $JWT" -F "image=@./test.png" http://localhost:3001/image ``` ## deploy to prod first time diff --git a/package.json b/package.json index a894dab..e5f8c6b 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dotenv": "^16.4.5", "ethr-did-resolver": "^10.1.5", "express": "^4.18.2", + "luxon": "^3.4.4", "multer": "1.4.5-lts.1", "sqlite3": "^5.1.7" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 47959ab..5ca5746 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ dependencies: express: specifier: ^4.18.2 version: 4.18.2 + luxon: + specifier: ^3.4.4 + version: 3.4.4 multer: specifier: 1.4.5-lts.1 version: 1.4.5-lts.1 @@ -2241,6 +2244,11 @@ packages: yallist: 4.0.0 dev: false + /luxon@3.4.4: + resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==} + engines: {node: '>=12'} + dev: false + /make-fetch-happen@9.1.0: resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==} engines: {node: '>= 10'} diff --git a/server.js b/server.js index 3c4d41b..41a8904 100644 --- a/server.js +++ b/server.js @@ -6,6 +6,7 @@ const { Resolver } = require('did-resolver'); const express = require('express'); const { getResolver } = require('ethr-did-resolver'); const fs = require('fs'); +const { DateTime } = require('luxon'); const multer = require('multer'); const path = require('path'); const sqlite3 = require('sqlite3').verbose(); @@ -15,7 +16,7 @@ require('dotenv').config() const app = express(); app.use(cors()); -const port = process.env.PORT || 3000; +const port = process.env.PORT || 3001; // file name also referenced in flyway.conf and potentially in .env files or in environment variables const dbFile = process.env.SQLITE_FILE || './image-db.sqlite'; @@ -34,6 +35,8 @@ const db = new sqlite3.Database(dbFile, (err) => { } }); +const endorserApiUrl = process.env.ENDORSER_API_URL || 'http://localhost:3000'; + // Configure AWS const s3Client = new S3Client({ region: process.env.AWS_REGION, @@ -56,12 +59,81 @@ app.post('/image', uploadMulter.single('image'), async (req, res) => { return res.status(401).send(JSON.stringify({ success: false, message: 'Missing "Bearer JWT" in Authorization header.'})); } const jwt = auth.substring('Bearer '.length); - const verified = await didJwt.verifyJWT(jwt, {resolver}); - if (!verified) { - return res.status(401).send(JSON.stringify({ success: false, message: 'Got invalid JWT in Authorization header.'})); + const verified = await didJwt.verifyJWT(jwt, { resolver }); + if (!verified.verified) { + const errorTime = new Date().toISOString(); + console.log(errorTime, 'Got invalid JWT in Authorization header:', verified); + return res.status(401).send(JSON.stringify({ success: false, message: 'Got invalid JWT in Authorization header. See server logs at ' + errorTime })); } const issuerDid = verified.issuer; + // Check the user's limits, first from the DB and then from the server + let limitPerWeek = await new Promise((resolve, reject) => { + db.get( + 'SELECT per_week FROM user WHERE did = ?', + [ issuerDid ], + (dbErr, row) => { + if (dbErr) { + console.error('Error getting user record from database:', dbErr) + // may not matter, so continue + } + resolve(row?.per_week); + } + ); + }); + if (limitPerWeek == null) { + const headers = { + 'Authorization': `Bearer ${jwt}`, + 'Content-Type': 'application/json' + } + const response = await fetch(endorserApiUrl + '/api/report/rateLimits', { headers }); + if (response.status !== 200) { + console.error("Got bad response of", response.status, "when checking rate limits for", issuerDid); + return res.status(400).send(JSON.stringify({ success: false, message: 'Got bad status of ' + response.status + ' when checking limits with endorser server. Verify that the account exists and that the JWT works for that server.'})); + } else { + const body = await response.json(); + limitPerWeek = body.maxClaimsPerWeek + + await new Promise((resolve, reject) => { + db.run( + 'INSERT INTO user (did, per_week) VALUES (?, ?)', + [issuerDid, limitPerWeek], + (dbErr) => { + if (dbErr) { + console.error("Error inserting user record for", issuerDid, "into database:", dbErr); + // we can continue... it just means we'll check the endorser server again next time + } + resolve(); + } + ); + }); + } + } + + if (limitPerWeek == null) { + return res.status(400).send(JSON.stringify({ success: false, message: 'Unable to determine rate limits for this user. Verify that the account exists and that the JWT works for that server.' })); + } + + // check the user's claims so far this week + const startOfWeekDate = DateTime.utc().startOf('week') // luxon weeks start on Mondays + const startOfWeekString = startOfWeekDate.toISO() + let imagesCount = await new Promise((resolve, reject) => { + db.get( + 'SELECT COUNT(*) AS week_count FROM image WHERE did = ? AND time >= ?', + [issuerDid, startOfWeekString], + (dbErr, row) => { + if (dbErr) { + console.error(currentDate, "Error counting records for", issuerDid, "into database:", dbErr); + // we can continue... it just means we'll check the endorser server again next time + } + resolve(row?.week_count); + } + ); + }); + if (imagesCount >= limitPerWeek) { + return res.status(400).send(JSON.stringify({ success: false, message: 'You have reached your weekly limit of ' + limitPerWeek + ' images.' })); + } + // Read the file from the temporary location fs.readFile(req.file.path, async (err, data) => { if (err) throw err; // Handle error @@ -85,19 +157,26 @@ app.post('/image', uploadMulter.single('image'), async (req, res) => { const currentDate = new Date().toISOString(); const localFile = req.file.path.startsWith(uploadDir + '/') ? req.file.path.substring(uploadDir.length + 1) : req.file.path; const finalUrl = `https://${bucketName}.s3.amazonaws.com/${fileName}`; - await db.run('INSERT INTO image (time, did, local_file, size, final_file, url) VALUES (?, ?, ?, ?, ?, ?)', [ - currentDate, - issuerDid, - localFile, - req.file.size, - fileName, - finalUrl - ], (dbErr) => { - if (dbErr) { - console.error(currentDate, "Error inserting record from", issuerDid, "into database:", dbErr); - // don't continue because then we'll have storage we cannot track (and potentially limit) - throw dbErr; - } + await new Promise((resolve, reject) => { + db.run( + 'INSERT INTO image (time, did, local_file, size, final_file, url) VALUES (?, ?, ?, ?, ?, ?)', + [ + currentDate, + issuerDid, + localFile, + req.file.size, + fileName, + finalUrl + ], + (dbErr) => { + if (dbErr) { + console.error(currentDate, "Error inserting record from", issuerDid, "into database:", dbErr); + // don't continue because then we'll have storage we cannot track (and potentially limit) + reject(dbErr); + } + resolve(); + } + ); }); // send to AWS diff --git a/sql/migrations/V1__Create_image_table.sql b/sql/migrations/V1__Create_image_table.sql index fdf5470..55db414 100644 --- a/sql/migrations/V1__Create_image_table.sql +++ b/sql/migrations/V1__Create_image_table.sql @@ -1,6 +1,6 @@ CREATE TABLE image ( - time TEXT NOT NULL, + time TEXT NOT NULL, -- ISO 8601 @ UTC, eg 2019-01-01T00:00:00Z did TEXT NOT NULL, local_file TEXT NOT NULL, size INTEGER NOT NULL, @@ -8,6 +8,13 @@ CREATE TABLE image ( url TEXT NOT NULL ); -CREATE INDEX image_date ON image(time); +CREATE INDEX image_time ON image(time); CREATE INDEX image_did ON image(did); -CREATE INDEX image_finalFile ON image(final_file); +CREATE INDEX image_final_file ON image(final_file); + +CREATE TABLE user ( + did TEXT NOT NULL, + per_week INTEGER NOT NULL +); + +CREATE INDEX user_did ON user(did);