diff --git a/supabase/functions/main/index.ts b/supabase/functions/main/index.ts new file mode 100644 index 0000000..29f66a1 --- /dev/null +++ b/supabase/functions/main/index.ts @@ -0,0 +1,99 @@ +import { serve } from "https://deno.land/std@0.131.0/http/server.ts"; +import * as jose from "https://deno.land/x/jose@v4.14.4/index.ts"; + +/** + * The `main` function is only used for the self-hosted FeedDeck setup, so that + * the edge functions are working. For FeedDeck hosted on the Supabase Cloud + * offering the function is ignored and not deployed. + */ +console.log("main function started"); + +const JWT_SECRET = Deno.env.get("JWT_SECRET"); +const VERIFY_JWT = Deno.env.get("VERIFY_JWT") === "true"; + +function getAuthToken(req: Request) { + const authHeader = req.headers.get("authorization"); + if (!authHeader) { + throw new Error("Missing authorization header"); + } + const [bearer, token] = authHeader.split(" "); + if (bearer !== "Bearer") { + throw new Error(`Auth header is not 'Bearer {token}'`); + } + return token; +} + +async function verifyJWT(jwt: string): Promise { + const encoder = new TextEncoder(); + const secretKey = encoder.encode(JWT_SECRET); + try { + await jose.jwtVerify(jwt, secretKey); + } catch (err) { + console.error(err); + return false; + } + return true; +} + +serve(async (req: Request) => { + if (req.method !== "OPTIONS" && VERIFY_JWT) { + try { + const token = getAuthToken(req); + const isValidJWT = await verifyJWT(token); + + if (!isValidJWT) { + return new Response(JSON.stringify({ msg: "Invalid JWT" }), { + status: 401, + headers: { "Content-Type": "application/json" }, + }); + } + } catch (e) { + console.error(e); + return new Response(JSON.stringify({ msg: e.toString() }), { + status: 401, + headers: { "Content-Type": "application/json" }, + }); + } + } + + const url = new URL(req.url); + const { pathname } = url; + const path_parts = pathname.split("/"); + const service_name = path_parts[1]; + + if (!service_name || service_name === "") { + const error = { msg: "missing function name in request" }; + return new Response(JSON.stringify(error), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + + const servicePath = `/home/deno/functions/${service_name}`; + console.error(`serving the request with ${servicePath}`); + + const memoryLimitMb = 150; + const workerTimeoutMs = 1 * 60 * 1000; + const noModuleCache = false; + const importMapPath = null; + const envVarsObj = Deno.env.toObject(); + const envVars = Object.keys(envVarsObj).map((k) => [k, envVarsObj[k]]); + + try { + const worker = await EdgeRuntime.userWorkers.create({ + servicePath, + memoryLimitMb, + workerTimeoutMs, + noModuleCache, + importMapPath, + envVars, + }); + return await worker.fetch(req); + } catch (e) { + const error = { msg: e.toString() }; + return new Response(JSON.stringify(error), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); + } +}); diff --git a/supabase/hosting/.gitignore b/supabase/hosting/.gitignore index 78f7737..c3e9ac2 100644 --- a/supabase/hosting/.gitignore +++ b/supabase/hosting/.gitignore @@ -1,6 +1,5 @@ !volumes/db/_supabase.sql volumes/db/data -volumes/functions/* -!volumes/functions/main +volumes/functions volumes/storage .env diff --git a/supabase/hosting/README.md b/supabase/hosting/README.md index 9ef9a90..24c3d3c 100644 --- a/supabase/hosting/README.md +++ b/supabase/hosting/README.md @@ -18,8 +18,7 @@ be found in the FeedDeck contributing guide and the Supabase documentation. 2. Copy all edge functions into the correct directory: ```sh - find ./volumes/functions -maxdepth 1 ! -name main -exec rm -rf {} + - cp -a ../functions/. volumes/functions/ + cp -r ../functions/. volumes/functions/ ``` 3. Create a `.env` file and adjust the environment variables as needed: @@ -59,4 +58,5 @@ be found in the FeedDeck contributing guide and the Supabase documentation. docker compose down -v rm -rf volumes/db/data/ rm -rf volumes/storage/ + rm -rf volumes/functions/ ``` diff --git a/supabase/hosting/volumes/functions/main/index.ts b/supabase/hosting/volumes/functions/main/index.ts deleted file mode 100644 index a094010..0000000 --- a/supabase/hosting/volumes/functions/main/index.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { serve } from 'https://deno.land/std@0.131.0/http/server.ts' -import * as jose from 'https://deno.land/x/jose@v4.14.4/index.ts' - -console.log('main function started') - -const JWT_SECRET = Deno.env.get('JWT_SECRET') -const VERIFY_JWT = Deno.env.get('VERIFY_JWT') === 'true' - -function getAuthToken(req: Request) { - const authHeader = req.headers.get('authorization') - if (!authHeader) { - throw new Error('Missing authorization header') - } - const [bearer, token] = authHeader.split(' ') - if (bearer !== 'Bearer') { - throw new Error(`Auth header is not 'Bearer {token}'`) - } - return token -} - -async function verifyJWT(jwt: string): Promise { - const encoder = new TextEncoder() - const secretKey = encoder.encode(JWT_SECRET) - try { - await jose.jwtVerify(jwt, secretKey) - } catch (err) { - console.error(err) - return false - } - return true -} - -serve(async (req: Request) => { - if (req.method !== 'OPTIONS' && VERIFY_JWT) { - try { - const token = getAuthToken(req) - const isValidJWT = await verifyJWT(token) - - if (!isValidJWT) { - return new Response(JSON.stringify({ msg: 'Invalid JWT' }), { - status: 401, - headers: { 'Content-Type': 'application/json' }, - }) - } - } catch (e) { - console.error(e) - return new Response(JSON.stringify({ msg: e.toString() }), { - status: 401, - headers: { 'Content-Type': 'application/json' }, - }) - } - } - - const url = new URL(req.url) - const { pathname } = url - const path_parts = pathname.split('/') - const service_name = path_parts[1] - - if (!service_name || service_name === '') { - const error = { msg: 'missing function name in request' } - return new Response(JSON.stringify(error), { - status: 400, - headers: { 'Content-Type': 'application/json' }, - }) - } - - const servicePath = `/home/deno/functions/${service_name}` - console.error(`serving the request with ${servicePath}`) - - const memoryLimitMb = 150 - const workerTimeoutMs = 1 * 60 * 1000 - const noModuleCache = false - const importMapPath = null - const envVarsObj = Deno.env.toObject() - const envVars = Object.keys(envVarsObj).map((k) => [k, envVarsObj[k]]) - - try { - const worker = await EdgeRuntime.userWorkers.create({ - servicePath, - memoryLimitMb, - workerTimeoutMs, - noModuleCache, - importMapPath, - envVars, - }) - return await worker.fetch(req) - } catch (e) { - const error = { msg: e.toString() } - return new Response(JSON.stringify(error), { - status: 500, - headers: { 'Content-Type': 'application/json' }, - }) - } -})