@ -4,13 +4,14 @@ const crypto = require('crypto');
const didJwt = require ( 'did-jwt' ) ;
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 ( ) ;
const { didEthLocalResolver } = require ( "./vc/did-eth-local-resolver" ) ;
require ( 'dotenv' ) . config ( )
const app = express ( ) ;
@ -18,17 +19,11 @@ app.use(cors());
const port = process . env . PORT || 3002 ;
// file name also referenced in flyway.conf and potentially in .env files or in environment variables
const dbFile = process . env . SQLITE_FILE || './image.sqlite' ;
const dbFile = process . env . SQLITE_FILE || './image-db .sqlite' ;
const bucketName = process . env . S3_BUCKET_NAME || 'gifts-image-test' ;
const imageServer = process . env . DOWNLOAD_IMAGE_SERVER || 'test-image.timesafari.app' ;
const ethrDidResolver = getResolver ;
const resolver =
new Resolver ( {
... ethrDidResolver ( {
infuraProjectId : process . env . INFURA_PROJECT_ID || 'fake-infura-project-id'
} )
} )
const resolver = new Resolver ( { 'ethr' : didEthLocalResolver } ) ;
// Open a connection to the SQLite database
const db = new sqlite3 . Database ( dbFile , ( err ) => {
@ -54,38 +49,51 @@ const uploadDir = 'uploads';
const uploadMulter = multer ( { dest : uploadDir + '/' } ) ;
app . get ( '/ping' , async ( req , res ) => {
res . send ( 'pong v1.0.0 ' ) ;
res . send ( 'pong - v 0.0.1 ' ) ; // version
} ) ;
app . get ( '/image-limits' , async ( req , res ) => {
limitsResult = await retrievelimits ( req , res ) ;
if ( ! limitsResult . success ) {
return limitsResult . result ;
try {
limitsResult = await retrievelimits ( req , res ) ;
if ( ! limitsResult . success ) {
return limitsResult . result ;
}
return res . status ( 200 ) . send ( JSON . stringify ( {
success : true ,
doneImagesThisWeek : limitsResult . doneImagesThisWeek ,
maxImagesPerWeek : limitsResult . maxImagesPerWeek ,
nextWeekBeginDateTime : limitsResult . nextWeekBeginDateTime
} ) ) ;
} catch ( e ) {
console . error ( 'Error getting image limits:' , e , ' ... with this string: ' + e ) ;
return res . status ( 500 ) . send ( JSON . stringify ( { success : false , message : 'Got this error retrieving limits: ' + e } ) ) ;
}
return res . status ( 200 ) . send ( JSON . stringify ( {
success : true ,
doneImagesThisWeek : limitsResult . doneImagesThisWeek ,
maxImagesPerWeek : limitsResult . maxImagesPerWeek ,
nextWeekBeginDateTime : limitsResult . nextWeekBeginDateTime
} ) ) ;
} ) ;
// POST endpoint to upload an image
/ * *
* POST endpoint to upload an image
*
* Send as FormData , with :
* - "image" file Blob
* - "claimType" ( optional , eg . "GiveAction" , "PlanAction" , "profile" )
* - "handleId" ( optional )
* - "fileName" ( optional , if you want to replace an previous image )
* /
app . post ( '/image' , uploadMulter . single ( 'image' ) , async ( req , res ) => {
const reqFile = req . file ;
if ( reqFile == null ) {
return res . status ( 400 ) . send ( JSON . stringify ( { success : false , message : 'No file uploaded.' } ) ) ;
}
if ( reqFile . size > 10000000 ) {
fs . rm ( reqFile . path , ( err ) => {
if ( err ) {
console . error ( "Error deleting too-large temp file" , reqFile . path , "with error (but continuing):" , err ) ;
}
} ) ;
return res . status ( 400 ) . send ( JSON . stringify ( { success : false , message : 'File size is too large. Maximum file size is 10MB.' } ) ) ;
}
try {
if ( reqFile . size > 10485760 ) { // 10MB
fs . rm ( reqFile . path , ( err ) => {
if ( err ) {
console . error ( "Error deleting too-large temp file" , reqFile . path , "with error (but continuing):" , err ) ;
}
} ) ;
return res . status ( 400 ) . send ( JSON . stringify ( { success : false , message : 'File size is too large. Maximum file size is 10MB.' } ) ) ;
}
limitsResult = await retrievelimits ( req , res ) ;
if ( ! limitsResult . success ) {
return limitsResult . result ;
@ -102,41 +110,123 @@ app.post('/image', uploadMulter.single('image'), async (req, res) => {
fs . readFile ( reqFile . path , async ( err , data ) => {
if ( err ) throw err ; // Handle error
const hashSum = crypto . createHash ( 'sha256' ) ;
hashSum . update ( data ) ;
const hashHex = hashSum . digest ( 'hex' ) ;
try {
let finalFileName ;
if ( req . body . fileName ) {
// replacement file name given
finalFileName = req . body . fileName ;
// check if the file to replace was sent by this user earlier
const didForOriginal = await new Promise ( ( resolve , reject ) => {
// For some reason, this prepared-statement SQL gives seg fault: "SELECT did FROM image WHERE did = ? and final_file = ?"
if ( issuerDid . indexOf ( "'" ) >= 0 || finalFileName . indexOf ( "'" ) >= 0 ) {
console . error ( "Error: SQL injection attempt with" , issuerDid , finalFileName ) ;
return res . status ( 400 ) . send ( JSON . stringify ( { success : false , message : 'SQL injection attempt detected.' } ) ) ;
}
const sql = "SELECT did FROM image WHERE did = '" + issuerDid + "' and final_file = '" + finalFileName + "'" ;
db . get (
sql ,
[ ] ,
( dbErr , row ) => {
if ( dbErr ) {
console . error ( currentDate , 'Error getting image for user from database:' , dbErr )
reject ( dbErr ) ;
}
resolve ( row ? . did ) ;
}
) ;
} ) ;
if ( ! didForOriginal ) {
return res . status ( 404 ) . send ( JSON . stringify ( { success : false , message : 'No image entry found for user ' + issuerDid + ' for file ' + finalFileName } ) ) ;
}
const fileName = hashHex + path . extname ( reqFile . originalname ) ;
// check if any other user recorded this image
const othersWhoSentImage = await new Promise ( ( resolve , reject ) => {
db . get (
'SELECT did FROM image WHERE final_file = ? and did != ?' ,
[ finalFileName , issuerDid ] ,
( dbErr , row ) => {
if ( dbErr ) {
console . error ( currentDate , 'Error getting image for other users from database:' , dbErr )
reject ( dbErr ) ;
}
resolve ( row ? . did ) ;
}
) ;
} ) ;
if ( othersWhoSentImage ) {
return res . status ( 400 ) . send ( JSON . stringify ( { success : false , message : 'Other users have also saved this image so it cannot be modified. You will have to replace your own references.' } ) ) ;
}
try {
// remove from S3
const params = {
Bucket : bucketName , // S3 Bucket name
Key : finalFileName , // 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
} ) ) ;
}
// 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
// might as well remove from DB and add it all back again later
await new Promise ( ( resolve , reject ) => {
db . run (
'DELETE FROM image where did = ? and final_file = ?' ,
[ issuerDid , finalFileName ] ,
( dbErr ) => {
if ( dbErr ) {
const currentDate = new Date ( ) . toISOString ( ) ;
console . error ( currentDate , "Error deleting record by" , issuerDid , "named" , finalFileName , "from database:" , dbErr ) ;
// don't continue because then we'll have storage we cannot track (and potentially limit)
reject ( dbErr ) ;
}
resolve ( ) ;
}
resolve ( row ? . url ) ;
}
) ;
} ) ;
if ( imageUrl ) {
return res . status ( 201 ) . send ( JSON . stringify ( { success : true , url : imageUrl , message : 'This image already existed.' } ) ) ;
) ;
} ) ;
} else {
// no replacement file name given so it's a new file
const hashSum = crypto . createHash ( 'sha256' ) ;
hashSum . update ( data ) ;
const hashHex = hashSum . digest ( 'hex' ) ;
finalFileName = hashHex + path . extname ( reqFile . originalname ) ;
// look to see if this image already exists for this user
const imageUrl = await new Promise ( ( resolve , reject ) => {
db . get (
'SELECT url FROM image WHERE final_file = ? and did = ?' ,
[ finalFileName , 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 ;
const finalUrl = ` https:// ${ imageServer } / ${ fileName } ` ;
const finalUrl = ` https:// ${ imageServer } / ${ finalFi leName } ` ;
const claimType = req . body . claimType ;
const handleId = req . body . handleId ;
await new Promise ( ( resolve , reject ) => {
db . run (
'INSERT INTO image (time, did, claim_type, handle_id, local_file, size, final_file, mime_type, url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)' ,
'INSERT INTO image (time, did, claim_type, handle_id, local_file, size, final_file, mime_type, url, is_replacement ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' ,
[
currentDate ,
issuerDid ,
@ -144,9 +234,10 @@ app.post('/image', uploadMulter.single('image'), async (req, res) => {
handleId ,
localFile ,
reqFile . size ,
fileName ,
finalFi leName ,
reqFile . mimetype ,
finalUrl
finalUrl ,
! ! req . body . fileName ,
] ,
( dbErr ) => {
if ( dbErr ) {
@ -164,7 +255,7 @@ app.post('/image', uploadMulter.single('image'), async (req, res) => {
Body : data ,
Bucket : bucketName , // S3 Bucket name
ContentType : reqFile . mimetype , // File content type
Key : fileName , // File name to use in S3
Key : finalFi leName , // File name to use in S3
} ;
if ( process . env . S3_SET_ACL === 'true' ) {
params . ACL = 'public-read' ;
@ -186,7 +277,7 @@ app.post('/image', uploadMulter.single('image'), async (req, res) => {
} ) ;
// AWS URL: https://gifts-image-test.s3.amazonaws.com/gifts-image-test/FILE
// American Cloud URL: https://a2-west.americancloud.com/TENANT:giftsimagetest/FILE
return res . status ( 200 ) . send ( JSON . stringify ( { success : true , url : finalUrl } ) ) ;
return res . status ( 201 ) . send ( { success : true , url : finalUrl } ) ;
}
} catch ( uploadError ) {
const errorTime = new Date ( ) . toISOString ( ) ;
@ -240,11 +331,11 @@ app.delete('/image/:url', async (req, res) => {
} ) ;
if ( ! thisUserImageFile ) {
console . error ( 'No image entry found for user' , issuerDid , '& URL' , url , 'so returning 404.' ) ;
return res . status ( 404 ) . send ( JSON . stringify ( { success : false , message : 'No image entry found for user ' + issuerDid + ' & URL ' + url } ) ) ;
return res . status ( 404 ) . send ( { success : false , message : 'No image entry found for user ' + issuerDid + ' & URL ' + url } ) ;
}
// check if any other user recorded this image
const otherUser Image = await new Promise ( ( resolve , reject ) => {
const othersWhoSent Image = await new Promise ( ( resolve , reject ) => {
db . get (
'SELECT did FROM image WHERE url = ? and did != ?' ,
[ url , issuerDid ] ,
@ -258,7 +349,7 @@ app.delete('/image/:url', async (req, res) => {
) ;
} ) ;
if ( ! otherUser Image ) {
if ( ! othersWhoSent Image ) {
// remove from S3 since nobody else recorded it
const params = {
Bucket : bucketName , // S3 Bucket name
@ -271,10 +362,10 @@ app.delete('/image/:url', async (req, res) => {
&& 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 ( {
return res . status ( 500 ) . send ( {
success : false ,
message : "Got bad status of " + response . $metadata . httpStatusCode + " from S3. See server logs at " + errorTime
} ) ) ;
} ) ;
}
}
@ -286,22 +377,22 @@ app.delete('/image/:url', async (req, res) => {
( 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)
console . error ( currentDate , "Error deleting record by " , issuerDid , "with URL" , url , "from database:" , dbErr ) ;
// we'll let them know that it's not all cleaned up so they can try again
reject ( dbErr ) ;
}
resolve ( ) ;
}
) ;
} ) ;
return res . status ( 204 ) . send ( JSON . stringify ( { success : true } ) ) ;
return res . status ( 204 ) . send ( { success : true } ) ;
} catch ( error ) {
const errorTime = new Date ( ) . toISOString ( ) ;
console . error ( errorTime , "Error processing image delete:" , error ) ;
return res . status ( 500 ) . send ( JSON . stringify ( {
return res . status ( 500 ) . send ( {
success : false ,
message : "Got error processing image delete. See server logs at " + errorTime + " Error Details: " + error
} ) ) ;
} ) ;
}
} ) ;