check image upload against user limits
This commit is contained in:
6
CONTRIBUTING.md
Normal file
6
CONTRIBUTING.md
Normal file
@@ -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
12
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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -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'}
|
||||
|
||||
113
server.js
113
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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user