Browse Source

add delete endpoint, plus some other sanity checks when adding

pull/1/head
Trent Larson 9 months ago
parent
commit
275c0aa35d
  1. 1
      README.md
  2. 176
      server.js
  3. 2
      sql/migrations/V1__Create_image_table.sql

1
README.md

@ -38,6 +38,7 @@ node server.js
# run this first command in a directory where `npm install did-jwt` has been run # 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)' 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 JWT=`node -e "$CODE"`; curl -X POST -H "Authorization: Bearer $JWT" -F "image=@./test.png" http://localhost:3001/image
JWT=`node -e "$CODE"`; curl -X DELETE -H "Authorization: Bearer $JWT" http://localhost:3001/image/https%3A%2F%2Fgifts-image-test.s3.amazonaws.com%2F4599145c3a8792a678f458747f2d8512c680e8680bf5563c35b06cd770051ed2.png
``` ```
## deploy to prod first time ## deploy to prod first time

176
server.js

@ -1,4 +1,4 @@
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3'); const { DeleteObjectCommand, PutObjectCommand, S3Client } = require('@aws-sdk/client-s3');
const cors = require('cors'); const cors = require('cors');
const crypto = require('crypto'); const crypto = require('crypto');
const didJwt = require('did-jwt'); const didJwt = require('did-jwt');
@ -57,20 +57,13 @@ app.post('/image', uploadMulter.single('image'), async (req, res) => {
return res.status(400).send(JSON.stringify({ success: false, message: 'No file uploaded.' })); return res.status(400).send(JSON.stringify({ success: false, message: 'No file uploaded.' }));
} }
// Verify the JWT
try { try {
const auth = req.headers.authorization; const decoded = await decodeJwt(req, res)
if (!auth || !auth.startsWith('Bearer ')) { if (!decoded.success) {
return res.status(401).send(JSON.stringify({ success: false, message: 'Missing "Bearer JWT" in Authorization header.'})); return decoded.result;
} }
const jwt = auth.substring('Bearer '.length); const issuerDid = decoded.issuerDid;
const verified = await didJwt.verifyJWT(jwt, { resolver }); const jwt = decoded.jwt;
if (!verified.verified) {
const errorTime = new Date().toISOString();
console.error(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 // Check the user's limits, first from the DB and then from the server
let limitPerWeek = await new Promise((resolve, reject) => { let limitPerWeek = await new Promise((resolve, reject) => {
@ -122,7 +115,7 @@ app.post('/image', uploadMulter.single('image'), async (req, res) => {
// check the user's claims so far this week // check the user's claims so far this week
const startOfWeekDate = DateTime.utc().startOf('week') // luxon weeks start on Mondays const startOfWeekDate = DateTime.utc().startOf('week') // luxon weeks start on Mondays
const startOfWeekString = startOfWeekDate.toISO() const startOfWeekString = startOfWeekDate.toISO()
let imagesCount = await new Promise((resolve, reject) => { const imagesCount = await new Promise((resolve, reject) => {
db.get( db.get(
'SELECT COUNT(*) AS week_count FROM image WHERE did = ? AND time >= ?', 'SELECT COUNT(*) AS week_count FROM image WHERE did = ? AND time >= ?',
[issuerDid, startOfWeekString], [issuerDid, startOfWeekString],
@ -151,6 +144,24 @@ app.post('/image', uploadMulter.single('image'), async (req, res) => {
try { try {
// look to see if this image already exists
const imageUrl = await new Promise((resolve, reject) => {
db.get(
'SELECT url FROM image WHERE final_file = ? and did = ?',
[ fileName, issuerDid ],
(dbErr, row) => {
if (dbErr) {
console.error(currentDate, 'Error getting image for user from database:', dbErr)
// continue anyway
}
resolve(row?.url);
}
);
});
if (imageUrl) {
return res.status(201).send(JSON.stringify({ success: true, url: imageUrl, message: 'This image already existed.' }));
}
// record the upload in the database // record the upload in the database
const currentDate = new Date().toISOString(); const currentDate = new Date().toISOString();
const localFile = reqFile.path.startsWith(uploadDir + '/') ? reqFile.path.substring(uploadDir.length + 1) : reqFile.path; const localFile = reqFile.path.startsWith(uploadDir + '/') ? reqFile.path.substring(uploadDir.length + 1) : reqFile.path;
@ -177,7 +188,7 @@ app.post('/image', uploadMulter.single('image'), async (req, res) => {
); );
}); });
// send to AWS // send to S3
const params = { const params = {
Body: data, Body: data,
Bucket: bucketName, // S3 Bucket name Bucket: bucketName, // S3 Bucket name
@ -189,9 +200,9 @@ app.post('/image', uploadMulter.single('image'), async (req, res) => {
if (response.$metadata.httpStatusCode !== 200) { if (response.$metadata.httpStatusCode !== 200) {
const errorTime = new Date().toISOString(); const errorTime = new Date().toISOString();
console.error(errorTime, "Error uploading to S3 with bad HTTP status, with metadata:", response.$metadata); console.error(errorTime, "Error uploading to S3 with bad HTTP status, with metadata:", response.$metadata);
res.status(500).send(JSON.stringify({ return res.status(500).send(JSON.stringify({
success: false, success: false,
message: "Got bad status of " + response.$metadata.httpStatusCode + " from AWS. See server logs at " + errorTime message: "Got bad status of " + response.$metadata.httpStatusCode + " from S3. See server logs at " + errorTime
})); }));
} else { } else {
fs.rm(reqFile.path, (err) => { fs.rm(reqFile.path, (err) => {
@ -199,12 +210,12 @@ app.post('/image', uploadMulter.single('image'), async (req, res) => {
console.error("Error deleting temp file", reqFile.path, "with error (but continuing):", err); console.error("Error deleting temp file", reqFile.path, "with error (but continuing):", err);
} }
}); });
res.send(JSON.stringify({success: true, url: finalUrl})); return res.status(200).send(JSON.stringify({success: true, url: finalUrl}));
} }
} catch (uploadError) { } catch (uploadError) {
const errorTime = new Date().toISOString(); const errorTime = new Date().toISOString();
console.error(errorTime, "Error uploading to S3:", uploadError); console.error(errorTime, "Error uploading to S3:", uploadError);
res.status(500).send(JSON.stringify({ return res.status(500).send(JSON.stringify({
success: false, success: false,
message: "Got error uploading file. See server logs at " + errorTime + " Error Details: " + uploadError message: "Got error uploading file. See server logs at " + errorTime + " Error Details: " + uploadError
})); }));
@ -220,6 +231,133 @@ app.post('/image', uploadMulter.single('image'), async (req, res) => {
} }
}); });
/**
* DELETE endpoint
* returns { success: true } if successful
* returns { success: false, message: string } if not successful
*/
app.delete('/image/:url', async (req, res) => {
try {
const decoded = await decodeJwt(req, res)
if (!decoded.success) {
return decoded.result;
}
const issuerDid = decoded.issuerDid;
const url = req.params.url;
const currentDate = new Date().toISOString();
// look for file name of this image
const thisUserImageFile = await new Promise((resolve, reject) => {
db.get(
'SELECT final_file FROM image WHERE url = ? and did = ?',
[ url, issuerDid ],
(dbErr, row) => {
if (dbErr) {
console.error(currentDate, 'Error getting image for user from database:', dbErr)
reject(dbErr);
}
resolve(row?.final_file);
}
);
});
if (!thisUserImageFile) {
return res.status(404).send(JSON.stringify({ success: false, message: 'No image entry found for user ' + issuerDid + ' & URL ' + url }));
}
// check if any other user recorded this image
const otherUserImage = await new Promise((resolve, reject) => {
db.get(
'SELECT did FROM image WHERE url = ? and did != ?',
[ url, issuerDid ],
(dbErr, row) => {
if (dbErr) {
console.error(currentDate, 'Error getting image for user from database:', dbErr)
reject(dbErr);
}
resolve(row?.did);
}
);
});
if (!otherUserImage) {
// remove from S3 since nobody else recorded it
const params = {
Bucket: bucketName, // S3 Bucket name
Key: thisUserImageFile, // File name to use in S3
};
const command = new DeleteObjectCommand(params);
const response = await s3Client.send(command);
if (response.$metadata.httpStatusCode !== 200
&& response.$metadata.httpStatusCode !== 202
&& response.$metadata.httpStatusCode !== 204) {
const errorTime = new Date().toISOString();
console.error(errorTime, "Error deleting from S3 with bad HTTP status, with metadata:", response.$metadata);
return res.status(500).send(JSON.stringify({
success: false,
message: "Got bad status of " + response.$metadata.httpStatusCode + " from S3. See server logs at " + errorTime
}));
}
}
// now remove the DB record for requesting user
await new Promise((resolve, reject) => {
db.run(
'DELETE FROM image where url = ? AND did = ?',
[ url, issuerDid ],
(dbErr) => {
if (dbErr) {
const currentDate = new Date().toISOString();
console.error(currentDate, "Error deleting record from", issuerDid, "into database:", dbErr);
// don't continue because then we'll have storage we cannot track (and potentially limit)
reject(dbErr);
}
resolve();
}
);
});
return res.status(204).send(JSON.stringify({ success: true }));
} catch (error) {
const errorTime = new Date().toISOString();
console.error(errorTime, "Error processing image delete:", error);
return res.status(500).send(JSON.stringify({
success: false,
message: "Got error processing image delete. See server logs at " + errorTime + " Error Details: " + error
}));
}
});
/**
* retrieve Bearer JWT from Authorization header and return either:
* { success: true, issuerDid: string, jwt: string }
* ... or ...
* { success: false, result: HTTP send result }
*/
async function decodeJwt(req, res) {
const auth = req.headers.authorization;
if (!auth || !auth.startsWith('Bearer ')) {
return {
success: false,
result: 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.verified) {
const errorTime = new Date().toISOString();
console.error(errorTime, 'Got invalid JWT in Authorization header:', verified);
return {
success: false,
result: res.status(401).send(JSON.stringify({
success: false,
message: 'Got invalid JWT in Authorization header. See server logs at ' + errorTime
}))
};
}
return { success: true, issuerDid: verified.issuer, jwt: jwt };
}
app.listen(port, () => { app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`); console.log(`Server running at http://localhost:${port}`);
}); });

2
sql/migrations/V1__Create_image_table.sql

@ -10,7 +10,7 @@ CREATE TABLE image (
CREATE INDEX image_time 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_final_file ON image(final_file); CREATE INDEX image_final_file ON image(url);
CREATE TABLE user ( CREATE TABLE user (
did TEXT NOT NULL, did TEXT NOT NULL,

Loading…
Cancel
Save