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/
|
* Learn more at https://developers.cloudflare.com/workers/
|
||||||
*/
|
*/
|
||||||
|
import getAccessTokenDispatcher from './getAccessToken.js';
|
||||||
async function getHashString(string, method = 'SHA-256') {
|
import authDispatcher from './auth.js';
|
||||||
const encoder = new TextEncoder();
|
import listDispatcher from './list.js';
|
||||||
const data = encoder.encode(string);
|
import downloadDispatcher from './download.js';
|
||||||
const hash = await crypto.subtle.digest(method, data);
|
import putDispatcher from './put.js';
|
||||||
const hashArray = Array.from(new Uint8Array(hash));
|
|
||||||
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async fetch(request, env, ctx) {
|
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