Implement full functionality
This commit is contained in:
parent
e86afb86ae
commit
63656db542
30
src/auth.js
Normal file
30
src/auth.js
Normal file
@ -0,0 +1,30 @@
|
||||
import getAccessTokenDispatcher from "./getAccessToken";
|
||||
|
||||
// test data: 6697e96f70b84092deb6132b, 61381e32f27ab4fbed1ec26e
|
||||
export async function authSubject(request, env, ctx) {
|
||||
const headers = request.headers;
|
||||
const studyId = headers.get('x-study-id');
|
||||
const subjectId = headers.get('x-subject-id');
|
||||
const prolificApiKey = env.PROLIFIC_API_KEY;
|
||||
|
||||
if (!studyId || !subjectId || !subjectId.match(/^[a-zA-Z0-9]{1,32}$/))
|
||||
return false;
|
||||
|
||||
try {
|
||||
const apiUrl = `https://api.prolific.com/api/v1/studies/${studyId}/submissions/`;
|
||||
const res = await fetch(apiUrl, {
|
||||
headers: { Authorization: `Token ${prolificApiKey}` }
|
||||
});
|
||||
let data = await res.json();
|
||||
return Array.from(data.results).some(p => p.participant_id === subjectId);
|
||||
|
||||
} catch (e) { console.log(e); return false; }
|
||||
}
|
||||
|
||||
export async function authDispatcher(request, env, ctx) {
|
||||
if (await authSubject(request, env, ctx)) {
|
||||
return await getAccessTokenDispatcher(request, env, ctx);
|
||||
} else return new Response(null, { status: 401, statusText: 'Unauthorized' });
|
||||
}
|
||||
|
||||
export default authDispatcher;
|
27
src/download.js
Normal file
27
src/download.js
Normal file
@ -0,0 +1,27 @@
|
||||
export async function downloadDispatcher(request, env, ctx) {
|
||||
const url = new URL(request.url);
|
||||
const uri = url.pathname;
|
||||
|
||||
const bucket = env.EXP_DATA;
|
||||
const adminSecret = env.ADMIN_SECRET;
|
||||
|
||||
if (request.headers.get('x-access-token') !== adminSecret)
|
||||
return new Response(null, { status: 401, statusText: 'Unauthorized' });
|
||||
try {
|
||||
const key = uri.slice(10); // '/download/'.length
|
||||
const object = await bucket.get(key);
|
||||
if (object === null)
|
||||
return new Response(null, { status: 404, statusText: 'Not Found' });
|
||||
|
||||
const { body, etag, httpMetadata, customMetadata } = object;
|
||||
return new Response(body, { headers: {
|
||||
etag: etag,
|
||||
'content-type': httpMetadata.contentType,
|
||||
'content-length': body.length,
|
||||
'x-metadata': JSON.stringify(customMetadata),
|
||||
}});
|
||||
|
||||
} catch (e) { return new Response(null, { status: 400 }); }
|
||||
}
|
||||
|
||||
export default downloadDispatcher;
|
29
src/getAccessToken.js
Normal file
29
src/getAccessToken.js
Normal file
@ -0,0 +1,29 @@
|
||||
import { getHashString } from './utils.js'
|
||||
|
||||
export async function getAccessToken(subid, timestamp, secretBase) {
|
||||
return await getHashString(`${subid}${timestamp}${secretBase}`);
|
||||
}
|
||||
|
||||
export async function verifyAccessToken(request, env, ctx) {
|
||||
const headers = request.headers;
|
||||
const subid = headers.get('x-subject-id');
|
||||
const timestamp = headers.get('x-timestamp');
|
||||
const token = headers.get('x-access-token');
|
||||
const secretBase = env.ACCESS_TOKEN_BASE;
|
||||
return token === await getAccessToken(subid, timestamp, secretBase);
|
||||
}
|
||||
|
||||
export async function getAccessTokenDispatcher(request, env, ctx) {
|
||||
const subid = request.headers.get('x-subject-id');
|
||||
if (subid === null)
|
||||
return new Response(null, { status: 400 });
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/:/g, '_');
|
||||
const secretBase = env.ACCESS_TOKEN_BASE;
|
||||
|
||||
const token = await getAccessToken(subid, timestamp, secretBase);
|
||||
const res = JSON.stringify({ token, timestamp });
|
||||
return new Response(res, { status: 200 });
|
||||
}
|
||||
|
||||
export default getAccessTokenDispatcher;
|
43
src/index.js
43
src/index.js
@ -7,17 +7,42 @@
|
||||
*
|
||||
* Learn more at https://developers.cloudflare.com/workers/
|
||||
*/
|
||||
|
||||
async function getHashString(string, method = 'SHA-256') {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(string);
|
||||
const hash = await crypto.subtle.digest(method, data);
|
||||
const hashArray = Array.from(new Uint8Array(hash));
|
||||
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
import getAccessTokenDispatcher from './getAccessToken.js';
|
||||
import authDispatcher from './auth.js';
|
||||
import listDispatcher from './list.js';
|
||||
import downloadDispatcher from './download.js';
|
||||
import putDispatcher from './put.js';
|
||||
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
return new Response('Hello World!');
|
||||
const url = new URL(request.url);
|
||||
const uri = url.pathname;
|
||||
if (url.hostname !== '127.0.0.1' && url.protocol === 'http:') {
|
||||
return new Response(null, { status: 301, headers: {
|
||||
Location: url.toString().replace(/^(http:)/, 'https:')
|
||||
}});
|
||||
}
|
||||
|
||||
if (request.method === 'GET' && uri === '/getAccessToken') {
|
||||
return await getAccessTokenDispatcher(request, env, ctx);
|
||||
}
|
||||
|
||||
if (request.method === 'GET' && uri === '/auth') {
|
||||
return await authDispatcher(request, env, ctx);
|
||||
}
|
||||
|
||||
if (request.method === 'GET' && uri === '/list') {
|
||||
return await listDispatcher(request, env, ctx);
|
||||
}
|
||||
|
||||
if (request.method === 'GET' && uri.startsWith('/download/')) {
|
||||
return await downloadDispatcher(request, env, ctx);
|
||||
}
|
||||
|
||||
if (request.method === 'PUT') {
|
||||
return await putDispatcher(request, env, ctx);
|
||||
}
|
||||
|
||||
return new Response('', { status: 200 });
|
||||
},
|
||||
};
|
||||
|
48
src/list.js
Normal file
48
src/list.js
Normal file
@ -0,0 +1,48 @@
|
||||
export async function listDispatcher(request, env, ctx) {
|
||||
const bucket = env.EXP_DATA;
|
||||
const adminSecret = env.ADMIN_SECRET;
|
||||
const badRequest = new Response(null, { status: 400 });
|
||||
|
||||
if (request.headers.get('x-access-token') !== adminSecret)
|
||||
return new Response(null, { status: 401, statusText: 'Unauthorized' });
|
||||
try {
|
||||
const type = request.headers.get('x-list-type') || 'prefix';
|
||||
let pattern = request.headers.get('x-pattern') || '';
|
||||
|
||||
let objects = [];
|
||||
let truncated = true;
|
||||
let cursor = "";
|
||||
switch (type) {
|
||||
case 'prefix':
|
||||
while (truncated) {
|
||||
const res = await bucket.list({ prefix: pattern, cursor });
|
||||
objects = objects.concat(res.objects);
|
||||
truncated = res.truncated;
|
||||
cursor = res.cursor;
|
||||
}
|
||||
break;
|
||||
case 'suffix':
|
||||
pattern += '$';
|
||||
case 'regex':
|
||||
while (truncated) {
|
||||
const res = await bucket.list({ cursor: cursor });
|
||||
objects = objects.concat(res.objects);
|
||||
truncated = res.truncated;
|
||||
cursor = res.cursor;
|
||||
}
|
||||
pattern = new RegExp(pattern);
|
||||
objects = objects.filter(obj => obj.key.match(pattern));
|
||||
break;
|
||||
default:
|
||||
return badRequest;
|
||||
}
|
||||
|
||||
const res = JSON.stringify(objects, ['key', 'etag', 'size', 'uploaded']);
|
||||
return new Response(res, { status: 200 });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return badRequest;
|
||||
}
|
||||
}
|
||||
|
||||
export default listDispatcher;
|
53
src/put.js
Normal file
53
src/put.js
Normal file
@ -0,0 +1,53 @@
|
||||
import { verifyAccessToken } from "./getAccessToken";
|
||||
|
||||
const allowedContentTypes = [
|
||||
'text/plain',
|
||||
'text/csv',
|
||||
];
|
||||
|
||||
async function invalidPutRequest(request, env, ctx) {
|
||||
const headers = request.headers;
|
||||
const subid = headers.get('x-subject-id');
|
||||
const contentType = headers.get('content-type');
|
||||
|
||||
return (subid === null)
|
||||
|| (!subid.match(/^[a-zA-Z0-9]{1,32}$/))
|
||||
|| (!await verifyAccessToken(request, env, ctx))
|
||||
|| (!allowedContentTypes.includes(contentType));
|
||||
}
|
||||
|
||||
export async function putDispatcher(request, env, ctx) {
|
||||
const url = new URL(request.url);
|
||||
const uri = url.pathname;
|
||||
const headers = request.headers;
|
||||
|
||||
const bucket = env.EXP_DATA;
|
||||
const keyPrefix = env.KEY_PREFIX;
|
||||
const timestamp = new Date().toISOString().replace(/:/g, '_');
|
||||
|
||||
try {
|
||||
if (await invalidPutRequest(request, env, ctx))
|
||||
return new Response(null, { status: 401, statusText: 'Unauthorized' });
|
||||
|
||||
const subid = headers.get('x-subject-id');
|
||||
const data = await request.text();
|
||||
const objectKey = `${subid}${uri}`;
|
||||
const customMetadata = JSON.parse(headers.get('x-metadata') || '{}');
|
||||
const httpMetadata = { contentType: headers.get('content-type') };
|
||||
const result = await bucket.put(
|
||||
keyPrefix + objectKey,
|
||||
data,
|
||||
{ customMetadata, httpMetadata }
|
||||
);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
timestamp,
|
||||
objectKey,
|
||||
etag: result.etag,
|
||||
message: 'Data saved successfully',
|
||||
}), { status: 200 });
|
||||
|
||||
} catch (e) { console.log(e); return new Response(null, { status: 400 }); }
|
||||
}
|
||||
|
||||
export default putDispatcher;
|
7
src/utils.js
Normal file
7
src/utils.js
Normal file
@ -0,0 +1,7 @@
|
||||
export async function getHashString(string, method = 'SHA-256') {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(string);
|
||||
const hash = await crypto.subtle.digest(method, data);
|
||||
const hashArray = Array.from(new Uint8Array(hash));
|
||||
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
Loading…
Reference in New Issue
Block a user