diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 949cbf2..401338e 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -10,10 +10,6 @@ jobs: build-and-test: runs-on: ubuntu-latest - env: - TOKEN: ${{ secrets.TOKEN }} - FUNCTION_ENDPOINT: ${{ secrets.FUNCTION_ENDPOINT }} - steps: - uses: actions/checkout@v3 @@ -28,5 +24,8 @@ jobs: - name: Build run: dotnet build --configuration Release --no-restore + - name: Initialize Testing Stack + run: docker-compose up -d + - name: Test run: dotnet test --no-restore diff --git a/FunctionsTests/ClientTests.cs b/FunctionsTests/ClientTests.cs index 99c60a3..570c84b 100644 --- a/FunctionsTests/ClientTests.cs +++ b/FunctionsTests/ClientTests.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Text; using System.Threading.Tasks; +using Microsoft.IdentityModel.Tokens; using Microsoft.VisualStudio.TestTools.UnitTesting; using Supabase.Functions; using static Supabase.Functions.Client; @@ -16,10 +19,8 @@ public class ClientTests [TestInitialize] public void Initialize() { - var endpoint = Environment.GetEnvironmentVariable("FUNCTION_ENDPOINT"); - - _token = Environment.GetEnvironmentVariable("TOKEN"); - _client = new Client(endpoint); + _token = GenerateToken("37c304f8-51aa-419a-a1af-06154e63707a"); + _client = new Client("http://localhost:9000"); } [TestMethod("Invokes a function.")] @@ -63,5 +64,19 @@ public async Task Invokes() Assert.IsInstanceOfType(bytes, typeof(byte[])); } + + private static string GenerateToken(string secret) + { + var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)); + + var tokenDescriptor = new SecurityTokenDescriptor + { + SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256Signature) + }; + + var tokenHandler = new JwtSecurityTokenHandler(); + var securityToken = tokenHandler.CreateToken(tokenDescriptor); + return tokenHandler.WriteToken(securityToken); + } } } diff --git a/README.md b/README.md index 817ec37..9656fa4 100644 --- a/README.md +++ b/README.md @@ -26,3 +26,10 @@ Join the ranks! See a problem? Help fix it! ## Contributing We are more than happy to have contributions! Please submit a PR. + +### Testing + +To run the tests locally you must have docker and docker-compose installed. Then in the root of the repository run: + +- `docker-compose up -d` +- `dotnet test` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c25f22a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +version: "3" + +services: + functions: + image: supabase/edge-runtime:v1.30.0 + ports: + - "9000:9000" + environment: + JWT_SECRET: "37c304f8-51aa-419a-a1af-06154e63707a" + VERIFY_JWT: "true" + volumes: + - ./supabase/functions:/home/deno/functions:Z + command: + - start + - --main-service + - /home/deno/functions/main + restart: unless-stopped diff --git a/supabase/functions/main/index.ts b/supabase/functions/main/index.ts new file mode 100644 index 0000000..f924477 --- /dev/null +++ b/supabase/functions/main/index.ts @@ -0,0 +1,94 @@ +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' }, + }) + } +}) \ No newline at end of file