-
Notifications
You must be signed in to change notification settings - Fork 12
/
Copy pathapp.js
372 lines (319 loc) · 10.9 KB
/
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
// Sample app to store and access files securely
//
// The app provides a simple web-based service to upload, store, and access
// files. Files can be shared via an expiring file link.
// The app uses IBM Cloudant to store file metadata and IBM Cloud Object Storage
// for the actual file object.
//
// The API functions are called from client-side JavaScript
var express = require('express'),
session=require('express-session'),
formidable = require('formidable'),
fs = require("fs");
const {Strategy, Issuer} = require('openid-client');
// Load environment variables from .env file
require('dotenv').config({
path: 'credentials.env'
});
var CloudObjectStorage = require('ibm-cos-sdk');
const { IamAuthenticator } = require('ibm-cloud-sdk-core');
const { CloudantV1 } = require('@ibm-cloud/cloudant');
const passport = require('passport');
// some values taken from the environment
const CLOUDANT_APIKEY = process.env.cloudant_iam_apikey;
const CLOUDANT_URL = process.env.cloudant_url;
const CLOUDANT_DB = process.env.cloudant_database || 'secure-file-storage-metadata';
const COS_BUCKET_NAME = process.env.cos_bucket_name;
const COS_ENDPOINT = process.env.cos_endpoint;
const COS_APIKEY = process.env.cos_apiKey;
const COS_IAM_AUTH_ENDPOINT = process.env.cos_ibmAuthEndpoint || 'https://iam.cloud.ibm.com/identity/token';
const COS_INSTANCE_ID = process.env.cos_resourceInstanceID;
const COS_ACCESS_KEY_ID = process.env.cos_access_key_id;
const COS_SECRET_ACCESS_KEY = process.env.cos_secret_access_key;
const APPID_OAUTH_SERVER_URL= process.env.appid_oauth_server_url;
const APPID_CLIENT_ID= process.env.appid_client_id;
const APPID_SECRET= process.env.appid_secret;
const APPID_REDIRECT_URIS=process.env.appid_redirect_uris.split(',');
const DEBUG_FLAG=process.env.LOCAL_DEBUG;
console.log(DEBUG_FLAG);
// Express setup, including session and passport support
var app = express();
app.use(session({
secret:'keyboard cat',
resave: true,
saveUninitialized: true}));
app.use(passport.initialize());
app.use(passport.session());
// Configure the OIDC client
async function configureOIDC(req, res, next) {
if (req.app.authIssuer) {
return next();
}
const issuer = await Issuer.discover(APPID_OAUTH_SERVER_URL) // connect to oidc application
const client = new issuer.Client({ // Initialize issuer information
client_id: APPID_CLIENT_ID,
client_secret: APPID_SECRET,
redirect_uris: APPID_REDIRECT_URIS
});
const params = {
redirect_uri: APPID_REDIRECT_URIS[0],
scope:'openid',
grant_type:'authorization_code',
response_type:'code',
}
req.app.authIssuer = issuer;
req.app.authClient = client;
// Register oidc strategy with passport
passport.use('oidc', new Strategy({ client }, (tokenset, userinfo, done) => {
return done(null, userinfo); // return user information
}));
// Want to know more about the OpenID Connect provider? Uncomment the next line...
// console.log('Discovered issuer %s %O', issuer.issuer, issuer.metadata);
next();
}
// Initialize Cloudant
const authenticator = new IamAuthenticator({
apikey: CLOUDANT_APIKEY
});
const cloudant = CloudantV1.newInstance({ authenticator: authenticator });
cloudant.setServiceUrl(CLOUDANT_URL);
// Initialize the COS connection.
// This connection is used when interacting with the bucket from the app to upload/delete files.
var config = {
endpoint: COS_ENDPOINT,
apiKeyId: COS_APIKEY,
ibmAuthEndpoint: COS_IAM_AUTH_ENDPOINT,
serviceInstanceId: COS_INSTANCE_ID,
};
var cos = new CloudObjectStorage.S3(config);
// Then this other connection is only used to generate the pre-signed URLs.
// Pre-signed URLs require the COS public endpoint if we want the users to be
// able to access the content from their own computer.
//
// We derive the COS public endpoint from what should be the private/direct endpoint.
let cosPublicEndpoint = COS_ENDPOINT;
if (cosPublicEndpoint.startsWith('s3.private')) {
cosPublicEndpoint = `s3${cosPublicEndpoint.substring('s3.private'.length)}`;
} else if (cosPublicEndpoint.startsWith('s3.direct')) {
cosPublicEndpoint = `s3${cosPublicEndpoint.substring('s3.direct'.length)}`;
}
console.log('Public endpoint for COS is', cosPublicEndpoint);
var cosUrlGenerator = new CloudObjectStorage.S3({
endpoint: cosPublicEndpoint,
credentials: new CloudObjectStorage.Credentials(
COS_ACCESS_KEY_ID,
COS_SECRET_ACCESS_KEY, sessionToken = null),
signatureVersion: 'v4',
});
// serialize and deserialize the user information
passport.serializeUser(function(user, done) {
//console.log("Got authenticated user", JSON.stringify(user));
done(null, {
id: user["id"],
name: user["name"],
email: user["email"],
picture: user["picture"],
});
});
passport.deserializeUser(function(user, done) {
done(null, user);
});
app.use(configureOIDC);
// default protected route /authtest
app.get('/authtest', (req, res, next) => {
// instead of looking for the debug flag, another option would be to
// evaluate "x-forwarded-proto" or "req.secure"
passport.authenticate('oidc', {
redirect_uri: `${DEBUG_FLAG ? 'http' : 'https'}` + `://${req.headers.host}/redirect_uri`,
})(req, res, next);
});
// callback for the OpenID Connect identity provider
// in the case of an error go back to authentication
app.get('/redirect_uri', (req, res, next) => {
passport.authenticate('oidc', {
redirect_uri: `${DEBUG_FLAG ? 'http' : 'https'}`+`://${req.headers.host}/redirect_uri`,
successRedirect: '/',
failureRedirect: '/authtest'
})(req, res, next);
});
// check that the user is authenticated, else redirect
var checkAuthenticated = (req, res, next) => {
if (req.isAuthenticated()) {
return next()
}
res.redirect("/authtest")
}
//
// Define routes
//
// The index document is redirected here and protected
app.use('/secure', checkAuthenticated, express.static(__dirname + '/public'));
// Makes sure that all requests to /api are authenticated
app.use('/api/',checkAuthenticated , (req, res, next) => {
next();
});
// Return the headers as health info
app.get('/health', async function (req, res) {
res.send(req.headers);
});
// Redirect the index to a secure path
app.get('/', async function (req, res) {
res.redirect("/secure")
});
// Returns all files associated to the current user
app.get('/api/files', async function (req, res) {
// filter on the userId (email)
const selector = {
userId: {
'$eq': req.user.email
}
};
// Cloudant API to find documents
cloudant.postFind({
db: CLOUDANT_DB,
selector: selector,
}).then(response => {
// remove some metadata
res.send(response.result.docs.map(function (item) {
item.id = item._id
delete item._id;
delete item._rev;
return item;
}));
}).catch(error => {
console.log(error.status, error.message);
res.status(500).send(error.message);
}
);
});
// Generates a pre-signed URL to access a file owned by the current user
app.get('/api/files/:id/url', async function (req, res) {
const selector = {
userId: {
'$eq': req.user.email,
},
_id: {
'$eq': req.params.id,
}
};
// Cloudant API to find documents
cloudant.postFind({
db: CLOUDANT_DB,
selector: selector,
}).then(response => {
if (response.result.docs.length === 0) {
res.status(404).send({ message: 'Document not found' });
return;
}
const doc = response.result.docs[0];
const url = cosUrlGenerator.getSignedUrl('getObject', {
Bucket: COS_BUCKET_NAME,
Key: `${doc.userId}/${doc._id}/${doc.name}`,
Expires: 60 * 5, // 5 minutes
});
console.log(`[OK] Built signed url for ${req.params.id}`);
res.send({ url });
}).catch(error => {
console.log(`[KO] Could not retrieve document ${req.params.id}`, err);
res.status(500).send(err);
});
});
// Uploads files, associating them to the current user
app.post('/api/files', function (req, res) {
const form = new formidable.IncomingForm();
form.multiples = false;
form.parse(req);
form.on('error', (err) => {
res.status(500).send(err);
});
form.on('file', (name, file) => {
//console.log(file);
var fileDetails = {
name: file.originalFilename,
type: file.type,
size: file.size,
createdAt: new Date(),
userId: req.user.email,
};
console.log(`New file to upload: ${fileDetails.name} (${fileDetails.size} bytes)`);
// create Cloudant document
cloudant.postDocument({
db: CLOUDANT_DB,
document: fileDetails
}).then(async response => {
fileDetails.id = response.result.id;
// upload to COS
await cos.upload({
Bucket: COS_BUCKET_NAME,
Key: `${fileDetails.userId}/${fileDetails.id}/${fileDetails.name}`,
Body: fs.createReadStream(file.filepath),
ContentType: fileDetails.type,
}).promise()
// reply with the document
console.log(`[OK] Document ${fileDetails.id} uploaded to storage`);
res.send(fileDetails);
// delete the local file copy once uploaded
fs.unlink(file.filepath, (err) => {
if (err) { console.log(err) }
});
}).catch(error => {
console.log(`[KO] Failed to upload ${fileDetails.name}`, error.message);
res.status(500).send(error.status, error.message);
});
});
});
// Deletes a file associated with the current user
app.delete('/api/files/:id', async function (req, res) {
console.log(`Deleting document ${req.params.id}`);
// get the doc from Cloudant, ensuring it is owned by the current user
// filter on the userId (email) AND the document ID
const selector = {
userId: {
'$eq': req.user.email
},
_id: {
'$eq': req.params.id,
}
};
// Cloudant API to find documents
cloudant.postFind({
db: CLOUDANT_DB,
selector: selector
}).then(response => {
if (response.result.docs.length === 0) {
res.status(404).send({ message: 'Document not found' });
return;
}
const doc = response.result.docs[0];
// remove the COS object
console.log(`Removing file ${doc.userId}/${doc._id}/${doc.name}`);
cos.deleteObject({
Bucket: COS_BUCKET_NAME,
Key: `${doc.userId}/${doc._id}/${doc.name}`
}).promise();
// remove the Cloudant object
cloudant.deleteDocument({
db: CLOUDANT_DB,
docId: doc._id,
rev: doc._rev
}).then(response => {
console.log(`[OK] Successfully deleted ${doc._id}`);
res.status(204).send();
}).catch(error => {
console.log(error.status, error.message);
res.status(500).send(error.message);
});
}).catch(error => {
console.log(error.status, error.message);
res.status(500).send(error.message);
});
});
// return user information
app.get('/api/user', function (req, res) {
res.send(req.user);
});
// start the server
const server = app.listen(process.env.PORT || 8081, () => {
console.log(APPID_REDIRECT_URIS[0]);
console.log(`Listening on port http://localhost:${server.address().port}`);
});