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
```
This commit is contained in:
Rico Berger
2025-04-19 09:10:43 +02:00
committed by GitHub
parent 4e0296e226
commit a9b4bef96f
17 changed files with 599 additions and 615 deletions

View File

@@ -1,5 +1,4 @@
# FROM denoland/deno:1.34.1
FROM lukechannings/deno:v1.40.5
FROM denoland/deno:1.45.2
WORKDIR /app

View File

@@ -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);
}
};

View File

@@ -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 });
}
};

View File

@@ -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 });
}
};

View File

@@ -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");
}
};

View File

@@ -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<string | undefined> => {
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;
};

View File

@@ -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,
},

View File

@@ -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,
},

View File

@@ -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,
},

View File

@@ -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,
},

View File

@@ -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",

View File

@@ -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,
},

View File

@@ -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<Response> => {
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,
},

View File

@@ -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,
});
}
});

View File

@@ -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,
},

View File

@@ -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,
},

View File

@@ -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,
});
}
});