Browse Source

check image upload against user limits

pull/1/head
Trent Larson 9 months ago
parent
commit
502997f168
  1. 6
      CONTRIBUTING.md
  2. 12
      README.md
  3. 1
      package.json
  4. 8
      pnpm-lock.yaml
  5. 113
      server.js
  6. 13
      sql/migrations/V1__Create_image_table.sql

6
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).

12
README.md

@ -1,5 +1,11 @@
# Image Server # Image Server
Remaining:
- parameterize bucket for test server
- try American Cloud
- dockerize
- pretty-up the client, show thumbnail
## setup ## setup
@ -28,8 +34,10 @@ node server.js
## test ## test
``` ```shell
curl -X POST -F "image=@./test.png" http://localhost:3000/image # 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 ## deploy to prod first time

1
package.json

@ -12,6 +12,7 @@
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"ethr-did-resolver": "^10.1.5", "ethr-did-resolver": "^10.1.5",
"express": "^4.18.2", "express": "^4.18.2",
"luxon": "^3.4.4",
"multer": "1.4.5-lts.1", "multer": "1.4.5-lts.1",
"sqlite3": "^5.1.7" "sqlite3": "^5.1.7"
}, },

8
pnpm-lock.yaml

@ -29,6 +29,9 @@ dependencies:
express: express:
specifier: ^4.18.2 specifier: ^4.18.2
version: 4.18.2 version: 4.18.2
luxon:
specifier: ^3.4.4
version: 3.4.4
multer: multer:
specifier: 1.4.5-lts.1 specifier: 1.4.5-lts.1
version: 1.4.5-lts.1 version: 1.4.5-lts.1
@ -2241,6 +2244,11 @@ packages:
yallist: 4.0.0 yallist: 4.0.0
dev: false 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: /make-fetch-happen@9.1.0:
resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==} resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}

113
server.js

@ -6,6 +6,7 @@ const { Resolver } = require('did-resolver');
const express = require('express'); const express = require('express');
const { getResolver } = require('ethr-did-resolver'); const { getResolver } = require('ethr-did-resolver');
const fs = require('fs'); const fs = require('fs');
const { DateTime } = require('luxon');
const multer = require('multer'); const multer = require('multer');
const path = require('path'); const path = require('path');
const sqlite3 = require('sqlite3').verbose(); const sqlite3 = require('sqlite3').verbose();
@ -15,7 +16,7 @@ require('dotenv').config()
const app = express(); const app = express();
app.use(cors()); 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 // 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'; 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 // Configure AWS
const s3Client = new S3Client({ const s3Client = new S3Client({
region: process.env.AWS_REGION, 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.'})); return res.status(401).send(JSON.stringify({ success: false, message: 'Missing "Bearer JWT" in Authorization header.'}));
} }
const jwt = auth.substring('Bearer '.length); const jwt = auth.substring('Bearer '.length);
const verified = await didJwt.verifyJWT(jwt, {resolver}); const verified = await didJwt.verifyJWT(jwt, { resolver });
if (!verified) { if (!verified.verified) {
return res.status(401).send(JSON.stringify({ success: false, message: 'Got invalid JWT in Authorization header.'})); 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; 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 // Read the file from the temporary location
fs.readFile(req.file.path, async (err, data) => { fs.readFile(req.file.path, async (err, data) => {
if (err) throw err; // Handle error 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 currentDate = new Date().toISOString();
const localFile = req.file.path.startsWith(uploadDir + '/') ? req.file.path.substring(uploadDir.length + 1) : req.file.path; 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}`; const finalUrl = `https://${bucketName}.s3.amazonaws.com/${fileName}`;
await db.run('INSERT INTO image (time, did, local_file, size, final_file, url) VALUES (?, ?, ?, ?, ?, ?)', [ await new Promise((resolve, reject) => {
currentDate, db.run(
issuerDid, 'INSERT INTO image (time, did, local_file, size, final_file, url) VALUES (?, ?, ?, ?, ?, ?)',
localFile, [
req.file.size, currentDate,
fileName, issuerDid,
finalUrl localFile,
], (dbErr) => { req.file.size,
if (dbErr) { fileName,
console.error(currentDate, "Error inserting record from", issuerDid, "into database:", dbErr); finalUrl
// don't continue because then we'll have storage we cannot track (and potentially limit) ],
throw dbErr; (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 // send to AWS

13
sql/migrations/V1__Create_image_table.sql

@ -1,6 +1,6 @@
CREATE TABLE image ( CREATE TABLE image (
time TEXT NOT NULL, time TEXT NOT NULL, -- ISO 8601 @ UTC, eg 2019-01-01T00:00:00Z
did TEXT NOT NULL, did TEXT NOT NULL,
local_file TEXT NOT NULL, local_file TEXT NOT NULL,
size INTEGER NOT NULL, size INTEGER NOT NULL,
@ -8,6 +8,13 @@ CREATE TABLE image (
url TEXT NOT NULL 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_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);

Loading…
Cancel
Save