From a9b4bef96f34973dcb31b103fe783c1cd866f7e0 Mon Sep 17 00:00:00 2001 From: Rico Berger Date: Sat, 19 Apr 2025 09:10:43 +0200 Subject: [PATCH] Update Deno Version to 1.45.2 (#242) * Update Deno Version to 1.45.2 Update Deno Version in the Docker image to version 1.45.2 and update the `@supabase/supabase-js` dependency specified in the `import_map.json` file. **NOTES:** - The current recommended approach for managing dependencies is using a `deno.json` file, which is currently not working locally and returns the following error: ``` serving the request with supabase/functions/add-or-update-source-v1 worker boot error: failed to create the graph: Relative import path "@supabase/supabase-js" not prefixed with / or ./ or ../ at file:///Users/ricoberger/Documents/GitHub/feeddeck/feeddeck/supabase/functions/add-or-update-source-v1/index.ts:1:30 worker boot error: failed to create the graph: Relative import path "@supabase/supabase-js" not prefixed with / or ./ or ../ at file:///Users/ricoberger/Documents/GitHub/feeddeck/feeddeck/supabase/functions/add-or-update-source-v1/index.ts:1:30 InvalidWorkerCreation: worker boot error: failed to create the graph: Relative import path "@supabase/supabase-js" not prefixed with / or ./ or ../ at file:///Users/ricoberger/Documents/GitHub/feeddeck/feeddeck/supabase/functions/add-or-update-source-v1/index.ts:1:30 at async UserWorker.create (ext:sb_user_workers/user_workers.js:139:15) at async Object.handler (file:///root/index.ts:157:22) at async respond (ext:sb_core_main_js/js/http.js:197:14) { name: "InvalidWorkerCreation" } ``` - When using Deno v2 the dependencies via the `deno.json` file are working. To enable Deno v2 the following must be set in the `config.toml` file: ``` [edge_runtime] deno_version = 2 ``` - Deno v2 is currently only supported locally, see https://supabase.com/blog/supabase-edge-functions-deploy-dashboard-deno-2-1#deno-21-preview - Once Deno v2 is supported in the hosted platform, we should switch from the `import_map.json` to `deno.json` to manage dependencies and update the existing dependencies. * Fix Errors - Replace `err.toString()` with just `err` when logging errors. - Add new `blobToFile` function, to upload files to the Supabase storage, otherwise we receive the following error (https://github.com/feeddeck/feeddeck/actions/runs/14546547197/job/40812707871): ``` error: TS2345 [ERROR]: Argument of type 'Blob' is not assignable to parameter of type 'FileBody'. Type 'Blob' is missing the following properties from type 'File': lastModified, name, webkitRelativePath file, ~~~~ at file:///home/runner/work/feeddeck/feeddeck/supabase/functions/_shared/feed/utils/uploadFile.ts:66:9 ``` --- supabase/functions/_cmd/Dockerfile | 3 +- supabase/functions/_cmd/cmd.ts | 64 ++++--- .../functions/_cmd/scheduler/scheduler.ts | 79 ++++---- supabase/functions/_cmd/worker/worker.ts | 54 +++--- .../_shared/feed/utils/getAndParseFeed.ts | 49 ++--- .../_shared/feed/utils/uploadFile.ts | 60 +++--- .../add-or-update-source-v1/index.ts | 166 ++++++++--------- supabase/functions/add-source-v1/index.ts | 176 +++++++++--------- supabase/functions/delete-user-v1/index.ts | 67 +++---- .../functions/generate-magic-link-v1/index.ts | 46 ++--- supabase/functions/import_map.json | 2 +- supabase/functions/profile-v1/index.ts | 163 ++++++++-------- supabase/functions/profile-v2/index.ts | 82 ++++---- .../functions/revenuecat-webhooks-v1/index.ts | 98 +++++----- .../index.ts | 32 ++-- .../index.ts | 32 ++-- .../functions/stripe-webhooks-v1/index.ts | 41 ++-- 17 files changed, 599 insertions(+), 615 deletions(-) diff --git a/supabase/functions/_cmd/Dockerfile b/supabase/functions/_cmd/Dockerfile index c5cca53..04fe67b 100644 --- a/supabase/functions/_cmd/Dockerfile +++ b/supabase/functions/_cmd/Dockerfile @@ -1,5 +1,4 @@ -# FROM denoland/deno:1.34.1 -FROM lukechannings/deno:v1.40.5 + FROM denoland/deno:1.45.2 WORKDIR /app diff --git a/supabase/functions/_cmd/cmd.ts b/supabase/functions/_cmd/cmd.ts index 08a56ac..b697004 100644 --- a/supabase/functions/_cmd/cmd.ts +++ b/supabase/functions/_cmd/cmd.ts @@ -1,7 +1,7 @@ -import { log } from '../_shared/utils/log.ts'; -import { runScheduler } from './scheduler/scheduler.ts'; -import { runWorker } from './worker/worker.ts'; -import { runTools } from './tools/tools.ts'; +import { log } from "../_shared/utils/log.ts"; +import { runScheduler } from "./scheduler/scheduler.ts"; +import { runWorker } from "./worker/worker.ts"; +import { runTools } from "./tools/tools.ts"; /** * Next to the Supabase Edge functions we also have to create an command which @@ -9,32 +9,38 @@ import { runTools } from './tools/tools.ts'; * scheduler or worker, to refetch the feeds for all user sources. */ const main = (args: string[]) => { - if (args.length === 1 && args[0] === 'scheduler') { - log('info', 'Start scheduler...'); - runScheduler().then(() => { - Deno.exit(0); - }).catch((err) => { - log('error', 'Scheduler crashed', { error: err.toString() }); - Deno.exit(1); - }); - } else if (args.length === 1 && args[0] === 'worker') { - log('info', 'Start worker...'); - runWorker().then(() => { - Deno.exit(0); - }).catch((err) => { - log('error', 'Worker crashed', { error: err.toString() }); - Deno.exit(1); - }); - } else if (args.length >= 2 && args[0] === 'tools') { - log('info', 'Start tools...'); - runTools(args).then(() => { - Deno.exit(0); - }).catch((err) => { - log('error', 'Tools crashed', { error: err.toString() }); - Deno.exit(1); - }); + if (args.length === 1 && args[0] === "scheduler") { + log("info", "Start scheduler..."); + runScheduler() + .then(() => { + Deno.exit(0); + }) + .catch((err) => { + log("error", "Scheduler crashed", { error: err }); + Deno.exit(1); + }); + } else if (args.length === 1 && args[0] === "worker") { + log("info", "Start worker..."); + runWorker() + .then(() => { + Deno.exit(0); + }) + .catch((err) => { + log("error", "Worker crashed", { error: err }); + Deno.exit(1); + }); + } else if (args.length >= 2 && args[0] === "tools") { + log("info", "Start tools..."); + runTools(args) + .then(() => { + Deno.exit(0); + }) + .catch((err) => { + log("error", "Tools crashed", { error: err }); + Deno.exit(1); + }); } else { - log('error', 'Invalid command-line arguments', { args: args }); + log("error", "Invalid command-line arguments", { args: args }); Deno.exit(1); } }; diff --git a/supabase/functions/_cmd/scheduler/scheduler.ts b/supabase/functions/_cmd/scheduler/scheduler.ts index e0d98e3..d5aff19 100644 --- a/supabase/functions/_cmd/scheduler/scheduler.ts +++ b/supabase/functions/_cmd/scheduler/scheduler.ts @@ -1,7 +1,7 @@ -import { connect, Redis } from 'redis'; -import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { connect, Redis } from "redis"; +import { createClient, SupabaseClient } from "@supabase/supabase-js"; -import { log } from '../../_shared/utils/log.ts'; +import { log } from "../../_shared/utils/log.ts"; import { FEEDDECK_REDIS_HOSTNAME, FEEDDECK_REDIS_PASSWORD, @@ -9,7 +9,7 @@ import { FEEDDECK_REDIS_USERNAME, FEEDDECK_SUPABASE_SERVICE_ROLE_KEY, FEEDDECK_SUPABASE_URL, -} from '../../_shared/utils/constants.ts'; +} from "../../_shared/utils/constants.ts"; /** * `runScheduler` starts the scheduler which is responsible for fetching all @@ -78,11 +78,10 @@ const scheduleSources = async ( * The `sourcesUpdatedAt` is used to fetch all sources which where not * updated in the last hour. */ - const profileCreatedAt = Math.floor(new Date().getTime() / 1000) - - (60 * 60 * 24 * 7); - const sourcesUpdatedAt = Math.floor(new Date().getTime() / 1000) - - (60 * 60); - log('info', 'Schedule sources', { + const profileCreatedAt = + Math.floor(new Date().getTime() / 1000) - 60 * 60 * 24 * 7; + const sourcesUpdatedAt = Math.floor(new Date().getTime() / 1000) - 60 * 60; + log("info", "Schedule sources", { sourcesUpdatedAt: sourcesUpdatedAt, profileCreatedAt: profileCreatedAt, }); @@ -98,19 +97,17 @@ const scheduleSources = async ( let offset = 0; while (true) { - log('debug', 'Fetching profiles', { offset: offset }); + log("debug", "Fetching profiles", { offset: offset }); const { data: tmpProfiles, error: profilesError } = await supabaseClient - .from( - 'profiles', - ).select('*').or(`tier.eq.premium,createdAt.gt.${profileCreatedAt}`) - .order( - 'createdAt', - ) + .from("profiles") + .select("*") + .or(`tier.eq.premium,createdAt.gt.${profileCreatedAt}`) + .order("createdAt") .range(offset, offset + batchSize); if (profilesError) { - log('error', 'Failed to get user profiles', { - 'error': profilesError, + log("error", "Failed to get user profiles", { + error: profilesError, }); } else { profiles.push(...tmpProfiles); @@ -123,27 +120,25 @@ const scheduleSources = async ( } } - log('info', 'Fetched profiles', { profilesCount: profiles.length }); + log("info", "Fetched profiles", { profilesCount: profiles.length }); for (const profile of profiles) { /** * Fetch all sources for the current user profile which where not updated * in the last hour. */ const { data: sources, error: sourcesError } = await supabaseClient - .from( - 'sources', - ).select('*').eq('userId', profile.id).lt( - 'updatedAt', - sourcesUpdatedAt, - ); + .from("sources") + .select("*") + .eq("userId", profile.id) + .lt("updatedAt", sourcesUpdatedAt); if (sourcesError) { - log('error', 'Failed to get user sources', { - 'profile': profile.id, - 'error': sourcesError, + log("error", "Failed to get user sources", { + profile: profile.id, + error: sourcesError, }); } else { - log('info', 'Fetched sources', { - 'profile': profile.id, + log("info", "Fetched sources", { + profile: profile.id, sourcesCount: sources.length, }); for (const source of sources) { @@ -152,7 +147,7 @@ const scheduleSources = async ( * for updates anymore. * See https://github.com/zedeus/nitter/issues/1155#issuecomment-1913361757 */ - if (source.type === 'nitter') { + if (source.type === "nitter") { continue; } @@ -161,14 +156,14 @@ const scheduleSources = async ( * was already updated in the last 24 hours. This is done to avoid * hitting the rate limits of the Reddit API. */ - if (profile.tier === 'free' && source.type === 'reddit') { + if (profile.tier === "free" && source.type === "reddit") { if ( - source.updatedAt > Math.floor(new Date().getTime() / 1000) - - (60 * 60 * 24) + source.updatedAt > + Math.floor(new Date().getTime() / 1000) - 60 * 60 * 24 ) { - log('debug', 'Skip source', { - 'source': source.id, - 'profile': profile.id, + log("debug", "Skip source", { + source: source.id, + profile: profile.id, }); continue; } @@ -180,12 +175,12 @@ const scheduleSources = async ( * we need the users account information to fetch the sources data, * e.g. the users GitHub token. */ - log('info', 'Scheduling source', { - 'source': source.id, - 'profile': profile.id, + log("info", "Scheduling source", { + source: source.id, + profile: profile.id, }); await redisClient.rpush( - 'sources', + "sources", JSON.stringify({ source: source, profile: profile, @@ -195,6 +190,6 @@ const scheduleSources = async ( } } } catch (err) { - log('error', 'Failed to schedule sources...', { error: err.toString() }); + log("error", "Failed to schedule sources...", { error: err }); } }; diff --git a/supabase/functions/_cmd/worker/worker.ts b/supabase/functions/_cmd/worker/worker.ts index 04d30d1..5b6e818 100644 --- a/supabase/functions/_cmd/worker/worker.ts +++ b/supabase/functions/_cmd/worker/worker.ts @@ -1,16 +1,16 @@ -import { connect, Redis } from 'redis'; -import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { connect, Redis } from "redis"; +import { createClient, SupabaseClient } from "@supabase/supabase-js"; -import { log } from '../../_shared/utils/log.ts'; -import { getFeed } from '../../_shared/feed/feed.ts'; +import { log } from "../../_shared/utils/log.ts"; +import { getFeed } from "../../_shared/feed/feed.ts"; import { FEEDDECK_REDIS_HOSTNAME, FEEDDECK_REDIS_PASSWORD, FEEDDECK_REDIS_PORT, FEEDDECK_REDIS_USERNAME, FEEDDECK_SUPABASE_URL, -} from '../../_shared/utils/constants.ts'; -import { FEEDDECK_SUPABASE_SERVICE_ROLE_KEY } from '../../_shared/utils/constants.ts'; +} from "../../_shared/utils/constants.ts"; +import { FEEDDECK_SUPABASE_SERVICE_ROLE_KEY } from "../../_shared/utils/constants.ts"; /** * `runWorker` starts the worker which is responsible for fetching the feeds for @@ -63,14 +63,14 @@ const listenForSources = async ( * Listen for new sources in the Redis queue. Once a valid source is * received we get the source and profile from the Redis data. */ - const data = await redisClient.blpop(1000 * 60, 'sources'); - if (data && data[0] === 'sources') { + const data = await redisClient.blpop(1000 * 60, "sources"); + if (data && data[0] === "sources") { const { source: redisSource, profile: redisProfile } = JSON.parse( data[1], ); - log('info', 'Received source', { - 'source': redisSource.id, - 'profile': redisProfile.id, + log("info", "Received source", { + source: redisSource.id, + profile: redisProfile.id, }); try { @@ -93,36 +93,34 @@ const listenForSources = async ( * sources and items. */ if (items.length > 0) { - const { error: sourceError } = await supabaseClient.from('sources') - .upsert( - source, - ); + const { error: sourceError } = await supabaseClient + .from("sources") + .upsert(source); if (sourceError) { - log('error', 'Failed to save sources', { 'error': sourceError }); + log("error", "Failed to save sources", { error: sourceError }); } - const { error: itemsError } = await supabaseClient.from('items') - .upsert( - items, - ); + const { error: itemsError } = await supabaseClient + .from("items") + .upsert(items); if (itemsError) { - log('error', 'Failed to save items', { 'error': itemsError }); + log("error", "Failed to save items", { error: itemsError }); } - log('info', 'Updated source', { - 'source': redisSource.id, - 'profile': redisProfile.id, + log("info", "Updated source", { + source: redisSource.id, + profile: redisProfile.id, }); } } catch (err) { - log('error', 'Failed to fetch feed', { + log("error", "Failed to fetch feed", { source: redisSource, - 'profile': redisProfile.id, - 'error': err.toString(), + profile: redisProfile.id, + error: err, }); } } } catch (err) { - log('error', 'Failed to listen for sources', { 'error': err.toString() }); + log("error", "Failed to listen for sources", { error: err }); } }; diff --git a/supabase/functions/_shared/feed/utils/getAndParseFeed.ts b/supabase/functions/_shared/feed/utils/getAndParseFeed.ts index 05fbefa..68460c3 100644 --- a/supabase/functions/_shared/feed/utils/getAndParseFeed.ts +++ b/supabase/functions/_shared/feed/utils/getAndParseFeed.ts @@ -1,9 +1,9 @@ -import { Feed, parseFeed } from 'rss'; +import { Feed, parseFeed } from "rss"; -import { utils } from '../../utils/index.ts'; -import { feedutils } from './index.ts'; -import { ISource } from '../../models/source.ts'; -import { FEEDDECK_LOG_LEVEL } from '../../utils/constants.ts'; +import { utils } from "../../utils/index.ts"; +import { feedutils } from "./index.ts"; +import { ISource } from "../../models/source.ts"; +import { FEEDDECK_LOG_LEVEL } from "../../utils/constants.ts"; export const getAndParseFeed = async ( requestUrl: string, @@ -16,7 +16,7 @@ export const getAndParseFeed = async ( } try { - utils.log('debug', 'Get and parse feed', { + utils.log("debug", "Get and parse feed", { sourceType: source.type, requestUrl: requestUrl, }); @@ -24,10 +24,10 @@ export const getAndParseFeed = async ( requestUrl, requestOptions ? { - ...requestOptions, - method: 'get', - } - : { method: 'get' }, + ...requestOptions, + method: "get", + } + : { method: "get" }, 5000, ); @@ -36,11 +36,11 @@ export const getAndParseFeed = async ( if (err instanceof feedutils.FeedValidationError) { throw err; } else { - utils.log('error', 'Failed to get feed', { + utils.log("error", "Failed to get feed", { source: source, - error: err.toString(), + error: err, }); - throw new feedutils.FeedGetAndParseError('Failed to get feed'); + throw new feedutils.FeedGetAndParseError("Failed to get feed"); } } }; @@ -56,17 +56,18 @@ const _parseFeed = async ( const feed = await parseFeed(feedData); return feed; } catch (err) { - utils.log('error', 'Failed to parse feed', { + utils.log("error", "Failed to parse feed", { source: source, requestUrl: requestUrl, responseStatus: response.status, - responseBody: FEEDDECK_LOG_LEVEL === 'debug' ? feedData : '', - responseHeaders: FEEDDECK_LOG_LEVEL === 'debug' - ? Object.fromEntries(response.headers.entries()) - : '', - error: err.toString(), + responseBody: FEEDDECK_LOG_LEVEL === "debug" ? feedData : "", + responseHeaders: + FEEDDECK_LOG_LEVEL === "debug" + ? Object.fromEntries(response.headers.entries()) + : "", + error: err, }); - throw new feedutils.FeedGetAndParseError('Failed to parse feed'); + throw new feedutils.FeedGetAndParseError("Failed to parse feed"); } }; @@ -78,11 +79,11 @@ const _parseFeedData = async ( const feed = await parseFeed(feedData); return feed; } catch (err) { - utils.log('error', 'Failed to parse feed', { + utils.log("error", "Failed to parse feed", { source: source, - feedData: FEEDDECK_LOG_LEVEL === 'debug' ? feedData : '', - error: err.toString(), + feedData: FEEDDECK_LOG_LEVEL === "debug" ? feedData : "", + error: err, }); - throw new feedutils.FeedGetAndParseError('Failed to parse feed'); + throw new feedutils.FeedGetAndParseError("Failed to parse feed"); } }; diff --git a/supabase/functions/_shared/feed/utils/uploadFile.ts b/supabase/functions/_shared/feed/utils/uploadFile.ts index 382b55d..413fc17 100644 --- a/supabase/functions/_shared/feed/utils/uploadFile.ts +++ b/supabase/functions/_shared/feed/utils/uploadFile.ts @@ -1,8 +1,8 @@ -import { SupabaseClient } from '@supabase/supabase-js'; +import { SupabaseClient } from "@supabase/supabase-js"; -import { ISource } from '../../models/source.ts'; -import { fetchWithTimeout } from '../../utils/fetchWithTimeout.ts'; -import { log } from '../../utils/log.ts'; +import { ISource } from "../../models/source.ts"; +import { fetchWithTimeout } from "../../utils/fetchWithTimeout.ts"; +import { log } from "../../utils/log.ts"; /** * `uploadSourceIcon` uploads the `icon` of the provided `source` to the @@ -16,8 +16,9 @@ export const uploadSourceIcon = async ( source: ISource, ): Promise => { if ( - !source.icon || source.icon === '' || (!source.icon.startsWith('http://') && - !source.icon.startsWith('https://')) + !source.icon || + source.icon === "" || + (!source.icon.startsWith("http://") && !source.icon.startsWith("https://")) ) { return undefined; } @@ -25,9 +26,9 @@ export const uploadSourceIcon = async ( try { const cdnIcon = await uploadFile( supabaseClient, - 'sources', + "sources", source.icon, - `${source.userId}/${source.id}.${source.icon.split('.').pop()}`, + `${source.userId}/${source.id}.${source.icon.split(".").pop()}`, ); if (cdnIcon) { @@ -54,30 +55,45 @@ const uploadFile = async ( try { const fileResponse = await fetchWithTimeout( sourcePath, - { method: 'get' }, + { method: "get" }, 5000, ); - const file = await fileResponse.blob(); + const blob = await fileResponse.blob(); - const { data: uploadData, error: uploadError } = await supabaseClient - .storage.from(bucket) - .upload( - targetPath, - file, - { - upsert: true, - }, - ); + const { data: uploadData, error: uploadError } = + await supabaseClient.storage + .from(bucket) + .upload( + targetPath, + blobToFile(blob, targetPath.replace(/^.*[\\/]/, "")), + { + upsert: true, + }, + ); if (uploadError) { - log('error', 'Failed to upload source icon', { - 'error': uploadError, + log("error", "Failed to upload source icon", { + error: uploadError, }); return undefined; } return uploadData?.path; } catch (err) { - log('error', 'Failed to upload source icon', { 'error': err.toString() }); + log("error", "Failed to upload source icon", { error: err }); return undefined; } }; + +/** + * `blobToFile` converts a Blob to a File. This is needed because the Supabase + * client only accepts File objects for uploading files. The `File` object + * will be created with the provided `fileName` and the current date as + * last modified date. + */ +const blobToFile = (blob: Blob, fileName: string): File => { + // deno-lint-ignore no-explicit-any + const b: any = blob; + b.lastModifiedDate = new Date(); + b.name = fileName; + return blob as File; +}; diff --git a/supabase/functions/add-or-update-source-v1/index.ts b/supabase/functions/add-or-update-source-v1/index.ts index 7e31bd1..e152e87 100644 --- a/supabase/functions/add-or-update-source-v1/index.ts +++ b/supabase/functions/add-or-update-source-v1/index.ts @@ -1,16 +1,16 @@ -import { createClient } from '@supabase/supabase-js'; +import { createClient } from "@supabase/supabase-js"; -import { corsHeaders } from '../_shared/utils/cors.ts'; -import { getFeed } from '../_shared/feed/feed.ts'; -import { ISource } from '../_shared/models/source.ts'; -import { IProfile } from '../_shared/models/profile.ts'; -import { utils } from '../_shared/utils/index.ts'; -import { feedutils } from '../_shared/feed/utils/index.ts'; +import { corsHeaders } from "../_shared/utils/cors.ts"; +import { getFeed } from "../_shared/feed/feed.ts"; +import { ISource } from "../_shared/models/source.ts"; +import { IProfile } from "../_shared/models/profile.ts"; +import { utils } from "../_shared/utils/index.ts"; +import { feedutils } from "../_shared/feed/utils/index.ts"; import { FEEDDECK_SUPABASE_ANON_KEY, FEEDDECK_SUPABASE_SERVICE_ROLE_KEY, FEEDDECK_SUPABASE_URL, -} from '../_shared/utils/constants.ts'; +} from "../_shared/utils/constants.ts"; /** * The `add-or-update-source-v1` edge function is used to add a new source and @@ -24,8 +24,8 @@ Deno.serve(async (req) => { * We need to handle the preflight request for CORS as it is described in the * Supabase documentation: https://supabase.com/docs/guides/functions/cors */ - if (req.method === 'OPTIONS') { - return new Response('ok', { headers: corsHeaders }); + if (req.method === "OPTIONS") { + return new Response("ok", { headers: corsHeaders }); } try { @@ -39,7 +39,7 @@ Deno.serve(async (req) => { FEEDDECK_SUPABASE_ANON_KEY, { global: { - headers: { Authorization: req.headers.get('Authorization')! }, + headers: { Authorization: req.headers.get("Authorization")! }, }, auth: { autoRefreshToken: false, @@ -51,12 +51,14 @@ Deno.serve(async (req) => { /** * Get the user from the request. If there is no user, we return an error. */ - const { data: { user } } = await userSupabaseClient.auth.getUser(); + const { + data: { user }, + } = await userSupabaseClient.auth.getUser(); if (!user) { - return new Response(JSON.stringify({ error: 'Unauthorized' }), { + return new Response(JSON.stringify({ error: "Unauthorized" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 401, }); @@ -84,21 +86,20 @@ Deno.serve(async (req) => { * than one profile, we return an error. */ const { data: profile, error: profileError } = await adminSupabaseClient - .from( - 'profiles', - ) - .select('*').eq('id', user.id); + .from("profiles") + .select("*") + .eq("id", user.id); if (profileError || profile?.length !== 1) { - utils.log('error', 'Failed to get user profile', { - 'user': user, - 'error': profileError, + utils.log("error", "Failed to get user profile", { + user: user, + error: profileError, }); return new Response( - JSON.stringify({ error: 'Failed to get user profile' }), + JSON.stringify({ error: "Failed to get user profile" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 500, }, @@ -116,58 +117,53 @@ Deno.serve(async (req) => { * sources. */ const { count: sourcesCount, error: countError } = await adminSupabaseClient - .from('sources') - .select( - '*', - { count: 'exact' }, - ).eq('userId', user.id); + .from("sources") + .select("*", { count: "exact" }) + .eq("userId", user.id); if (countError || sourcesCount === null) { - utils.log('error', 'Failed to get sources', { 'error': countError }); - return new Response( - JSON.stringify({ error: 'Failed to get sources' }), - { - headers: { - ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', - }, - status: 500, + utils.log("error", "Failed to get sources", { error: countError }); + return new Response(JSON.stringify({ error: "Failed to get sources" }), { + headers: { + ...corsHeaders, + "Content-Type": "application/json; charset=utf-8", }, - ); + status: 500, + }); } - if (profile[0].tier === 'free' && sourcesCount >= 10) { + if (profile[0].tier === "free" && sourcesCount >= 10) { utils.log( - 'warning', - 'User is on the free tier and has reached the maximum number of sources', + "warning", + "User is on the free tier and has reached the maximum number of sources", ); return new Response( JSON.stringify({ error: - 'You reached the maximum number of sources you can create, please upgrade your account to the premium tier.', + "You reached the maximum number of sources you can create, please upgrade your account to the premium tier.", }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 400, }, ); } - if (profile[0].tier === 'premium' && sourcesCount >= 1000) { + if (profile[0].tier === "premium" && sourcesCount >= 1000) { utils.log( - 'warning', - 'User is on the premium tier and has reached the maximum number of sources', + "warning", + "User is on the premium tier and has reached the maximum number of sources", ); return new Response( JSON.stringify({ - error: 'You reached the maximum number of sources you can create.', + error: "You reached the maximum number of sources you can create.", }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 400, }, @@ -197,32 +193,30 @@ Deno.serve(async (req) => { * If there is an error, we return an error. If the insert succeeds we * return the source object. */ - const { error: sourceError } = await adminSupabaseClient.from('sources') - .insert( - source, - ); + const { error: sourceError } = await adminSupabaseClient + .from("sources") + .insert(source); if (sourceError) { - utils.log('error', 'Failed to save sources', { 'error': sourceError }); - return new Response(JSON.stringify({ error: 'Failed to save sources' }), { + utils.log("error", "Failed to save sources", { error: sourceError }); + return new Response(JSON.stringify({ error: "Failed to save sources" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 500, }); } if (items.length > 0) { - const { error: itemsError } = await adminSupabaseClient.from('items') - .insert( - items, - ); + const { error: itemsError } = await adminSupabaseClient + .from("items") + .insert(items); if (itemsError) { - utils.log('error', 'Failed to save items', { 'error': itemsError }); - return new Response(JSON.stringify({ error: 'Failed to save items' }), { + utils.log("error", "Failed to save items", { error: itemsError }); + return new Response(JSON.stringify({ error: "Failed to save items" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 500, }); @@ -232,49 +226,43 @@ Deno.serve(async (req) => { return new Response(JSON.stringify(source), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 200, }); } catch (err) { if (err instanceof feedutils.FeedValidationError) { - utils.log('error', 'FeedValidationError', { - 'error': err.toString(), + utils.log("error", "FeedValidationError", { + error: err, }); - return new Response( - JSON.stringify({ error: err.message }), - { - headers: { - ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', - }, - status: 400, + return new Response(JSON.stringify({ error: err.message }), { + headers: { + ...corsHeaders, + "Content-Type": "application/json; charset=utf-8", }, - ); + status: 400, + }); } else if (err instanceof feedutils.FeedGetAndParseError) { - utils.log('error', 'FeedGetAndParseError', { - 'error': err.toString(), + utils.log("error", "FeedGetAndParseError", { + error: err, }); - return new Response( - JSON.stringify({ error: err.message }), - { - headers: { - ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', - }, - status: 400, + return new Response(JSON.stringify({ error: err.message }), { + headers: { + ...corsHeaders, + "Content-Type": "application/json; charset=utf-8", }, - ); + status: 400, + }); } else { - utils.log('error', 'An unexpected error occured', { - 'error': err.toString(), + utils.log("error", "An unexpected error occured", { + error: err, }); return new Response( - JSON.stringify({ error: 'An unexpected error occured' }), + JSON.stringify({ error: "An unexpected error occured" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 400, }, diff --git a/supabase/functions/add-source-v1/index.ts b/supabase/functions/add-source-v1/index.ts index e68e11d..9eec34e 100644 --- a/supabase/functions/add-source-v1/index.ts +++ b/supabase/functions/add-source-v1/index.ts @@ -1,16 +1,16 @@ -import { createClient } from '@supabase/supabase-js'; +import { createClient } from "@supabase/supabase-js"; -import { corsHeaders } from '../_shared/utils/cors.ts'; -import { getFeed } from '../_shared/feed/feed.ts'; -import { ISourceOptions, TSourceType } from '../_shared/models/source.ts'; -import { IProfile } from '../_shared/models/profile.ts'; -import { utils } from '../_shared/utils/index.ts'; -import { feedutils } from '../_shared/feed/utils/index.ts'; +import { corsHeaders } from "../_shared/utils/cors.ts"; +import { getFeed } from "../_shared/feed/feed.ts"; +import { ISourceOptions, TSourceType } from "../_shared/models/source.ts"; +import { IProfile } from "../_shared/models/profile.ts"; +import { utils } from "../_shared/utils/index.ts"; +import { feedutils } from "../_shared/feed/utils/index.ts"; import { FEEDDECK_SUPABASE_ANON_KEY, FEEDDECK_SUPABASE_SERVICE_ROLE_KEY, FEEDDECK_SUPABASE_URL, -} from '../_shared/utils/constants.ts'; +} from "../_shared/utils/constants.ts"; /** * DEPRECATED: This function is deprecated and will be removed in the future. @@ -21,8 +21,8 @@ Deno.serve(async (req) => { * We need to handle the preflight request for CORS as it is described in the * Supabase documentation: https://supabase.com/docs/guides/functions/cors */ - if (req.method === 'OPTIONS') { - return new Response('ok', { headers: corsHeaders }); + if (req.method === "OPTIONS") { + return new Response("ok", { headers: corsHeaders }); } try { @@ -36,7 +36,7 @@ Deno.serve(async (req) => { FEEDDECK_SUPABASE_ANON_KEY, { global: { - headers: { Authorization: req.headers.get('Authorization')! }, + headers: { Authorization: req.headers.get("Authorization")! }, }, auth: { autoRefreshToken: false, @@ -48,12 +48,14 @@ Deno.serve(async (req) => { /** * Get the user from the request. If there is no user, we return an error. */ - const { data: { user } } = await userSupabaseClient.auth.getUser(); + const { + data: { user }, + } = await userSupabaseClient.auth.getUser(); if (!user) { - return new Response(JSON.stringify({ error: 'Unauthorized' }), { + return new Response(JSON.stringify({ error: "Unauthorized" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 401, }); @@ -81,21 +83,20 @@ Deno.serve(async (req) => { * than one profile, we return an error. */ const { data: profile, error: profileError } = await adminSupabaseClient - .from( - 'profiles', - ) - .select('*').eq('id', user.id); + .from("profiles") + .select("*") + .eq("id", user.id); if (profileError || profile?.length !== 1) { - utils.log('error', 'Failed to get user profile', { - 'user': user, - 'error': profileError, + utils.log("error", "Failed to get user profile", { + user: user, + error: profileError, }); return new Response( - JSON.stringify({ error: 'Failed to get user profile' }), + JSON.stringify({ error: "Failed to get user profile" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 500, }, @@ -113,58 +114,53 @@ Deno.serve(async (req) => { * sources. */ const { count: sourcesCount, error: countError } = await adminSupabaseClient - .from('sources') - .select( - '*', - { count: 'exact' }, - ).eq('userId', user.id); + .from("sources") + .select("*", { count: "exact" }) + .eq("userId", user.id); if (countError || sourcesCount === null) { - utils.log('error', 'Failed to get sources', { 'error': countError }); - return new Response( - JSON.stringify({ error: 'Failed to get sources' }), - { - headers: { - ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', - }, - status: 500, + utils.log("error", "Failed to get sources", { error: countError }); + return new Response(JSON.stringify({ error: "Failed to get sources" }), { + headers: { + ...corsHeaders, + "Content-Type": "application/json; charset=utf-8", }, - ); + status: 500, + }); } - if (profile[0].tier === 'free' && sourcesCount >= 10) { + if (profile[0].tier === "free" && sourcesCount >= 10) { utils.log( - 'warning', - 'User is on the free tier and has reached the maximum number of sources', + "warning", + "User is on the free tier and has reached the maximum number of sources", ); return new Response( JSON.stringify({ error: - 'You reached the maximum number of sources you can create, please upgrade your account to the premium tier.', + "You reached the maximum number of sources you can create, please upgrade your account to the premium tier.", }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 400, }, ); } - if (profile[0].tier === 'premium' && sourcesCount >= 1000) { + if (profile[0].tier === "premium" && sourcesCount >= 1000) { utils.log( - 'warning', - 'User is on the premium tier and has reached the maximum number of sources', + "warning", + "User is on the premium tier and has reached the maximum number of sources", ); return new Response( JSON.stringify({ - error: 'You reached the maximum number of sources you can create.', + error: "You reached the maximum number of sources you can create.", }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 400, }, @@ -175,7 +171,11 @@ Deno.serve(async (req) => { * Get the column, source typ and options from the request. Based on this * information, we get the source and items from the `getFeed` function. */ - const { columnId, type, options }: { + const { + columnId, + type, + options, + }: { columnId: string; type: TSourceType; options: ISourceOptions; @@ -186,12 +186,12 @@ Deno.serve(async (req) => { undefined, profile[0] as IProfile, { - id: '', + id: "", userId: user.id, columnId: columnId, type: type, options: options, - title: '', + title: "", }, undefined, ); @@ -204,32 +204,30 @@ Deno.serve(async (req) => { * If there is an error, we return an error. If the insert succeeds we * return the source object. */ - const { error: sourceError } = await adminSupabaseClient.from('sources') - .insert( - source, - ); + const { error: sourceError } = await adminSupabaseClient + .from("sources") + .insert(source); if (sourceError) { - utils.log('error', 'Failed to save sources', { 'error': sourceError }); - return new Response(JSON.stringify({ error: 'Failed to save sources' }), { + utils.log("error", "Failed to save sources", { error: sourceError }); + return new Response(JSON.stringify({ error: "Failed to save sources" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 500, }); } if (items.length > 0) { - const { error: itemsError } = await adminSupabaseClient.from('items') - .insert( - items, - ); + const { error: itemsError } = await adminSupabaseClient + .from("items") + .insert(items); if (itemsError) { - utils.log('error', 'Failed to save items', { 'error': itemsError }); - return new Response(JSON.stringify({ error: 'Failed to save items' }), { + utils.log("error", "Failed to save items", { error: itemsError }); + return new Response(JSON.stringify({ error: "Failed to save items" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 500, }); @@ -239,49 +237,43 @@ Deno.serve(async (req) => { return new Response(JSON.stringify(source), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 200, }); } catch (err) { if (err instanceof feedutils.FeedValidationError) { - utils.log('error', 'FeedValidationError', { - 'error': err.toString(), + utils.log("error", "FeedValidationError", { + error: err, }); - return new Response( - JSON.stringify({ error: err.message }), - { - headers: { - ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', - }, - status: 400, + return new Response(JSON.stringify({ error: err.message }), { + headers: { + ...corsHeaders, + "Content-Type": "application/json; charset=utf-8", }, - ); + status: 400, + }); } else if (err instanceof feedutils.FeedGetAndParseError) { - utils.log('error', 'FeedGetAndParseError', { - 'error': err.toString(), + utils.log("error", "FeedGetAndParseError", { + error: err, }); - return new Response( - JSON.stringify({ error: err.message }), - { - headers: { - ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', - }, - status: 400, + return new Response(JSON.stringify({ error: err.message }), { + headers: { + ...corsHeaders, + "Content-Type": "application/json; charset=utf-8", }, - ); + status: 400, + }); } else { - utils.log('error', 'An unexpected error occured', { - 'error': err.toString(), + utils.log("error", "An unexpected error occured", { + error: err, }); return new Response( - JSON.stringify({ error: 'An unexpected error occured' }), + JSON.stringify({ error: "An unexpected error occured" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 400, }, diff --git a/supabase/functions/delete-user-v1/index.ts b/supabase/functions/delete-user-v1/index.ts index ea388d4..edd840d 100644 --- a/supabase/functions/delete-user-v1/index.ts +++ b/supabase/functions/delete-user-v1/index.ts @@ -1,12 +1,12 @@ -import { createClient } from '@supabase/supabase-js'; +import { createClient } from "@supabase/supabase-js"; -import { corsHeaders } from '../_shared/utils/cors.ts'; -import { log } from '../_shared/utils/log.ts'; +import { corsHeaders } from "../_shared/utils/cors.ts"; +import { log } from "../_shared/utils/log.ts"; import { FEEDDECK_SUPABASE_ANON_KEY, FEEDDECK_SUPABASE_SERVICE_ROLE_KEY, FEEDDECK_SUPABASE_URL, -} from '../_shared/utils/constants.ts'; +} from "../_shared/utils/constants.ts"; /** * The `delete-user-v1` edge function is used to delete the current user. When @@ -18,8 +18,8 @@ Deno.serve(async (req) => { * We need to handle the preflight request for CORS as it is described in the * Supabase documentation: https://supabase.com/docs/guides/functions/cors */ - if (req.method === 'OPTIONS') { - return new Response('ok', { headers: corsHeaders }); + if (req.method === "OPTIONS") { + return new Response("ok", { headers: corsHeaders }); } try { @@ -33,7 +33,7 @@ Deno.serve(async (req) => { FEEDDECK_SUPABASE_ANON_KEY, { global: { - headers: { Authorization: req.headers.get('Authorization')! }, + headers: { Authorization: req.headers.get("Authorization")! }, }, auth: { autoRefreshToken: false, @@ -45,12 +45,14 @@ Deno.serve(async (req) => { /** * Get the user from the request. If there is no user, we return an error. */ - const { data: { user } } = await userSupabaseClient.auth.getUser(); + const { + data: { user }, + } = await userSupabaseClient.auth.getUser(); if (!user) { - return new Response(JSON.stringify({ error: 'Unauthorized' }), { + return new Response(JSON.stringify({ error: "Unauthorized" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 401, }); @@ -80,36 +82,35 @@ Deno.serve(async (req) => { * account is already deleted. */ const { data: profile, error: profileError } = await adminSupabaseClient - .from( - 'profiles', - ) - .select('*').eq('id', user.id); + .from("profiles") + .select("*") + .eq("id", user.id); if (profileError || profile?.length !== 1) { - log('error', 'Failed to get user profile', { - 'user': user, - 'error': profileError, + log("error", "Failed to get user profile", { + user: user, + error: profileError, }); return new Response( - JSON.stringify({ error: 'Failed to get user profile' }), + JSON.stringify({ error: "Failed to get user profile" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 500, }, ); } - if (profile[0].tier !== 'free') { + if (profile[0].tier !== "free") { return new Response( JSON.stringify({ error: - 'User can not be deleted, because of an active subscription, please cancel your subscription first', + "User can not be deleted, because of an active subscription, please cancel your subscription first", }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 400, }, @@ -121,19 +122,19 @@ Deno.serve(async (req) => { * return an error. If the user was deleted successfully we return a 204 * response. */ - const { error: deleteError } = await adminSupabaseClient.auth.admin - .deleteUser(user.id); + const { error: deleteError } = + await adminSupabaseClient.auth.admin.deleteUser(user.id); if (deleteError) { - log('error', 'Failed to get delete user', { - 'user': user, - 'error': deleteError, + log("error", "Failed to get delete user", { + user: user, + error: deleteError, }); return new Response( - JSON.stringify({ error: 'Failed to get delete user' }), + JSON.stringify({ error: "Failed to get delete user" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 500, }, @@ -143,18 +144,18 @@ Deno.serve(async (req) => { return new Response(undefined, { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 200, }); } catch (err) { - log('error', 'An unexpected error occured', { 'error': err.toString() }); + log("error", "An unexpected error occured", { error: err }); return new Response( - JSON.stringify({ error: 'An unexpected error occured' }), + JSON.stringify({ error: "An unexpected error occured" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 400, }, diff --git a/supabase/functions/generate-magic-link-v1/index.ts b/supabase/functions/generate-magic-link-v1/index.ts index cb35af8..9af24cd 100644 --- a/supabase/functions/generate-magic-link-v1/index.ts +++ b/supabase/functions/generate-magic-link-v1/index.ts @@ -1,12 +1,12 @@ -import { createClient } from '@supabase/supabase-js'; +import { createClient } from "@supabase/supabase-js"; -import { corsHeaders } from '../_shared/utils/cors.ts'; -import { log } from '../_shared/utils/log.ts'; +import { corsHeaders } from "../_shared/utils/cors.ts"; +import { log } from "../_shared/utils/log.ts"; import { FEEDDECK_SUPABASE_ANON_KEY, FEEDDECK_SUPABASE_SERVICE_ROLE_KEY, FEEDDECK_SUPABASE_URL, -} from '../_shared/utils/constants.ts'; +} from "../_shared/utils/constants.ts"; /** * The `generate-magic-link-v1` edge function is used to generate a magic link @@ -17,8 +17,8 @@ Deno.serve(async (req) => { * We need to handle the preflight request for CORS as it is described in the * Supabase documentation: https://supabase.com/docs/guides/functions/cors */ - if (req.method === 'OPTIONS') { - return new Response('ok', { headers: corsHeaders }); + if (req.method === "OPTIONS") { + return new Response("ok", { headers: corsHeaders }); } try { @@ -32,7 +32,7 @@ Deno.serve(async (req) => { FEEDDECK_SUPABASE_ANON_KEY, { global: { - headers: { Authorization: req.headers.get('Authorization')! }, + headers: { Authorization: req.headers.get("Authorization")! }, }, auth: { autoRefreshToken: false, @@ -44,12 +44,14 @@ Deno.serve(async (req) => { /** * Get the user from the request. If there is no user, we return an error. */ - const { data: { user } } = await userSupabaseClient.auth.getUser(); + const { + data: { user }, + } = await userSupabaseClient.auth.getUser(); if (!user || !user.email) { - return new Response(JSON.stringify({ error: 'Unauthorized' }), { + return new Response(JSON.stringify({ error: "Unauthorized" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 401, }); @@ -71,22 +73,22 @@ Deno.serve(async (req) => { }, ); - const { data: linkData, error: linkError } = await adminSupabaseClient.auth - .admin.generateLink({ - type: 'magiclink', + const { data: linkData, error: linkError } = + await adminSupabaseClient.auth.admin.generateLink({ + type: "magiclink", email: user.email, }); if (linkError) { - log('error', 'Failed to generate magic link', { - 'user': user, - 'error': linkError, + log("error", "Failed to generate magic link", { + user: user, + error: linkError, }); return new Response( - JSON.stringify({ error: 'Failed to generate magic link' }), + JSON.stringify({ error: "Failed to generate magic link" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 500, }, @@ -98,19 +100,19 @@ Deno.serve(async (req) => { { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 200, }, ); } catch (err) { - log('error', 'An unexpected error occured', { 'error': err.toString() }); + log("error", "An unexpected error occured", { error: err }); return new Response( - JSON.stringify({ error: 'An unexpected error occured' }), + JSON.stringify({ error: "An unexpected error occured" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 400, }, diff --git a/supabase/functions/import_map.json b/supabase/functions/import_map.json index cfc20dd..36048d1 100644 --- a/supabase/functions/import_map.json +++ b/supabase/functions/import_map.json @@ -4,7 +4,7 @@ "std/testing/mock": "https://deno.land/std@0.208.0/testing/mock.ts", "std/hex": "https://deno.land/std@0.208.0/encoding/hex.ts", "std/crypto": "https://deno.land/std@0.208.0/crypto/mod.ts", - "@supabase/supabase-js": "https://esm.sh/@supabase/supabase-js@2.39.0", + "@supabase/supabase-js": "jsr:@supabase/supabase-js@2", "rss": "https://deno.land/x/rss@1.0.0/mod.ts", "rss/types": "https://deno.land/x/rss@1.0.0/src/types/mod.ts", "cheerio": "https://esm.sh/cheerio@1.0.0-rc.12", diff --git a/supabase/functions/profile-v1/index.ts b/supabase/functions/profile-v1/index.ts index 10f1f1e..c671963 100644 --- a/supabase/functions/profile-v1/index.ts +++ b/supabase/functions/profile-v1/index.ts @@ -1,15 +1,15 @@ -import { createClient } from '@supabase/supabase-js'; +import { createClient } from "@supabase/supabase-js"; -import { corsHeaders } from '../_shared/utils/cors.ts'; -import { log } from '../_shared/utils/log.ts'; -import { IProfile } from '../_shared/models/profile.ts'; -import { TSourceType } from '../_shared/models/source.ts'; -import { encrypt } from '../_shared/utils/encrypt.ts'; +import { corsHeaders } from "../_shared/utils/cors.ts"; +import { log } from "../_shared/utils/log.ts"; +import { IProfile } from "../_shared/models/profile.ts"; +import { TSourceType } from "../_shared/models/source.ts"; +import { encrypt } from "../_shared/utils/encrypt.ts"; import { FEEDDECK_SUPABASE_ANON_KEY, FEEDDECK_SUPABASE_SERVICE_ROLE_KEY, FEEDDECK_SUPABASE_URL, -} from '../_shared/utils/constants.ts'; +} from "../_shared/utils/constants.ts"; /** * DEPRECATED: This function is deprecated and will be removed in the future. @@ -20,8 +20,8 @@ Deno.serve(async (req) => { * We need to handle the preflight request for CORS as it is described in the * Supabase documentation: https://supabase.com/docs/guides/functions/cors */ - if (req.method === 'OPTIONS') { - return new Response('ok', { headers: corsHeaders }); + if (req.method === "OPTIONS") { + return new Response("ok", { headers: corsHeaders }); } try { @@ -35,7 +35,7 @@ Deno.serve(async (req) => { FEEDDECK_SUPABASE_ANON_KEY, { global: { - headers: { Authorization: req.headers.get('Authorization')! }, + headers: { Authorization: req.headers.get("Authorization")! }, }, auth: { autoRefreshToken: false, @@ -47,12 +47,14 @@ Deno.serve(async (req) => { /** * Get the user from the request. If there is no user, we return an error. */ - const { data: { user } } = await userSupabaseClient.auth.getUser(); + const { + data: { user }, + } = await userSupabaseClient.auth.getUser(); if (!user) { - return new Response(JSON.stringify({ error: 'Unauthorized' }), { + return new Response(JSON.stringify({ error: "Unauthorized" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 401, }); @@ -81,26 +83,22 @@ Deno.serve(async (req) => { * convert them to `true` or `false`, which indicates if the account is * connected or not. */ - if (req.method === 'GET') { + if (req.method === "GET") { const { data: profile, error: profileError } = await adminSupabaseClient - .from( - 'profiles', - ) - .select('*').eq( - 'id', - user.id, - ); + .from("profiles") + .select("*") + .eq("id", user.id); if (profileError || profile?.length !== 1) { - log('error', 'Failed to get user profile', { - 'user': user, - 'error': profileError, + log("error", "Failed to get user profile", { + user: user, + error: profileError, }); return new Response( - JSON.stringify({ error: 'Failed to get delete user' }), + JSON.stringify({ error: "Failed to get delete user" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 500, }, @@ -109,19 +107,19 @@ Deno.serve(async (req) => { return new Response( JSON.stringify({ - 'id': (profile[0] as IProfile).id, - 'tier': (profile[0] as IProfile).tier, - 'subscriptionProvider': (profile[0] as IProfile).subscriptionProvider, - 'accountGithub': (profile[0] as IProfile).accountGithub?.token + id: (profile[0] as IProfile).id, + tier: (profile[0] as IProfile).tier, + subscriptionProvider: (profile[0] as IProfile).subscriptionProvider, + accountGithub: (profile[0] as IProfile).accountGithub?.token ? true : false, - 'createdAt': (profile[0] as IProfile).createdAt, - 'updatedAt': (profile[0] as IProfile).updatedAt, + createdAt: (profile[0] as IProfile).createdAt, + updatedAt: (profile[0] as IProfile).updatedAt, }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 200, }, @@ -136,7 +134,7 @@ Deno.serve(async (req) => { * process, e.g. setting a verify token and returning an url for * authentication. */ - if (req.method === 'POST') { + if (req.method === "POST") { const data: { action?: string; sourceType?: TSourceType; @@ -152,40 +150,39 @@ Deno.serve(async (req) => { * account to the users profile. */ if ( - data.action === 'add-account' && data.sourceType === 'github' && + data.action === "add-account" && + data.sourceType === "github" && data.options?.token ) { - const { error: updateError } = await adminSupabaseClient.from( - 'profiles', - ).update({ - 'accountGithub': { token: await encrypt(data.options.token) }, - }).eq('id', user.id); + const { error: updateError } = await adminSupabaseClient + .from("profiles") + .update({ + accountGithub: { token: await encrypt(data.options.token) }, + }) + .eq("id", user.id); if (updateError) { - log('error', 'Failed to update user profile', { - 'user': user, - 'error': updateError, + log("error", "Failed to update user profile", { + user: user, + error: updateError, }); return new Response( - JSON.stringify({ error: 'Failed to update profile' }), + JSON.stringify({ error: "Failed to update profile" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 500, }, ); } - return new Response( - undefined, - { - headers: { - ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', - }, - status: 200, + return new Response(undefined, { + headers: { + ...corsHeaders, + "Content-Type": "application/json; charset=utf-8", }, - ); + status: 200, + }); } /** @@ -194,52 +191,50 @@ Deno.serve(async (req) => { * the users GitHub account from his profile by setting the value of the * `accountGithub` column to `null`. */ - if (data.action === 'delete-account' && data.sourceType === 'github') { - const { error: updateError } = await adminSupabaseClient.from( - 'profiles', - ).update({ - 'accountGithub': null, - }).eq('id', user.id); + if (data.action === "delete-account" && data.sourceType === "github") { + const { error: updateError } = await adminSupabaseClient + .from("profiles") + .update({ + accountGithub: null, + }) + .eq("id", user.id); if (updateError) { - log('error', 'Failed to update user profile', { - 'user': user, - 'error': updateError, + log("error", "Failed to update user profile", { + user: user, + error: updateError, }); return new Response( - JSON.stringify({ error: 'Failed to update profile' }), + JSON.stringify({ error: "Failed to update profile" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 500, }, ); } - return new Response( - undefined, - { - headers: { - ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', - }, - status: 200, + return new Response(undefined, { + headers: { + ...corsHeaders, + "Content-Type": "application/json; charset=utf-8", }, - ); + status: 200, + }); } /** * If the request data doesn't match any of the above conditions, we * return an error. */ - log('error', 'Invalid request data', { - 'user': user, - 'request': data, + log("error", "Invalid request data", { + user: user, + request: data, }); - return new Response(JSON.stringify({ error: 'Invalid request data' }), { + return new Response(JSON.stringify({ error: "Invalid request data" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 400, }); @@ -248,21 +243,21 @@ Deno.serve(async (req) => { /** * If the request method is not GET, POST or DELETE, we return an error. */ - return new Response(JSON.stringify({ error: 'Method Not Allowed' }), { + return new Response(JSON.stringify({ error: "Method Not Allowed" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 405, }); } catch (err) { - log('error', 'An unexpected error occured', { 'error': err.toString() }); + log("error", "An unexpected error occured", { error: err }); return new Response( - JSON.stringify({ error: 'An unexpected error occured' }), + JSON.stringify({ error: "An unexpected error occured" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 400, }, diff --git a/supabase/functions/profile-v2/index.ts b/supabase/functions/profile-v2/index.ts index 7741ee5..6b27ef8 100644 --- a/supabase/functions/profile-v2/index.ts +++ b/supabase/functions/profile-v2/index.ts @@ -1,14 +1,14 @@ -import { createClient, SupabaseClient, User } from '@supabase/supabase-js'; +import { createClient, SupabaseClient, User } from "@supabase/supabase-js"; -import { corsHeaders } from '../_shared/utils/cors.ts'; -import { log } from '../_shared/utils/log.ts'; -import { IProfile } from '../_shared/models/profile.ts'; +import { corsHeaders } from "../_shared/utils/cors.ts"; +import { log } from "../_shared/utils/log.ts"; +import { IProfile } from "../_shared/models/profile.ts"; import { FEEDDECK_SUPABASE_ANON_KEY, FEEDDECK_SUPABASE_SERVICE_ROLE_KEY, FEEDDECK_SUPABASE_URL, -} from '../_shared/utils/constants.ts'; -import { githubAddAccount, githubDeleteAccount } from './github.ts'; +} from "../_shared/utils/constants.ts"; +import { githubAddAccount, githubDeleteAccount } from "./github.ts"; /** * `getProfile` returns the users profile. The user profile contains information @@ -22,24 +22,20 @@ const getProfile = async ( user: User, ): Promise => { const { data: profile, error: profileError } = await supabaseClient - .from( - 'profiles', - ) - .select('*').eq( - 'id', - user.id, - ); + .from("profiles") + .select("*") + .eq("id", user.id); if (profileError || profile?.length !== 1) { - log('error', 'Failed to get user profile', { - 'user': user, - 'error': profileError, + log("error", "Failed to get user profile", { + user: user, + error: profileError, }); return new Response( - JSON.stringify({ error: 'Failed to get delete user' }), + JSON.stringify({ error: "Failed to get delete user" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 500, }, @@ -48,19 +44,19 @@ const getProfile = async ( return new Response( JSON.stringify({ - 'id': (profile[0] as IProfile).id, - 'tier': (profile[0] as IProfile).tier, - 'subscriptionProvider': (profile[0] as IProfile).subscriptionProvider, - 'accountGithub': (profile[0] as IProfile).accountGithub?.token + id: (profile[0] as IProfile).id, + tier: (profile[0] as IProfile).tier, + subscriptionProvider: (profile[0] as IProfile).subscriptionProvider, + accountGithub: (profile[0] as IProfile).accountGithub?.token ? true : false, - 'createdAt': (profile[0] as IProfile).createdAt, - 'updatedAt': (profile[0] as IProfile).updatedAt, + createdAt: (profile[0] as IProfile).createdAt, + updatedAt: (profile[0] as IProfile).updatedAt, }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 200, }, @@ -79,8 +75,8 @@ Deno.serve(async (req) => { * We need to handle the preflight request for CORS as it is described in the * Supabase documentation: https://supabase.com/docs/guides/functions/cors */ - if (method === 'OPTIONS') { - return new Response('ok', { headers: corsHeaders }); + if (method === "OPTIONS") { + return new Response("ok", { headers: corsHeaders }); } try { @@ -94,7 +90,7 @@ Deno.serve(async (req) => { FEEDDECK_SUPABASE_ANON_KEY, { global: { - headers: { Authorization: req.headers.get('Authorization')! }, + headers: { Authorization: req.headers.get("Authorization")! }, }, auth: { autoRefreshToken: false, @@ -106,12 +102,14 @@ Deno.serve(async (req) => { /** * Get the user from the request. If there is no user, we return an error. */ - const { data: { user } } = await userSupabaseClient.auth.getUser(); + const { + data: { user }, + } = await userSupabaseClient.auth.getUser(); if (!user) { - return new Response(JSON.stringify({ error: 'Unauthorized' }), { + return new Response(JSON.stringify({ error: "Unauthorized" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 401, }); @@ -139,16 +137,16 @@ Deno.serve(async (req) => { * multiple endpoints. If the request method is `POST` we also parse the * request body. */ - const urlPattern = new URLPattern({ pathname: '/profile-v2/:id' }); + const urlPattern = new URLPattern({ pathname: "/profile-v2/:id" }); const matchingPath = urlPattern.exec(url); const id = matchingPath ? matchingPath.pathname.groups.id : null; let data = null; - if (method === 'POST') { + if (method === "POST") { data = await req.json(); } - log('debug', 'Request data', { + log("debug", "Request data", { user: user, method: method, id: id, @@ -160,11 +158,11 @@ Deno.serve(async (req) => { * action we need to execute. */ switch (true) { - case method === 'GET' && id === 'getProfile': + case method === "GET" && id === "getProfile": return await getProfile(adminSupabaseClient, user); - case method === 'POST' && id === 'githubAddAccount': + case method === "POST" && id === "githubAddAccount": return await githubAddAccount(adminSupabaseClient, user, data); - case method === 'DELETE' && id === 'githubDeleteAccount': + case method === "DELETE" && id === "githubDeleteAccount": return await githubDeleteAccount(adminSupabaseClient, user); default: /** @@ -172,22 +170,22 @@ Deno.serve(async (req) => { * doesn't match the request method and id we return a `400 Bad Request` * error. */ - return new Response(JSON.stringify({ error: 'Bad Request' }), { + return new Response(JSON.stringify({ error: "Bad Request" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 400, }); } } catch (err) { - log('error', 'An unexpected error occured', { 'error': err.toString() }); + log("error", "An unexpected error occured", { error: err }); return new Response( - JSON.stringify({ error: 'An unexpected error occured' }), + JSON.stringify({ error: "An unexpected error occured" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 400, }, diff --git a/supabase/functions/revenuecat-webhooks-v1/index.ts b/supabase/functions/revenuecat-webhooks-v1/index.ts index 85adf31..9606450 100644 --- a/supabase/functions/revenuecat-webhooks-v1/index.ts +++ b/supabase/functions/revenuecat-webhooks-v1/index.ts @@ -1,11 +1,11 @@ -import { createClient } from '@supabase/supabase-js'; +import { createClient } from "@supabase/supabase-js"; -import { log } from '../_shared/utils/log.ts'; +import { log } from "../_shared/utils/log.ts"; import { FEEDDECK_REVENUECAT_WEBHOOK_HEADER, FEEDDECK_SUPABASE_SERVICE_ROLE_KEY, FEEDDECK_SUPABASE_URL, -} from '../_shared/utils/constants.ts'; +} from "../_shared/utils/constants.ts"; /** * The `IEventPayload` interface represents the payload of a RevenueCat webhook @@ -29,13 +29,13 @@ interface IEvent { * header in RevenueCat. */ const isAuthorized = (req: Request): boolean => { - const authorizationHeader = req.headers.get('Authorization'); + const authorizationHeader = req.headers.get("Authorization"); - if (!authorizationHeader || !authorizationHeader.startsWith('Bearer ')) { + if (!authorizationHeader || !authorizationHeader.startsWith("Bearer ")) { return false; } - const authToken = authorizationHeader.split('Bearer ')[1]; + const authToken = authorizationHeader.split("Bearer ")[1]; if (authToken !== FEEDDECK_REVENUECAT_WEBHOOK_HEADER) { return false; } @@ -72,39 +72,40 @@ export const manageSubscriptionStatusChange = async ( * one profile, we return an error. */ const { data: profile, error: profileError } = await adminSupabaseClient - .from( - 'profiles', - ) - .select('*').eq('id', userId); + .from("profiles") + .select("*") + .eq("id", userId); if (profileError || profile?.length !== 1) { - log('error', 'Failed to get user profile', { - 'userId': userId, - 'error': profileError, + log("error", "Failed to get user profile", { + userId: userId, + error: profileError, }); - throw new Error('Failed to get user profile'); + throw new Error("Failed to get user profile"); } /** * If the user is already on the correct tier, we return early. */ if ( - (profile[0].tier === 'free' && !isCreated) || - (profile[0].tier === 'premium' && isCreated) + (profile[0].tier === "free" && !isCreated) || + (profile[0].tier === "premium" && isCreated) ) { return; } - const { error: updateError } = await adminSupabaseClient.from('profiles') + const { error: updateError } = await adminSupabaseClient + .from("profiles") .update({ - tier: isCreated ? 'premium' : 'free', - subscriptionProvider: 'revenuecat', - }).eq('id', profile[0].id); + tier: isCreated ? "premium" : "free", + subscriptionProvider: "revenuecat", + }) + .eq("id", profile[0].id); if (updateError) { - log('error', 'Failed to update user profile with new tier value', { - 'userId': userId, - 'error': updateError, + log("error", "Failed to update user profile with new tier value", { + userId: userId, + error: updateError, }); - throw new Error('Failed to update user profile with new tier value'); + throw new Error("Failed to update user profile with new tier value"); } }; @@ -120,29 +121,23 @@ Deno.serve(async (req) => { * is done because we only want to accept POST requests. If the request is * not authorized, we return a 401 Unauthorized error. */ - if (req.method !== 'POST') { - return new Response( - 'Forbidden', - { - status: 403, - }, - ); + if (req.method !== "POST") { + return new Response("Forbidden", { + status: 403, + }); } if (!isAuthorized(req)) { - return new Response( - 'Unauthorized', - { - status: 401, - }, - ); + return new Response("Unauthorized", { + status: 401, + }); } /** * Get the payload of the received webhook event. */ - const payload = await req.json() as IEventPayload; - log('debug', 'Received event', { 'event': payload.event }); + const payload = (await req.json()) as IEventPayload; + log("debug", "Received event", { event: payload.event }); /** * If the event type is `INITIAL_PURCHASE`, `RENEWAL` or `UNCANCELLATION`, @@ -151,31 +146,28 @@ Deno.serve(async (req) => { * `free`. All other event types are ignored. */ if ( - payload.event.type === 'INITIAL_PURCHASE' || - payload.event.type === 'RENEWAL' || - payload.event.type === 'UNCANCELLATION' + payload.event.type === "INITIAL_PURCHASE" || + payload.event.type === "RENEWAL" || + payload.event.type === "UNCANCELLATION" ) { await manageSubscriptionStatusChange(payload.event.app_user_id, true); - return new Response('ok', { + return new Response("ok", { status: 200, }); - } else if (payload.event.type === 'EXPIRATION') { + } else if (payload.event.type === "EXPIRATION") { await manageSubscriptionStatusChange(payload.event.app_user_id, false); - return new Response('ok', { + return new Response("ok", { status: 200, }); } else { - return new Response('ok', { + return new Response("ok", { status: 200, }); } } catch (err) { - log('error', 'An unexpected error occured', { 'error': err.toString() }); - return new Response( - 'An unexpected error occured', - { - status: 500, - }, - ); + log("error", "An unexpected error occured", { error: err }); + return new Response("An unexpected error occured", { + status: 500, + }); } }); diff --git a/supabase/functions/stripe-create-billing-portal-link-v1/index.ts b/supabase/functions/stripe-create-billing-portal-link-v1/index.ts index fb66ae6..7c60ba2 100644 --- a/supabase/functions/stripe-create-billing-portal-link-v1/index.ts +++ b/supabase/functions/stripe-create-billing-portal-link-v1/index.ts @@ -1,15 +1,15 @@ -import { createClient } from '@supabase/supabase-js'; +import { createClient } from "@supabase/supabase-js"; -import { corsHeaders } from '../_shared/utils/cors.ts'; -import { log } from '../_shared/utils/log.ts'; +import { corsHeaders } from "../_shared/utils/cors.ts"; +import { log } from "../_shared/utils/log.ts"; import { createBillingPortalSession, createOrRetrieveCustomer, -} from '../_shared/stripe/stripe.ts'; +} from "../_shared/stripe/stripe.ts"; import { FEEDDECK_SUPABASE_ANON_KEY, FEEDDECK_SUPABASE_URL, -} from '../_shared/utils/constants.ts'; +} from "../_shared/utils/constants.ts"; /** * The `stripe-create-billing-portal-link-v1` edge function is used to create a @@ -20,8 +20,8 @@ Deno.serve(async (req) => { * We need to handle the preflight request for CORS as it is described in the * Supabase documentation: https://supabase.com/docs/guides/functions/cors */ - if (req.method === 'OPTIONS') { - return new Response('ok', { headers: corsHeaders }); + if (req.method === "OPTIONS") { + return new Response("ok", { headers: corsHeaders }); } try { @@ -30,7 +30,7 @@ Deno.serve(async (req) => { FEEDDECK_SUPABASE_ANON_KEY, { global: { - headers: { Authorization: req.headers.get('Authorization')! }, + headers: { Authorization: req.headers.get("Authorization")! }, }, auth: { autoRefreshToken: false, @@ -42,12 +42,14 @@ Deno.serve(async (req) => { /** * Get the user from the request. If there is no user, we return an error. */ - const { data: { user } } = await userSupabaseClient.auth.getUser(); + const { + data: { user }, + } = await userSupabaseClient.auth.getUser(); if (!user) { - return new Response(JSON.stringify({ error: 'Unauthorized' }), { + return new Response(JSON.stringify({ error: "Unauthorized" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 401, }); @@ -62,18 +64,18 @@ Deno.serve(async (req) => { return new Response(JSON.stringify({ url: url }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 200, }); } catch (err) { - log('error', 'An unexpected error occured', { 'error': err.toString() }); + log("error", "An unexpected error occured", { error: err }); return new Response( - JSON.stringify({ error: 'An unexpected error occured' }), + JSON.stringify({ error: "An unexpected error occured" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 400, }, diff --git a/supabase/functions/stripe-create-checkout-session-v1/index.ts b/supabase/functions/stripe-create-checkout-session-v1/index.ts index 71d6a43..14d4607 100644 --- a/supabase/functions/stripe-create-checkout-session-v1/index.ts +++ b/supabase/functions/stripe-create-checkout-session-v1/index.ts @@ -1,15 +1,15 @@ -import { createClient } from '@supabase/supabase-js'; +import { createClient } from "@supabase/supabase-js"; -import { corsHeaders } from '../_shared/utils/cors.ts'; -import { log } from '../_shared/utils/log.ts'; +import { corsHeaders } from "../_shared/utils/cors.ts"; +import { log } from "../_shared/utils/log.ts"; import { createCheckoutSession, createOrRetrieveCustomer, -} from '../_shared/stripe/stripe.ts'; +} from "../_shared/stripe/stripe.ts"; import { FEEDDECK_SUPABASE_ANON_KEY, FEEDDECK_SUPABASE_URL, -} from '../_shared/utils/constants.ts'; +} from "../_shared/utils/constants.ts"; /** * The `stripe-create-checkout-session-v1` edge function is used to create a new @@ -21,8 +21,8 @@ Deno.serve(async (req) => { * We need to handle the preflight request for CORS as it is described in the * Supabase documentation: https://supabase.com/docs/guides/functions/cors */ - if (req.method === 'OPTIONS') { - return new Response('ok', { headers: corsHeaders }); + if (req.method === "OPTIONS") { + return new Response("ok", { headers: corsHeaders }); } try { @@ -31,7 +31,7 @@ Deno.serve(async (req) => { FEEDDECK_SUPABASE_ANON_KEY, { global: { - headers: { Authorization: req.headers.get('Authorization')! }, + headers: { Authorization: req.headers.get("Authorization")! }, }, auth: { autoRefreshToken: false, @@ -43,12 +43,14 @@ Deno.serve(async (req) => { /** * Get the user from the request. If there is no user, we return an error. */ - const { data: { user } } = await userSupabaseClient.auth.getUser(); + const { + data: { user }, + } = await userSupabaseClient.auth.getUser(); if (!user) { - return new Response(JSON.stringify({ error: 'Unauthorized' }), { + return new Response(JSON.stringify({ error: "Unauthorized" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 401, }); @@ -63,18 +65,18 @@ Deno.serve(async (req) => { return new Response(JSON.stringify({ url: url }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 200, }); } catch (err) { - log('error', 'An unexpected error occured', { 'error': err.toString() }); + log("error", "An unexpected error occured", { error: err }); return new Response( - JSON.stringify({ error: 'An unexpected error occured' }), + JSON.stringify({ error: "An unexpected error occured" }), { headers: { ...corsHeaders, - 'Content-Type': 'application/json; charset=utf-8', + "Content-Type": "application/json; charset=utf-8", }, status: 400, }, diff --git a/supabase/functions/stripe-webhooks-v1/index.ts b/supabase/functions/stripe-webhooks-v1/index.ts index 559983e..847c506 100644 --- a/supabase/functions/stripe-webhooks-v1/index.ts +++ b/supabase/functions/stripe-webhooks-v1/index.ts @@ -1,17 +1,17 @@ -import Stripe from 'stripe'; +import Stripe from "stripe"; -import { log } from '../_shared/utils/log.ts'; +import { log } from "../_shared/utils/log.ts"; import { cryptoProvider, manageSubscriptionStatusChange, stripe, -} from '../_shared/stripe/stripe.ts'; -import { FEEDDECK_STRIPE_WEBHOOK_SIGNING_SECRET } from '../_shared/utils/constants.ts'; +} from "../_shared/stripe/stripe.ts"; +import { FEEDDECK_STRIPE_WEBHOOK_SIGNING_SECRET } from "../_shared/utils/constants.ts"; const relevantEvents = new Set([ - 'checkout.session.completed', - 'customer.subscription.created', - 'customer.subscription.deleted', + "checkout.session.completed", + "customer.subscription.created", + "customer.subscription.deleted", ]); /** @@ -24,7 +24,7 @@ const relevantEvents = new Set([ */ Deno.serve(async (req) => { try { - const signature = req.headers.get('Stripe-Signature'); + const signature = req.headers.get("Stripe-Signature"); const body = await req.text(); const event = await stripe.webhooks.constructEventAsync( @@ -35,25 +35,25 @@ Deno.serve(async (req) => { cryptoProvider, ); - log('debug', 'Received event', { 'event': event.type }); + log("debug", "Received event", { event: event.type }); if (!relevantEvents.has(event.type)) { return new Response(JSON.stringify({ received: true })); } switch (event.type) { - case 'customer.subscription.created': - case 'customer.subscription.deleted': { + case "customer.subscription.created": + case "customer.subscription.deleted": { const subscription = event.data.object as Stripe.Subscription; await manageSubscriptionStatusChange( subscription.customer as string, - event.type === 'customer.subscription.created', + event.type === "customer.subscription.created", ); return new Response(JSON.stringify({ received: true })); } - case 'checkout.session.completed': { + case "checkout.session.completed": { const checkoutSession = event.data.object as Stripe.Checkout.Session; - if (checkoutSession.mode === 'subscription') { + if (checkoutSession.mode === "subscription") { await manageSubscriptionStatusChange( checkoutSession.customer as string, true, @@ -62,15 +62,12 @@ Deno.serve(async (req) => { return new Response(JSON.stringify({ received: true })); } default: - throw new Error('Unhandled relevant event'); + throw new Error("Unhandled relevant event"); } } catch (err) { - log('error', 'An unexpected error occured', { 'error': err.toString() }); - return new Response( - 'An unexpected error occured', - { - status: 400, - }, - ); + log("error", "An unexpected error occured", { error: err }); + return new Response("An unexpected error occured", { + status: 400, + }); } });