1+ // Sample app to store and access files securely
2+ //
3+ // The app provides a simple web-based service to upload, store, and access
4+ // files. Files can be shared via an expiring file link.
5+ // The app uses IBM Cloudant to store file metadata and IBM Cloud Object Storage
6+ // for the actual file object.
7+ //
8+ // The API functions are called from client-side JavaScript
9+
110var express = require ( 'express' ) ,
211 formidable = require ( 'formidable' ) ,
312 cookieParser = require ( 'cookie-parser' ) ,
@@ -12,30 +21,38 @@ require('dotenv').config({
1221
1322var allowAnonymousAccess = process . env . allow_anonymous || false ;
1423
24+ // some values taken from the environment
25+ const CLOUDANT_APIKEY = process . env . cloudant_iam_apikey ;
26+ const CLOUDANT_URL = process . env . cloudant_url ;
27+ const CLOUDANT_DB = process . env . cloudant_database || 'secure-file-storage-metadata' ;
28+
29+ const COS_BUCKET_NAME = process . env . cos_bucket_name ;
30+ const COS_ENDPOINT = process . env . cos_endpoint ;
31+ const COS_APIKEY = process . env . cos_apiKey ;
32+ const COS_IAM_AUTH_ENDPOINT = process . env . cos_ibmAuthEndpoint || 'https://iam.cloud.ibm.com/identity/token' ;
33+ const COS_INSTANCE_ID = process . env . cos_resourceInstanceID ;
34+ const COS_ACCESS_KEY_ID = process . env . cos_access_key_id ;
35+ const COS_SECRET_ACCESS_KEY = process . env . cos_secret_access_key ;
36+
1537// Initialize Cloudant
16- var Cloudant = require ( '@cloudant/cloudant' ) ;
17- var cloudant = new Cloudant ( {
18- url : process . env . cloudant_url ,
19- plugins : [
20- 'promises' ,
21- {
22- iamauth : {
23- iamApiKey : process . env . cloudant_iam_apikey
24- }
25- }
26- ]
38+ const { IamAuthenticator } = require ( 'ibm-cloud-sdk-core' ) ;
39+ const authenticator = new IamAuthenticator ( {
40+ apikey : CLOUDANT_APIKEY
2741} ) ;
28- var db = cloudant . db . use ( process . env . cloudant_database || 'secure-file-storage-metadata' ) ;
2942
30- var CloudObjectStorage = require ( 'ibm-cos-sdk' ) ;
43+ const { CloudantV1 } = require ( '@ibm-cloud/cloudant' ) ;
44+
45+ const cloudant = CloudantV1 . newInstance ( { authenticator : authenticator } ) ;
46+ cloudant . setServiceUrl ( CLOUDANT_URL ) ;
3147
3248// Initialize the COS connection.
49+ var CloudObjectStorage = require ( 'ibm-cos-sdk' ) ;
3350// This connection is used when interacting with the bucket from the app to upload/delete files.
3451var config = {
35- endpoint : process . env . cos_endpoint ,
36- apiKeyId : process . env . cos_apiKey ,
37- ibmAuthEndpoint : process . env . cos_ibmAuthEndpoint || 'https://iam.cloud.ibm.com/identity/token' ,
38- serviceInstanceId : process . env . cos_resourceInstanceID ,
52+ endpoint : COS_ENDPOINT ,
53+ apiKeyId : COS_APIKEY ,
54+ ibmAuthEndpoint : COS_IAM_AUTH_ENDPOINT ,
55+ serviceInstanceId : COS_INSTANCE_ID ,
3956} ;
4057var cos = new CloudObjectStorage . S3 ( config ) ;
4158
@@ -44,7 +61,7 @@ var cos = new CloudObjectStorage.S3(config);
4461// able to access the content from their own computer.
4562//
4663// We derive the COS public endpoint from what should be the private/direct endpoint.
47- let cosPublicEndpoint = process . env . cos_endpoint ;
64+ let cosPublicEndpoint = COS_ENDPOINT ;
4865if ( cosPublicEndpoint . startsWith ( 's3.private' ) ) {
4966 cosPublicEndpoint = `s3${ cosPublicEndpoint . substring ( 's3.private' . length ) } ` ;
5067} else if ( cosPublicEndpoint . startsWith ( 's3.direct' ) ) {
@@ -55,16 +72,15 @@ console.log('Public endpoint for COS is', cosPublicEndpoint);
5572var cosUrlGenerator = new CloudObjectStorage . S3 ( {
5673 endpoint : cosPublicEndpoint ,
5774 credentials : new CloudObjectStorage . Credentials (
58- process . env . cos_access_key_id ,
59- process . env . cos_secret_access_key , sessionToken = null ) ,
75+ COS_ACCESS_KEY_ID ,
76+ COS_SECRET_ACCESS_KEY , sessionToken = null ) ,
6077 signatureVersion : 'v4' ,
6178} ) ;
6279
63- const COS_BUCKET_NAME = process . env . cos_bucket_name ;
64-
65- // Define routes
80+ // Simple Express setup
6681var app = express ( ) ;
6782app . use ( cookieParser ( ) ) ;
83+ // Define routes
6884app . use ( '/' , express . static ( __dirname + '/public' ) ) ;
6985
7086// Decodes access and identity tokens sent by App ID in the Authorization header
@@ -114,6 +130,7 @@ app.use('/api/', (req, res, next) => {
114130 }
115131} ) ;
116132
133+ // Extract the subject out of the access token
117134function getSub ( req ) {
118135 if ( req . appIdAuthorizationContext ) {
119136 return req . appIdAuthorizationContext . access_token . sub ;
@@ -126,38 +143,52 @@ function getSub(req) {
126143
127144// Returns all files associated to the current user
128145app . get ( '/api/files' , async function ( req , res ) {
129- try {
130- const body = await db . find ( {
131- selector : {
132- userId : getSub ( req ) ,
133- }
134- } ) ;
135- res . send ( body . docs . map ( function ( item ) {
146+ // filter on the userId which is the subject in the access token
147+ const selector = {
148+ userId : {
149+ '$eq' : getSub ( req )
150+ }
151+ } ;
152+ // Cloudant API to find documents
153+ cloudant . postFind ( {
154+ db : CLOUDANT_DB ,
155+ selector : selector ,
156+ } ) . then ( response => {
157+ // remove some metadata
158+ res . send ( response . result . docs . map ( function ( item ) {
136159 item . id = item . _id
137160 delete item . _id ;
138161 delete item . _rev ;
139162 return item ;
140163 } ) ) ;
141- } catch ( err ) {
142- console . log ( err ) ;
143- res . status ( 500 ) . send ( err ) ;
164+ } ) . catch ( error => {
165+ console . log ( error . status , error . message ) ;
166+ res . status ( 500 ) . send ( error . message ) ;
144167 }
168+ ) ;
145169} ) ;
146170
171+
147172// Generates a pre-signed URL to access a file owned by the current user
148173app . get ( '/api/files/:id/url' , async function ( req , res ) {
149- try {
150- const result = await db . find ( {
151- selector : {
152- _id : req . params . id ,
153- userId : getSub ( req ) ,
154- }
155- } ) ;
156- if ( result . docs . length === 0 ) {
174+ const selector = {
175+ userId : {
176+ '$eq' : getSub ( req )
177+ } ,
178+ _id : {
179+ '$eq' : req . params . id ,
180+ }
181+ } ;
182+ // Cloudant API to find documents
183+ cloudant . postFind ( {
184+ db : CLOUDANT_DB ,
185+ selector : selector ,
186+ } ) . then ( response => {
187+ if ( response . result . docs . length === 0 ) {
157188 res . status ( 404 ) . send ( { message : 'Document not found' } ) ;
158189 return ;
159190 }
160- const doc = result . docs [ 0 ] ;
191+ const doc = response . result . docs [ 0 ] ;
161192 const url = cosUrlGenerator . getSignedUrl ( 'getObject' , {
162193 Bucket : COS_BUCKET_NAME ,
163194 Key : `${ doc . userId } /${ doc . _id } /${ doc . name } ` ,
@@ -166,10 +197,10 @@ app.get('/api/files/:id/url', async function (req, res) {
166197
167198 console . log ( `[OK] Built signed url for ${ req . params . id } ` ) ;
168199 res . send ( { url } ) ;
169- } catch ( err ) {
200+ } ) . catch ( error => {
170201 console . log ( `[KO] Could not retrieve document ${ req . params . id } ` , err ) ;
171202 res . status ( 500 ) . send ( err ) ;
172- }
203+ } ) ;
173204} ) ;
174205
175206// Uploads files, associating them to the current user
@@ -191,69 +222,90 @@ app.post('/api/files', function (req, res) {
191222 userId : getSub ( req ) ,
192223 } ;
193224
194- try {
195- console . log ( `New file to upload: ${ fileDetails . name } (${ fileDetails . size } bytes)` ) ;
225+ console . log ( `New file to upload: ${ fileDetails . name } (${ fileDetails . size } bytes)` ) ;
196226
197- // create Cloudant document
198- const doc = await db . insert ( fileDetails ) ;
199- fileDetails . id = doc . id ;
227+ // create Cloudant document
228+ cloudant . postDocument ( {
229+ db : CLOUDANT_DB ,
230+ document : fileDetails
231+ } ) . then ( async response => {
232+ console . log ( response ) ;
233+ fileDetails . id = response . result . id ;
200234
201235 // upload to COS
202236 await cos . upload ( {
203237 Bucket : COS_BUCKET_NAME ,
204238 Key : `${ fileDetails . userId } /${ fileDetails . id } /${ fileDetails . name } ` ,
205239 Body : fs . createReadStream ( file . path ) ,
206240 ContentType : fileDetails . type ,
207- } ) . promise ( ) ;
241+ } ) . promise ( )
208242
209243 // reply with the document
210244 console . log ( `[OK] Document ${ fileDetails . id } uploaded to storage` ) ;
211245 res . send ( fileDetails ) ;
212- } catch ( err ) {
213- console . log ( `[KO] Failed to upload ${ fileDetails . name } ` , err ) ;
214- res . status ( 500 ) . send ( err ) ;
215- }
216-
217- // delete the file once uploaded
218- fs . unlink ( file . path , ( err ) => {
219- if ( err ) { console . log ( err ) }
246+ // delete the file once uploaded
247+ fs . unlink ( file . path , ( err ) => {
248+ if ( err ) { console . log ( err ) }
249+ } ) ;
250+ } ) . catch ( error => {
251+ console . log ( `[KO] Failed to upload ${ fileDetails . name } ` , error . message ) ;
252+ res . status ( 500 ) . send ( error . status , error . message ) ;
220253 } ) ;
221254 } ) ;
222255} ) ;
223256
257+
224258// Deletes a file associated with the current user
225259app . delete ( '/api/files/:id' , async function ( req , res ) {
226- try {
227- console . log ( `Deleting document ${ req . params . id } ` ) ;
228-
229- // get the doc from cloudant, ensuring it is owned by the current user
230- const result = await db . find ( {
231- selector : {
232- _id : req . params . id ,
233- userId : getSub ( req ) ,
234- }
235- } ) ;
236- if ( result . docs . length === 0 ) {
260+
261+ console . log ( `Deleting document ${ req . params . id } ` ) ;
262+ // get the doc from cloudant, ensuring it is owned by the current user
263+ // filter on the userId which is the subject in the access token
264+ // AND the document ID
265+ const selector = {
266+ userId : {
267+ '$eq' : getSub ( req )
268+ } ,
269+ _id : {
270+ '$eq' : req . params . id ,
271+ }
272+ } ;
273+ // Cloudant API to find documents
274+ cloudant . postFind ( {
275+ db : CLOUDANT_DB ,
276+ selector : selector
277+ } ) . then ( response => {
278+ if ( response . result . docs . length === 0 ) {
237279 res . status ( 404 ) . send ( { message : 'Document not found' } ) ;
238280 return ;
239281 }
240- const doc = result . docs [ 0 ] ;
241-
282+ const doc = response . result . docs [ 0 ] ;
242283 // remove the COS object
243284 console . log ( `Removing file ${ doc . userId } /${ doc . _id } /${ doc . name } ` ) ;
244- await cos . deleteObject ( {
285+
286+ cos . deleteObject ( {
245287 Bucket : COS_BUCKET_NAME ,
246288 Key : `${ doc . userId } /${ doc . _id } /${ doc . name } `
247289 } ) . promise ( ) ;
248290
249291 // remove the cloudant object
250- await db . destroy ( doc . _id , doc . _rev ) ;
292+ cloudant . deleteDocument ( {
293+ db : CLOUDANT_DB ,
294+ docId : doc . _id ,
295+ rev : doc . _rev
296+ } ) . then ( response => {
297+ console . log ( `[OK] Successfully deleted ${ doc . _id } ` ) ;
298+ res . status ( 204 ) . send ( ) ;
299+ } ) . catch ( error => {
300+ console . log ( error . status , error . message ) ;
301+ res . status ( 500 ) . send ( error . message ) ;
302+ } ) ;
303+
304+ } ) . catch ( error => {
305+ console . log ( error . status , error . message ) ;
306+ res . status ( 500 ) . send ( error . message ) ;
307+ } ) ;
251308
252- console . log ( `[OK] Successfully deleted ${ doc . _id } ` ) ;
253- res . status ( 204 ) . send ( ) ;
254- } catch ( err ) {
255- res . status ( 500 ) . send ( err ) ;
256- }
257309} ) ;
258310
259311// Called by App ID when the authorization flow completes
@@ -265,7 +317,7 @@ app.get('/api/tokens', function (req, res) {
265317 res . send ( req . appIdAuthorizationContext ) ;
266318} ) ;
267319
268- app . get ( '/api/user' , function ( req , res ) {
320+ app . get ( '/api/user' , function ( req , res ) {
269321 let result = { } ;
270322 if ( req . appIdAuthorizationContext ) {
271323 result = {
@@ -280,6 +332,7 @@ app.get('/api/user', function(req, res) {
280332 res . send ( result ) ;
281333} ) ;
282334
335+ // start the server
283336const server = app . listen ( process . env . PORT || 8081 , ( ) => {
284337 console . log ( `Listening on port http://0.0.0.0:${ server . address ( ) . port } ` ) ;
285338} ) ;
0 commit comments