diff --git a/README.md b/README.md index aa361c2..2b80e7b 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ node server.js # 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 +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 diff --git a/server.js b/server.js index 95b2d02..0228cda 100644 --- a/server.js +++ b/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 crypto = require('crypto'); 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.' })); } - // Verify the JWT try { - const auth = req.headers.authorization; - if (!auth || !auth.startsWith('Bearer ')) { - return res.status(401).send(JSON.stringify({ success: false, message: 'Missing "Bearer JWT" in Authorization header.'})); + const decoded = await decodeJwt(req, res) + if (!decoded.success) { + return decoded.result; } - 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 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 = decoded.issuerDid; + const jwt = decoded.jwt; // Check the user's limits, first from the DB and then from the server 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 const startOfWeekDate = DateTime.utc().startOf('week') // luxon weeks start on Mondays const startOfWeekString = startOfWeekDate.toISO() - let imagesCount = await new Promise((resolve, reject) => { + const imagesCount = await new Promise((resolve, reject) => { db.get( 'SELECT COUNT(*) AS week_count FROM image WHERE did = ? AND time >= ?', [issuerDid, startOfWeekString], @@ -151,6 +144,24 @@ app.post('/image', uploadMulter.single('image'), async (req, res) => { 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 const currentDate = new Date().toISOString(); 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 = { Body: data, Bucket: bucketName, // S3 Bucket name @@ -189,9 +200,9 @@ app.post('/image', uploadMulter.single('image'), async (req, res) => { if (response.$metadata.httpStatusCode !== 200) { const errorTime = new Date().toISOString(); 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, - 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 { 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); } }); - res.send(JSON.stringify({success: true, url: finalUrl})); + return res.status(200).send(JSON.stringify({success: true, url: finalUrl})); } } catch (uploadError) { const errorTime = new Date().toISOString(); console.error(errorTime, "Error uploading to S3:", uploadError); - res.status(500).send(JSON.stringify({ + return res.status(500).send(JSON.stringify({ success: false, 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, () => { console.log(`Server running at http://localhost:${port}`); }); diff --git a/sql/migrations/V1__Create_image_table.sql b/sql/migrations/V1__Create_image_table.sql index 55db414..ec8c45b 100644 --- a/sql/migrations/V1__Create_image_table.sql +++ b/sql/migrations/V1__Create_image_table.sql @@ -10,7 +10,7 @@ CREATE TABLE image ( CREATE INDEX image_time ON image(time); 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 ( did TEXT NOT NULL,