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
|
# 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
|
||||||
|
|||||||
@@ -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
generated
8
pnpm-lock.yaml
generated
@@ -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
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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user