From aad4073c952c746514e34f47e9e921929dad947a Mon Sep 17 00:00:00 2001 From: ff6347 Date: Thu, 16 Mar 2023 20:25:00 +0100 Subject: [PATCH 01/51] feat(supabase auth): First working supabase auth auth0 still kept in to make the transition easier --- _utils/errors.ts | 6 ++++++ _utils/{verify.ts => verify-auth0.ts} | 2 +- _utils/verify-supabase-token.ts | 22 ++++++++++++++++++++++ api/post/[type].ts | 14 ++++++++++++-- 4 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 _utils/errors.ts rename _utils/{verify.ts => verify-auth0.ts} (85%) create mode 100644 _utils/verify-supabase-token.ts diff --git a/_utils/errors.ts b/_utils/errors.ts new file mode 100644 index 00000000..f1fd331b --- /dev/null +++ b/_utils/errors.ts @@ -0,0 +1,6 @@ +export class GDKAuthError extends Error { + constructor(message: string) { + super(message); + this.name = "GDKAuthError"; + } +} diff --git a/_utils/verify.ts b/_utils/verify-auth0.ts similarity index 85% rename from _utils/verify.ts rename to _utils/verify-auth0.ts index 49d68df7..eb997247 100644 --- a/_utils/verify.ts +++ b/_utils/verify-auth0.ts @@ -1,7 +1,7 @@ import { VercelRequest } from "@vercel/node"; import { options, verifyAuth0Token } from "./verify-token"; -export async function verifyRequest(request: VercelRequest) { +export async function verifyAuth0Request(request: VercelRequest) { const { authorization } = request.headers; if (!authorization) { return false; diff --git a/_utils/verify-supabase-token.ts b/_utils/verify-supabase-token.ts new file mode 100644 index 00000000..f3bf5e40 --- /dev/null +++ b/_utils/verify-supabase-token.ts @@ -0,0 +1,22 @@ +import { VercelRequest } from "@vercel/node"; +import { GDKAuthError } from "./errors"; +import { supabase } from "./supabase"; + +export async function verifySupabaseToken(request: VercelRequest) { + const { authorization } = request.headers; + + if (!authorization) { + return { data: null, error: new GDKAuthError("not authorized") }; + } + + const access_token = authorization.split("Bearer ").pop(); + if (!access_token) { + return { data: null, error: new GDKAuthError("not authorized") }; + } + const { data, error } = await supabase.auth.getUser(access_token); + + if (error) { + return { data: null, error }; + } + return { data: data.user, error }; +} diff --git a/api/post/[type].ts b/api/post/[type].ts index 938298c3..3a3176b6 100644 --- a/api/post/[type].ts +++ b/api/post/[type].ts @@ -26,8 +26,18 @@ export default async function postHandler( if (request.method === "OPTIONS") { return response.status(200).end(); } - const authorized = await verifyRequest(request); - if (!authorized) { + // const { data: userData, error } = await verifySupabaseToken(request); + // if (error) { + // console.error("error from supabase auth", error); + // return response.status(401).json({ error: "unauthorized" }); + // } + // if (!userData) { + // console.error("no user data from supabase auth"); + // return response.status(401).json({ error: "unauthorized" }); + // } + /** + * We will remove auth0 but for now we can auth with both + */ return response.status(401).json({ error: "unauthorized" }); } const { type } = request.query; From 8313f918f280f024303542260d80e0c687f16c51 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Thu, 16 Mar 2023 20:25:58 +0100 Subject: [PATCH 02/51] refactor(api v3): Move files and functions to allow v3 api --- .../__snapshots__/get-routes.test.ts.snap | 11 -- __tests__/__snapshots__/index.test.ts.snap | 3 - _requests/delete/unadopt.ts | 18 +++ _requests/delete/unwater.ts | 21 +++ .../_requests => _requests/get}/adopted.ts | 26 ++-- {api/get/_requests => _requests/get}/all.ts | 16 +-- {api/get/_requests => _requests/get}/byage.ts | 16 +-- {api/get/_requests => _requests/get}/byid.ts | 6 +- .../_requests => _requests/get}/countbyage.ts | 6 +- .../get}/istreeadopted.ts | 12 +- .../get}/lastwatered.ts | 16 +-- .../_requests => _requests/get}/treesbyids.ts | 16 +-- .../_requests => _requests/get}/watered.ts | 16 +-- .../get}/wateredandadopted.ts | 16 +-- .../get}/wateredbyuser.ts | 24 ++-- _requests/post/adopt.ts | 24 ++++ _requests/post/water.ts | 28 ++++ _utils/setup-response.ts | 2 +- api/delete/[type].ts | 36 +----- api/get/[type].ts | 35 +++-- api/post/[type].ts | 53 ++------ api/v3/get/[type].ts | 110 ++++++++++++++++ api/v3/get/index.ts | 20 +++ api/v3/index.ts | 20 +++ docs/api.http | 121 +++++++++++++++++- package.json | 2 +- tsconfig.json | 1 + 27 files changed, 486 insertions(+), 189 deletions(-) create mode 100644 _requests/delete/unadopt.ts create mode 100644 _requests/delete/unwater.ts rename {api/get/_requests => _requests/get}/adopted.ts (62%) rename {api/get/_requests => _requests/get}/all.ts (78%) rename {api/get/_requests => _requests/get}/byage.ts (79%) rename {api/get/_requests => _requests/get}/byid.ts (73%) rename {api/get/_requests => _requests/get}/countbyage.ts (84%) rename {api/get/_requests => _requests/get}/istreeadopted.ts (69%) rename {api/get/_requests => _requests/get}/lastwatered.ts (73%) rename {api/get/_requests => _requests/get}/treesbyids.ts (72%) rename {api/get/_requests => _requests/get}/watered.ts (69%) rename {api/get/_requests => _requests/get}/wateredandadopted.ts (70%) rename {api/get/_requests => _requests/get}/wateredbyuser.ts (64%) create mode 100644 _requests/post/adopt.ts create mode 100644 _requests/post/water.ts create mode 100644 api/v3/get/[type].ts create mode 100644 api/v3/get/index.ts create mode 100644 api/v3/index.ts diff --git a/__tests__/__snapshots__/get-routes.test.ts.snap b/__tests__/__snapshots__/get-routes.test.ts.snap index 07c8d121..da9f832c 100644 --- a/__tests__/__snapshots__/get-routes.test.ts.snap +++ b/__tests__/__snapshots__/get-routes.test.ts.snap @@ -27,7 +27,6 @@ exports[`GET routes snapshot tests default responses Should return 200 on tree a "total": 14, }, "url": "/?type=all&limit=2&offset=0", - "version": "2.0.0", } `; @@ -1568,7 +1567,6 @@ exports[`GET routes snapshot tests default responses Should return 200 on treesb "total": 2, }, "url": "/?type=treesbyids&tree_ids=_2100294b1f%2C_210028b9c8", - "version": "2.0.0", } `; @@ -1584,7 +1582,6 @@ exports[`GET routes snapshot tests default responses should return 200 on adopte "total": 0, }, "url": "/?type=adopted&uuid=auth0%7Cabc", - "version": "2.0.0", } `; @@ -1643,7 +1640,6 @@ exports[`GET routes snapshot tests default responses should return 200 on byage "total": 14, }, "url": "/?type=byage&start=1800&end=3000", - "version": "2.0.0", } `; @@ -1655,7 +1651,6 @@ exports[`GET routes snapshot tests default responses should return 200 on countb "error": null, "name": "@technologiestiftung/giessdenkiez-de-postgres-api", "url": "/?type=countbyage&start=1800&end=3000", - "version": "2.0.0", } `; @@ -1665,7 +1660,6 @@ exports[`GET routes snapshot tests default responses should return 200 on istree "error": null, "name": "@technologiestiftung/giessdenkiez-de-postgres-api", "url": "/?type=istreeadopted&id=_210028b9c8&uuid=auth0%7Cabc", - "version": "2.0.0", } `; @@ -1681,7 +1675,6 @@ exports[`GET routes snapshot tests default responses should return 200 on lastwa "total": 0, }, "url": "/?type=lastwatered&id=_210028b9c8", - "version": "2.0.0", } `; @@ -2454,7 +2447,6 @@ exports[`GET routes snapshot tests default responses should return 200 on tree b "error": null, "name": "@technologiestiftung/giessdenkiez-de-postgres-api", "url": "/?type=byid&id=_2100294b1f", - "version": "2.0.0", } `; @@ -2483,7 +2475,6 @@ exports[`GET routes snapshot tests default responses should return 200 on trees_ "total": 4, }, "url": "/?type=watered", - "version": "2.0.0", } `; @@ -2499,7 +2490,6 @@ exports[`GET routes snapshot tests default responses should return 200 on watere "total": 0, }, "url": "/?type=wateredandadopted", - "version": "2.0.0", } `; @@ -2515,7 +2505,6 @@ exports[`GET routes snapshot tests default responses should return 200 on watere "total": 0, }, "url": "/?type=wateredbyuser&uuid=auth0%7Cabc", - "version": "2.0.0", } `; diff --git a/__tests__/__snapshots__/index.test.ts.snap b/__tests__/__snapshots__/index.test.ts.snap index c6fcd8f4..0c754ddc 100644 --- a/__tests__/__snapshots__/index.test.ts.snap +++ b/__tests__/__snapshots__/index.test.ts.snap @@ -376,7 +376,6 @@ exports[`GET/POST/DELETE routes index should list all routes on /delete 1`] = ` }, }, ], - "version": "2.0.0", } `; @@ -756,7 +755,6 @@ exports[`GET/POST/DELETE routes index should list all routes on /get 1`] = ` }, }, ], - "version": "2.0.0", } `; @@ -1136,6 +1134,5 @@ exports[`GET/POST/DELETE routes index should list all routes on /post 1`] = ` }, }, ], - "version": "2.0.0", } `; diff --git a/_requests/delete/unadopt.ts b/_requests/delete/unadopt.ts new file mode 100644 index 00000000..51a07f54 --- /dev/null +++ b/_requests/delete/unadopt.ts @@ -0,0 +1,18 @@ +import { VercelRequest, VercelResponse } from "@vercel/node"; +import { supabase } from "../../_utils/supabase"; + +export default async function handler( + request: VercelRequest, + response: VercelResponse +) { + const { tree_id, uuid } = request.body; + const { error } = await supabase + .from("trees_adopted") + .delete() + .eq("tree_id", tree_id) + .eq("uuid", uuid); + if (error) { + return response.status(500).json({ error }); + } + return response.status(204).json({ message: `unadopted tree ${tree_id}` }); +} diff --git a/_requests/delete/unwater.ts b/_requests/delete/unwater.ts new file mode 100644 index 00000000..7fa4557c --- /dev/null +++ b/_requests/delete/unwater.ts @@ -0,0 +1,21 @@ +import { VercelRequest, VercelResponse } from "@vercel/node"; +import { supabase } from "../../_utils/supabase"; + +export default async function ( + request: VercelRequest, + response: VercelResponse +) { + // FIXME: [GDK-221] API (with supabase) Find out why delete/unwater route does not work + + const { tree_id, uuid, watering_id } = request.body; + const { error } = await supabase + .from("trees_watered") + .delete() + .eq("tree_id", tree_id) + .eq("uuid", uuid) + .eq("id", watering_id); + if (error) { + return response.status(500).json({ error }); + } + return response.status(204).json({ message: `unwatered tree ${tree_id} ` }); +} diff --git a/api/get/_requests/adopted.ts b/_requests/get/adopted.ts similarity index 62% rename from api/get/_requests/adopted.ts rename to _requests/get/adopted.ts index 19e2803b..eec1e5ea 100644 --- a/api/get/_requests/adopted.ts +++ b/_requests/get/adopted.ts @@ -1,26 +1,22 @@ import { VercelRequest, VercelResponse } from "@vercel/node"; -import { checkDataError } from "../../../_utils/data-error-response"; +import { checkDataError } from "../../_utils/data-error-response"; import { checkLimitAndOffset, getLimitAndOffeset, -} from "../../../_utils/limit-and-offset"; -import { getRange } from "../../../_utils/parse-content-range"; -import { setupResponseData } from "../../../_utils/setup-response"; -import { supabase } from "../../../_utils/supabase"; -import { verifyRequest } from "../../../_utils/verify"; -import { getEnvs } from "../../../_utils/envs"; -import { checkRangeError } from "../../../_utils/range-error-response"; -import { createLinks } from "../../../_utils/create-links"; +} from "../../_utils/limit-and-offset"; +import { getRange } from "../../_utils/parse-content-range"; +import { setupResponseData } from "../../_utils/setup-response"; +import { supabase } from "../../_utils/supabase"; +import { getEnvs } from "../../_utils/envs"; +import { checkRangeError } from "../../_utils/range-error-response"; +import { createLinks } from "../../_utils/create-links"; +import { User } from "@supabase/supabase-js"; const { SUPABASE_URL } = getEnvs(); export default async function handler( request: VercelRequest, - response: VercelResponse + response: VercelResponse, + _user?: User ) { - const authorized = await verifyRequest(request); - if (!authorized) { - return response.status(401).json({ error: "unauthorized" }); - } - checkLimitAndOffset(request, response); const { limit, offset } = getLimitAndOffeset(request.query); const { uuid } = <{ uuid: string }>request.query; diff --git a/api/get/_requests/all.ts b/_requests/get/all.ts similarity index 78% rename from api/get/_requests/all.ts rename to _requests/get/all.ts index e5704edf..f4b2e17f 100644 --- a/api/get/_requests/all.ts +++ b/_requests/get/all.ts @@ -7,14 +7,14 @@ import type { Point } from "geojson"; import { checkLimitAndOffset, getLimitAndOffeset, -} from "../../../_utils/limit-and-offset"; -import { createLinks } from "../../../_utils/create-links"; -import { getEnvs } from "../../../_utils/envs"; -import { getRange } from "../../../_utils/parse-content-range"; -import { setupResponseData } from "../../../_utils/setup-response"; -import { supabase } from "../../../_utils/supabase"; -import { checkRangeError } from "../../../_utils/range-error-response"; -import { checkDataError } from "../../../_utils/data-error-response"; +} from "../../_utils/limit-and-offset"; +import { createLinks } from "../../_utils/create-links"; +import { getEnvs } from "../../_utils/envs"; +import { getRange } from "../../_utils/parse-content-range"; +import { setupResponseData } from "../../_utils/setup-response"; +import { supabase } from "../../_utils/supabase"; +import { checkRangeError } from "../../_utils/range-error-response"; +import { checkDataError } from "../../_utils/data-error-response"; const { SUPABASE_URL } = getEnvs(); export default async function handler( request: VercelRequest, diff --git a/api/get/_requests/byage.ts b/_requests/get/byage.ts similarity index 79% rename from api/get/_requests/byage.ts rename to _requests/get/byage.ts index ab72a170..cc8af1c6 100644 --- a/api/get/_requests/byage.ts +++ b/_requests/get/byage.ts @@ -1,15 +1,15 @@ import { VercelRequest, VercelResponse } from "@vercel/node"; -import { checkDataError } from "../../../_utils/data-error-response"; +import { checkDataError } from "../../_utils/data-error-response"; import { checkLimitAndOffset, getLimitAndOffeset, -} from "../../../_utils/limit-and-offset"; -import { setupResponseData } from "../../../_utils/setup-response"; -import { supabase } from "../../../_utils/supabase"; -import { getEnvs } from "../../../_utils/envs"; -import { getRange } from "../../../_utils/parse-content-range"; -import { checkRangeError } from "../../../_utils/range-error-response"; -import { createLinks } from "../../../_utils/create-links"; +} from "../../_utils/limit-and-offset"; +import { setupResponseData } from "../../_utils/setup-response"; +import { supabase } from "../../_utils/supabase"; +import { getEnvs } from "../../_utils/envs"; +import { getRange } from "../../_utils/parse-content-range"; +import { checkRangeError } from "../../_utils/range-error-response"; +import { createLinks } from "../../_utils/create-links"; const { SUPABASE_URL } = getEnvs(); export default async function handler( request: VercelRequest, diff --git a/api/get/_requests/byid.ts b/_requests/get/byid.ts similarity index 73% rename from api/get/_requests/byid.ts rename to _requests/get/byid.ts index b4e82b23..c7dfbd7b 100644 --- a/api/get/_requests/byid.ts +++ b/_requests/get/byid.ts @@ -1,8 +1,8 @@ // FIXME: Request could be done from the frontend import { VercelRequest, VercelResponse } from "@vercel/node"; -import { supabase } from "../../../_utils/supabase"; -import { setupResponseData } from "../../../_utils/setup-response"; -import { checkDataError } from "../../../_utils/data-error-response"; +import { supabase } from "../../_utils/supabase"; +import { setupResponseData } from "../../_utils/setup-response"; +import { checkDataError } from "../../_utils/data-error-response"; export default async function handler( request: VercelRequest, diff --git a/api/get/_requests/countbyage.ts b/_requests/get/countbyage.ts similarity index 84% rename from api/get/_requests/countbyage.ts rename to _requests/get/countbyage.ts index b381e6a5..95f8a5bf 100644 --- a/api/get/_requests/countbyage.ts +++ b/_requests/get/countbyage.ts @@ -1,7 +1,7 @@ import { VercelRequest, VercelResponse } from "@vercel/node"; -import { checkDataError } from "../../../_utils/data-error-response"; -import { setupResponseData } from "../../../_utils/setup-response"; -import { supabase } from "../../../_utils/supabase"; +import { checkDataError } from "../../_utils/data-error-response"; +import { setupResponseData } from "../../_utils/setup-response"; +import { supabase } from "../../_utils/supabase"; export default async function handler( request: VercelRequest, diff --git a/api/get/_requests/istreeadopted.ts b/_requests/get/istreeadopted.ts similarity index 69% rename from api/get/_requests/istreeadopted.ts rename to _requests/get/istreeadopted.ts index c2793458..72f0360d 100644 --- a/api/get/_requests/istreeadopted.ts +++ b/_requests/get/istreeadopted.ts @@ -1,13 +1,15 @@ +import { User } from "@supabase/supabase-js"; import { VercelRequest, VercelResponse } from "@vercel/node"; -import { setupResponseData } from "../../../_utils/setup-response"; -import { supabase } from "../../../_utils/supabase"; -import { verifyRequest } from "../../../_utils/verify"; +import { setupResponseData } from "../../_utils/setup-response"; +import { supabase } from "../../_utils/supabase"; +import { verifyAuth0Request } from "../../_utils/verify-auth0"; export default async function handler( request: VercelRequest, - response: VercelResponse + response: VercelResponse, + _user?: User ) { - const authorized = await verifyRequest(request); + const authorized = await verifyAuth0Request(request); if (!authorized) { return response.status(401).json({ error: "unauthorized" }); } diff --git a/api/get/_requests/lastwatered.ts b/_requests/get/lastwatered.ts similarity index 73% rename from api/get/_requests/lastwatered.ts rename to _requests/get/lastwatered.ts index 31c41ccf..6ceb509e 100644 --- a/api/get/_requests/lastwatered.ts +++ b/_requests/get/lastwatered.ts @@ -3,14 +3,14 @@ import { VercelRequest, VercelResponse } from "@vercel/node"; import { checkLimitAndOffset, getLimitAndOffeset, -} from "../../../_utils/limit-and-offset"; -import { getRange } from "../../../_utils/parse-content-range"; -import { setupResponseData } from "../../../_utils/setup-response"; -import { supabase } from "../../../_utils/supabase"; -import { getEnvs } from "../../../_utils/envs"; -import { checkRangeError } from "../../../_utils/range-error-response"; -import { checkDataError } from "../../../_utils/data-error-response"; -import { createLinks } from "../../../_utils/create-links"; +} from "../../_utils/limit-and-offset"; +import { getRange } from "../../_utils/parse-content-range"; +import { setupResponseData } from "../../_utils/setup-response"; +import { supabase } from "../../_utils/supabase"; +import { getEnvs } from "../../_utils/envs"; +import { checkRangeError } from "../../_utils/range-error-response"; +import { checkDataError } from "../../_utils/data-error-response"; +import { createLinks } from "../../_utils/create-links"; const { SUPABASE_URL } = getEnvs(); export default async function handler( diff --git a/api/get/_requests/treesbyids.ts b/_requests/get/treesbyids.ts similarity index 72% rename from api/get/_requests/treesbyids.ts rename to _requests/get/treesbyids.ts index eb26e56a..841e9694 100644 --- a/api/get/_requests/treesbyids.ts +++ b/_requests/get/treesbyids.ts @@ -1,17 +1,17 @@ // FIXME: Request could be done from the frontend import { VercelRequest, VercelResponse } from "@vercel/node"; -import { createLinks } from "../../../_utils/create-links"; -import { getEnvs } from "../../../_utils/envs"; +import { createLinks } from "../../_utils/create-links"; +import { getEnvs } from "../../_utils/envs"; import { checkLimitAndOffset, getLimitAndOffeset, -} from "../../../_utils/limit-and-offset"; -import { getRange } from "../../../_utils/parse-content-range"; -import { checkDataError } from "../../../_utils/data-error-response"; +} from "../../_utils/limit-and-offset"; +import { getRange } from "../../_utils/parse-content-range"; +import { checkDataError } from "../../_utils/data-error-response"; -import { checkRangeError } from "../../../_utils/range-error-response"; -import { setupResponseData } from "../../../_utils/setup-response"; -import { supabase } from "../../../_utils/supabase"; +import { checkRangeError } from "../../_utils/range-error-response"; +import { setupResponseData } from "../../_utils/setup-response"; +import { supabase } from "../../_utils/supabase"; const { SUPABASE_URL } = getEnvs(); export default async function handler( diff --git a/api/get/_requests/watered.ts b/_requests/get/watered.ts similarity index 69% rename from api/get/_requests/watered.ts rename to _requests/get/watered.ts index 3a3551d9..baaabbcc 100644 --- a/api/get/_requests/watered.ts +++ b/_requests/get/watered.ts @@ -3,14 +3,14 @@ import { VercelRequest, VercelResponse } from "@vercel/node"; import { checkLimitAndOffset, getLimitAndOffeset, -} from "../../../_utils/limit-and-offset"; -import { createLinks } from "../../../_utils/create-links"; -import { getEnvs } from "../../../_utils/envs"; -import { getRange } from "../../../_utils/parse-content-range"; -import { setupResponseData } from "../../../_utils/setup-response"; -import { supabase } from "../../../_utils/supabase"; -import { checkRangeError } from "../../../_utils/range-error-response"; -import { checkDataError } from "../../../_utils/data-error-response"; +} from "../../_utils/limit-and-offset"; +import { createLinks } from "../../_utils/create-links"; +import { getEnvs } from "../../_utils/envs"; +import { getRange } from "../../_utils/parse-content-range"; +import { setupResponseData } from "../../_utils/setup-response"; +import { supabase } from "../../_utils/supabase"; +import { checkRangeError } from "../../_utils/range-error-response"; +import { checkDataError } from "../../_utils/data-error-response"; const { SUPABASE_URL } = getEnvs(); export default async function handler( diff --git a/api/get/_requests/wateredandadopted.ts b/_requests/get/wateredandadopted.ts similarity index 70% rename from api/get/_requests/wateredandadopted.ts rename to _requests/get/wateredandadopted.ts index 6faafdaa..3aecda52 100644 --- a/api/get/_requests/wateredandadopted.ts +++ b/_requests/get/wateredandadopted.ts @@ -1,16 +1,16 @@ // // FIXME: Request could be done from the frontend import { VercelRequest, VercelResponse } from "@vercel/node"; -import { createLinks } from "../../../_utils/create-links"; -import { checkDataError } from "../../../_utils/data-error-response"; -import { getEnvs } from "../../../_utils/envs"; +import { createLinks } from "../../_utils/create-links"; +import { checkDataError } from "../../_utils/data-error-response"; +import { getEnvs } from "../../_utils/envs"; import { checkLimitAndOffset, getLimitAndOffeset, -} from "../../../_utils/limit-and-offset"; -import { getRange } from "../../../_utils/parse-content-range"; -import { checkRangeError } from "../../../_utils/range-error-response"; -import { setupResponseData } from "../../../_utils/setup-response"; -import { supabase } from "../../../_utils/supabase"; +} from "../../_utils/limit-and-offset"; +import { getRange } from "../../_utils/parse-content-range"; +import { checkRangeError } from "../../_utils/range-error-response"; +import { setupResponseData } from "../../_utils/setup-response"; +import { supabase } from "../../_utils/supabase"; const { SUPABASE_URL } = getEnvs(); export default async function handler( diff --git a/api/get/_requests/wateredbyuser.ts b/_requests/get/wateredbyuser.ts similarity index 64% rename from api/get/_requests/wateredbyuser.ts rename to _requests/get/wateredbyuser.ts index 4bef5b57..f55f3d1f 100644 --- a/api/get/_requests/wateredbyuser.ts +++ b/_requests/get/wateredbyuser.ts @@ -1,22 +1,24 @@ import { VercelRequest, VercelResponse } from "@vercel/node"; -import { createLinks } from "../../../_utils/create-links"; -import { checkDataError } from "../../../_utils/data-error-response"; +import { createLinks } from "../../_utils/create-links"; +import { checkDataError } from "../../_utils/data-error-response"; import { checkLimitAndOffset, getLimitAndOffeset, -} from "../../../_utils/limit-and-offset"; -import { getRange } from "../../../_utils/parse-content-range"; -import { checkRangeError } from "../../../_utils/range-error-response"; -import { setupResponseData } from "../../../_utils/setup-response"; -import { supabase } from "../../../_utils/supabase"; -import { verifyRequest } from "../../../_utils/verify"; -import { getEnvs } from "../../../_utils/envs"; +} from "../../_utils/limit-and-offset"; +import { getRange } from "../../_utils/parse-content-range"; +import { checkRangeError } from "../../_utils/range-error-response"; +import { setupResponseData } from "../../_utils/setup-response"; +import { supabase } from "../../_utils/supabase"; +import { verifyAuth0Request } from "../../_utils/verify-auth0"; +import { getEnvs } from "../../_utils/envs"; +import { User } from "@supabase/supabase-js"; const { SUPABASE_URL } = getEnvs(); export default async function handler( request: VercelRequest, - response: VercelResponse + response: VercelResponse, + _user?: User ) { - const authorized = await verifyRequest(request); + const authorized = await verifyAuth0Request(request); if (!authorized) { return response.status(401).json({ error: "unauthorized" }); } diff --git a/_requests/post/adopt.ts b/_requests/post/adopt.ts new file mode 100644 index 00000000..48bc3b64 --- /dev/null +++ b/_requests/post/adopt.ts @@ -0,0 +1,24 @@ +import { VercelRequest, VercelResponse } from "@vercel/node"; +import { supabase } from "../../_utils/supabase"; + +export default async function handler( + request: VercelRequest, + response: VercelResponse +) { + const { tree_id, uuid } = request.body; + const { data, error } = await supabase + .from("trees_adopted") + .upsert( + { + tree_id, + uuid, + }, + + { onConflict: "uuid,tree_id" } + ) + .select(); + if (error) { + return response.status(500).json({ error }); + } + return response.status(201).json({ message: "adopted", data }); +} diff --git a/_requests/post/water.ts b/_requests/post/water.ts new file mode 100644 index 00000000..cb65b040 --- /dev/null +++ b/_requests/post/water.ts @@ -0,0 +1,28 @@ +import { VercelRequest, VercelResponse } from "@vercel/node"; +import { Database } from "../../_types/database"; +import { supabase } from "../../_utils/supabase"; +type TreesWatered = Database["public"]["Tables"]["trees_watered"]["Insert"]; + +export default async function handler( + request: VercelRequest, + response: VercelResponse +) { + const body = request.body as TreesWatered; + const { tree_id, username, timestamp, uuid, amount } = body; + const { data, error } = await supabase + .from("trees_watered") + .insert({ + // TODO: [GDK-220] Remove time from db schema trees_watered it is a legacy value not used anymore + // https://github.com/technologiestiftung/giessdenkiez-de-postgres-api/issues/160 + tree_id, + username, + timestamp, + uuid, + amount, + }) + .select(); + if (error) { + return response.status(500).json({ error }); + } + return response.status(201).json({ message: "watered", data }); +} diff --git a/_utils/setup-response.ts b/_utils/setup-response.ts index da8d5e2d..be5a94e5 100644 --- a/_utils/setup-response.ts +++ b/_utils/setup-response.ts @@ -8,7 +8,7 @@ const pkg = getPackage(); // } export function setupResponseData(overrides?: T) { return { - version: pkg.version, + // version: pkg.version, name: pkg.name, // bugs: pkg.bugs?.url, // home: pkg.homepage, diff --git a/api/delete/[type].ts b/api/delete/[type].ts index 4a12a31f..0a7bf191 100644 --- a/api/delete/[type].ts +++ b/api/delete/[type].ts @@ -1,9 +1,10 @@ import { VercelRequest, VercelResponse } from "@vercel/node"; -import { verifyRequest } from "../../_utils/verify"; +import { verifyAuth0Request } from "../../_utils/verify-auth0"; import setHeaders from "../../_utils/set-headers"; -import { supabase } from "../../_utils/supabase"; import { deleteSchemas, validate } from "../../_utils/validation"; +import unadoptHandler from "../../_requests/delete/unadopt"; +import unwaterHandler from "../../_requests/delete/unwater"; export const queryTypes = ["unadopt", "unwater"]; // const schemas: Record = { // unadopt: unadoptSchema, @@ -19,7 +20,7 @@ export default async function deleteHandler( if (request.method === "OPTIONS") { return response.status(200).end(); } - const authorized = await verifyRequest(request); + const authorized = await verifyAuth0Request(request); if (!authorized) { return response.status(401).json({ error: "unauthorized" }); } @@ -48,35 +49,10 @@ export default async function deleteHandler( return response.status(400).json({ error: "invalid query type" }); } case "unadopt": { - const { tree_id, uuid } = request.body; - const { error } = await supabase - .from("trees_adopted") - .delete() - .eq("tree_id", tree_id) - .eq("uuid", uuid); - if (error) { - return response.status(500).json({ error }); - } - return response - .status(204) - .json({ message: `unadopted tree ${tree_id}` }); + return await unadoptHandler(request, response); } case "unwater": { - // FIXME: [GDK-221] API (with supabase) Find out why delete/unwater route does not work - - const { tree_id, uuid, watering_id } = request.body; - const { error } = await supabase - .from("trees_watered") - .delete() - .eq("tree_id", tree_id) - .eq("uuid", uuid) - .eq("id", watering_id); - if (error) { - return response.status(500).json({ error }); - } - return response - .status(204) - .json({ message: `unwatered tree ${tree_id} ` }); + return await unwaterHandler(request, response); } } } diff --git a/api/get/[type].ts b/api/get/[type].ts index b72a1cc6..22cc6f97 100644 --- a/api/get/[type].ts +++ b/api/get/[type].ts @@ -3,17 +3,18 @@ import setHeaders from "../../_utils/set-headers"; import { queryTypes as queryTypesList } from "../../_utils/routes-listing"; import { getSchemas, paramsToObject, validate } from "../../_utils/validation"; -import allHandler from "./_requests/all"; -import byidHandler from "./_requests/byid"; -import wateredHandler from "./_requests/watered"; -import treesbyidsHandler from "./_requests/treesbyids"; -import wateredandadoptedHandler from "./_requests/wateredandadopted"; -import countbyageHandler from "./_requests/countbyage"; -import byageHandler from "./_requests/byage"; -import lastwateredHandler from "./_requests/lastwatered"; -import adoptedHandler from "./_requests/adopted"; -import istreeadoptedHandler from "./_requests/istreeadopted"; -import wateredbyuserHandler from "./_requests/wateredbyuser"; +import allHandler from "../../_requests/get/all"; +import byidHandler from "../../_requests/get/byid"; +import wateredHandler from "../../_requests/get/watered"; +import treesbyidsHandler from "../../_requests/get/treesbyids"; +import wateredandadoptedHandler from "../../_requests/get/wateredandadopted"; +import countbyageHandler from "../../_requests/get/countbyage"; +import byageHandler from "../../_requests/get/byage"; +import lastwateredHandler from "../../_requests/get/lastwatered"; +import adoptedHandler from "../../_requests/get/adopted"; +import istreeadoptedHandler from "../../_requests/get/istreeadopted"; +import wateredbyuserHandler from "../../_requests/get/wateredbyuser"; +import { verifyAuth0Request } from "../../_utils/verify-auth0"; export const method = "GET"; const queryTypes = Object.keys(queryTypesList[method]); @@ -80,13 +81,25 @@ export default async function handler( // All requests below this line are only available for authenticated users // -------------------------------------------------------------------- case "adopted": { + const authorized = await verifyAuth0Request(request); + if (!authorized) { + return response.status(401).json({ error: "unauthorized" }); + } return await adoptedHandler(request, response); } case "istreeadopted": { + const authorized = await verifyAuth0Request(request); + if (!authorized) { + return response.status(401).json({ error: "unauthorized" }); + } return await istreeadoptedHandler(request, response); } case "wateredbyuser": { + const authorized = await verifyAuth0Request(request); + if (!authorized) { + return response.status(401).json({ error: "unauthorized" }); + } return await wateredbyuserHandler(request, response); } } diff --git a/api/post/[type].ts b/api/post/[type].ts index 3a3176b6..30200d7d 100644 --- a/api/post/[type].ts +++ b/api/post/[type].ts @@ -1,23 +1,16 @@ import { VercelRequest, VercelResponse } from "@vercel/node"; import setHeaders from "../../_utils/set-headers"; -import { supabase } from "../../_utils/supabase"; import { postSchemas, validate } from "../../_utils/validation"; -import { Database } from "../../_types/database"; -import { verifyRequest } from "../../_utils/verify"; import { queryTypes as queryTypesList } from "../../_utils/routes-listing"; +import { verifyAuth0Request } from "../../_utils/verify-auth0"; +import adoptHandler from "../../_requests/post/adopt"; +import waterHandler from "../../_requests/post/water"; const queryTypes = Object.keys(queryTypesList["POST"]); // api/[name].ts -> /api/lee // req.query.name -> "lee" -// const schemas: Record = { -// adopt: adoptSchema, -// water: waterSchema, -// }; - -// type TreesAdopted = Database["public"]["Tables"]["trees_adopted"]["Insert"]; -type TreesWatered = Database["public"]["Tables"]["trees_watered"]["Insert"]; export default async function postHandler( request: VercelRequest, response: VercelResponse @@ -26,6 +19,7 @@ export default async function postHandler( if (request.method === "OPTIONS") { return response.status(200).end(); } + // const { data: userData, error } = await verifySupabaseToken(request); // if (error) { // console.error("error from supabase auth", error); @@ -38,6 +32,8 @@ export default async function postHandler( /** * We will remove auth0 but for now we can auth with both */ + const auth0RequestValid = await verifyAuth0Request(request); + if (!auth0RequestValid) { return response.status(401).json({ error: "unauthorized" }); } const { type } = request.query; @@ -66,43 +62,10 @@ export default async function postHandler( // https://github.com/technologiestiftung/giessdenkiez-de-postgres-api/issues/159 case "adopt": { - const { tree_id, uuid } = request.body; - const { data, error } = await supabase - .from("trees_adopted") - .upsert( - { - tree_id, - uuid, - }, - - { onConflict: "uuid,tree_id" } - ) - .select(); - if (error) { - // console.error(error); - return response.status(500).json({ error }); - } - return response.status(201).json({ message: "adopted", data }); + return await adoptHandler(request, response); } case "water": { - const body = request.body as TreesWatered; - const { tree_id, username, timestamp, uuid, amount } = body; - const { data, error } = await supabase - .from("trees_watered") - .insert({ - // TODO: [GDK-220] Remove time from db schema trees_watered it is a legacy value not used anymore - // https://github.com/technologiestiftung/giessdenkiez-de-postgres-api/issues/160 - tree_id, - username, - timestamp, - uuid, - amount, - }) - .select(); - if (error) { - return response.status(500).json({ error }); - } - return response.status(201).json({ message: "watered", data }); + return await waterHandler(request, response); } } } diff --git a/api/v3/get/[type].ts b/api/v3/get/[type].ts new file mode 100644 index 00000000..91e29a88 --- /dev/null +++ b/api/v3/get/[type].ts @@ -0,0 +1,110 @@ +import { VercelRequest, VercelResponse } from "@vercel/node"; +import setHeaders from "../../../_utils/set-headers"; +import { queryTypes as queryTypesList } from "../../../_utils/routes-listing"; +import { + getSchemas, + paramsToObject, + validate, +} from "../../../_utils/validation"; + +import allHandler from "../../../_requests/get/all"; +import byidHandler from "../../../_requests/get/byid"; +import wateredHandler from "../../../_requests/get/watered"; +import treesbyidsHandler from "../../../_requests/get/treesbyids"; +import wateredandadoptedHandler from "../../../_requests/get/wateredandadopted"; +import countbyageHandler from "../../../_requests/get/countbyage"; +import byageHandler from "../../../_requests/get/byage"; +import lastwateredHandler from "../../../_requests/get/lastwatered"; +import adoptedHandler from "../../../_requests/get/adopted"; +import istreeadoptedHandler from "../../../_requests/get/istreeadopted"; +import wateredbyuserHandler from "../../../_requests/get/wateredbyuser"; +import { verifySupabaseToken } from "../../../_utils/verify-supabase-token"; + +export const method = "GET"; +const queryTypes = Object.keys(queryTypesList[method]); + +// api/[type].ts -> /api/lee +// req.query.type -> "lee" +export default async function handler( + request: VercelRequest, + response: VercelResponse +): Promise { + setHeaders(response, method); + if (request.method === "OPTIONS") { + return response.status(200).end(); + } + const { type } = request.query; + if (Array.isArray(type)) { + return response.status(400).json({ error: `${type} needs to be a string` }); + } + if (!queryTypes.includes(type)) { + return response.status(404).json({ error: `invalid route ${type}` }); + } + if (!request.url) { + return response.status(500).json({ error: "request url not available" }); + } + const params = paramsToObject( + request.url + .replace(`/${method.toLowerCase()}/${type}`, "") + .replace(`/?type=${type}`, "") + ); + const [paramsAreValid, validationError] = validate(params, getSchemas[type]); + if (!paramsAreValid) { + return response + .status(400) + .json({ error: `invalid params: ${JSON.stringify(validationError)}` }); + } + + switch (type) { + default: + return response.status(400).json({ error: "invalid query type" }); + case "byid": { + return await byidHandler(request, response); + } + case "watered": { + return await wateredHandler(request, response); + } + case "treesbyids": { + return await treesbyidsHandler(request, response); + } + case "wateredandadopted": { + return await wateredandadoptedHandler(request, response); + } + case "all": { + return await allHandler(request, response); + } + case "countbyage": { + return await countbyageHandler(request, response); + } + case "byage": { + return await byageHandler(request, response); + } + case "lastwatered": { + return await lastwateredHandler(request, response); + } + // All requests below this line are only available for authenticated users + // -------------------------------------------------------------------- + case "adopted": + case "istreeadopted": + case "wateredbyuser": { + const { data: userData, error } = await verifySupabaseToken(request); + if (error) { + console.error("error from supabase auth", error); + return response.status(401).json({ error: "unauthorized" }); + } + if (!userData) { + console.error("no user data from supabase auth"); + return response.status(401).json({ error: "unauthorized" }); + } + if (type === "adopted") { + return await adoptedHandler(request, response, userData); + } else if (type === "istreeadopted") { + return await istreeadoptedHandler(request, response, userData); + } else if (type === "wateredbyuser") { + return await wateredbyuserHandler(request, response, userData); + } else { + return response.status(400).json({ error: "invalid query type" }); + } + } + } +} diff --git a/api/v3/get/index.ts b/api/v3/get/index.ts new file mode 100644 index 00000000..2a482380 --- /dev/null +++ b/api/v3/get/index.ts @@ -0,0 +1,20 @@ +import { VercelRequest, VercelResponse } from "@vercel/node"; +import setHeaders from "../../../_utils/set-headers"; +import { setupResponseData } from "../../../_utils/setup-response"; +import { routes } from "../../../_utils/routes-listing"; + +export default async function handler( + _request: VercelRequest, + response: VercelResponse +) { + setHeaders(response, "GET"); + try { + return response + .status(200) + .json(setupResponseData({ message: "its working", routes })); + } catch (error) { + return response + .status(500) + .json(setupResponseData({ error: "its not working", routes })); + } +} diff --git a/api/v3/index.ts b/api/v3/index.ts new file mode 100644 index 00000000..14fb4aa5 --- /dev/null +++ b/api/v3/index.ts @@ -0,0 +1,20 @@ +import { VercelRequest, VercelResponse } from "@vercel/node"; +import { routes } from "../../_utils/routes-listing"; +import setHeaders from "../../_utils/set-headers"; +import { setupResponseData } from "../../_utils/setup-response"; + +export default async function handler( + _request: VercelRequest, + response: VercelResponse +) { + setHeaders(response, "GET"); + try { + return response + .status(200) + .json(setupResponseData({ message: "its working", routes })); + } catch (error) { + return response + .status(500) + .json(setupResponseData({ error: "its not working", routes })); + } +} diff --git a/docs/api.http b/docs/api.http index 0d3eafea..b1be5142 100644 --- a/docs/api.http +++ b/docs/api.http @@ -4,7 +4,7 @@ # -------------------------------------------------- @protocol = http @host = localhost -@port = 3000 +@port = 8080 @API_HOST = {{protocol}}://{{host}}:{{port}} @@ -13,6 +13,15 @@ @USER_ID = auth0|abc @USER_NAME = foo + +#SUPABASE VARS + +@SUPABASE_USER_EMAIL = someone@email.com +@SUPABASE_USER_PASSWORD = 1234567890 +@SUPABASE_USER_UUID = db640d6c-1ac9-4a4d-accc-0adacbf6d9ad +@SUPABASE_USER_NAME = someone +@SUPABASE_USER_ACCESS_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjc4OTkzMDUzLCJzdWIiOiJkYjY0MGQ2Yy0xYWM5LTRhNGQtYWNjYy0wYWRhY2JmNmQ5YWQiLCJlbWFpbCI6InNvbWVvbmVAZW1haWwuY29tIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJlbWFpbCIsInByb3ZpZGVycyI6WyJlbWFpbCJdfSwidXNlcl9tZXRhZGF0YSI6e30sInJvbGUiOiJhdXRoZW50aWNhdGVkIiwiYWFsIjoiYWFsMSIsImFtciI6W3sibWV0aG9kIjoicGFzc3dvcmQiLCJ0aW1lc3RhbXAiOjE2Nzg5ODk0NTN9XSwic2Vzc2lvbl9pZCI6ImRhZGZmNDNmLWJkMTItNGNmYi1iZWEzLWNlNjVlMDU0MzAyYiJ9.acj7bwhju_bJ6zFz842oeG7iPNvgzcWtoP7Bji80wZk + # @API_HOST = https://giessdenkiez-de-postgres-api-git-dev-technologiestiftung1.vercel.app # These needs a .env in the root of the project @@ -40,6 +49,9 @@ ### Healthcheck GET {{API_HOST}} + +### v3 +GET {{API_HOST}}/v3 ### GET tree by its id byid βœ“ GET {{API_HOST}}/get/byid&id={{TREE_ID}} @@ -87,6 +99,108 @@ GET {{API_HOST}}/get/byage&start=1800&end=2023&limit=10000&offset=0 GET {{API_HOST}}/get/countbyage&start=1800&end=2023 +######################## +# +# SUPABASE AUTH +# +######################### + + + + + + +### Signup + +POST {{SUPABASE_URL}}/auth/v1/signup +apikey: {{SUPABASE_ANON_KEY}} +Content-Type: application/json + +{ + "email": "{{SUPABASE_USER_EMAIL}}", + "password": "{{SUPABASE_USER_PASSWORD}}" +} + +### Login + + +POST {{SUPABASE_URL}}/auth/v1/token?grant_type=password +apikey: {{SUPABASE_ANON_KEY}} +Content-Type: application/json + +{ + "email": "{{SUPABASE_USER_EMAIL}}", + "password": "{{SUPABASE_USER_PASSWORD}}" +} + + +### Login with magic link +# look ont oinbucket of the email +# http://localhost:54324 +POST {{SUPABASE_URL}}/auth/v1/magiclink +apikey: {{SUPABASE_ANON_KEY}} +Content-Type: application/json + +{ + "email": "{{SUPABASE_USER_EMAIL}}" +} + +### Get user JSON + +GET {{SUPABASE_URL}}/auth/v1/user +apikey: {{SUPABASE_ANON_KEY}} +Content-Type: application/json +Authorization: Bearer {{SUPABASE_USER_ACCESS_TOKEN}} + + +### Password recovery +# in local developement look into the inbuckt of the email +# http://localhost:54324 +# This will send you to http://localhost:3000 ??? +# with a token und the recovery url Param + +POST {{SUPABASE_URL}}/auth/v1/recover +apikey: {{SUPABASE_ANON_KEY}} +Content-Type: application/json + +{ + "email": "{{SUPABASE_USER_EMAIL}}" +} + + +### Update the user data password and/or email +# data is optional + +PUT {{SUPABASE_URL}}/auth/v1/user +apikey: {{SUPABASE_ANON_KEY}} +Authorization: Bearer {{SUPABASE_USER_ACCESS_TOKEN}} +Content-Type: application/json + +{ + "email": "{{SUPABASE_USER_EMAIL}}", + "password": "{{SUPABASE_USER_PASSWORD}}", + "data": { + "key": "value" + } +} + + +### Logout + +POST {{SUPABASE_URL}}/auth/v1/logout +apikey {{SUPABASE_ANON_KEY}} +Authorization: Bearer {{SUPABASE_USER_ACCESS_TOKEN}} + + + + + +######################## +# +# SUPABASE AUTH END +# +######################### + ##### ####### ####### @@ -128,6 +242,9 @@ Content-Type: application/json + + + ##### ####### ####### # # # # # # # @@ -185,7 +302,7 @@ Authorization: Bearer {{token}} ### POST water a tree POST {{API_HOST}}/post/water -Authorization: Bearer {{token}} +Authorization: Bearer {{SUPABASE_USER_ACCESS_TOKEN}} Content-Type: application/json { diff --git a/package.json b/package.json index 4e55ae17..ecc79486 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "license": "MIT", "scripts": { "test": "jest", - "vercel:dev": "vercel dev", + "vercel:dev": "vercel dev --listen 8080", "lint": "eslint ./**/*.ts ", "format": "prettier ./**/*.ts --write", "generate:types": "npx just generate-types" diff --git a/tsconfig.json b/tsconfig.json index 3cae854a..960a8d27 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "include": [ "api/**/*.ts", "_utils/**/*.ts", + "_requests/**/*.ts", "__tests__/**/*.ts", "__test-utils__/**/*.ts", "_types/**/*.ts" From 39f8040bb7ef4b821b979282c3554cf51db9c836 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Fri, 17 Mar 2023 10:24:46 +0100 Subject: [PATCH 03/51] docs(auth): Add link to discussion about jwt verification --- _utils/verify-supabase-token.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/_utils/verify-supabase-token.ts b/_utils/verify-supabase-token.ts index f3bf5e40..74aa8c36 100644 --- a/_utils/verify-supabase-token.ts +++ b/_utils/verify-supabase-token.ts @@ -1,3 +1,5 @@ +// based on this thread "Verify access token on node.js" +// https://github.com/supabase/supabase/issues/491 import { VercelRequest } from "@vercel/node"; import { GDKAuthError } from "./errors"; import { supabase } from "./supabase"; From 3e7c53a67e83a4585e32bb4c35a00f17b2ce50a5 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Fri, 17 Mar 2023 11:01:30 +0100 Subject: [PATCH 04/51] chore: Remove unused routes --- api/v3/get/[type].ts | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/api/v3/get/[type].ts b/api/v3/get/[type].ts index 91e29a88..27c9bdda 100644 --- a/api/v3/get/[type].ts +++ b/api/v3/get/[type].ts @@ -7,13 +7,9 @@ import { validate, } from "../../../_utils/validation"; -import allHandler from "../../../_requests/get/all"; import byidHandler from "../../../_requests/get/byid"; -import wateredHandler from "../../../_requests/get/watered"; import treesbyidsHandler from "../../../_requests/get/treesbyids"; import wateredandadoptedHandler from "../../../_requests/get/wateredandadopted"; -import countbyageHandler from "../../../_requests/get/countbyage"; -import byageHandler from "../../../_requests/get/byage"; import lastwateredHandler from "../../../_requests/get/lastwatered"; import adoptedHandler from "../../../_requests/get/adopted"; import istreeadoptedHandler from "../../../_requests/get/istreeadopted"; @@ -56,29 +52,18 @@ export default async function handler( } switch (type) { - default: + default: { return response.status(400).json({ error: "invalid query type" }); + } case "byid": { return await byidHandler(request, response); } - case "watered": { - return await wateredHandler(request, response); - } case "treesbyids": { return await treesbyidsHandler(request, response); } case "wateredandadopted": { return await wateredandadoptedHandler(request, response); } - case "all": { - return await allHandler(request, response); - } - case "countbyage": { - return await countbyageHandler(request, response); - } - case "byage": { - return await byageHandler(request, response); - } case "lastwatered": { return await lastwateredHandler(request, response); } From b1d0af7905ab886a6d8cbdee5ab140beb5afcb18 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Fri, 17 Mar 2023 12:20:35 +0100 Subject: [PATCH 05/51] chore(housekeeping): Remove dead routes --- .../__snapshots__/get-routes.test.ts.snap | 127 -------- __tests__/__snapshots__/index.test.ts.snap | 285 ------------------ __tests__/get-routes.test.ts | 67 +--- __tests__/route-listing.test.ts | 97 +----- _requests/get/all.ts | 74 ----- _requests/get/byage.ts | 79 ----- _requests/get/countbyage.ts | 42 --- _requests/get/watered.ts | 56 ---- _utils/routes-listing.ts | 8 - _utils/validation.ts | 50 --- 10 files changed, 2 insertions(+), 883 deletions(-) delete mode 100644 _requests/get/all.ts delete mode 100644 _requests/get/byage.ts delete mode 100644 _requests/get/countbyage.ts delete mode 100644 _requests/get/watered.ts diff --git a/__tests__/__snapshots__/get-routes.test.ts.snap b/__tests__/__snapshots__/get-routes.test.ts.snap index da9f832c..48b2040c 100644 --- a/__tests__/__snapshots__/get-routes.test.ts.snap +++ b/__tests__/__snapshots__/get-routes.test.ts.snap @@ -1,35 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`GET routes snapshot tests default responses Should return 200 on tree all route 1`] = ` -{ - "data": [ - [ - "_0mfm21mdc", - 13.3883, - 52.48415, - 524, - ], - [ - "_2100186a5c", - 13.49968, - 52.64586, - 205, - ], - ], - "error": null, - "links": { - "next": "/get/all?limit=2&offset=2&type=all", - }, - "name": "@technologiestiftung/giessdenkiez-de-postgres-api", - "range": { - "end": 13, - "start": 0, - "total": 14, - }, - "url": "/?type=all&limit=2&offset=0", -} -`; - exports[`GET routes snapshot tests default responses Should return 200 on treesbyid route 1`] = ` { "data": [ @@ -1585,75 +1555,6 @@ exports[`GET routes snapshot tests default responses should return 200 on adopte } `; -exports[`GET routes snapshot tests default responses should return 200 on byage route 1`] = ` -{ - "data": [ - { - "id": "_0mfm21mdc", - }, - { - "id": "_2100186a5c", - }, - { - "id": "_2100186c08", - }, - { - "id": "_2100186c09", - }, - { - "id": "_2100186d6f", - }, - { - "id": "_2100186dca", - }, - { - "id": "_2100186feb", - }, - { - "id": "_2100194ce4", - }, - { - "id": "_210028b9c8", - }, - { - "id": "_21002949fc", - }, - { - "id": "_2100294b1f", - }, - { - "id": "_agi2nuc3l", - }, - { - "id": "_gu1p1fon1", - }, - { - "id": "_uip8uzpq0", - }, - ], - "error": null, - "links": {}, - "name": "@technologiestiftung/giessdenkiez-de-postgres-api", - "range": { - "end": 13, - "start": 0, - "total": 14, - }, - "url": "/?type=byage&start=1800&end=3000", -} -`; - -exports[`GET routes snapshot tests default responses should return 200 on countbyage route 1`] = ` -{ - "data": { - "count": 14, - }, - "error": null, - "name": "@technologiestiftung/giessdenkiez-de-postgres-api", - "url": "/?type=countbyage&start=1800&end=3000", -} -`; - exports[`GET routes snapshot tests default responses should return 200 on istreeadopted route authenticated 1`] = ` { "data": false, @@ -2450,34 +2351,6 @@ exports[`GET routes snapshot tests default responses should return 200 on tree b } `; -exports[`GET routes snapshot tests default responses should return 200 on trees_watered watered route 1`] = ` -{ - "data": [ - { - "tree_id": "_2100186c08", - }, - { - "tree_id": "_2100186c08", - }, - { - "tree_id": "_2100294b1f", - }, - { - "tree_id": "_2100294b1f", - }, - ], - "error": null, - "links": {}, - "name": "@technologiestiftung/giessdenkiez-de-postgres-api", - "range": { - "end": 3, - "start": 0, - "total": 4, - }, - "url": "/?type=watered", -} -`; - exports[`GET routes snapshot tests default responses should return 200 on wateredandadopted route 1`] = ` { "data": [], diff --git a/__tests__/__snapshots__/index.test.ts.snap b/__tests__/__snapshots__/index.test.ts.snap index 0c754ddc..e574da90 100644 --- a/__tests__/__snapshots__/index.test.ts.snap +++ b/__tests__/__snapshots__/index.test.ts.snap @@ -33,58 +33,6 @@ exports[`GET/POST/DELETE routes index should list all routes on /delete 1`] = ` }, "url": "get/adopted", }, - "all": { - "schema": { - "additionalProperties": false, - "properties": { - "limit": { - "type": "string", - }, - "offset": { - "type": "string", - }, - "type": { - "description": "The type property is atomaticaly added by dynamic vercel api routes. You should not add it yourself", - "type": "string", - }, - }, - "required": [ - "limit", - "offset", - ], - "type": "object", - }, - "url": "get/all", - }, - "byage": { - "schema": { - "additionalProperties": false, - "properties": { - "end": { - "type": "string", - }, - "limit": { - "type": "string", - }, - "offset": { - "type": "string", - }, - "start": { - "type": "string", - }, - "type": { - "description": "The type property is atomaticaly added by dynamic vercel api routes. You should not add it yourself", - "type": "string", - }, - }, - "required": [ - "start", - "end", - ], - "type": "object", - }, - "url": "get/byage", - }, "byid": { "schema": { "additionalProperties": false, @@ -104,29 +52,6 @@ exports[`GET/POST/DELETE routes index should list all routes on /delete 1`] = ` }, "url": "get/byid", }, - "countbyage": { - "schema": { - "additionalProperties": false, - "properties": { - "end": { - "type": "string", - }, - "start": { - "type": "string", - }, - "type": { - "description": "The type property is atomaticaly added by dynamic vercel api routes. You should not add it yourself", - "type": "string", - }, - }, - "required": [ - "start", - "end", - ], - "type": "object", - }, - "url": "get/countbyage", - }, "istreeadopted": { "schema": { "additionalProperties": false, @@ -200,26 +125,6 @@ exports[`GET/POST/DELETE routes index should list all routes on /delete 1`] = ` }, "url": "get/treesbyids", }, - "watered": { - "schema": { - "additionalProperties": false, - "properties": { - "limit": { - "type": "string", - }, - "offset": { - "type": "string", - }, - "type": { - "description": "The type property is atomaticaly added by dynamic vercel api routes. You should not add it yourself", - "type": "string", - }, - }, - "required": [], - "type": "object", - }, - "url": "get/watered", - }, "wateredandadopted": { "schema": { "additionalProperties": false, @@ -412,58 +317,6 @@ exports[`GET/POST/DELETE routes index should list all routes on /get 1`] = ` }, "url": "get/adopted", }, - "all": { - "schema": { - "additionalProperties": false, - "properties": { - "limit": { - "type": "string", - }, - "offset": { - "type": "string", - }, - "type": { - "description": "The type property is atomaticaly added by dynamic vercel api routes. You should not add it yourself", - "type": "string", - }, - }, - "required": [ - "limit", - "offset", - ], - "type": "object", - }, - "url": "get/all", - }, - "byage": { - "schema": { - "additionalProperties": false, - "properties": { - "end": { - "type": "string", - }, - "limit": { - "type": "string", - }, - "offset": { - "type": "string", - }, - "start": { - "type": "string", - }, - "type": { - "description": "The type property is atomaticaly added by dynamic vercel api routes. You should not add it yourself", - "type": "string", - }, - }, - "required": [ - "start", - "end", - ], - "type": "object", - }, - "url": "get/byage", - }, "byid": { "schema": { "additionalProperties": false, @@ -483,29 +336,6 @@ exports[`GET/POST/DELETE routes index should list all routes on /get 1`] = ` }, "url": "get/byid", }, - "countbyage": { - "schema": { - "additionalProperties": false, - "properties": { - "end": { - "type": "string", - }, - "start": { - "type": "string", - }, - "type": { - "description": "The type property is atomaticaly added by dynamic vercel api routes. You should not add it yourself", - "type": "string", - }, - }, - "required": [ - "start", - "end", - ], - "type": "object", - }, - "url": "get/countbyage", - }, "istreeadopted": { "schema": { "additionalProperties": false, @@ -579,26 +409,6 @@ exports[`GET/POST/DELETE routes index should list all routes on /get 1`] = ` }, "url": "get/treesbyids", }, - "watered": { - "schema": { - "additionalProperties": false, - "properties": { - "limit": { - "type": "string", - }, - "offset": { - "type": "string", - }, - "type": { - "description": "The type property is atomaticaly added by dynamic vercel api routes. You should not add it yourself", - "type": "string", - }, - }, - "required": [], - "type": "object", - }, - "url": "get/watered", - }, "wateredandadopted": { "schema": { "additionalProperties": false, @@ -791,58 +601,6 @@ exports[`GET/POST/DELETE routes index should list all routes on /post 1`] = ` }, "url": "get/adopted", }, - "all": { - "schema": { - "additionalProperties": false, - "properties": { - "limit": { - "type": "string", - }, - "offset": { - "type": "string", - }, - "type": { - "description": "The type property is atomaticaly added by dynamic vercel api routes. You should not add it yourself", - "type": "string", - }, - }, - "required": [ - "limit", - "offset", - ], - "type": "object", - }, - "url": "get/all", - }, - "byage": { - "schema": { - "additionalProperties": false, - "properties": { - "end": { - "type": "string", - }, - "limit": { - "type": "string", - }, - "offset": { - "type": "string", - }, - "start": { - "type": "string", - }, - "type": { - "description": "The type property is atomaticaly added by dynamic vercel api routes. You should not add it yourself", - "type": "string", - }, - }, - "required": [ - "start", - "end", - ], - "type": "object", - }, - "url": "get/byage", - }, "byid": { "schema": { "additionalProperties": false, @@ -862,29 +620,6 @@ exports[`GET/POST/DELETE routes index should list all routes on /post 1`] = ` }, "url": "get/byid", }, - "countbyage": { - "schema": { - "additionalProperties": false, - "properties": { - "end": { - "type": "string", - }, - "start": { - "type": "string", - }, - "type": { - "description": "The type property is atomaticaly added by dynamic vercel api routes. You should not add it yourself", - "type": "string", - }, - }, - "required": [ - "start", - "end", - ], - "type": "object", - }, - "url": "get/countbyage", - }, "istreeadopted": { "schema": { "additionalProperties": false, @@ -958,26 +693,6 @@ exports[`GET/POST/DELETE routes index should list all routes on /post 1`] = ` }, "url": "get/treesbyids", }, - "watered": { - "schema": { - "additionalProperties": false, - "properties": { - "limit": { - "type": "string", - }, - "offset": { - "type": "string", - }, - "type": { - "description": "The type property is atomaticaly added by dynamic vercel api routes. You should not add it yourself", - "type": "string", - }, - }, - "required": [], - "type": "object", - }, - "url": "get/watered", - }, "wateredandadopted": { "schema": { "additionalProperties": false, diff --git a/__tests__/get-routes.test.ts b/__tests__/get-routes.test.ts index 4c3db680..80abf1ee 100644 --- a/__tests__/get-routes.test.ts +++ b/__tests__/get-routes.test.ts @@ -4,17 +4,12 @@ import { test, describe, expect } from "@jest/globals"; import handler from "../api/get/[type]"; import { createTestServer } from "../__test-utils/create-test-server"; import { - createWateredTrees, truncateTreesAdopted, truncateTreesWaterd, } from "../__test-utils/postgres"; import { requestTestToken } from "../__test-utils/req-test-token"; // byid βœ“ -// watered βœ“ -// all βœ“ // treesbyids βœ“ -// countbyage βœ“ -// byage βœ“ // wateredandadopted βœ“ // lastwatered βœ“ // @@ -105,30 +100,6 @@ describe("GET routes snapshot tests default responses", () => { expect(json).toMatchSnapshot(); }); - test("should return 200 on byage route", async () => { - const { server, url } = await createTestServer( - { type: "byage", start: "1800", end: "3000" }, - handler - ); - const response = await fetch(`${url}`); - server.close(); - const json = await response.json(); - expect(response.status).toBe(200); - expect(json).toMatchSnapshot(); - }); - - test("should return 200 on countbyage route", async () => { - const { server, url } = await createTestServer( - { type: "countbyage", start: "1800", end: "3000" }, - handler - ); - const response = await fetch(`${url}`); - server.close(); - const json = await response.json(); - expect(response.status).toBe(200); - expect(json).toMatchSnapshot(); - }); - test("Should return 200 on treesbyid route", async () => { const { server, url } = await createTestServer( { type: "treesbyids", tree_ids: "_2100294b1f,_210028b9c8" }, @@ -140,21 +111,6 @@ describe("GET routes snapshot tests default responses", () => { expect(response.status).toBe(200); expect(json).toMatchSnapshot(); }); - test("should return 200 on trees_watered watered route", async () => { - await truncateTreesWaterd(); - await createWateredTrees(); - const { server, url } = await createTestServer( - { type: "watered" }, - handler - ); - const response = await fetch(`${url}`); - server.close(); - const json = await response.json(); - expect(response.status).toBe(200); - - expect(json).toMatchSnapshot(); - await truncateTreesWaterd(); - }); test("should return 200 on tree by id route", async () => { const { server, url } = await createTestServer( @@ -167,17 +123,6 @@ describe("GET routes snapshot tests default responses", () => { expect(response.status).toBe(200); expect(json).toMatchSnapshot(); }); - test("Should return 200 on tree all route", async () => { - const { server, url } = await createTestServer( - { type: "all", limit: "2", offset: "0" }, - handler - ); - const response = await fetch(`${url}`); - server.close(); - const json = await response.json(); - expect(response.status).toBe(200); - expect(json).toMatchSnapshot(); - }); test("should return 404 on invalid route", async () => { const { server, url } = await createTestServer( @@ -208,19 +153,9 @@ each([ [400, "adopted", {}, "due to not uuid missing"], [401, "adopted", { uuid: "123" }, "due to not being authorized"], - [400, "all", { limit: "abc" }, "due to limit being NaN"], - [400, "all", { limit: 10000000 }, "due to limit being to large"], - [400, "all", { offset: "abc" }, "due to offset being NaN"], [400, "byid", {}, "due to missing id serachParam"], [400, "treesbyids", {}, "due to tree_ids missing"], - [400, "countbyage", {}, "due to start query is missing"], - [400, "countbyage", { start: "1800" }, "due to end query is missing"], - [400, "countbyage", { start: "1800", end: "abc" }, "due to end being NaN"], - [400, "countbyage", { start: "abc", end: "3000" }, "due to start being NaN"], - [400, "byage", {}, "due to start query is missing"], - [400, "byage", { start: "1800" }, "due to end query is missing"], - [400, "byage", { start: "1800", end: "abc" }, "due to end being NaN"], - [400, "byage", { start: "abc", end: "3000" }, "due to start being NaN"], + [400, "lastwatered", {}, "due to id missing"], [ 400, diff --git a/__tests__/route-listing.test.ts b/__tests__/route-listing.test.ts index 04af55f0..7bf84ff9 100644 --- a/__tests__/route-listing.test.ts +++ b/__tests__/route-listing.test.ts @@ -8,7 +8,7 @@ describe("route listing", () => { const params = paramsToObject("uuid=1234&limit=10&offset=0"); const [valid, _validationErrors] = validate( params, - getRoutesList.routes.all.schema + getRoutesList.routes.lastwatered.schema ); // const validate = ajv.compile(getRoutesList.routes.adopted.schema); // const valid = validate(params); @@ -169,58 +169,6 @@ describe("route listing", () => { }, "url": "get/adopted", }, - "all": { - "schema": { - "additionalProperties": false, - "properties": { - "limit": { - "type": "string", - }, - "offset": { - "type": "string", - }, - "type": { - "description": "The type property is atomaticaly added by dynamic vercel api routes. You should not add it yourself", - "type": "string", - }, - }, - "required": [ - "limit", - "offset", - ], - "type": "object", - }, - "url": "get/all", - }, - "byage": { - "schema": { - "additionalProperties": false, - "properties": { - "end": { - "type": "string", - }, - "limit": { - "type": "string", - }, - "offset": { - "type": "string", - }, - "start": { - "type": "string", - }, - "type": { - "description": "The type property is atomaticaly added by dynamic vercel api routes. You should not add it yourself", - "type": "string", - }, - }, - "required": [ - "start", - "end", - ], - "type": "object", - }, - "url": "get/byage", - }, "byid": { "schema": { "additionalProperties": false, @@ -240,29 +188,6 @@ describe("route listing", () => { }, "url": "get/byid", }, - "countbyage": { - "schema": { - "additionalProperties": false, - "properties": { - "end": { - "type": "string", - }, - "start": { - "type": "string", - }, - "type": { - "description": "The type property is atomaticaly added by dynamic vercel api routes. You should not add it yourself", - "type": "string", - }, - }, - "required": [ - "start", - "end", - ], - "type": "object", - }, - "url": "get/countbyage", - }, "istreeadopted": { "schema": { "additionalProperties": false, @@ -336,26 +261,6 @@ describe("route listing", () => { }, "url": "get/treesbyids", }, - "watered": { - "schema": { - "additionalProperties": false, - "properties": { - "limit": { - "type": "string", - }, - "offset": { - "type": "string", - }, - "type": { - "description": "The type property is atomaticaly added by dynamic vercel api routes. You should not add it yourself", - "type": "string", - }, - }, - "required": [], - "type": "object", - }, - "url": "get/watered", - }, "wateredandadopted": { "schema": { "additionalProperties": false, diff --git a/_requests/get/all.ts b/_requests/get/all.ts deleted file mode 100644 index f4b2e17f..00000000 --- a/_requests/get/all.ts +++ /dev/null @@ -1,74 +0,0 @@ -// to match the old structure we need to transform the data a little -// FIXME: [GDK-217] API (with supabase): GET "all" should work with result that is returned without transforming the data into the current structure -// FIXME: Request could be done from the frontend - -import { VercelRequest, VercelResponse } from "@vercel/node"; -import type { Point } from "geojson"; -import { - checkLimitAndOffset, - getLimitAndOffeset, -} from "../../_utils/limit-and-offset"; -import { createLinks } from "../../_utils/create-links"; -import { getEnvs } from "../../_utils/envs"; -import { getRange } from "../../_utils/parse-content-range"; -import { setupResponseData } from "../../_utils/setup-response"; -import { supabase } from "../../_utils/supabase"; -import { checkRangeError } from "../../_utils/range-error-response"; -import { checkDataError } from "../../_utils/data-error-response"; -const { SUPABASE_URL } = getEnvs(); -export default async function handler( - request: VercelRequest, - response: VercelResponse -) { - checkLimitAndOffset(request, response); - const { limit, offset } = getLimitAndOffeset(request.query); - const { range, error: rangeError } = await getRange( - `${SUPABASE_URL}/rest/v1/trees` - ); - checkRangeError(response, rangeError, range); - - const { data, error } = await supabase - .from("trees") - .select< - "id,radolan_sum,geom", - { - id: string; - } & { - radolan_sum: number | null; - } & { - geom: Point; - } - >("id,radolan_sum,geom") - .range(offset, offset + (limit - 1)) - .order("id", { ascending: true }); - - checkDataError({ data, error, response, errorMessage: "trees not found" }); - type TreeArray = NonNullable; - - const watered = (data as TreeArray).map((tree) => { - return [ - tree.id, - tree.geom.coordinates[0] ? tree.geom.coordinates[0] : 0, - tree.geom.coordinates[1] ? tree.geom.coordinates[1] : 0, - tree.radolan_sum ? tree.radolan_sum : 0, - ]; - }); - - const links = createLinks({ - limit, - offset, - range, - type: "all", - method: "get", - requestUrl: request.url ?? "", - }); - - const result = setupResponseData({ - url: request.url, - data: watered, - error, - range, - links, - }); - return response.status(200).json(result); -} diff --git a/_requests/get/byage.ts b/_requests/get/byage.ts deleted file mode 100644 index cc8af1c6..00000000 --- a/_requests/get/byage.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { VercelRequest, VercelResponse } from "@vercel/node"; -import { checkDataError } from "../../_utils/data-error-response"; -import { - checkLimitAndOffset, - getLimitAndOffeset, -} from "../../_utils/limit-and-offset"; -import { setupResponseData } from "../../_utils/setup-response"; -import { supabase } from "../../_utils/supabase"; -import { getEnvs } from "../../_utils/envs"; -import { getRange } from "../../_utils/parse-content-range"; -import { checkRangeError } from "../../_utils/range-error-response"; -import { createLinks } from "../../_utils/create-links"; -const { SUPABASE_URL } = getEnvs(); -export default async function handler( - request: VercelRequest, - response: VercelResponse -) { - checkLimitAndOffset(request, response); - const { limit, offset } = getLimitAndOffeset(request.query); - const { start: startStr, end: endStr } = <{ start: string; end: string }>( - request.query - ); - - const start = isNaN(parseInt(startStr, 10)) - ? undefined - : parseInt(startStr, 10); - const end = isNaN(parseInt(endStr, 10)) ? undefined : parseInt(endStr, 10); - if (start === undefined) { - return response.status(400).json({ error: "start needs to be a number" }); - } - if (end === undefined) { - return response.status(400).json({ error: "end needs to be a number" }); - } - const { range, error: rangeError } = await getRange( - `${SUPABASE_URL}/rest/v1/trees?pflanzjahr=gte.${start}&pflanzjahr=lte.${end}` - ); - checkRangeError(response, rangeError, range); - - // FIXME: Request could be done from the frontend - const { data, error } = await supabase - .from("trees") - .select("id") - .gte("pflanzjahr", start) - .lte("pflanzjahr", end) - .range(offset, offset + (limit - 1)) - .order("id", { ascending: true }); - - checkDataError({ - data, - error, - response, - errorMessage: "trees not found", - }); - - // get searchParams from request - const searchParams = new URLSearchParams(request.url?.split("?")[1]); - // remove limit and offset from searchParams - searchParams.delete("limit"); - searchParams.delete("offset"); - // add limit and offset to searchParams - - const links = createLinks({ - limit, - offset, - range, - type: "byage", - method: "get", - requestUrl: request.url ?? "", - }); - const result = setupResponseData({ - url: request.url, - data, - error, - links, - range, - }); - - return response.status(200).json(result); -} diff --git a/_requests/get/countbyage.ts b/_requests/get/countbyage.ts deleted file mode 100644 index 95f8a5bf..00000000 --- a/_requests/get/countbyage.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { VercelRequest, VercelResponse } from "@vercel/node"; -import { checkDataError } from "../../_utils/data-error-response"; -import { setupResponseData } from "../../_utils/setup-response"; -import { supabase } from "../../_utils/supabase"; - -export default async function handler( - request: VercelRequest, - response: VercelResponse -) { - const { start: startStr, end: endStr } = <{ start: string; end: string }>( - request.query - ); - const start = isNaN(parseInt(startStr, 10)) - ? undefined - : parseInt(startStr, 10); - const end = isNaN(parseInt(endStr, 10)) ? undefined : parseInt(endStr, 10); - if (start === undefined) { - return response.status(400).json({ error: "start needs to be a number" }); - } - if (end === undefined) { - return response.status(400).json({ error: "end needs to be a number" }); - } - // FIXME: Request could be done from the frontend - const { data, error } = await supabase.rpc("count_by_age", { - start_year: start, - end_year: end, - }); - - checkDataError({ - data, - error, - response, - errorMessage: "could not call function count_by_age", - }); - - const result = setupResponseData({ - url: request.url, - data: { count: data }, - error, - }); - return response.status(200).json(result); -} diff --git a/_requests/get/watered.ts b/_requests/get/watered.ts deleted file mode 100644 index baaabbcc..00000000 --- a/_requests/get/watered.ts +++ /dev/null @@ -1,56 +0,0 @@ -// FIXME: Request could be done from the frontend -import { VercelRequest, VercelResponse } from "@vercel/node"; -import { - checkLimitAndOffset, - getLimitAndOffeset, -} from "../../_utils/limit-and-offset"; -import { createLinks } from "../../_utils/create-links"; -import { getEnvs } from "../../_utils/envs"; -import { getRange } from "../../_utils/parse-content-range"; -import { setupResponseData } from "../../_utils/setup-response"; -import { supabase } from "../../_utils/supabase"; -import { checkRangeError } from "../../_utils/range-error-response"; -import { checkDataError } from "../../_utils/data-error-response"; -const { SUPABASE_URL } = getEnvs(); - -export default async function handler( - request: VercelRequest, - response: VercelResponse -) { - checkLimitAndOffset(request, response); - const { limit, offset } = getLimitAndOffeset(request.query); - const { range, error: rangeError } = await getRange( - `${SUPABASE_URL}/rest/v1/trees_watered?select=tree_id&order=tree_id.asc` - ); - checkRangeError(response, rangeError, range); - - const { data, error } = await supabase - .from("trees_watered") - .select("tree_id") - .range(offset, offset + (limit - 1)) - .order("tree_id", { ascending: true }); - - checkDataError({ - data, - error, - response, - errorMessage: "trees_watered not found", - }); - - const links = createLinks({ - limit, - offset, - range, - type: "watered", - method: "get", - requestUrl: request.url ?? "", - }); - const result = setupResponseData({ - url: request.url, - data, - error, - range, - links, - }); - return response.status(200).json(result); -} diff --git a/_utils/routes-listing.ts b/_utils/routes-listing.ts index e8d75b68..51079814 100644 --- a/_utils/routes-listing.ts +++ b/_utils/routes-listing.ts @@ -2,10 +2,7 @@ import { adoptedSchema, adoptSchema, AjvSchema, - allSchema, - byageSchema, byidSchema, - countbyageSchema, istreeadoptedSchema, lastwateredSchema, treesbyidsSchema, @@ -13,7 +10,6 @@ import { unwaterSchema, wateredandadoptedSchemata, wateredbyuserSchema, - wateredSchema, waterSchema, } from "./validation"; @@ -23,12 +19,8 @@ export const queryTypes: Record> = { byid: byidSchema, treesbyids: treesbyidsSchema, adopted: adoptedSchema, - countbyage: countbyageSchema, - watered: wateredSchema, - all: allSchema, istreeadopted: istreeadoptedSchema, wateredandadopted: wateredandadoptedSchemata, - byage: byageSchema, lastwatered: lastwateredSchema, wateredbyuser: wateredbyuserSchema, }, diff --git a/_utils/validation.ts b/_utils/validation.ts index 421a2a99..74384295 100644 --- a/_utils/validation.ts +++ b/_utils/validation.ts @@ -39,17 +39,6 @@ export const byidSchema: AjvSchema = { additionalProperties: false, }; -export const wateredSchema: AjvSchema = { - type: "object", - properties: { - type, - limit: { type: "string" }, - offset: { type: "string" }, - }, - required: [], - additionalProperties: false, -}; - export const treesbyidsSchema: AjvSchema = { type: "object", properties: { @@ -73,41 +62,6 @@ export const wateredandadoptedSchemata: AjvSchema = { additionalProperties: false, }; -export const allSchema: AjvSchema = { - type: "object", - properties: { - type, - limit: { type: "string" }, - offset: { type: "string" }, - }, - required: ["limit", "offset"], - additionalProperties: false, -}; - -export const countbyageSchema: AjvSchema = { - type: "object", - properties: { - type, - start: { type: "string" }, - end: { type: "string" }, - }, - required: ["start", "end"], - additionalProperties: false, -}; - -export const byageSchema: AjvSchema = { - type: "object", - properties: { - type, - start: { type: "string" }, - end: { type: "string" }, - limit: { type: "string" }, - offset: { type: "string" }, - }, - required: ["start", "end"], - additionalProperties: false, -}; - export const lastwateredSchema: AjvSchema = { type: "object", properties: { @@ -155,12 +109,8 @@ export const wateredbyuserSchema: AjvSchema = { export const getSchemas: Record = { byid: byidSchema, - watered: wateredSchema, treesbyids: treesbyidsSchema, wateredandadopted: wateredandadoptedSchemata, - all: allSchema, - countbyage: countbyageSchema, - byage: byageSchema, lastwatered: lastwateredSchema, adopted: adoptedSchema, istreeadopted: istreeadoptedSchema, From 1482cca169c0ca3a210ef8255501a7d5322255c4 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Fri, 17 Mar 2023 12:21:55 +0100 Subject: [PATCH 06/51] chore(housekeeping): Remove dead routes --- api/get/[type].ts | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/api/get/[type].ts b/api/get/[type].ts index 22cc6f97..071386ee 100644 --- a/api/get/[type].ts +++ b/api/get/[type].ts @@ -3,13 +3,9 @@ import setHeaders from "../../_utils/set-headers"; import { queryTypes as queryTypesList } from "../../_utils/routes-listing"; import { getSchemas, paramsToObject, validate } from "../../_utils/validation"; -import allHandler from "../../_requests/get/all"; import byidHandler from "../../_requests/get/byid"; -import wateredHandler from "../../_requests/get/watered"; import treesbyidsHandler from "../../_requests/get/treesbyids"; import wateredandadoptedHandler from "../../_requests/get/wateredandadopted"; -import countbyageHandler from "../../_requests/get/countbyage"; -import byageHandler from "../../_requests/get/byage"; import lastwateredHandler from "../../_requests/get/lastwatered"; import adoptedHandler from "../../_requests/get/adopted"; import istreeadoptedHandler from "../../_requests/get/istreeadopted"; @@ -57,24 +53,14 @@ export default async function handler( case "byid": { return await byidHandler(request, response); } - case "watered": { - return await wateredHandler(request, response); - } + case "treesbyids": { return await treesbyidsHandler(request, response); } case "wateredandadopted": { return await wateredandadoptedHandler(request, response); } - case "all": { - return await allHandler(request, response); - } - case "countbyage": { - return await countbyageHandler(request, response); - } - case "byage": { - return await byageHandler(request, response); - } + case "lastwatered": { return await lastwateredHandler(request, response); } From 985470f29c12bd813a8dd53d79eb146e5ae92ebc Mon Sep 17 00:00:00 2001 From: ff6347 Date: Fri, 17 Mar 2023 12:22:10 +0100 Subject: [PATCH 07/51] chore(housekeeping): Remove dead routes --- api/get/[type].ts | 1 - 1 file changed, 1 deletion(-) diff --git a/api/get/[type].ts b/api/get/[type].ts index 071386ee..128b21a8 100644 --- a/api/get/[type].ts +++ b/api/get/[type].ts @@ -53,7 +53,6 @@ export default async function handler( case "byid": { return await byidHandler(request, response); } - case "treesbyids": { return await treesbyidsHandler(request, response); } From 3f922a293cfc25aaa094c6658756aad2502640f1 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Fri, 17 Mar 2023 12:23:10 +0100 Subject: [PATCH 08/51] test: token verification --- __tests__/verify-supabase-token.test.ts | 84 +++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 __tests__/verify-supabase-token.test.ts diff --git a/__tests__/verify-supabase-token.test.ts b/__tests__/verify-supabase-token.test.ts new file mode 100644 index 00000000..12abe9f8 --- /dev/null +++ b/__tests__/verify-supabase-token.test.ts @@ -0,0 +1,84 @@ +// FIXME: Mocking is a code smell. Get a token from the local dev server and use that instead. +import { verifySupabaseToken } from "../_utils/verify-supabase-token"; +import { VercelRequest } from "@vercel/node"; +import { supabase } from "../_utils/supabase"; +import { GDKAuthError } from "../_utils/errors"; +import { AuthError, User } from "@supabase/supabase-js"; +jest.mock("../_utils/supabase", () => ({ + supabase: { + auth: { + getUser: jest.fn(), + }, + }, +})); + +describe("verifySupabaseToken", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test("should return an error when authorization header is missing", async () => { + const request = { + headers: {}, + } as VercelRequest; + + const { data, error } = await verifySupabaseToken(request); + + expect(data).toBeNull(); + expect(error).toBeInstanceOf(GDKAuthError); + expect(error?.message).toBe("not authorized"); + }); + + test("should return an error when access_token is missing", async () => { + const request = { + headers: { + authorization: "Bearer ", + }, + } as VercelRequest; + + const { data, error } = await verifySupabaseToken(request); + + expect(data).toBeNull(); + expect(error).toBeInstanceOf(GDKAuthError); + expect(error?.message).toBe("not authorized"); + }); + test("should return an error if the access token is invalid", async () => { + const request = { + headers: { authorization: "Bearer invalid" }, + } as VercelRequest; + const getUserMock = supabase.auth.getUser as jest.MockedFunction< + typeof supabase.auth.getUser + >; + getUserMock.mockResolvedValueOnce({ + error: new AuthError("Invalid token"), + data: { user: null }, + }); + const result = await verifySupabaseToken(request); + expect(getUserMock).toHaveBeenCalledWith("invalid"); + expect(result).toEqual({ + data: null, + error: new AuthError("Invalid token"), + }); + }); + + test("should return the user data if the access token is valid", async () => { + const request = { + headers: { authorization: "Bearer valid" }, + } as VercelRequest; + const getUserMock = supabase.auth.getUser as jest.MockedFunction< + typeof supabase.auth.getUser + >; + const userData = { + user: { id: "123", email: "test@example.com" }, + } as { + user: User; + }; + getUserMock.mockResolvedValueOnce({ data: userData, error: null }); + const result = await verifySupabaseToken(request); + expect(getUserMock).toHaveBeenCalledWith("valid"); + expect(result).toEqual({ + data: userData.user, + error: null, + }); + }); +}); From 3bebf69e2c3155cf9bda71abee798d5965b8c30f Mon Sep 17 00:00:00 2001 From: ff6347 Date: Fri, 17 Mar 2023 12:28:12 +0100 Subject: [PATCH 09/51] feat(v2/3): Allow handle v2 and v3 requests --- __tests__/check-if-v3.test.ts | 22 ++++++++++++ _requests/delete/unadopt.ts | 14 ++++++-- _requests/delete/unwater.ts | 16 ++++++--- _requests/get/adopted.ts | 12 +++++-- _requests/get/istreeadopted.ts | 15 ++++---- _requests/get/wateredbyuser.ts | 17 ++++----- _requests/post/adopt.ts | 14 ++++++-- _requests/post/water.ts | 30 ++++++++++++++-- _utils/check-if-v3.ts | 3 ++ api/v3/delete/[type].ts | 64 +++++++++++++++++++++++++++++++++ api/v3/post/[type].ts | 65 ++++++++++++++++++++++++++++++++++ 11 files changed, 246 insertions(+), 26 deletions(-) create mode 100644 __tests__/check-if-v3.test.ts create mode 100644 _utils/check-if-v3.ts create mode 100644 api/v3/delete/[type].ts create mode 100644 api/v3/post/[type].ts diff --git a/__tests__/check-if-v3.test.ts b/__tests__/check-if-v3.test.ts new file mode 100644 index 00000000..fc78eed8 --- /dev/null +++ b/__tests__/check-if-v3.test.ts @@ -0,0 +1,22 @@ +import { urlContainsV3 } from "../_utils/check-if-v3"; +describe("urlContainsV3", () => { + test('returns true if the URL contains the word "v3"', () => { + const url = "https://example.com/api/v3/users"; + expect(urlContainsV3(url)).toBe(true); + }); + + test('returns false if the URL does not contain the word "v3"', () => { + const url = "https://example.com/api/v2/users"; + expect(urlContainsV3(url)).toBe(false); + }); + + test('returns true if the URL contains the word "v3" multiple times', () => { + const url = "https://example.com/api/v3/v3/users"; + expect(urlContainsV3(url)).toBe(true); + }); + + test("returns false if the URL is an empty string", () => { + const url = ""; + expect(urlContainsV3(url)).toBe(false); + }); +}); diff --git a/_requests/delete/unadopt.ts b/_requests/delete/unadopt.ts index 51a07f54..15ef58bc 100644 --- a/_requests/delete/unadopt.ts +++ b/_requests/delete/unadopt.ts @@ -1,11 +1,21 @@ +import { User } from "@supabase/supabase-js"; import { VercelRequest, VercelResponse } from "@vercel/node"; +import { urlContainsV3 } from "../../_utils/check-if-v3"; import { supabase } from "../../_utils/supabase"; export default async function handler( request: VercelRequest, - response: VercelResponse + response: VercelResponse, + user?: User ) { - const { tree_id, uuid } = request.body; + let { uuid } = request.body; + const { tree_id } = request.body; + if (!request.url) { + return response.status(500).json({ error: "no url in request" }); + } + if (urlContainsV3(request.url)) { + uuid = user?.id || uuid; + } const { error } = await supabase .from("trees_adopted") .delete() diff --git a/_requests/delete/unwater.ts b/_requests/delete/unwater.ts index 7fa4557c..0893859a 100644 --- a/_requests/delete/unwater.ts +++ b/_requests/delete/unwater.ts @@ -1,13 +1,21 @@ +import { User } from "@supabase/supabase-js"; import { VercelRequest, VercelResponse } from "@vercel/node"; +import { urlContainsV3 } from "../../_utils/check-if-v3"; import { supabase } from "../../_utils/supabase"; export default async function ( request: VercelRequest, - response: VercelResponse + response: VercelResponse, + user?: User ) { - // FIXME: [GDK-221] API (with supabase) Find out why delete/unwater route does not work - - const { tree_id, uuid, watering_id } = request.body; + let { uuid } = request.body; + const { tree_id, watering_id } = request.body; + if (!request.url) { + return response.status(500).json({ error: "no url in request" }); + } + if (urlContainsV3(request.url)) { + uuid = user?.id || uuid; + } const { error } = await supabase .from("trees_watered") .delete() diff --git a/_requests/get/adopted.ts b/_requests/get/adopted.ts index eec1e5ea..86dd4811 100644 --- a/_requests/get/adopted.ts +++ b/_requests/get/adopted.ts @@ -11,19 +11,27 @@ import { getEnvs } from "../../_utils/envs"; import { checkRangeError } from "../../_utils/range-error-response"; import { createLinks } from "../../_utils/create-links"; import { User } from "@supabase/supabase-js"; +import { urlContainsV3 } from "../../_utils/check-if-v3"; const { SUPABASE_URL } = getEnvs(); export default async function handler( request: VercelRequest, response: VercelResponse, - _user?: User + user?: User ) { checkLimitAndOffset(request, response); const { limit, offset } = getLimitAndOffeset(request.query); - const { uuid } = <{ uuid: string }>request.query; + let { uuid } = <{ uuid: string }>request.query; const { range, error: rangeError } = await getRange( `${SUPABASE_URL}/rest/v1/trees_adopted?uuid=eq.${uuid}` ); checkRangeError(response, rangeError, range); + if (!request.url) { + return response.status(500).json({ error: "no url in request" }); + } + if (urlContainsV3(request.url)) { + uuid = user?.id || uuid; + } + const { data, error } = await supabase .from("trees_adopted") .select("tree_id,uuid") diff --git a/_requests/get/istreeadopted.ts b/_requests/get/istreeadopted.ts index 72f0360d..6796342b 100644 --- a/_requests/get/istreeadopted.ts +++ b/_requests/get/istreeadopted.ts @@ -1,19 +1,22 @@ import { User } from "@supabase/supabase-js"; import { VercelRequest, VercelResponse } from "@vercel/node"; +import { urlContainsV3 } from "../../_utils/check-if-v3"; import { setupResponseData } from "../../_utils/setup-response"; import { supabase } from "../../_utils/supabase"; -import { verifyAuth0Request } from "../../_utils/verify-auth0"; export default async function handler( request: VercelRequest, response: VercelResponse, - _user?: User + user?: User ) { - const authorized = await verifyAuth0Request(request); - if (!authorized) { - return response.status(401).json({ error: "unauthorized" }); + const { id } = <{ uuid: string; id: string }>request.query; + let { uuid } = <{ uuid: string; id: string }>request.query; + if (!request.url) { + return response.status(500).json({ error: "no url in request" }); + } + if (urlContainsV3(request.url)) { + uuid = user?.id || uuid; } - const { uuid, id } = <{ uuid: string; id: string }>request.query; const { data, error } = await supabase .from("trees_adopted") diff --git a/_requests/get/wateredbyuser.ts b/_requests/get/wateredbyuser.ts index f55f3d1f..cbfa9986 100644 --- a/_requests/get/wateredbyuser.ts +++ b/_requests/get/wateredbyuser.ts @@ -9,27 +9,28 @@ import { getRange } from "../../_utils/parse-content-range"; import { checkRangeError } from "../../_utils/range-error-response"; import { setupResponseData } from "../../_utils/setup-response"; import { supabase } from "../../_utils/supabase"; -import { verifyAuth0Request } from "../../_utils/verify-auth0"; import { getEnvs } from "../../_utils/envs"; import { User } from "@supabase/supabase-js"; +import { urlContainsV3 } from "../../_utils/check-if-v3"; const { SUPABASE_URL } = getEnvs(); export default async function handler( request: VercelRequest, response: VercelResponse, - _user?: User + user?: User ) { - const authorized = await verifyAuth0Request(request); - if (!authorized) { - return response.status(401).json({ error: "unauthorized" }); - } checkLimitAndOffset(request, response); const { limit, offset } = getLimitAndOffeset(request.query); - const { uuid } = <{ uuid: string }>request.query; + let { uuid } = <{ uuid: string }>request.query; const { range, error: rangeError } = await getRange( `${SUPABASE_URL}/rest/v1/trees_watered?uuid=eq.${uuid}` ); checkRangeError(response, rangeError, range); - + if (!request.url) { + return response.status(500).json({ error: "no url in request" }); + } + if (urlContainsV3(request.url)) { + uuid = user?.id || uuid; + } const { data, error } = await supabase .from("trees_watered") .select("*") diff --git a/_requests/post/adopt.ts b/_requests/post/adopt.ts index 48bc3b64..bdebe97f 100644 --- a/_requests/post/adopt.ts +++ b/_requests/post/adopt.ts @@ -1,11 +1,21 @@ +import { User } from "@supabase/supabase-js"; import { VercelRequest, VercelResponse } from "@vercel/node"; +import { urlContainsV3 } from "../../_utils/check-if-v3"; import { supabase } from "../../_utils/supabase"; export default async function handler( request: VercelRequest, - response: VercelResponse + response: VercelResponse, + user?: User ) { - const { tree_id, uuid } = request.body; + const { tree_id } = request.body; + let { uuid } = request.body; + if (!request.url) { + return response.status(500).json({ error: "no url in request" }); + } + if (urlContainsV3(request.url)) { + uuid = user?.id || uuid; + } const { data, error } = await supabase .from("trees_adopted") .upsert( diff --git a/_requests/post/water.ts b/_requests/post/water.ts index cb65b040..5cea11c0 100644 --- a/_requests/post/water.ts +++ b/_requests/post/water.ts @@ -1,14 +1,40 @@ +import { User } from "@supabase/supabase-js"; import { VercelRequest, VercelResponse } from "@vercel/node"; import { Database } from "../../_types/database"; +import { urlContainsV3 } from "../../_utils/check-if-v3"; +import { checkDataError } from "../../_utils/data-error-response"; import { supabase } from "../../_utils/supabase"; type TreesWatered = Database["public"]["Tables"]["trees_watered"]["Insert"]; export default async function handler( request: VercelRequest, - response: VercelResponse + response: VercelResponse, + user?: User ) { const body = request.body as TreesWatered; - const { tree_id, username, timestamp, uuid, amount } = body; + const { tree_id, timestamp, amount } = body; + let { uuid, username } = request.body; + if (!request.url) { + return response.status(500).json({ error: "no url in request" }); + } + + if (urlContainsV3(request.url)) { + uuid = user?.id || uuid; + const { data, error } = await supabase + .from("profiles") + .select("*") + .eq("id", uuid); + checkDataError({ + data, + error, + response, + errorMessage: "no user profile found", + }); + + type UserProfiles = NonNullable; + username = (data as UserProfiles)[0].username || username; + } + const { data, error } = await supabase .from("trees_watered") .insert({ diff --git a/_utils/check-if-v3.ts b/_utils/check-if-v3.ts new file mode 100644 index 00000000..3575591d --- /dev/null +++ b/_utils/check-if-v3.ts @@ -0,0 +1,3 @@ +export function urlContainsV3(url: string): boolean { + return url.includes("v3"); +} diff --git a/api/v3/delete/[type].ts b/api/v3/delete/[type].ts new file mode 100644 index 00000000..bcac7ab0 --- /dev/null +++ b/api/v3/delete/[type].ts @@ -0,0 +1,64 @@ +import { VercelRequest, VercelResponse } from "@vercel/node"; +import setHeaders from "../../../_utils/set-headers"; +import { deleteSchemas, validate } from "../../../_utils/validation"; + +import unadoptHandler from "../../../_requests/delete/unadopt"; +import unwaterHandler from "../../../_requests/delete/unwater"; +import { verifySupabaseToken } from "../../../_utils/verify-supabase-token"; +export const queryTypes = ["unadopt", "unwater"]; +// const schemas: Record = { +// unadopt: unadoptSchema, +// unwater: unwaterSchema, +// }; +// api/[name].ts -> /api/lee +// req.query.name -> "lee" +export default async function deleteHandler( + request: VercelRequest, + response: VercelResponse +) { + setHeaders(response, "DELETE"); + if (request.method === "OPTIONS") { + return response.status(200).end(); + } + const { data: userData, error } = await verifySupabaseToken(request); + if (error) { + console.error("error from supabase auth", error); + return response.status(401).json({ error: "unauthorized" }); + } + if (!userData) { + console.error("no user data from supabase auth"); + return response.status(401).json({ error: "unauthorized" }); + } + + const { type } = request.query; + if (Array.isArray(type)) { + return response.status(400).json({ error: "type needs to be a string" }); + } + if (!queryTypes.includes(type)) { + return response.status(400).json({ error: "invalid query type" }); + } + const [isBodyValid, validationErrors] = validate( + request.body, + deleteSchemas[type] + ); + if (!isBodyValid) { + return response + .status(400) + .json({ error: `invalid body: ${JSON.stringify(validationErrors)}` }); + } + + switch (type) { + default: { + // this is here to be sure there is no fall through case, + // but we actually already checked for the type above. + // So this is actually unreachable + return response.status(400).json({ error: "invalid query type" }); + } + case "unadopt": { + return await unadoptHandler(request, response, userData); + } + case "unwater": { + return await unwaterHandler(request, response, userData); + } + } +} diff --git a/api/v3/post/[type].ts b/api/v3/post/[type].ts new file mode 100644 index 00000000..34cd9a76 --- /dev/null +++ b/api/v3/post/[type].ts @@ -0,0 +1,65 @@ +import { VercelRequest, VercelResponse } from "@vercel/node"; +import setHeaders from "../../../_utils/set-headers"; +import { postSchemas, validate } from "../../../_utils/validation"; +import { queryTypes as queryTypesList } from "../../../_utils/routes-listing"; +import adoptHandler from "../../../_requests/post/adopt"; +import waterHandler from "../../../_requests/post/water"; +import { verifySupabaseToken } from "../../../_utils/verify-supabase-token"; + +const queryTypes = Object.keys(queryTypesList["POST"]); + +// api/[name].ts -> /api/lee +// req.query.name -> "lee" + +export default async function postHandler( + request: VercelRequest, + response: VercelResponse +) { + setHeaders(response, "POST"); + if (request.method === "OPTIONS") { + return response.status(200).end(); + } + + const { data: userData, error } = await verifySupabaseToken(request); + if (error) { + console.error("error from supabase auth", error); + return response.status(401).json({ error: "unauthorized" }); + } + if (!userData) { + console.error("no user data from supabase auth"); + return response.status(401).json({ error: "unauthorized" }); + } + + const { type } = request.query; + if (Array.isArray(type)) { + return response.status(400).json({ error: "type needs to be a string" }); + } + if (!queryTypes.includes(type)) { + return response.status(400).json({ error: "invalid query type" }); + } + const [isBodyValid, validationErrors] = validate( + request.body, + postSchemas[type] + ); + if (!isBodyValid) { + return response + .status(400) + .json({ error: `invalid body: ${JSON.stringify(validationErrors)}` }); + } + switch (type) { + default: { + // Since we safegaurd agains invalid types, + // we can safely assume that the type is valid. + // Should not be a fall through case. + return response.status(400).json({ error: "invalid query type" }); + } + // https://github.com/technologiestiftung/giessdenkiez-de-postgres-api/issues/159 + + case "adopt": { + return await adoptHandler(request, response, userData); + } + case "water": { + return await waterHandler(request, response, userData); + } + } +} From 0666b28b44442c8eeee869e8ae30f750bdc1c54e Mon Sep 17 00:00:00 2001 From: ff6347 Date: Fri, 17 Mar 2023 12:34:16 +0100 Subject: [PATCH 10/51] chore: Rename test function to make clear that it is using auth0 --- __test-utils/req-test-token.ts | 2 +- __tests__/delete-routes.test.ts | 6 +++--- __tests__/delete.test.ts | 10 +++++----- __tests__/get-routes.test.ts | 12 ++++++------ __tests__/post-routes.test.ts | 6 +++--- __tests__/post.test.ts | 12 ++++++------ 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/__test-utils/req-test-token.ts b/__test-utils/req-test-token.ts index f8c768d0..49a2bd79 100644 --- a/__test-utils/req-test-token.ts +++ b/__test-utils/req-test-token.ts @@ -3,7 +3,7 @@ const client_id = process.env.client_id || ""; const client_secret = process.env.client_secret || ""; const audience = process.env.audience || ""; -export async function requestTestToken() { +export async function requestAuth0TestToken() { const response = await fetch(`${issuer}oauth/token`, { method: "POST", headers: { diff --git a/__tests__/delete-routes.test.ts b/__tests__/delete-routes.test.ts index a78759b9..6735cd21 100644 --- a/__tests__/delete-routes.test.ts +++ b/__tests__/delete-routes.test.ts @@ -3,7 +3,7 @@ import each from "jest-each"; import fetch from "cross-fetch"; import handler from "../api/delete/[type]"; import { createTestServer } from "../__test-utils/create-test-server"; -import { requestTestToken } from "../__test-utils/req-test-token"; +import { requestAuth0TestToken } from "../__test-utils/req-test-token"; describe("deleting data", () => { test("should return 200 on options route", async () => { @@ -31,7 +31,7 @@ describe("deleting data", () => { method: "DELETE", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${await requestTestToken()}`, + Authorization: `Bearer ${await requestAuth0TestToken()}`, }, body: JSON.stringify({ uuid: "test", tree_id: "test", watering_id: 123 }), }); @@ -167,7 +167,7 @@ each([ method: "DELETE", headers: { ...(auth === true && { - Authorization: `Bearer ${await requestTestToken()}`, + Authorization: `Bearer ${await requestAuth0TestToken()}`, "Content-Type": "application/json", }), }, diff --git a/__tests__/delete.test.ts b/__tests__/delete.test.ts index fa2d8348..4a14162a 100644 --- a/__tests__/delete.test.ts +++ b/__tests__/delete.test.ts @@ -2,7 +2,7 @@ import { test, describe, expect } from "@jest/globals"; import { faker } from "@faker-js/faker"; -import { requestTestToken } from "../__test-utils/req-test-token"; +import { requestAuth0TestToken } from "../__test-utils/req-test-token"; import { supabase } from "../_utils/supabase"; import { Database } from "../_types/database"; import { @@ -34,7 +34,7 @@ describe("api/delete/[type]", () => { { type: "unwater" }, deleteHandler ); - const token = await requestTestToken(); + const token = await requestAuth0TestToken(); const response = await fetch(url, { method: "DELETE", headers: { @@ -51,7 +51,7 @@ describe("api/delete/[type]", () => { { type: "unwater" }, deleteHandler ); - const token = await requestTestToken(); + const token = await requestAuth0TestToken(); const response = await fetch(url, { method: "DELETE", headers: { @@ -102,7 +102,7 @@ describe("api/delete/[type]", () => { { type: "unwater" }, deleteHandler ); - const token = await requestTestToken(); + const token = await requestAuth0TestToken(); const response = await fetch(url, { method: "DELETE", headers: { @@ -151,7 +151,7 @@ describe("api/delete/[type]", () => { { type: "unadopt" }, deleteHandler ); - const token = await requestTestToken(); + const token = await requestAuth0TestToken(); const response = await fetch(url, { method: "DELETE", headers: { diff --git a/__tests__/get-routes.test.ts b/__tests__/get-routes.test.ts index 80abf1ee..89a777db 100644 --- a/__tests__/get-routes.test.ts +++ b/__tests__/get-routes.test.ts @@ -7,7 +7,7 @@ import { truncateTreesAdopted, truncateTreesWaterd, } from "../__test-utils/postgres"; -import { requestTestToken } from "../__test-utils/req-test-token"; +import { requestAuth0TestToken } from "../__test-utils/req-test-token"; // byid βœ“ // treesbyids βœ“ // wateredandadopted βœ“ @@ -21,7 +21,7 @@ import { requestTestToken } from "../__test-utils/req-test-token"; describe("GET routes snapshot tests default responses", () => { test("should return 200 on wateredbyuser route authenticated", async () => { await truncateTreesWaterd(); - const token = await requestTestToken(); + const token = await requestAuth0TestToken(); const { server, url } = await createTestServer( { type: "wateredbyuser", uuid: "auth0|abc" }, handler @@ -40,7 +40,7 @@ describe("GET routes snapshot tests default responses", () => { }); test("should return 200 on istreeadopted route authenticated", async () => { await truncateTreesWaterd(); - const token = await requestTestToken(); + const token = await requestAuth0TestToken(); const { server, url } = await createTestServer( { type: "istreeadopted", id: "_210028b9c8", uuid: "auth0|abc" }, handler @@ -58,7 +58,7 @@ describe("GET routes snapshot tests default responses", () => { test("should return 200 on adopted route authenticated", async () => { await truncateTreesWaterd(); - const token = await requestTestToken(); + const token = await requestAuth0TestToken(); const { server, url } = await createTestServer( { type: "adopted", uuid: "auth0|abc" }, handler @@ -173,7 +173,7 @@ each([ needsAuth?: boolean ) => { test(`should return ${statusCode} on route "${type}" ${description}`, async () => { - // const token = await requestTestToken(); + // const token = await requestAuth0TestToken(); const { server, url } = await createTestServer( { type, ...overrides }, @@ -182,7 +182,7 @@ each([ const response = await fetch(`${url}`, { headers: { ...(needsAuth === true && { - Authorization: `Bearer ${await requestTestToken()}`, + Authorization: `Bearer ${await requestAuth0TestToken()}`, }), "Content-Type": "application/json", }, diff --git a/__tests__/post-routes.test.ts b/__tests__/post-routes.test.ts index 55cbe743..1cf60dd4 100644 --- a/__tests__/post-routes.test.ts +++ b/__tests__/post-routes.test.ts @@ -3,7 +3,7 @@ import each from "jest-each"; import fetch from "cross-fetch"; import handler from "../api/post/[type]"; import { createTestServer } from "../__test-utils/create-test-server"; -import { requestTestToken } from "../__test-utils/req-test-token"; +import { requestAuth0TestToken } from "../__test-utils/req-test-token"; import { truncateTreesWaterd, truncateTreesAdopted, @@ -26,7 +26,7 @@ describe("posting data", () => { const response = await fetch(url, { method: "POST", headers: { - Authorization: `Bearer ${await requestTestToken()}`, + Authorization: `Bearer ${await requestAuth0TestToken()}`, "Content-Type": "application/json", }, @@ -219,7 +219,7 @@ each([ method: "POST", headers: { ...(auth === true && { - Authorization: `Bearer ${await requestTestToken()}`, + Authorization: `Bearer ${await requestAuth0TestToken()}`, }), "Content-Type": "application/json", }, diff --git a/__tests__/post.test.ts b/__tests__/post.test.ts index 6c724051..2b1d3f4f 100644 --- a/__tests__/post.test.ts +++ b/__tests__/post.test.ts @@ -1,7 +1,7 @@ import { test, describe, expect } from "@jest/globals"; import postHandler from "../api/post/[type]"; -import { requestTestToken } from "../__test-utils/req-test-token"; +import { requestAuth0TestToken } from "../__test-utils/req-test-token"; import { supabase } from "../_utils/supabase"; import { truncateTreesAdopted, @@ -42,7 +42,7 @@ describe("api/post/[type]", () => { { type: "watered" }, postHandler ); - const token = await requestTestToken(); + const token = await requestAuth0TestToken(); const response = await fetch(url, { method: "POST", headers: { @@ -60,7 +60,7 @@ describe("api/post/[type]", () => { { type: "water" }, postHandler ); - const token = await requestTestToken(); + const token = await requestAuth0TestToken(); const response = await fetch(url, { method: "POST", headers: { @@ -77,7 +77,7 @@ describe("api/post/[type]", () => { { type: "adopt" }, postHandler ); - const token = await requestTestToken(); + const token = await requestAuth0TestToken(); const response = await fetch(url, { method: "POST", headers: { @@ -96,7 +96,7 @@ describe("api/post/[type]", () => { { type: "water" }, postHandler ); - const token = await requestTestToken(); + const token = await requestAuth0TestToken(); const { data: trees, error } = await supabase.from("trees").select("*"); if (error) { throw error; @@ -134,7 +134,7 @@ describe("api/post/[type]", () => { { type: "adopt" }, postHandler ); - const token = await requestTestToken(); + const token = await requestAuth0TestToken(); const { data: trees, error: treeError } = await supabase .from("trees") .select("id") From 40f543d7fe0d3560c208374d0ab9e4cdaff979c2 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Fri, 17 Mar 2023 13:10:01 +0100 Subject: [PATCH 11/51] chore(Housekeeping): Remove logging --- api/v3/delete/[type].ts | 2 -- api/v3/get/[type].ts | 2 -- api/v3/post/[type].ts | 2 -- 3 files changed, 6 deletions(-) diff --git a/api/v3/delete/[type].ts b/api/v3/delete/[type].ts index bcac7ab0..142f2355 100644 --- a/api/v3/delete/[type].ts +++ b/api/v3/delete/[type].ts @@ -22,11 +22,9 @@ export default async function deleteHandler( } const { data: userData, error } = await verifySupabaseToken(request); if (error) { - console.error("error from supabase auth", error); return response.status(401).json({ error: "unauthorized" }); } if (!userData) { - console.error("no user data from supabase auth"); return response.status(401).json({ error: "unauthorized" }); } diff --git a/api/v3/get/[type].ts b/api/v3/get/[type].ts index 27c9bdda..2d2a3bca 100644 --- a/api/v3/get/[type].ts +++ b/api/v3/get/[type].ts @@ -74,11 +74,9 @@ export default async function handler( case "wateredbyuser": { const { data: userData, error } = await verifySupabaseToken(request); if (error) { - console.error("error from supabase auth", error); return response.status(401).json({ error: "unauthorized" }); } if (!userData) { - console.error("no user data from supabase auth"); return response.status(401).json({ error: "unauthorized" }); } if (type === "adopted") { diff --git a/api/v3/post/[type].ts b/api/v3/post/[type].ts index 34cd9a76..28729a3b 100644 --- a/api/v3/post/[type].ts +++ b/api/v3/post/[type].ts @@ -22,11 +22,9 @@ export default async function postHandler( const { data: userData, error } = await verifySupabaseToken(request); if (error) { - console.error("error from supabase auth", error); return response.status(401).json({ error: "unauthorized" }); } if (!userData) { - console.error("no user data from supabase auth"); return response.status(401).json({ error: "unauthorized" }); } From ea836599bd71500cd07871a24040306ad8e80a6e Mon Sep 17 00:00:00 2001 From: ff6347 Date: Fri, 17 Mar 2023 13:10:46 +0100 Subject: [PATCH 12/51] test(supabase): Add tess for authenticated GET routes --- __test-utils/postgres.ts | 24 +- __test-utils/req-test-token.ts | 55 +- .../__snapshots__/get-routes.test.ts.snap | 2387 +++++++++++++++++ __tests__/get-routes.test.ts | 217 +- docs/api.http | 2 +- 5 files changed, 2666 insertions(+), 19 deletions(-) diff --git a/__test-utils/postgres.ts b/__test-utils/postgres.ts index 0c306bc4..fedd3b3b 100644 --- a/__test-utils/postgres.ts +++ b/__test-utils/postgres.ts @@ -19,12 +19,24 @@ export async function truncateTreesAdopted() { export async function createWateredTrees() { const sql = postgres(url); await sql` - INSERT INTO trees_watered (uuid, tree_id, amount, timestamp) - VALUES - ('test', '_2100294b1f', 1, '2023-01-01 00:00:00'), - ('test', '_2100294b1f', 1, '2023-01-01 00:00:00'), - ('test', '_2100186c08', 1, '2023-01-01 00:00:00'), - ('test', '_2100186c08', 1, '2023-01-01 00:00:00'); + INSERT INTO trees_watered (uuid, amount, timestamp, username, tree_id) + SELECT + md5(random()::text), + random() * 10, + NOW() - (random() * INTERVAL '7 days'), + md5(random()::text), + id + FROM + trees + ORDER BY + random() + LIMIT 10; `; sql.end(); } + +export async function deleteSupabaseUser(email: string): Promise { + const sql = postgres(url); + await sql`DELETE FROM auth.users WHERE email = ${email}`; + sql.end(); +} diff --git a/__test-utils/req-test-token.ts b/__test-utils/req-test-token.ts index 49a2bd79..dc918b32 100644 --- a/__test-utils/req-test-token.ts +++ b/__test-utils/req-test-token.ts @@ -2,7 +2,8 @@ const issuer = process.env.issuer || ""; const client_id = process.env.client_id || ""; const client_secret = process.env.client_secret || ""; const audience = process.env.audience || ""; - +const SUPABASE_URL = process.env.SUPABASE_URL || "http://localhost:54321"; +const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || ""; export async function requestAuth0TestToken() { const response = await fetch(`${issuer}oauth/token`, { method: "POST", @@ -23,3 +24,55 @@ export async function requestAuth0TestToken() { const json = await response.json(); return json.access_token; } + +export async function requestSupabaseTestToken( + email: string, + password: string +) { + const response = await fetch( + `${SUPABASE_URL}/auth/v1/token?grant_type=password`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + apikey: SUPABASE_ANON_KEY, + }, + body: JSON.stringify({ + email, + password, + }), + } + ); + if (!response.ok) { + const json = await response.text(); + throw new Error(`Could not get test token, ${json}`); + } + const json = (await response.json()) as { + access_token: string; + user: { id: string }; + }; + return json.access_token; +} + +export async function createSupabaseUser(email: string, password: string) { + const response = await fetch(`${SUPABASE_URL}/auth/v1/signup`, { + method: "POST", + headers: { + "Content-Type": "application/json", + apikey: SUPABASE_ANON_KEY, + }, + body: JSON.stringify({ + email, + password, + }), + }); + if (!response.ok) { + const json = await response.text(); + throw new Error(`Could not create test user, ${json}`); + } + const json = (await response.json()) as { + access_token: string; + user: { id: string }; + }; + return json.access_token; +} diff --git a/__tests__/__snapshots__/get-routes.test.ts.snap b/__tests__/__snapshots__/get-routes.test.ts.snap index 48b2040c..2ecaecb5 100644 --- a/__tests__/__snapshots__/get-routes.test.ts.snap +++ b/__tests__/__snapshots__/get-routes.test.ts.snap @@ -2386,3 +2386,2390 @@ exports[`GET routes snapshot tests default responses should return 404 on invali "error": "invalid route invalid", } `; + +exports[`GET v3 routes snapshot tests default responses Should return 200 on treesbyid route 1`] = ` +{ + "data": [ + { + "adopted": null, + "artbot": "Fraxinus ornus", + "artdtsch": "Blumen-Esche", + "baumhoehe": "0", + "bezirk": "Pankow", + "caretaker": null, + "eigentuemer": "Land Berlin", + "gattung": "FRAXINUS", + "gattungdeutsch": "ESCHE", + "geom": { + "coordinates": [ + 13.50326, + 52.64844, + ], + "crs": { + "properties": { + "name": "EPSG:4326", + }, + "type": "name", + }, + "type": "Point", + }, + "gmlid": "00008100:0028b9c8", + "hausnr": null, + "id": "_210028b9c8", + "kennzeich": "00885", + "kronedurch": "0", + "lat": "13.50326", + "lng": "52.64844", + "pflanzjahr": 2019, + "radolan_days": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 3, + 5, + 6, + 10, + 29, + 37, + 43, + 12, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 3, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 3, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 16, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 2, + 0, + 0, + 0, + 10, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 8, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "radolan_sum": 205, + "stammumfg": "0", + "standalter": "3", + "standortnr": "5", + "strname": null, + "type": null, + "watered": null, + "zusatz": null, + }, + { + "adopted": null, + "artbot": "Carpinus betulus", + "artdtsch": "Hainbuche", + "baumhoehe": "7", + "bezirk": "Pankow", + "caretaker": null, + "eigentuemer": "Land Berlin", + "gattung": "CARPINUS", + "gattungdeutsch": "HAINBUCHE", + "geom": { + "coordinates": [ + 13.50295, + 52.64778, + ], + "crs": { + "properties": { + "name": "EPSG:4326", + }, + "type": "name", + }, + "type": "Point", + }, + "gmlid": "00008100:00294b1f", + "hausnr": null, + "id": "_2100294b1f", + "kennzeich": "41374", + "kronedurch": "2", + "lat": "13.50295", + "lng": "52.64778", + "pflanzjahr": 2019, + "radolan_days": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 3, + 5, + 6, + 10, + 29, + 37, + 43, + 12, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 3, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 3, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 16, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 2, + 0, + 0, + 0, + 10, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 8, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "radolan_sum": 205, + "stammumfg": "31", + "standalter": "3", + "standortnr": "74/2", + "strname": "HΓΆrstenweg", + "type": null, + "watered": null, + "zusatz": null, + }, + ], + "error": null, + "links": {}, + "name": "@technologiestiftung/giessdenkiez-de-postgres-api", + "range": { + "end": 1, + "start": 0, + "total": 2, + }, + "url": "/?type=treesbyids&tree_ids=_2100294b1f%2C_210028b9c8", +} +`; + +exports[`GET v3 routes snapshot tests default responses should return 200 on adopted route authenticated 1`] = ` +{ + "data": [], + "error": null, + "links": {}, + "name": "@technologiestiftung/giessdenkiez-de-postgres-api", + "range": { + "end": -1, + "start": -1, + "total": 0, + }, + "url": "/?type=adopted&uuid=auth0%7Cabc", +} +`; + +exports[`GET v3 routes snapshot tests default responses should return 200 on istreeadopted route authenticated 1`] = ` +{ + "data": false, + "error": null, + "name": "@technologiestiftung/giessdenkiez-de-postgres-api", + "url": "/?type=istreeadopted&id=_210028b9c8&uuid=auth0%7Cabc", +} +`; + +exports[`GET v3 routes snapshot tests default responses should return 200 on lastwatered route 1`] = ` +{ + "data": [], + "error": null, + "links": {}, + "name": "@technologiestiftung/giessdenkiez-de-postgres-api", + "range": { + "end": -1, + "start": -1, + "total": 0, + }, + "url": "/?type=lastwatered&id=_210028b9c8", +} +`; + +exports[`GET v3 routes snapshot tests default responses should return 200 on tree by id route 1`] = ` +{ + "data": [ + { + "adopted": null, + "artbot": "Carpinus betulus", + "artdtsch": "Hainbuche", + "baumhoehe": "7", + "bezirk": "Pankow", + "caretaker": null, + "eigentuemer": "Land Berlin", + "gattung": "CARPINUS", + "gattungdeutsch": "HAINBUCHE", + "geom": { + "coordinates": [ + 13.50295, + 52.64778, + ], + "crs": { + "properties": { + "name": "EPSG:4326", + }, + "type": "name", + }, + "type": "Point", + }, + "gmlid": "00008100:00294b1f", + "hausnr": null, + "id": "_2100294b1f", + "kennzeich": "41374", + "kronedurch": "2", + "lat": "13.50295", + "lng": "52.64778", + "pflanzjahr": 2019, + "radolan_days": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 3, + 5, + 6, + 10, + 29, + 37, + 43, + 12, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 3, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 3, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 16, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 2, + 0, + 0, + 0, + 10, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 8, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "radolan_sum": 205, + "stammumfg": "31", + "standalter": "3", + "standortnr": "74/2", + "strname": "HΓΆrstenweg", + "type": null, + "watered": null, + "zusatz": null, + }, + ], + "error": null, + "name": "@technologiestiftung/giessdenkiez-de-postgres-api", + "url": "/?type=byid&id=_2100294b1f", +} +`; + +exports[`GET v3 routes snapshot tests default responses should return 200 on wateredandadopted route 1`] = ` +{ + "data": [], + "error": null, + "links": {}, + "name": "@technologiestiftung/giessdenkiez-de-postgres-api", + "range": { + "end": -1, + "start": -1, + "total": 0, + }, + "url": "/?type=wateredandadopted", +} +`; + +exports[`GET v3 routes snapshot tests default responses should return 200 on wateredbyuser route authenticated 1`] = ` +{ + "data": [], + "error": null, + "links": {}, + "name": "@technologiestiftung/giessdenkiez-de-postgres-api", + "range": { + "end": -1, + "start": -1, + "total": 0, + }, + "url": "/?type=wateredbyuser&uuid=auth0%7Cabc", +} +`; + +exports[`GET v3 routes snapshot tests default responses should return 404 on invalid route 1`] = ` +{ + "error": "invalid route invalid", +} +`; diff --git a/__tests__/get-routes.test.ts b/__tests__/get-routes.test.ts index 89a777db..43dd251b 100644 --- a/__tests__/get-routes.test.ts +++ b/__tests__/get-routes.test.ts @@ -1,13 +1,20 @@ import each from "jest-each"; import fetch from "cross-fetch"; +import { faker } from "@faker-js/faker"; import { test, describe, expect } from "@jest/globals"; -import handler from "../api/get/[type]"; +import v2handler from "../api/get/[type]"; +import v3handler from "../api/v3/get/[type]"; import { createTestServer } from "../__test-utils/create-test-server"; import { + deleteSupabaseUser, truncateTreesAdopted, truncateTreesWaterd, } from "../__test-utils/postgres"; -import { requestAuth0TestToken } from "../__test-utils/req-test-token"; +import { + createSupabaseUser, + requestAuth0TestToken, + requestSupabaseTestToken, +} from "../__test-utils/req-test-token"; // byid βœ“ // treesbyids βœ“ // wateredandadopted βœ“ @@ -24,7 +31,7 @@ describe("GET routes snapshot tests default responses", () => { const token = await requestAuth0TestToken(); const { server, url } = await createTestServer( { type: "wateredbyuser", uuid: "auth0|abc" }, - handler + v2handler ); // console.log(url); const response = await fetch(url, { @@ -43,7 +50,7 @@ describe("GET routes snapshot tests default responses", () => { const token = await requestAuth0TestToken(); const { server, url } = await createTestServer( { type: "istreeadopted", id: "_210028b9c8", uuid: "auth0|abc" }, - handler + v2handler ); const response = await fetch(url, { headers: { @@ -61,7 +68,7 @@ describe("GET routes snapshot tests default responses", () => { const token = await requestAuth0TestToken(); const { server, url } = await createTestServer( { type: "adopted", uuid: "auth0|abc" }, - handler + v2handler ); const response = await fetch(url, { headers: { @@ -78,7 +85,7 @@ describe("GET routes snapshot tests default responses", () => { const { server, url } = await createTestServer( { type: "lastwatered", id: "_210028b9c8" }, - handler + v2handler ); const response = await fetch(url); server.close(); @@ -91,7 +98,7 @@ describe("GET routes snapshot tests default responses", () => { await truncateTreesAdopted(); const { server, url } = await createTestServer( { type: "wateredandadopted" }, - handler + v2handler ); const response = await fetch(url); server.close(); @@ -103,7 +110,7 @@ describe("GET routes snapshot tests default responses", () => { test("Should return 200 on treesbyid route", async () => { const { server, url } = await createTestServer( { type: "treesbyids", tree_ids: "_2100294b1f,_210028b9c8" }, - handler + v2handler ); const response = await fetch(`${url}`); server.close(); @@ -115,7 +122,7 @@ describe("GET routes snapshot tests default responses", () => { test("should return 200 on tree by id route", async () => { const { server, url } = await createTestServer( { type: "byid", id: "_2100294b1f" }, - handler + v2handler ); const response = await fetch(`${url}`); server.close(); @@ -127,7 +134,7 @@ describe("GET routes snapshot tests default responses", () => { test("should return 404 on invalid route", async () => { const { server, url } = await createTestServer( { type: "invalid" }, - handler + v2handler ); const response = await fetch(`${url}`); server.close(); @@ -177,7 +184,195 @@ each([ const { server, url } = await createTestServer( { type, ...overrides }, - handler + v2handler + ); + const response = await fetch(`${url}`, { + headers: { + ...(needsAuth === true && { + Authorization: `Bearer ${await requestAuth0TestToken()}`, + }), + "Content-Type": "application/json", + }, + }); + server.close(); + expect(response.status).toBe(statusCode); + }); + } +); + +// β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ +// β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ +// β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ +// β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ +// β–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ + +describe("GET v3 routes snapshot tests default responses", () => { + const email = "foo@example.com"; + const password = "1234567890@"; + beforeAll(async () => { + await createSupabaseUser(email, password); + }); + afterAll(async () => { + await deleteSupabaseUser(email); + }); + test("should return 200 on wateredbyuser route authenticated", async () => { + await truncateTreesWaterd(); + const token = await requestSupabaseTestToken(email, password); + const { server, url } = await createTestServer( + { type: "wateredbyuser", uuid: "auth0|abc" }, + v3handler + ); + // console.log(url); + const response = await fetch(url, { + headers: { + authorization: `Bearer ${token}`, + }, + }); + server.close(); + const json = await response.json(); + // console.log(json); + expect(response.status).toBe(200); + expect(json).toMatchSnapshot(); + }); + test("should return 200 on istreeadopted route authenticated", async () => { + await truncateTreesWaterd(); + const token = await requestSupabaseTestToken(email, password); + const { server, url } = await createTestServer( + { type: "istreeadopted", id: "_210028b9c8", uuid: "auth0|abc" }, + v3handler + ); + const response = await fetch(url, { + headers: { + authorization: `Bearer ${token}`, + }, + }); + server.close(); + const json = await response.json(); + expect(response.status).toBe(200); + expect(json).toMatchSnapshot(); + }); + + test("should return 200 on adopted route authenticated", async () => { + await truncateTreesWaterd(); + const token = await requestSupabaseTestToken(email, password); + const { server, url } = await createTestServer( + { type: "adopted", uuid: "auth0|abc" }, + v3handler + ); + const response = await fetch(url, { + headers: { + authorization: `Bearer ${token}`, + }, + }); + server.close(); + const json = await response.json(); + expect(response.status).toBe(200); + expect(json).toMatchSnapshot(); + }); + test("should return 200 on lastwatered route", async () => { + await truncateTreesWaterd(); + + const { server, url } = await createTestServer( + { type: "lastwatered", id: "_210028b9c8" }, + v3handler + ); + const response = await fetch(url); + server.close(); + const json = await response.json(); + expect(response.status).toBe(200); + expect(json).toMatchSnapshot(); + }); + test("should return 200 on wateredandadopted route", async () => { + await truncateTreesWaterd(); + await truncateTreesAdopted(); + const { server, url } = await createTestServer( + { type: "wateredandadopted" }, + v3handler + ); + const response = await fetch(url); + server.close(); + const json = await response.json(); + expect(response.status).toBe(200); + expect(json).toMatchSnapshot(); + }); + + test("Should return 200 on treesbyid route", async () => { + const { server, url } = await createTestServer( + { type: "treesbyids", tree_ids: "_2100294b1f,_210028b9c8" }, + v3handler + ); + const response = await fetch(`${url}`); + server.close(); + const json = await response.json(); + expect(response.status).toBe(200); + expect(json).toMatchSnapshot(); + }); + + test("should return 200 on tree by id route", async () => { + const { server, url } = await createTestServer( + { type: "byid", id: "_2100294b1f" }, + v3handler + ); + const response = await fetch(`${url}`); + server.close(); + const json = await response.json(); + expect(response.status).toBe(200); + expect(json).toMatchSnapshot(); + }); + + test("should return 404 on invalid route", async () => { + const { server, url } = await createTestServer( + { type: "invalid" }, + v3handler + ); + const response = await fetch(`${url}`); + server.close(); + expect(response.status).toBe(404); + expect(response.statusText).toBe("Not Found"); + expect(await response.json()).toMatchSnapshot(); + }); +}); +each([ + [401, "wateredbyuser", { uuid: "123" }, "due to not being authorized"], + [400, "wateredbyuser", {}, "due to uuid missing", true], + + [400, "istreeadopted", {}, "due to uuid missing", true], + [400, "istreeadopted", { uuid: "abc" }, "due to id missing", true], + [ + 401, + "istreeadopted", + { uuid: "abc", id: "_21000c10a9" }, + "due to not being authorized", + ], + + [400, "adopted", {}, "due to not uuid missing"], + [401, "adopted", { uuid: "123" }, "due to not being authorized"], + + [400, "byid", {}, "due to missing id serachParam"], + [400, "treesbyids", {}, "due to tree_ids missing"], + + [400, "lastwatered", {}, "due to id missing"], + [ + 400, + "treesbyids", + {}, + "due to missing tree_ids list serachParam (_2100294b1f,_210028b9c8)", + ], +]).describe( + "error tests for GET routes", + ( + statusCode: number, + type: string, + overrides: Record, + description: string, + needsAuth?: boolean + ) => { + test(`should return ${statusCode} on route "${type}" ${description}`, async () => { + // const token = await requestAuth0TestToken(); + + const { server, url } = await createTestServer( + { type, ...overrides }, + v3handler ); const response = await fetch(`${url}`, { headers: { diff --git a/docs/api.http b/docs/api.http index b1be5142..1aeec45a 100644 --- a/docs/api.http +++ b/docs/api.http @@ -20,7 +20,7 @@ @SUPABASE_USER_PASSWORD = 1234567890 @SUPABASE_USER_UUID = db640d6c-1ac9-4a4d-accc-0adacbf6d9ad @SUPABASE_USER_NAME = someone -@SUPABASE_USER_ACCESS_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjc4OTkzMDUzLCJzdWIiOiJkYjY0MGQ2Yy0xYWM5LTRhNGQtYWNjYy0wYWRhY2JmNmQ5YWQiLCJlbWFpbCI6InNvbWVvbmVAZW1haWwuY29tIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJlbWFpbCIsInByb3ZpZGVycyI6WyJlbWFpbCJdfSwidXNlcl9tZXRhZGF0YSI6e30sInJvbGUiOiJhdXRoZW50aWNhdGVkIiwiYWFsIjoiYWFsMSIsImFtciI6W3sibWV0aG9kIjoicGFzc3dvcmQiLCJ0aW1lc3RhbXAiOjE2Nzg5ODk0NTN9XSwic2Vzc2lvbl9pZCI6ImRhZGZmNDNmLWJkMTItNGNmYi1iZWEzLWNlNjVlMDU0MzAyYiJ9.acj7bwhju_bJ6zFz842oeG7iPNvgzcWtoP7Bji80wZk +@SUPABASE_USER_ACCESS_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjc5MDU2OTUyLCJzdWIiOiJkYjY0MGQ2Yy0xYWM5LTRhNGQtYWNjYy0wYWRhY2JmNmQ5YWQiLCJlbWFpbCI6InNvbWVvbmVAZW1haWwuY29tIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJlbWFpbCIsInByb3ZpZGVycyI6WyJlbWFpbCJdfSwidXNlcl9tZXRhZGF0YSI6e30sInJvbGUiOiJhdXRoZW50aWNhdGVkIiwiYWFsIjoiYWFsMSIsImFtciI6W3sibWV0aG9kIjoicGFzc3dvcmQiLCJ0aW1lc3RhbXAiOjE2NzkwNTMzNTJ9XSwic2Vzc2lvbl9pZCI6IjA2ZDMwYjk5LTRhYTAtNGE0Yi05MmMxLTYxZTkyMzNmMTNiMSJ9.g_gWFl2ewOdzG6VNn5WE5Fn0_tBW_NZ1C3UyqLdSa6c # @API_HOST = https://giessdenkiez-de-postgres-api-git-dev-technologiestiftung1.vercel.app From 840ba09992107e240dcf655d4ed92429dd6ce885 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Fri, 17 Mar 2023 13:13:29 +0100 Subject: [PATCH 13/51] chore: remove unused import --- __tests__/get-routes.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/__tests__/get-routes.test.ts b/__tests__/get-routes.test.ts index 43dd251b..2198cd6c 100644 --- a/__tests__/get-routes.test.ts +++ b/__tests__/get-routes.test.ts @@ -1,6 +1,5 @@ import each from "jest-each"; import fetch from "cross-fetch"; -import { faker } from "@faker-js/faker"; import { test, describe, expect } from "@jest/globals"; import v2handler from "../api/get/[type]"; import v3handler from "../api/v3/get/[type]"; From d7159ef07385626cd87b9eedcad94fa5f075b6fe Mon Sep 17 00:00:00 2001 From: ff6347 Date: Fri, 17 Mar 2023 13:27:13 +0100 Subject: [PATCH 14/51] test(v3): Add tests for delete and post --- .../__snapshots__/post-routes-v3.test.ts.snap | 13 + __tests__/delete-v3.test.ts | 181 ++++++++++++ __tests__/delete.test.ts | 12 +- __tests__/post-routes-v3.test.ts | 263 ++++++++++++++++++ 4 files changed, 463 insertions(+), 6 deletions(-) create mode 100644 __tests__/__snapshots__/post-routes-v3.test.ts.snap create mode 100644 __tests__/delete-v3.test.ts create mode 100644 __tests__/post-routes-v3.test.ts diff --git a/__tests__/__snapshots__/post-routes-v3.test.ts.snap b/__tests__/__snapshots__/post-routes-v3.test.ts.snap new file mode 100644 index 00000000..5c1e9642 --- /dev/null +++ b/__tests__/__snapshots__/post-routes-v3.test.ts.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`posting data should return 201 on water route invalid body missing uuid 1`] = ` +{ + "amount": Any, + "id": Any, + "time": null, + "timestamp": Any, + "tree_id": Any, + "username": Any, + "uuid": Any, +} +`; diff --git a/__tests__/delete-v3.test.ts b/__tests__/delete-v3.test.ts new file mode 100644 index 00000000..00d8a0d6 --- /dev/null +++ b/__tests__/delete-v3.test.ts @@ -0,0 +1,181 @@ +// import path from "node:path"; +import { test, describe, expect } from "@jest/globals"; +import { faker } from "@faker-js/faker"; + +import { supabase } from "../_utils/supabase"; +import { Database } from "../_types/database"; +import { + deleteSupabaseUser, + truncateTreesAdopted, + truncateTreesWaterd, +} from "../__test-utils/postgres"; +import { createTestServer } from "../__test-utils/create-test-server"; +import v3DeleteHandler from "../api/v3/delete/[type]"; +import { + createSupabaseUser, + requestSupabaseTestToken, +} from "../__test-utils/req-test-token"; +// const envs = config({ path: path.resolve(process.cwd(), ".env") }); +process.env.NODE_ENV = "test"; +const email = "deleter@example.com"; +const password = "1234567890@"; +describe("api/v3/delete/[type]", () => { + beforeAll(async () => { + await createSupabaseUser(email, password); + }); + afterAll(async () => { + await deleteSupabaseUser(email); + }); + + test("should make a request to delete/unwater and fail unauthorized", async () => { + const { server, url } = await createTestServer( + { type: "unwater" }, + v3DeleteHandler + ); + const response = await fetch(url, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + }); + server.close(); + expect(response.status).toBe(401); + }); + test("should make a request to api/delete/unwater and fail due to missing body", async () => { + const { server, url } = await createTestServer( + { type: "unwater" }, + v3DeleteHandler + ); + const token = await requestSupabaseTestToken(email, password); + const response = await fetch(url, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }); + server.close(); + expect(response.status).toBe(400); + }); + + test("should make a request to api/delete/unwater and fail due to wrong body", async () => { + const { server, url } = await createTestServer( + { type: "unwater" }, + v3DeleteHandler + ); + const token = await requestSupabaseTestToken(email, password); + const response = await fetch(url, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({}), + }); + server.close(); + expect(response.status).toBe(400); + }); + + test("should make request to delete/unwater and succeed", async () => { + await truncateTreesWaterd(); + + const uuid = faker.internet.userName(); + const timestamp = new Date().toISOString().slice(0, 19).replace("T", " "); + const amount = 1; + + // get a tree_id + const { data: treeData, error: treeError } = await supabase + .from("trees") + .select("id") + .limit(1); + expect(treeError).toBe(null); + expect(treeData).not.toBe(null); + if (treeData === null) throw new Error("treeData is null"); + const tree_id = treeData[0].id; + + // insert watering into trees_waterd and get watering id + const { data: waterData, error: waterError } = await supabase + .from("trees_watered") + .insert({ + tree_id, + uuid, + amount, + timestamp, + time: timestamp, + username: uuid, + }) + .select("id"); + expect(waterError).toBe(null); + expect(waterData).not.toBe(null); + if (waterData === null) throw new Error("waterData is null"); + + const watering_id = waterData[0].id; + const { server, url } = await createTestServer( + { type: "unwater" }, + v3DeleteHandler + ); + const token = await requestSupabaseTestToken(email, password); + const response = await fetch(url, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + tree_id, + uuid, + watering_id, + }), + }); + server.close(); + expect(response.status).toBe(204); + }); + + test("should make request to delete/unadopt and succeed", async () => { + await truncateTreesAdopted(); + const uuid = faker.internet.userName(); + + // get a tree_id + const { data: treeData, error: treeError } = await supabase + .from("trees") + .select("id") + .limit(1); + + expect(treeError).toBe(null); + expect(treeData).not.toBe(null); + if (treeData === null) throw new Error("treeData is null"); + const tree_id = treeData[0].id; + + // insert adoption into trees_adopted and + // get adoption id + const { data: adoptData, error: adoptError } = await supabase + .from("trees_adopted") + .insert({ + tree_id, + uuid, + }) + .select(); + expect(adoptError).toBe(null); + expect(adoptData).not.toBe(null); + if (adoptData === null) throw new Error("adoptData is null"); + + const { server, url } = await createTestServer( + { type: "unadopt" }, + v3DeleteHandler + ); + const token = await requestSupabaseTestToken(email, password); + const response = await fetch(url, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + tree_id, + uuid, + }), + }); + server.close(); + expect(response.status).toBe(204); + }); +}); diff --git a/__tests__/delete.test.ts b/__tests__/delete.test.ts index 4a14162a..5b8f2dee 100644 --- a/__tests__/delete.test.ts +++ b/__tests__/delete.test.ts @@ -10,7 +10,7 @@ import { truncateTreesWaterd, } from "../__test-utils/postgres"; import { createTestServer } from "../__test-utils/create-test-server"; -import deleteHandler from "../api/delete/[type]"; +import v2DeleteHandler from "../api/delete/[type]"; // const envs = config({ path: path.resolve(process.cwd(), ".env") }); process.env.NODE_ENV = "test"; @@ -18,7 +18,7 @@ describe("api/delete/[type]", () => { test("should make a request to delete/unwater and fail unauthorized", async () => { const { server, url } = await createTestServer( { type: "unwater" }, - deleteHandler + v2DeleteHandler ); const response = await fetch(url, { method: "DELETE", @@ -32,7 +32,7 @@ describe("api/delete/[type]", () => { test("should make a request to api/delete/unwater and fail due to missing body", async () => { const { server, url } = await createTestServer( { type: "unwater" }, - deleteHandler + v2DeleteHandler ); const token = await requestAuth0TestToken(); const response = await fetch(url, { @@ -49,7 +49,7 @@ describe("api/delete/[type]", () => { test("should make a request to api/delete/unwater and fail due to wrong body", async () => { const { server, url } = await createTestServer( { type: "unwater" }, - deleteHandler + v2DeleteHandler ); const token = await requestAuth0TestToken(); const response = await fetch(url, { @@ -100,7 +100,7 @@ describe("api/delete/[type]", () => { const watering_id = waterData[0].id; const { server, url } = await createTestServer( { type: "unwater" }, - deleteHandler + v2DeleteHandler ); const token = await requestAuth0TestToken(); const response = await fetch(url, { @@ -149,7 +149,7 @@ describe("api/delete/[type]", () => { const { server, url } = await createTestServer( { type: "unadopt" }, - deleteHandler + v2DeleteHandler ); const token = await requestAuth0TestToken(); const response = await fetch(url, { diff --git a/__tests__/post-routes-v3.test.ts b/__tests__/post-routes-v3.test.ts new file mode 100644 index 00000000..2f3d7aab --- /dev/null +++ b/__tests__/post-routes-v3.test.ts @@ -0,0 +1,263 @@ +import { test, describe, expect } from "@jest/globals"; +import each from "jest-each"; +import fetch from "cross-fetch"; +import handler from "../api/v3/post/[type]"; +import { createTestServer } from "../__test-utils/create-test-server"; +import { + createSupabaseUser, + requestSupabaseTestToken, +} from "../__test-utils/req-test-token"; +import { + truncateTreesWaterd, + truncateTreesAdopted, + deleteSupabaseUser, +} from "../__test-utils/postgres"; + +// adopt +// water +describe("posting data", () => { + const email = "poster@example.com"; + const password = "1234567890@"; + beforeAll(async () => { + await createSupabaseUser(email, password); + }); + afterAll(async () => { + await deleteSupabaseUser(email); + }); + test("should return 200 on options route", async () => { + const { server, url } = await createTestServer({ type: "water" }, handler); + const response = await fetch(url, { + method: "OPTIONS", + }); + server.close(); + expect(response.status).toBe(200); + }); + test("should return 201 on water route invalid body missing uuid", async () => { + await truncateTreesWaterd(); + const { server, url } = await createTestServer({ type: "water" }, handler); + const response = await fetch(url, { + method: "POST", + headers: { + Authorization: `Bearer ${await requestSupabaseTestToken( + email, + password + )}`, + + "Content-Type": "application/json", + }, + body: JSON.stringify({ + tree_id: "_2100186a5c", + uuid: "123", + username: "test", + timestamp: "2023-01-01T00:00:00", + amount: 200, + }), + }); + server.close(); + const json = await response.json(); + expect(response.status).toBe(201); + json.data.forEach((data: unknown) => { + expect(data).toMatchSnapshot({ + uuid: expect.any(String), + tree_id: expect.any(String), + username: expect.any(String), + timestamp: expect.any(String), + amount: expect.any(Number), + id: expect.any(Number), + }); + }); + }); +}); + +each([ + { + statusCode: 401, + type: "water", + description: "due to not authorized", + auth: false, + overrides: {}, + }, + { + statusCode: 400, + type: ["water"], + description: "due to type not being a string", + auth: true, + overrides: {}, + }, + { + statusCode: 400, + type: "water", + description: "due to invalid body missing uuid", + auth: true, + overrides: {}, + }, + { + statusCode: 400, + type: "water", + description: "due to invalid body missing tree_id", + auth: true, + overrides: {}, + body: { uuid: "123" }, + }, + { + statusCode: 201, + type: "water", + description: "(all valid)", + auth: true, + overrides: {}, + body: { + uuid: "123", + tree_id: "_2100186a5c", + username: "foo", + timestamp: "2023-01-01T00:00:00", + amount: 200, + }, + }, + { + statusCode: 500, + type: "water", + description: "fail due to invalid tree id", + auth: true, + overrides: {}, + body: { + uuid: "123", + tree_id: "123", + username: "foo", + timestamp: "2023-01-01T00:00:00", + amount: 200, + }, + }, + { + statusCode: 400, + type: "water", + description: "due to invalid body missing amount", + auth: true, + overrides: {}, + body: { + uuid: "123", + tree_id: "_2100186a5c", + username: "foo", + timestamp: "2023-01-01T00:00:00", + }, + }, + { + statusCode: 400, + type: "water", + description: "due to invalid body missing timestamp", + auth: true, + overrides: {}, + body: { uuid: "123", tree_id: "_2100186a5c", username: "foo" }, + }, + { + statusCode: 400, + type: "water", + description: "due to invalid body missing username", + auth: true, + overrides: {}, + body: { uuid: "123", tree_id: "_2100186a5c" }, + }, + { + statusCode: 401, + type: "adopt", + description: "due to not authorized", + auth: false, + overrides: {}, + }, + { + statusCode: 400, + type: ["adopt"], + description: "due to type not being a string", + auth: true, + overrides: {}, + }, + { + statusCode: 400, + type: "foo", + description: "due to type being a invalid query type", + auth: true, + overrides: {}, + }, + { + statusCode: 400, + type: "adopt", + description: "due to invalid body missing uuid", + auth: true, + overrides: {}, + }, + { + statusCode: 400, + type: "adopt", + description: "due to invalid body missing tree_id", + auth: true, + overrides: {}, + body: { uuid: "123" }, + }, + { + statusCode: 500, + type: "adopt", + description: "due to invalid tree_id", + auth: true, + overrides: {}, + body: { uuid: "123", tree_id: "123" }, + }, + { + statusCode: 201, + type: "adopt", + description: "(all valid)", + auth: true, + overrides: {}, + body: { uuid: "123", tree_id: "_2100186a5c" }, + }, +]).describe( + "error tests for POST routes", + ({ + statusCode, + type, + description, + overrides, + auth, + body, + }: { + statusCode: number; + type: string; + description: string; + overrides: Record; + auth?: boolean; + body?: Record; + }) => { + const email = "poster2@example.com"; + const password = "1234567890@"; + beforeAll(async () => { + await createSupabaseUser(email, password); + }); + afterAll(async () => { + await deleteSupabaseUser(email); + }); + test(`should return ${statusCode} on route "${type} ${description}"`, async () => { + const { server, url } = await createTestServer( + { type, ...overrides }, + handler + ); + const response = await fetch(url, { + method: "POST", + headers: { + ...(auth === true && { + Authorization: `Bearer ${await requestSupabaseTestToken( + email, + password + )}`, + }), + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + server.close(); + // for debugging it is useful to log the results of the request + // const json = await response.json(); + // console.log(json); + expect(response.status).toBe(statusCode); + await truncateTreesWaterd(); + await truncateTreesAdopted(); + }); + } +); From 54837cba3aeb1e84aad7a7166cfbf9a3008bcee8 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Fri, 17 Mar 2023 13:55:10 +0100 Subject: [PATCH 15/51] docs(v3); Update docs to reflect v3 --- README.md | 171 +++++++++++++++++++++++++++++--------------------- docs/api.http | 2 - 2 files changed, 100 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 7a221568..c7ae6224 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,20 @@ - [W.I.P. API Migration](#wip-api-migration) - [Prerequisites](#prerequisites) - [Setup](#setup) + - [Supabase (local)](#supabase-local) - [Environments and Variables](#environments-and-variables) - - [Auth0](#auth0) + - [Auth0 (deprecated)](#auth0-deprecated) - [Vercel](#vercel) - - [Vercel Environment Variables](#vercel-environment-variables) - - [API Routes](#api-routes) - - [API Authorization](#api-authorization) - - [Tests](#tests) + - [Vercel Environment Variables](#vercel-environment-variables) - [Supabase](#supabase) - [Migrations and Types](#migrations-and-types) - [Deployment](#deployment) - [Radolan Harvester](#radolan-harvester) + - [API Routes](#api-routes) + - [API Authorization](#api-authorization) + - [Supabase](#supabase-1) + - [Auth0 (deprecated)](#auth0-deprecated-1) + - [Tests](#tests) - [Contributors ✨](#contributors-) - [Credits](#credits) @@ -28,7 +31,7 @@ Built with Typescript connects to Supabase and (still) Auth0.com, runs on vercel ![](./docs/wip.png) -We are in the process of migrating the API fully to supabase. These docs are not up to date yet. +We are in the process of migrating the API fully to supabase. These docs might have some missing information. ## Prerequisites @@ -36,10 +39,12 @@ We are in the process of migrating the API fully to supabase. These docs are not - [Supabase](https://supabase.com) Account - Supabase CLI install with brew `brew install supabase/tap/supabase` - [Docker](https://www.docker.com/) Dependency for Supabase -- [Auth0.com](https://auth0.com) Account +- (deprecated) [Auth0.com](https://auth0.com) Account ## Setup +### Supabase (local) + ```bash git clone git@github.com:technologiestiftung/giessdenkiez-de-postgres-api.git cd ./giessdenkiez-de-postgres-api @@ -74,9 +79,9 @@ In the example code above the Postgres database Postgrest API are run locally. Y Again. Be a smart developer, read https://12factor.net/config, https://github.com/motdotla/dotenv#should-i-have-multiple-env-files and never ever touch production with your local code! -### Auth0 +### Auth0 (deprecated) -**!Hint: We are working on replacing Auth0 with Supabase. This is not yet implemented.** +**!Hint: We still support using Auth0 in this API but will eventually remove it. Using Supabase is preferered.** Setup your auth0.com account and create a new API. Get your `jwksUri`, `issuer`, `audience`, `client_id` and `client_secret` values and add them to the `.env` file as well. The values for `client_id` and `client_secret` are only needed if you want to run local integration tests and use tools like rest-client, Postman, Insomnia or Paw to obtain a token. This is explained later in this document. @@ -111,6 +116,53 @@ vercel --prod +## Supabase + +### Migrations and Types + +- Run `supabase start` to start the supabase stack +- make changes to your db using sql and run `supabase db diff --file --schema public --use-migra` to create migrations +- Run `supabase gen types typescript --local > ./_types/database.ts` to generate typescript types for your DB. + +### Deployment + +- Create a project on supabase.com +- Configure your GitHub actions to deploy all migrations to staging and production. See [.github/workflows/deploy-to-supabase-staging.yml](.github/workflows/deploy-to-supabase-staging.yml) and [.github/workflows/deploy-to-supabase-production.yml](.github/workflows/deploy-to-supabase-production.yml) for an example. We are using actions environments to deploy to different environments. You can read more about it here: https://docs.github.com/en/actions/reference/environments. + - Needed variables are: + - `DB_PASSWORD` + - `PROJECT_ID` + - `SUPABASE_ACCESS_TOKEN` +- **(Not recommended but possible)** Link your local project directly to the remote `supabase link --project-ref ` (will ask you for your database password from the creation process) +- **(Not recommended but possible)** Push your local state directly to your remote project `supabase db push` (will ask you for your database password from the creation process) + +### Radolan Harvester + +if you want to use the [DWD Radolan harvester](https://github.com/technologiestiftung/giessdenkiez-de-dwd-harvester) you need to prepare some data in your database + +- Update the table `radolan_harvester` with a time range for the last 30 days + +```sql +INSERT INTO "public"."radolan_harvester" ("id", "collection_date", "start_date", "end_date") + VALUES (1, ( + SELECT + CURRENT_DATE - INTEGER '1' AS yesterday_date), + ( + SELECT + ( + SELECT + CURRENT_DATE - INTEGER '31')::timestamp + '00:50:00'), + ( + SELECT + ( + SELECT + CURRENT_DATE - INTEGER '1')::timestamp + '23:50:00')); +``` + +- Update the table `radolan_geometry` with sql file [radolan_geometry.sql](sql/radolan_geometry.sql) This geometry is Berlin only. +- Populate the table radolan_data with the content of [radolan_data.sql](sql/radolan_data.sql) + +This process is actually a little blackbox we need to solve. + ## API Routes There are 3 main routes `/get`, `/post` and `/delete`. @@ -125,24 +177,48 @@ curl --request GET \ You can see all the available routes in the [docs/api.http](./docs/api.http) file with all their needed `URLSearchParams` and JSON bodies or by inspecting the JSON Schema that is returned when you do a request to the `/get`, `/post` or `/delete` route. -Currently we have these routes - -| `/get` | `/post` | `/delete` | -| -------------------- | -------- | ---------- | -| `/byid` | `/adopt` | `/unadopt` | -| `/treesbyids` | `/water` | `/unwater` | -| `/adopted` | | | -| `/countbyage` | | | -| `/watered` | | | -| `/all` | | | -| `/istreeadopted` | | | -| `/wateredandadopted` | | | -| `/byage` | | | -| `/lastwatered` | | | -| `/wateredbyuser` | | | +Currently we have these routes (for routes that still use auth0 remove the v3 prefix) + +| `/v3/get` | `/v3/post` | `/v3/delete` | +| -------------------- | ---------- | ------------ | +| `/byid` | `/adopt` | `/unadopt` | +| `/treesbyids` | `/water` | `/unwater` | +| `/adopted` | | | +| `/istreeadopted` | | | +| `/wateredandadopted` | | | +| `/lastwatered` | | | +| `/wateredbyuser` | | | ### API Authorization +#### Supabase + +Some of the requests need a authorized user. You can create a new user using email password via the Supabase API. + +```bash +curl --request POST \ + --url http://localhost:54321/auth/v1/signup \ + --header 'apikey: ' \ + --header 'content-type: application/json' \ + --data '{"email": "someone@email.com","password": "1234567890"}' +``` + +This will give you in development already an aceess token. In production you will need to confirm your email address first. + +A login can be done like this: + +```bash +curl --request POST \ + --url 'http://localhost:54321/auth/v1/token?grant_type=password' \ + --header 'apikey: ' \ + --header 'content-type: application/json' \ + --data '{"email": "someone@email.com","password": "1234567890"}' +``` + +See the [docs/api.http](./docs/api.http) file for more examples or take a look into the API documentation in your local supabase instance under http://localhost:54323/project/default/api?page=users + +#### Auth0 (deprecated) + Some of the request will need an authorization header. You can obtain a token by making a request to your auth0 token issuer. ```bash @@ -180,53 +256,6 @@ npm test On CI the Supabase is started automagically. See [.github/workflows/tests.yml](.github/workflows/tests.yml) you still need an API on Auth0.com -## Supabase - -### Migrations and Types - -- Run `supabase start` to start the supabase stack -- make changes to your db using sql and run `supabase db diff --file --schema public --use-migra` to create migrations -- Run `supabase gen types typescript --local > ./_types/database.ts` to generate typescript types for your DB. - -### Deployment - -- Create a project on supabase.com -- Configure your GitHub actions to deploy all migrations to staging and production. See [.github/workflows/deploy-to-supabase-staging.yml](.github/workflows/deploy-to-supabase-staging.yml) and [.github/workflows/deploy-to-supabase-production.yml](.github/workflows/deploy-to-supabase-production.yml) for an example. We are using actions environments to deploy to different environments. You can read more about it here: https://docs.github.com/en/actions/reference/environments. - - Needed variables are: - - `DB_PASSWORD` - - `PROJECT_ID` - - `SUPABASE_ACCESS_TOKEN` -- **(Not recommended but possible)** Link your local project directly to the remote `supabase link --project-ref ` (will ask you for your database password from the creation process) -- **(Not recommended but possible)** Push your local state directly to your remote project `supabase db push` (will ask you for your database password from the creation process) - -### Radolan Harvester - -if you want to use the [DWD Radolan harvester](https://github.com/technologiestiftung/giessdenkiez-de-dwd-harvester) you need to prepare some data in your database - -- Update the table `radolan_harvester` with a time range for the last 30 days - -```sql -INSERT INTO "public"."radolan_harvester" ("id", "collection_date", "start_date", "end_date") - VALUES (1, ( - SELECT - CURRENT_DATE - INTEGER '1' AS yesterday_date), - ( - SELECT - ( - SELECT - CURRENT_DATE - INTEGER '31')::timestamp + '00:50:00'), - ( - SELECT - ( - SELECT - CURRENT_DATE - INTEGER '1')::timestamp + '23:50:00')); -``` - -- Update the table `radolan_geometry` with sql file [radolan_geometry.sql](sql/radolan_geometry.sql) This geometry is Berlin only. -- Populate the table radolan_data with the content of [radolan_data.sql](sql/radolan_data.sql) - -This process is actually a little blackbox we need to solve. - ## Contributors ✨ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): diff --git a/docs/api.http b/docs/api.http index 1aeec45a..198e9955 100644 --- a/docs/api.http +++ b/docs/api.http @@ -108,8 +108,6 @@ GET {{API_HOST}}/get/countbyage&start=1800&end=2023 - - ### Signup POST {{SUPABASE_URL}}/auth/v1/signup From e137749237987002904a11fdc1e5d0284c10ae0f Mon Sep 17 00:00:00 2001 From: ff6347 Date: Fri, 17 Mar 2023 13:59:44 +0100 Subject: [PATCH 16/51] test: Update route listing snapshot --- .../__snapshots__/route-listing.test.ts.snap | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 __tests__/__snapshots__/route-listing.test.ts.snap diff --git a/__tests__/__snapshots__/route-listing.test.ts.snap b/__tests__/__snapshots__/route-listing.test.ts.snap new file mode 100644 index 00000000..7ea83c49 --- /dev/null +++ b/__tests__/__snapshots__/route-listing.test.ts.snap @@ -0,0 +1,121 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`route listing should list all DELETE routes 1`] = ` +{ + "method": "DELETE", + "routes": { + "unadopt": { + "schema": { + "additionalProperties": false, + "properties": { + "queryType": { + "type": "string", + }, + "tree_id": { + "type": "string", + }, + "uuid": { + "type": "string", + }, + }, + "required": [ + "uuid", + "tree_id", + ], + "type": "object", + }, + "url": "delete/unadopt", + }, + "unwater": { + "schema": { + "additionalProperties": false, + "properties": { + "queryType": { + "type": "string", + }, + "tree_id": { + "type": "string", + }, + "uuid": { + "type": "string", + }, + "watering_id": { + "type": "number", + }, + }, + "required": [ + "uuid", + "tree_id", + "watering_id", + ], + "type": "object", + }, + "url": "delete/unwater", + }, + }, +} +`; + +exports[`route listing should list all POST routes 1`] = ` +{ + "method": "POST", + "routes": { + "adopt": { + "schema": { + "additionalProperties": false, + "properties": { + "queryType": { + "type": "string", + }, + "tree_id": { + "type": "string", + }, + "uuid": { + "type": "string", + }, + }, + "required": [ + "uuid", + "tree_id", + ], + "type": "object", + }, + "url": "post/adopt", + }, + "water": { + "schema": { + "additionalProperties": false, + "properties": { + "amount": { + "type": "number", + }, + "queryType": { + "type": "string", + }, + "timestamp": { + "type": "string", + }, + "tree_id": { + "type": "string", + }, + "username": { + "type": "string", + }, + "uuid": { + "type": "string", + }, + }, + "required": [ + "uuid", + "tree_id", + "username", + "timestamp", + "amount", + ], + "type": "object", + }, + "url": "post/water", + }, + }, +} +`; From ecfa4b65581ff100b6825c915470d7c7835fbe11 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Tue, 21 Mar 2023 12:20:25 +0100 Subject: [PATCH 17/51] fix: supabase config max rows --- supabase/config.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supabase/config.toml b/supabase/config.toml index c7b51dda..f5f73065 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -12,7 +12,7 @@ schemas = [] extra_search_path = ["extensions"] # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size # for accidental or malicious requests. -max_rows = 100 +max_rows = 10000 [db] # Port to use for the local database URL. From ea27e922fde8d54c77427eaff3df4ed9a1536dd3 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Tue, 21 Mar 2023 12:35:10 +0100 Subject: [PATCH 18/51] ci: Update backmerge --- package-lock.json | 22 +++++++++++----------- package.json | 2 +- release.config.cjs | 28 ++++++++++++++-------------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0fa53db6..83b4ec36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ }, "devDependencies": { "@faker-js/faker": "7.6.0", - "@saithodev/semantic-release-backmerge": "2.1.2", + "@saithodev/semantic-release-backmerge": "2.2.0", "@technologiestiftung/semantic-release-config": "1.1.0", "@types/geojson": "7946.0.10", "@types/jest": "29.4.0", @@ -3520,17 +3520,17 @@ ] }, "node_modules/@saithodev/semantic-release-backmerge": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@saithodev/semantic-release-backmerge/-/semantic-release-backmerge-2.1.2.tgz", - "integrity": "sha512-fNd8cmijjFIMp4GcdTAcug/7tr4k+8bAyvSsbLOnfyKCWyq42lg14vFZOryLiyLUAe8gpPlI7XzDPWyFTR5zug==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@saithodev/semantic-release-backmerge/-/semantic-release-backmerge-2.2.0.tgz", + "integrity": "sha512-ZPqFNtF84Y9GhoiWZnd59+Sg/PS7/KYfpwGC76xnZiZqieEhF5lcKkkYbee/riDPTxNhz2PXK20qs9duYDh+ig==", "dev": true, "dependencies": { "@semantic-release/error": "^2.2.0 || ^3.0.0", "aggregate-error": "^3.1.0", - "debug": "^4.3.2", + "debug": "^4.3.4", "execa": "^5.1.1", "lodash": "^4.17.21", - "semantic-release": ">=13.0.0" + "semantic-release": ">=13.0.0 <20" } }, "node_modules/@semantic-release/changelog": { @@ -23262,17 +23262,17 @@ "optional": true }, "@saithodev/semantic-release-backmerge": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@saithodev/semantic-release-backmerge/-/semantic-release-backmerge-2.1.2.tgz", - "integrity": "sha512-fNd8cmijjFIMp4GcdTAcug/7tr4k+8bAyvSsbLOnfyKCWyq42lg14vFZOryLiyLUAe8gpPlI7XzDPWyFTR5zug==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@saithodev/semantic-release-backmerge/-/semantic-release-backmerge-2.2.0.tgz", + "integrity": "sha512-ZPqFNtF84Y9GhoiWZnd59+Sg/PS7/KYfpwGC76xnZiZqieEhF5lcKkkYbee/riDPTxNhz2PXK20qs9duYDh+ig==", "dev": true, "requires": { "@semantic-release/error": "^2.2.0 || ^3.0.0", "aggregate-error": "^3.1.0", - "debug": "^4.3.2", + "debug": "^4.3.4", "execa": "^5.1.1", "lodash": "^4.17.21", - "semantic-release": ">=13.0.0" + "semantic-release": ">=13.0.0 <20" } }, "@semantic-release/changelog": { diff --git a/package.json b/package.json index 4e55ae17..61252208 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ }, "devDependencies": { "@faker-js/faker": "7.6.0", - "@saithodev/semantic-release-backmerge": "2.1.2", + "@saithodev/semantic-release-backmerge": "2.2.0", "@technologiestiftung/semantic-release-config": "1.1.0", "@types/geojson": "7946.0.10", "@types/jest": "29.4.0", diff --git a/release.config.cjs b/release.config.cjs index e63a5461..44142757 100644 --- a/release.config.cjs +++ b/release.config.cjs @@ -1,16 +1,16 @@ module.exports = { - extends: "@technologiestiftung/semantic-release-config", - branches: [ - { name: "master" }, - { name: "staging", channel: "pre/rc", prerelease: "rc" }, - ], - plugins: [ - [ - "@saithodev/semantic-release-backmerge", - { - branch: [{ from: "master", to: "staging" }], - backmergeStrategy: "merge", - }, - ], - ], + extends: "@technologiestiftung/semantic-release-config", + branches: [ + { name: "master" }, + { name: "staging", channel: "pre/rc", prerelease: "rc" }, + ], + plugins: [ + [ + "@saithodev/semantic-release-backmerge", + { + backmergeBranches: [{ from: "master", to: "staging" }], + backmergeStrategy: "merge", + }, + ], + ], }; From f35b95688cf286394908523e36603b6e5442fe6f Mon Sep 17 00:00:00 2001 From: ff6347 Date: Tue, 21 Mar 2023 16:26:32 +0100 Subject: [PATCH 19/51] fix(validation): Remove v3 for validation from route If not it ends up as url param --- api/v3/get/[type].ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/v3/get/[type].ts b/api/v3/get/[type].ts index 2d2a3bca..f964f348 100644 --- a/api/v3/get/[type].ts +++ b/api/v3/get/[type].ts @@ -41,8 +41,8 @@ export default async function handler( } const params = paramsToObject( request.url - .replace(`/${method.toLowerCase()}/${type}`, "") - .replace(`/?type=${type}`, "") + .replace(`/v3/${method.toLowerCase()}/${type}`, "") + .replace(`/v3/?type=${type}`, "") ); const [paramsAreValid, validationError] = validate(params, getSchemas[type]); if (!paramsAreValid) { From d189dbd5ba1e93b622823955088fe5988e5f03ff Mon Sep 17 00:00:00 2001 From: ff6347 Date: Tue, 21 Mar 2023 16:27:09 +0100 Subject: [PATCH 20/51] feat(user name): Update trees_watered on profile change --- .../20230321152350_username_change.sql | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 supabase/migrations/20230321152350_username_change.sql diff --git a/supabase/migrations/20230321152350_username_change.sql b/supabase/migrations/20230321152350_username_change.sql new file mode 100644 index 00000000..face08f3 --- /dev/null +++ b/supabase/migrations/20230321152350_username_change.sql @@ -0,0 +1,22 @@ +SET check_function_bodies = OFF; + +CREATE OR REPLACE FUNCTION public.update_username_on_trees_watered () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $function$ +BEGIN + UPDATE + trees_watered + SET + username = NEW.username + WHERE + uuid = OLD.id::text; + RETURN NEW; +END; +$function$; + +CREATE TRIGGER update_username_on_trees_watered_trigger + AFTER INSERT OR UPDATE ON public.profiles + FOR EACH ROW + EXECUTE FUNCTION update_username_on_trees_watered (); + From c1d26b43f553e5d915c65f2011322a933dce1a0a Mon Sep 17 00:00:00 2001 From: ff6347 Date: Tue, 21 Mar 2023 16:40:10 +0100 Subject: [PATCH 21/51] feat(user data): Remove personal data on delete --- .../20230321153935_delete_user_data.sql | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 supabase/migrations/20230321153935_delete_user_data.sql diff --git a/supabase/migrations/20230321153935_delete_user_data.sql b/supabase/migrations/20230321153935_delete_user_data.sql new file mode 100644 index 00000000..70203323 --- /dev/null +++ b/supabase/migrations/20230321153935_delete_user_data.sql @@ -0,0 +1,29 @@ +SET check_function_bodies = OFF; + +CREATE OR REPLACE FUNCTION public.delete_user () + RETURNS TRIGGER + LANGUAGE plpgsql + SECURITY DEFINER + AS $function$ +DECLARE + row_count int; +BEGIN + DELETE FROM public.profiles p + WHERE p.id = OLD.id; + IF found THEN + GET DIAGNOSTICS row_count = ROW_COUNT; + RAISE NOTICE 'DELETEd % row(s) FROM profiles', row_count; + END IF; + UPDATE + trees_watered + SET + uuid = NULL, + username = NULL + WHERE + uuid = OLD.id::text; + DELETE FROM trees_adopted ta + WHERE ta.uuid = OLD.id::text; + RETURN OLD; +END; +$function$; + From 3f6e846a0eb6a91d04cba66fd558986be31743ba Mon Sep 17 00:00:00 2001 From: ff6347 Date: Tue, 21 Mar 2023 16:52:48 +0100 Subject: [PATCH 22/51] feat(users fk): Add forein key constraint to profiles --- supabase/migrations/20230321154859_constrain_profiles.sql | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 supabase/migrations/20230321154859_constrain_profiles.sql diff --git a/supabase/migrations/20230321154859_constrain_profiles.sql b/supabase/migrations/20230321154859_constrain_profiles.sql new file mode 100644 index 00000000..6d207257 --- /dev/null +++ b/supabase/migrations/20230321154859_constrain_profiles.sql @@ -0,0 +1,5 @@ +ALTER TABLE "public"."profiles" + ADD CONSTRAINT "fk_users_profiles" FOREIGN KEY (id) REFERENCES auth.users (id) ON DELETE CASCADE NOT valid; + +ALTER TABLE "public"."profiles" validate CONSTRAINT "fk_users_profiles"; + From 3b9b00368d37871c98cddbdc69c61925cdfe2dd3 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Tue, 21 Mar 2023 17:13:49 +0100 Subject: [PATCH 23/51] docs(delete account) --- docs/api.http | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/api.http b/docs/api.http index 198e9955..baef8fe2 100644 --- a/docs/api.http +++ b/docs/api.http @@ -20,7 +20,7 @@ @SUPABASE_USER_PASSWORD = 1234567890 @SUPABASE_USER_UUID = db640d6c-1ac9-4a4d-accc-0adacbf6d9ad @SUPABASE_USER_NAME = someone -@SUPABASE_USER_ACCESS_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjc5MDU2OTUyLCJzdWIiOiJkYjY0MGQ2Yy0xYWM5LTRhNGQtYWNjYy0wYWRhY2JmNmQ5YWQiLCJlbWFpbCI6InNvbWVvbmVAZW1haWwuY29tIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJlbWFpbCIsInByb3ZpZGVycyI6WyJlbWFpbCJdfSwidXNlcl9tZXRhZGF0YSI6e30sInJvbGUiOiJhdXRoZW50aWNhdGVkIiwiYWFsIjoiYWFsMSIsImFtciI6W3sibWV0aG9kIjoicGFzc3dvcmQiLCJ0aW1lc3RhbXAiOjE2NzkwNTMzNTJ9XSwic2Vzc2lvbl9pZCI6IjA2ZDMwYjk5LTRhYTAtNGE0Yi05MmMxLTYxZTkyMzNmMTNiMSJ9.g_gWFl2ewOdzG6VNn5WE5Fn0_tBW_NZ1C3UyqLdSa6c +@SUPABASE_USER_ACCESS_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjc5NDE4Mzc1LCJzdWIiOiI2NmE2ZWNjZS1hZDNhLTRkODctOTIzZS02OTFhMzFhYTMyNjMiLCJlbWFpbCI6ImZhYmlhbm1vcm9uemlyZmFzQHByb3Rvbm1haWwuY2giLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7fSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTY3OTQxNDc3NX1dLCJzZXNzaW9uX2lkIjoiOWYxYWM4OWItYTU4NC00Mjg5LWIxYWItY2IxNTBmYWZlMmVkIn0.QKf89g4ASbyC4txyHBWh7A_-nQyL93uY694S1Wm8TIY # @API_HOST = https://giessdenkiez-de-postgres-api-git-dev-technologiestiftung1.vercel.app @@ -362,7 +362,6 @@ Content-Type: application/json } - # β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ # β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ # β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ @@ -413,3 +412,11 @@ apikey: {{SUPABASE_ANON_KEY}} Range-Unit: items Prefer: count=exact + +### DELETE an account + +POST {{SUPABASE_URL}}/rest/v1/rpc/remove_account +apikey: {{SUPABASE_ANON_KEY}} +Authorization: Bearer {{SUPABASE_USER_ACCESS_TOKEN}} +Content-Type: application/json + From 523fe1aad2b7e7a69f8caf4427b3bb03ddbe6a59 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Tue, 21 Mar 2023 17:15:11 +0100 Subject: [PATCH 24/51] feat(account): Allow users to remove their data --- .../migrations/20230321161426_remove_account.sql | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 supabase/migrations/20230321161426_remove_account.sql diff --git a/supabase/migrations/20230321161426_remove_account.sql b/supabase/migrations/20230321161426_remove_account.sql new file mode 100644 index 00000000..ef8bda3c --- /dev/null +++ b/supabase/migrations/20230321161426_remove_account.sql @@ -0,0 +1,12 @@ +SET check_function_bodies = OFF; + +CREATE OR REPLACE FUNCTION public.remove_account () + RETURNS void + LANGUAGE sql + SECURITY DEFINER + AS $function$ + DELETE FROM auth.users + WHERE id = auth.uid (); + +$function$; + From 9d15e460405a56a59bbe5d5b42ad16fa33e25240 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Wed, 22 Mar 2023 11:22:31 +0100 Subject: [PATCH 25/51] fix(update): Change RLS to allow updates of username --- .../migrations/20230322102054_rls_for_users.sql | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 supabase/migrations/20230322102054_rls_for_users.sql diff --git a/supabase/migrations/20230322102054_rls_for_users.sql b/supabase/migrations/20230322102054_rls_for_users.sql new file mode 100644 index 00000000..df311d5f --- /dev/null +++ b/supabase/migrations/20230322102054_rls_for_users.sql @@ -0,0 +1,13 @@ +CREATE POLICY "Enable delete for users based on uuid" ON "public"."trees_adopted" AS permissive + FOR DELETE TO authenticated + USING (((auth.uid ())::text = uuid)); + +CREATE POLICY "Enable delete for users based on user_id" ON "public"."trees_watered" AS permissive + FOR DELETE TO authenticated + USING (((auth.uid ())::text = uuid)); + +CREATE POLICY "Enable update for users based on uuid" ON "public"."trees_watered" AS permissive + FOR UPDATE TO authenticated + USING (((auth.uid ())::text = uuid)) + WITH CHECK (((auth.uid ())::text = uuid)); + From e8f5fd158e773c72d5502af54b92a7bbe8c44df7 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Wed, 22 Mar 2023 13:35:32 +0100 Subject: [PATCH 26/51] test: Remove additional params added from vercel Before validation --- __tests__/get-routes.test.ts | 1 - api/v3/get/[type].ts | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/__tests__/get-routes.test.ts b/__tests__/get-routes.test.ts index 2198cd6c..6fc38336 100644 --- a/__tests__/get-routes.test.ts +++ b/__tests__/get-routes.test.ts @@ -229,7 +229,6 @@ describe("GET v3 routes snapshot tests default responses", () => { }); server.close(); const json = await response.json(); - // console.log(json); expect(response.status).toBe(200); expect(json).toMatchSnapshot(); }); diff --git a/api/v3/get/[type].ts b/api/v3/get/[type].ts index f964f348..9cf90c8f 100644 --- a/api/v3/get/[type].ts +++ b/api/v3/get/[type].ts @@ -42,7 +42,9 @@ export default async function handler( const params = paramsToObject( request.url .replace(`/v3/${method.toLowerCase()}/${type}`, "") + /* FIXME: this is to fix tests not production since the handler does not know the full route in tests */ .replace(`/v3/?type=${type}`, "") + .replace(`/?type=${type}`, "") ); const [paramsAreValid, validationError] = validate(params, getSchemas[type]); if (!paramsAreValid) { From 74735770e563545512f603dbe12067209388febf Mon Sep 17 00:00:00 2001 From: ff6347 Date: Thu, 23 Mar 2023 20:54:56 +0100 Subject: [PATCH 27/51] docs: Remove leftover from conflict --- README.md | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 9ec8bebe..8ea3e2d0 100644 --- a/README.md +++ b/README.md @@ -8,16 +8,19 @@ - [Environments and Variables](#environments-and-variables) - [Auth0 (deprecated)](#auth0-deprecated) - [Vercel](#vercel) - - [Vercel Environment Variables](#vercel-environment-variables) + - [Vercel Environment Variables](#vercel-environment-variables) + - [API Routes](#api-routes) + - [API Authorization](#api-authorization) + - [Tests](#tests) - [Supabase](#supabase) - [Migrations and Types](#migrations-and-types) - [Deployment](#deployment) - [Radolan Harvester](#radolan-harvester) - - [API Routes](#api-routes) - - [API Authorization](#api-authorization) + - [API Routes](#api-routes-1) + - [API Authorization](#api-authorization-1) - [Supabase](#supabase-1) - [Auth0 (deprecated)](#auth0-deprecated-1) - - [Tests](#tests) + - [Tests](#tests-1) - [Contributors ✨](#contributors-) - [Credits](#credits) @@ -81,7 +84,7 @@ Again. Be a smart developer, read https://12factor.net/config, https://github.co ### Auth0 (deprecated) -**!Hint: We still support using Auth0 in this API but will eventually remove it. Using Supabase is preferered.** +**!Hint: We still support using Auth0 in this API but will eventually remove it. Using Supabase is preferred.** Setup your auth0.com account and create a new API. Get your `jwksUri`, `issuer`, `audience`, `client_id` and `client_secret` values and add them to the `.env` file as well. The values for `client_id` and `client_secret` are only needed if you want to run local integration tests and use tools like rest-client, Postman, Insomnia or Paw to obtain a token. This is explained later in this document. @@ -114,10 +117,6 @@ To let these variables take effect you need to deploy your application once more vercel --prod ``` - - -<<<<<<< HEAD -======= ## API Routes There are 3 main routes `/get`, `/post` and `/delete`. @@ -183,7 +182,6 @@ npm test On CI the Supabase is started automagically. See [.github/workflows/tests.yml](.github/workflows/tests.yml) you still need an API on Auth0.com ->>>>>>> 2bb17e65f3e01f12c281c859f2af27b448986479 ## Supabase ### Migrations and Types @@ -378,5 +376,3 @@ This project follows the [all-contributors](https://github.com/all-contributors/ [gdk-supabase]: https://github.com/technologiestiftung/giessdenkiez-de-supabase/ [supabase]: https://supabase.com/ - - From 40ccaec3e7ef8fac9177041fa0b56f8069562a96 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 23 Mar 2023 19:57:16 +0000 Subject: [PATCH 28/51] docs: update README.md --- README.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 2dacb966..2723b14b 100644 --- a/README.md +++ b/README.md @@ -231,14 +231,17 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d - - - - - - - - + + + + + + + + + + +

Fabian MorΓ³n Zirfas

πŸ’» πŸ“–

Fabian

πŸ’» πŸ“–

warenix

πŸ’» πŸ“–

Daniel Sippel

πŸ“–

Sebastian Meier

πŸ’»

Lucas Vogel

πŸ“–
Fabian MorΓ³n Zirfas
Fabian MorΓ³n Zirfas

πŸ’» πŸ“–
Fabian
Fabian

πŸ’» πŸ“–
warenix
warenix

πŸ’» πŸ“–
Daniel Sippel
Daniel Sippel

πŸ“–
Sebastian Meier
Sebastian Meier

πŸ’»
Lucas Vogel
Lucas Vogel

πŸ“–
Dennis Ostendorf
Dennis Ostendorf

πŸ‘€
From cfe4f24148e17ea0cf4cc6686fdaa29c830ff64c Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 23 Mar 2023 19:57:17 +0000 Subject: [PATCH 29/51] docs: update .all-contributorsrc --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index f9559c99..57445a2e 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -66,6 +66,15 @@ "contributions": [ "doc" ] + }, + { + "login": "dnsos", + "name": "Dennis Ostendorf", + "avatar_url": "https://avatars.githubusercontent.com/u/15640196?v=4", + "profile": "https://github.com/dnsos", + "contributions": [ + "review" + ] } ], "contributorsPerLine": 7 From 0af09f4e7fa63d9e5a33f643af9debb0939a50d4 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 23 Mar 2023 19:57:54 +0000 Subject: [PATCH 30/51] docs: update README.md --- README.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 2dacb966..71f109ad 100644 --- a/README.md +++ b/README.md @@ -231,14 +231,17 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d - - - - - - - - + + + + + + + + + + +

Fabian MorΓ³n Zirfas

πŸ’» πŸ“–

Fabian

πŸ’» πŸ“–

warenix

πŸ’» πŸ“–

Daniel Sippel

πŸ“–

Sebastian Meier

πŸ’»

Lucas Vogel

πŸ“–
Fabian MorΓ³n Zirfas
Fabian MorΓ³n Zirfas

πŸ’» πŸ“–
Fabian
Fabian

πŸ’» πŸ“–
warenix
warenix

πŸ’» πŸ“–
Daniel Sippel
Daniel Sippel

πŸ“–
Sebastian Meier
Sebastian Meier

πŸ’»
Lucas Vogel
Lucas Vogel

πŸ“–
Julia Zet
Julia Zet

πŸ‘€
From 9ea6ae6e6b549ebf655e912410c2a736a3ed9345 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 23 Mar 2023 19:57:55 +0000 Subject: [PATCH 31/51] docs: update .all-contributorsrc --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index f9559c99..cfc62651 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -66,6 +66,15 @@ "contributions": [ "doc" ] + }, + { + "login": "julizet", + "name": "Julia Zet", + "avatar_url": "https://avatars.githubusercontent.com/u/52455010?v=4", + "profile": "https://github.com/julizet", + "contributions": [ + "review" + ] } ], "contributorsPerLine": 7 From 315d87e9bc687b8f6ab86b73214ba0b155628825 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Fri, 24 Mar 2023 08:27:08 +0100 Subject: [PATCH 32/51] docs(v3): Update docs to reflect v3 --- README.md | 80 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 56 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 8ea3e2d0..00b27fc9 100644 --- a/README.md +++ b/README.md @@ -8,18 +8,21 @@ - [Environments and Variables](#environments-and-variables) - [Auth0 (deprecated)](#auth0-deprecated) - [Vercel](#vercel) - - [Vercel Environment Variables](#vercel-environment-variables) - - [API Routes](#api-routes) + - [Vercel Environment Variables](#vercel-environment-variables) + - [API Routes /v3](#api-routes-v3) + - [](#) - [API Authorization](#api-authorization) + - [Supabase](#supabase) + - [Auth0 (deprecated)](#auth0-deprecated-1) - [Tests](#tests) - - [Supabase](#supabase) + - [Supabase](#supabase-1) - [Migrations and Types](#migrations-and-types) - [Deployment](#deployment) - [Radolan Harvester](#radolan-harvester) - - [API Routes](#api-routes-1) + - [API Routes](#api-routes) - [API Authorization](#api-authorization-1) - - [Supabase](#supabase-1) - - [Auth0 (deprecated)](#auth0-deprecated-1) + - [Supabase](#supabase-2) + - [Auth0 (deprecated)](#auth0-deprecated-2) - [Tests](#tests-1) - [Contributors ✨](#contributors-) - [Credits](#credits) @@ -106,9 +109,10 @@ vercel env add SUPABASE_ANON_KEY # the max rows allowed to fetch from supabase (default 1000) vercel env add SUPABASE_MAX_ROWS # below are all taken from auth0.com -vercel env add jwksuri -vercel env add audience -vercel env add issuer +# the v3 api does not need them anymore +# vercel env add jwksuri +# vercel env add audience +# vercel env add issuer ``` To let these variables take effect you need to deploy your application once more. @@ -117,9 +121,9 @@ To let these variables take effect you need to deploy your application once more vercel --prod ``` -## API Routes +## API Routes /v3 -There are 3 main routes `/get`, `/post` and `/delete`. +There are 3 main routes `/v3/get`, `/v3/post` and `/v3/delete`. On the `/get` route all actions are controlled by passing URL params. On the `/post` and `/delete` route you will have to work with additional POST bodies. For example to fetch a specific tree run the following command. @@ -133,20 +137,48 @@ You can see all the available routes in the [docs/api.http](./docs/api.http) fil Currently we have these routes -| `/get` | `/post` | `/delete` | -| -------------------- | -------- | ---------- | -| `/byid` | `/adopt` | `/unadopt` | -| `/treesbyids` | `/water` | `/unwater` | -| `/adopted` | | | -| `/istreeadopted` | | | -| `/wateredandadopted` | | | -| `/lastwatered` | | | -| `/wateredbyuser` | | | +| `/v3/get` | `/v3/post` | `/v3/delete` | +| :------------------- | :--------- | :----------- | +| `/byid` | `/adopt` | `/unadopt` | +| `/treesbyids` | `/water` | `/unwater` | +| `/adopted` | | | +| `/istreeadopted` | | | +| `/wateredandadopted` | | | +| `/lastwatered` | | | +| `/wateredbyuser` | | | + +### ### API Authorization Some of the request will need an authorization header. You can obtain a token by making a request to your auth0 token issuer. +### Supabase + +You can sign up with the request below. You will get an access token to use in your requests. + +```bash +curl --request POST \ + --url http://localhost:54321/auth/v1/signup \ + --header 'apikey: ' \ + --header 'content-type: application/json' \ + --header 'user-agent: vscode-restclient' \ + --data '{"email": "someone@email.com","password": "1234567890"}' +``` + +```bash +curl --request POST \ + --url http://localhost:8080/post/adopt \ + --header 'authorization: Bearer ' \ + --header 'content-type: application/json' \ + --data '{"tree_id":"_01","uuid": ""}' + +``` + +The user id will be removed in future versions since the supabase SDK can get the user id from the access token and each token is bound to a specific user. + +#### Auth0 (deprecated) + ```bash curl --request POST \ --url https://your-tenant.eu.auth0.com/oauth/token \ @@ -159,10 +191,10 @@ This will respond with an `access_token`. Use it to make authenticated requests. ```bash curl --request POST \ - --url http://localhost:3000/post \ + --url http://localhost:8080/post/adopt \ --header 'authorization: Bearer ' \ --header 'content-type: application/json' \ - --data '{"queryType":"adopt","tree_id":"_01","uuid": "auth0|123"}' + --data '{"tree_id":"_01","uuid": "auth0|123"}' ``` Take a look into [docs/api.http](./docs/api.http). The requests in this file can be run with the VSCode extension [REST Client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client). @@ -237,7 +269,7 @@ On the `/get` route all actions are controlled by passing URL params. On the `/p ```bash curl --request GET \ - --url 'http://localhost:3000/get/byid&id=_123456789' \ + --url 'http://localhost:8080/get/byid&id=_123456789' \ ``` @@ -299,7 +331,7 @@ This will respond with an `access_token`. Use it to make authenticated requests. ```bash curl --request POST \ - --url http://localhost:3000/post \ + --url http://localhost:8080/post \ --header 'authorization: Bearer ' \ --header 'content-type: application/json' \ --data '{"queryType":"adopt","tree_id":"_01","uuid": "auth0|123"}' From acbf24f74479295fa806bf6ccfc9162883241bff Mon Sep 17 00:00:00 2001 From: ff6347 Date: Wed, 29 Mar 2023 21:04:24 +0200 Subject: [PATCH 33/51] feat(profiles): Add constraint to unsername Make unique Make min 3 and max 50 (in the frontend we use only 20) Add trigger function on insert and update to add short uuid to username if we add an already existing name. This is needed so we can use the first part of the email on signup. The other functions are here because migra kept adding them. Maybe it will finally shut up about them when I add them now --- .../20230329185408_username_constraints.sql | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 supabase/migrations/20230329185408_username_constraints.sql diff --git a/supabase/migrations/20230329185408_username_constraints.sql b/supabase/migrations/20230329185408_username_constraints.sql new file mode 100644 index 00000000..3fbeaffc --- /dev/null +++ b/supabase/migrations/20230329185408_username_constraints.sql @@ -0,0 +1,88 @@ +CREATE UNIQUE INDEX username_unique_constraint ON public.profiles USING btree (username); + +ALTER TABLE "public"."profiles" + ADD CONSTRAINT "username_length_constraint" CHECK (((length(username) >= 3) AND (length(username) <= 50))) NOT valid; + +ALTER TABLE "public"."profiles" validate CONSTRAINT "username_length_constraint"; + +ALTER TABLE "public"."profiles" + ADD CONSTRAINT "username_unique_constraint" UNIQUE USING INDEX "username_unique_constraint"; + +SET check_function_bodies = OFF; + +CREATE OR REPLACE FUNCTION public.username_append_uuid () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $function$ +BEGIN + IF EXISTS ( + SELECT + 1 + FROM + public.profiles + WHERE + username = NEW.username) THEN + NEW.username := NEW.username || '-' || TRIM(BOTH FROM SUBSTRING( + LEFT (CAST(uuid_generate_v4 () AS text), 8), 1, 6)); +END IF; + RETURN NEW; +END; +$function$; + +CREATE TRIGGER username_check_trigger + BEFORE INSERT OR UPDATE ON public.profiles + FOR EACH ROW + EXECUTE FUNCTION username_append_uuid (); + +CREATE OR REPLACE FUNCTION public.delete_user () + RETURNS TRIGGER + LANGUAGE plpgsql + SECURITY DEFINER + AS $function$ +DECLARE + row_count int; +BEGIN + DELETE FROM public.profiles p + WHERE p.id = OLD.id; + IF found THEN + GET DIAGNOSTICS row_count = ROW_COUNT; + RAISE NOTICE 'DELETEd % row(s) FROM profiles', row_count; + END IF; + UPDATE + trees_watered + SET + uuid = NULL, + username = NULL + WHERE + uuid = OLD.id::text; + DELETE FROM trees_adopted ta + WHERE ta.uuid = OLD.id::text; + RETURN OLD; +END; +$function$; + +CREATE OR REPLACE FUNCTION public.remove_account () + RETURNS void + LANGUAGE sql + SECURITY DEFINER + AS $function$ + DELETE FROM auth.users + WHERE id = auth.uid (); + +$function$; + +CREATE OR REPLACE FUNCTION public.update_username_on_trees_watered () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $function$ +BEGIN + UPDATE + trees_watered + SET + username = NEW.username + WHERE + uuid = OLD.id::text; + RETURN NEW; +END; +$function$; + From 0980e2cfc4f1e3c368475d4a06e96f5db25c88c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Mor=C3=B3n=20Zirfas?= Date: Fri, 14 Apr 2023 11:49:18 +0200 Subject: [PATCH 34/51] Update tests.yml We use a semantic release action version that will stop working soon see https://github.blog/changelog/2022-***0-***-github-actions-deprecating-save-state-and-set-output-commands/ --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fd08ec9b..ba38e471 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -68,7 +68,7 @@ jobs: node-version-file: ".nvmrc" - run: npm ci - id: semantic-release - uses: cycjimmy/semantic-release-action@v2 + uses: cycjimmy/semantic-release-action@v3 with: semantic_version: 18 env: From 74034e57177a7a84ed58c42dfacddabd9f2f6e96 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Wed, 19 Apr 2023 17:24:11 +0200 Subject: [PATCH 35/51] fix(profile): Make usernames case insensitive This is done by using the citext extensions. Once we have imported all the users from auth0 we will remove the uuid trigger function and only have the DB reject duplicate names --- ...30419150648_unique_username_case_insensitive.sql | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 supabase/migrations/20230419150648_unique_username_case_insensitive.sql diff --git a/supabase/migrations/20230419150648_unique_username_case_insensitive.sql b/supabase/migrations/20230419150648_unique_username_case_insensitive.sql new file mode 100644 index 00000000..4c96effc --- /dev/null +++ b/supabase/migrations/20230419150648_unique_username_case_insensitive.sql @@ -0,0 +1,13 @@ +CREATE EXTENSION IF NOT EXISTS "citext" WITH SCHEMA "extensions"; + +ALTER TABLE "public"."profiles" + DROP CONSTRAINT "username_length_constraint"; + +ALTER TABLE "public"."profiles" + ALTER COLUMN "username" SET data TYPE citext USING "username"::citext; + +ALTER TABLE "public"."profiles" + ADD CONSTRAINT "username_length_constraint" CHECK (((length((username)::text) >= 3) AND (length((username)::text) <= 50))) NOT valid; + +ALTER TABLE "public"."profiles" validate CONSTRAINT "username_length_constraint"; + From e9a3c8013dd1239f092233c5356f4ce235d95480 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Wed, 19 Apr 2023 20:08:27 +0200 Subject: [PATCH 36/51] test(utils): Update test utils for schema tests --- __test-utils/postgres.ts | 7 ++++--- __test-utils/req-test-token.ts | 20 +++++++++++++------- __test-utils/supabase.ts | 16 ++++++++++++++++ 3 files changed, 33 insertions(+), 10 deletions(-) create mode 100644 __test-utils/supabase.ts diff --git a/__test-utils/postgres.ts b/__test-utils/postgres.ts index fedd3b3b..2a5e19a6 100644 --- a/__test-utils/postgres.ts +++ b/__test-utils/postgres.ts @@ -16,15 +16,16 @@ export async function truncateTreesAdopted() { sql.end(); } -export async function createWateredTrees() { +export async function createWateredTrees(userId?: string, userName?: string) { const sql = postgres(url); + const randomText = sql`md5(random()::text)`; await sql` INSERT INTO trees_watered (uuid, amount, timestamp, username, tree_id) SELECT - md5(random()::text), + ${userId ? userId : sql`extensions.uuid_generate_v4()::text`}, random() * 10, NOW() - (random() * INTERVAL '7 days'), - md5(random()::text), + ${userName ? userName : randomText}, id FROM trees diff --git a/__test-utils/req-test-token.ts b/__test-utils/req-test-token.ts index dc918b32..7dfcc5e5 100644 --- a/__test-utils/req-test-token.ts +++ b/__test-utils/req-test-token.ts @@ -1,9 +1,10 @@ +import { SignupResponse } from "../_types/user"; +import { SUPABASE_ANON_KEY, SUPABASE_URL } from "./supabase"; const issuer = process.env.issuer || ""; const client_id = process.env.client_id || ""; const client_secret = process.env.client_secret || ""; const audience = process.env.audience || ""; -const SUPABASE_URL = process.env.SUPABASE_URL || "http://localhost:54321"; -const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || ""; + export async function requestAuth0TestToken() { const response = await fetch(`${issuer}oauth/token`, { method: "POST", @@ -47,14 +48,15 @@ export async function requestSupabaseTestToken( const json = await response.text(); throw new Error(`Could not get test token, ${json}`); } - const json = (await response.json()) as { - access_token: string; - user: { id: string }; - }; + const json = (await response.json()) as SignupResponse; return json.access_token; } -export async function createSupabaseUser(email: string, password: string) { +export async function createSupabaseUser( + email: string, + password: string, + opts?: { returnFullUser: boolean } +) { const response = await fetch(`${SUPABASE_URL}/auth/v1/signup`, { method: "POST", headers: { @@ -67,6 +69,7 @@ export async function createSupabaseUser(email: string, password: string) { }), }); if (!response.ok) { + console.log(response.status); const json = await response.text(); throw new Error(`Could not create test user, ${json}`); } @@ -74,5 +77,8 @@ export async function createSupabaseUser(email: string, password: string) { access_token: string; user: { id: string }; }; + if (opts?.returnFullUser) { + return json; + } return json.access_token; } diff --git a/__test-utils/supabase.ts b/__test-utils/supabase.ts new file mode 100644 index 00000000..ffea419c --- /dev/null +++ b/__test-utils/supabase.ts @@ -0,0 +1,16 @@ +import { createClient } from "@supabase/supabase-js"; +import { Database } from "../_types/database"; +export const SUPABASE_URL = + process.env.SUPABASE_URL || "http://localhost:54321"; +export const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || ""; +export const SUPABASE_SERVICE_ROLE_KEY = + process.env.SUPABASE_SERVICE_ROLE_KEY || ""; + +export const supabaseAnonClient = createClient( + SUPABASE_URL, + SUPABASE_ANON_KEY +); +export const supabaseServiceRoleClient = createClient( + SUPABASE_URL, + SUPABASE_SERVICE_ROLE_KEY +); From f2c3f3e9491784bc4ef89526eadff6fa6f27cb5a Mon Sep 17 00:00:00 2001 From: ff6347 Date: Wed, 19 Apr 2023 20:09:12 +0200 Subject: [PATCH 37/51] fix(schema): Update outdated types --- _types/database.ts | 857 ++++++++++++++++++++++++--------------------- 1 file changed, 454 insertions(+), 403 deletions(-) diff --git a/_types/database.ts b/_types/database.ts index a4e7c8d3..5c6cbd13 100644 --- a/_types/database.ts +++ b/_types/database.ts @@ -1,407 +1,458 @@ export type Json = - | string - | number - | boolean - | null - | { [key: string]: Json } - | Json[]; + | string + | number + | boolean + | null + | { [key: string]: Json } + | Json[] export interface Database { - graphql_public: { - Tables: { - [_ in never]: never; - }; - Views: { - [_ in never]: never; - }; - Functions: { - graphql: { - Args: { - operationName: string; - query: string; - variables: Json; - extensions: Json; - }; - Returns: Json; - }; - }; - Enums: { - [_ in never]: never; - }; - }; - public: { - Tables: { - profiles: { - Row: { - id: string; - username: string | null; - }; - Insert: { - id: string; - username?: string | null; - }; - Update: { - id?: string; - username?: string | null; - }; - }; - radolan_data: { - Row: { - geom_id: number | null; - id: number; - measured_at: string | null; - value: number | null; - }; - Insert: { - geom_id?: number | null; - id?: number; - measured_at?: string | null; - value?: number | null; - }; - Update: { - geom_id?: number | null; - id?: number; - measured_at?: string | null; - value?: number | null; - }; - }; - radolan_geometry: { - Row: { - centroid: unknown | null; - geometry: unknown | null; - id: number; - }; - Insert: { - centroid?: unknown | null; - geometry?: unknown | null; - id?: number; - }; - Update: { - centroid?: unknown | null; - geometry?: unknown | null; - id?: number; - }; - }; - radolan_harvester: { - Row: { - collection_date: string | null; - end_date: string | null; - id: number; - start_date: string | null; - }; - Insert: { - collection_date?: string | null; - end_date?: string | null; - id?: number; - start_date?: string | null; - }; - Update: { - collection_date?: string | null; - end_date?: string | null; - id?: number; - start_date?: string | null; - }; - }; - radolan_temp: { - Row: { - geometry: unknown | null; - id: number; - measured_at: string | null; - value: number | null; - }; - Insert: { - geometry?: unknown | null; - id?: number; - measured_at?: string | null; - value?: number | null; - }; - Update: { - geometry?: unknown | null; - id?: number; - measured_at?: string | null; - value?: number | null; - }; - }; - trees: { - Row: { - adopted: string | null; - artbot: string | null; - artdtsch: string | null; - baumhoehe: string | null; - bezirk: string | null; - caretaker: string | null; - eigentuemer: string | null; - gattung: string | null; - gattungdeutsch: string | null; - geom: unknown | null; - gmlid: string | null; - hausnr: string | null; - id: string; - kennzeich: string | null; - kronedurch: string | null; - lat: string | null; - lng: string | null; - pflanzjahr: number | null; - radolan_days: number[] | null; - radolan_sum: number | null; - stammumfg: string | null; - standalter: string | null; - standortnr: string | null; - strname: string | null; - type: string | null; - watered: string | null; - zusatz: string | null; - }; - Insert: { - adopted?: string | null; - artbot?: string | null; - artdtsch?: string | null; - baumhoehe?: string | null; - bezirk?: string | null; - caretaker?: string | null; - eigentuemer?: string | null; - gattung?: string | null; - gattungdeutsch?: string | null; - geom?: unknown | null; - gmlid?: string | null; - hausnr?: string | null; - id: string; - kennzeich?: string | null; - kronedurch?: string | null; - lat?: string | null; - lng?: string | null; - pflanzjahr?: number | null; - radolan_days?: number[] | null; - radolan_sum?: number | null; - stammumfg?: string | null; - standalter?: string | null; - standortnr?: string | null; - strname?: string | null; - type?: string | null; - watered?: string | null; - zusatz?: string | null; - }; - Update: { - adopted?: string | null; - artbot?: string | null; - artdtsch?: string | null; - baumhoehe?: string | null; - bezirk?: string | null; - caretaker?: string | null; - eigentuemer?: string | null; - gattung?: string | null; - gattungdeutsch?: string | null; - geom?: unknown | null; - gmlid?: string | null; - hausnr?: string | null; - id?: string; - kennzeich?: string | null; - kronedurch?: string | null; - lat?: string | null; - lng?: string | null; - pflanzjahr?: number | null; - radolan_days?: number[] | null; - radolan_sum?: number | null; - stammumfg?: string | null; - standalter?: string | null; - standortnr?: string | null; - strname?: string | null; - type?: string | null; - watered?: string | null; - zusatz?: string | null; - }; - }; - trees_adopted: { - Row: { - id: number; - tree_id: string; - uuid: string | null; - }; - Insert: { - id?: number; - tree_id: string; - uuid?: string | null; - }; - Update: { - id?: number; - tree_id?: string; - uuid?: string | null; - }; - }; - trees_watered: { - Row: { - amount: number; - id: number; - time: string | null; - timestamp: string; - tree_id: string; - username: string | null; - uuid: string | null; - }; - Insert: { - amount: number; - id?: number; - time?: string | null; - timestamp: string; - tree_id: string; - username?: string | null; - uuid?: string | null; - }; - Update: { - amount?: number; - id?: number; - time?: string | null; - timestamp?: string; - tree_id?: string; - username?: string | null; - uuid?: string | null; - }; - }; - }; - Views: { - [_ in never]: never; - }; - Functions: { - count_by_age: { - Args: { start_year: number; end_year: number }; - Returns: number; - }; - get_watered_and_adopted: { - Args: Record; - Returns: { tree_id: string; adopted: number; watered: number }[]; - }; - }; - Enums: { - [_ in never]: never; - }; - }; - storage: { - Tables: { - buckets: { - Row: { - created_at: string | null; - id: string; - name: string; - owner: string | null; - public: boolean | null; - updated_at: string | null; - }; - Insert: { - created_at?: string | null; - id: string; - name: string; - owner?: string | null; - public?: boolean | null; - updated_at?: string | null; - }; - Update: { - created_at?: string | null; - id?: string; - name?: string; - owner?: string | null; - public?: boolean | null; - updated_at?: string | null; - }; - }; - migrations: { - Row: { - executed_at: string | null; - hash: string; - id: number; - name: string; - }; - Insert: { - executed_at?: string | null; - hash: string; - id: number; - name: string; - }; - Update: { - executed_at?: string | null; - hash?: string; - id?: number; - name?: string; - }; - }; - objects: { - Row: { - bucket_id: string | null; - created_at: string | null; - id: string; - last_accessed_at: string | null; - metadata: Json | null; - name: string | null; - owner: string | null; - path_tokens: string[] | null; - updated_at: string | null; - }; - Insert: { - bucket_id?: string | null; - created_at?: string | null; - id?: string; - last_accessed_at?: string | null; - metadata?: Json | null; - name?: string | null; - owner?: string | null; - path_tokens?: string[] | null; - updated_at?: string | null; - }; - Update: { - bucket_id?: string | null; - created_at?: string | null; - id?: string; - last_accessed_at?: string | null; - metadata?: Json | null; - name?: string | null; - owner?: string | null; - path_tokens?: string[] | null; - updated_at?: string | null; - }; - }; - }; - Views: { - [_ in never]: never; - }; - Functions: { - extension: { - Args: { name: string }; - Returns: string; - }; - filename: { - Args: { name: string }; - Returns: string; - }; - foldername: { - Args: { name: string }; - Returns: string[]; - }; - get_size_by_bucket: { - Args: Record; - Returns: { size: number; bucket_id: string }[]; - }; - search: { - Args: { - prefix: string; - bucketname: string; - limits: number; - levels: number; - offsets: number; - search: string; - sortcolumn: string; - sortorder: string; - }; - Returns: { - name: string; - id: string; - updated_at: string; - created_at: string; - last_accessed_at: string; - metadata: Json; - }[]; - }; - }; - Enums: { - [_ in never]: never; - }; - }; + graphql_public: { + Tables: { + [_ in never]: never + } + Views: { + [_ in never]: never + } + Functions: { + graphql: { + Args: { + operationName?: string + query?: string + variables?: Json + extensions?: Json + } + Returns: Json + } + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } + public: { + Tables: { + profiles: { + Row: { + id: string + username: string | null + } + Insert: { + id: string + username?: string | null + } + Update: { + id?: string + username?: string | null + } + } + radolan_data: { + Row: { + geom_id: number | null + id: number + measured_at: string | null + value: number | null + } + Insert: { + geom_id?: number | null + id?: number + measured_at?: string | null + value?: number | null + } + Update: { + geom_id?: number | null + id?: number + measured_at?: string | null + value?: number | null + } + } + radolan_geometry: { + Row: { + centroid: unknown | null + geometry: unknown | null + id: number + } + Insert: { + centroid?: unknown | null + geometry?: unknown | null + id?: number + } + Update: { + centroid?: unknown | null + geometry?: unknown | null + id?: number + } + } + radolan_harvester: { + Row: { + collection_date: string | null + end_date: string | null + id: number + start_date: string | null + } + Insert: { + collection_date?: string | null + end_date?: string | null + id?: number + start_date?: string | null + } + Update: { + collection_date?: string | null + end_date?: string | null + id?: number + start_date?: string | null + } + } + radolan_temp: { + Row: { + geometry: unknown | null + id: number + measured_at: string | null + value: number | null + } + Insert: { + geometry?: unknown | null + id?: number + measured_at?: string | null + value?: number | null + } + Update: { + geometry?: unknown | null + id?: number + measured_at?: string | null + value?: number | null + } + } + trees: { + Row: { + adopted: string | null + artbot: string | null + artdtsch: string | null + baumhoehe: string | null + bezirk: string | null + caretaker: string | null + eigentuemer: string | null + gattung: string | null + gattungdeutsch: string | null + geom: unknown | null + gmlid: string | null + hausnr: string | null + id: string + kennzeich: string | null + kronedurch: string | null + lat: string | null + lng: string | null + pflanzjahr: number | null + radolan_days: number[] | null + radolan_sum: number | null + stammumfg: string | null + standalter: string | null + standortnr: string | null + strname: string | null + type: string | null + watered: string | null + zusatz: string | null + } + Insert: { + adopted?: string | null + artbot?: string | null + artdtsch?: string | null + baumhoehe?: string | null + bezirk?: string | null + caretaker?: string | null + eigentuemer?: string | null + gattung?: string | null + gattungdeutsch?: string | null + geom?: unknown | null + gmlid?: string | null + hausnr?: string | null + id: string + kennzeich?: string | null + kronedurch?: string | null + lat?: string | null + lng?: string | null + pflanzjahr?: number | null + radolan_days?: number[] | null + radolan_sum?: number | null + stammumfg?: string | null + standalter?: string | null + standortnr?: string | null + strname?: string | null + type?: string | null + watered?: string | null + zusatz?: string | null + } + Update: { + adopted?: string | null + artbot?: string | null + artdtsch?: string | null + baumhoehe?: string | null + bezirk?: string | null + caretaker?: string | null + eigentuemer?: string | null + gattung?: string | null + gattungdeutsch?: string | null + geom?: unknown | null + gmlid?: string | null + hausnr?: string | null + id?: string + kennzeich?: string | null + kronedurch?: string | null + lat?: string | null + lng?: string | null + pflanzjahr?: number | null + radolan_days?: number[] | null + radolan_sum?: number | null + stammumfg?: string | null + standalter?: string | null + standortnr?: string | null + strname?: string | null + type?: string | null + watered?: string | null + zusatz?: string | null + } + } + trees_adopted: { + Row: { + id: number + tree_id: string + uuid: string | null + } + Insert: { + id?: number + tree_id: string + uuid?: string | null + } + Update: { + id?: number + tree_id?: string + uuid?: string | null + } + } + trees_watered: { + Row: { + amount: number + id: number + time: string | null + timestamp: string + tree_id: string + username: string | null + uuid: string | null + } + Insert: { + amount: number + id?: number + time?: string | null + timestamp: string + tree_id: string + username?: string | null + uuid?: string | null + } + Update: { + amount?: number + id?: number + time?: string | null + timestamp?: string + tree_id?: string + username?: string | null + uuid?: string | null + } + } + } + Views: { + [_ in never]: never + } + Functions: { + count_by_age: { + Args: { + start_year: number + end_year: number + } + Returns: number + } + get_watered_and_adopted: { + Args: Record + Returns: { + tree_id: string + adopted: number + watered: number + }[] + } + remove_account: { + Args: Record + Returns: undefined + } + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } + storage: { + Tables: { + buckets: { + Row: { + allowed_mime_types: string[] | null + avif_autodetection: boolean | null + created_at: string | null + file_size_limit: number | null + id: string + name: string + owner: string | null + public: boolean | null + updated_at: string | null + } + Insert: { + allowed_mime_types?: string[] | null + avif_autodetection?: boolean | null + created_at?: string | null + file_size_limit?: number | null + id: string + name: string + owner?: string | null + public?: boolean | null + updated_at?: string | null + } + Update: { + allowed_mime_types?: string[] | null + avif_autodetection?: boolean | null + created_at?: string | null + file_size_limit?: number | null + id?: string + name?: string + owner?: string | null + public?: boolean | null + updated_at?: string | null + } + } + migrations: { + Row: { + executed_at: string | null + hash: string + id: number + name: string + } + Insert: { + executed_at?: string | null + hash: string + id: number + name: string + } + Update: { + executed_at?: string | null + hash?: string + id?: number + name?: string + } + } + objects: { + Row: { + bucket_id: string | null + created_at: string | null + id: string + last_accessed_at: string | null + metadata: Json | null + name: string | null + owner: string | null + path_tokens: string[] | null + updated_at: string | null + version: string | null + } + Insert: { + bucket_id?: string | null + created_at?: string | null + id?: string + last_accessed_at?: string | null + metadata?: Json | null + name?: string | null + owner?: string | null + path_tokens?: string[] | null + updated_at?: string | null + version?: string | null + } + Update: { + bucket_id?: string | null + created_at?: string | null + id?: string + last_accessed_at?: string | null + metadata?: Json | null + name?: string | null + owner?: string | null + path_tokens?: string[] | null + updated_at?: string | null + version?: string | null + } + } + } + Views: { + [_ in never]: never + } + Functions: { + can_insert_object: { + Args: { + bucketid: string + name: string + owner: string + metadata: Json + } + Returns: undefined + } + extension: { + Args: { + name: string + } + Returns: string + } + filename: { + Args: { + name: string + } + Returns: string + } + foldername: { + Args: { + name: string + } + Returns: string[] + } + get_size_by_bucket: { + Args: Record + Returns: { + size: number + bucket_id: string + }[] + } + search: { + Args: { + prefix: string + bucketname: string + limits?: number + levels?: number + offsets?: number + search?: string + sortcolumn?: string + sortorder?: string + } + Returns: { + name: string + id: string + updated_at: string + created_at: string + last_accessed_at: string + metadata: Json + }[] + } + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } } + From be85f2bd7dcac3e4c7beed8ec846828dd09d20a2 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Wed, 19 Apr 2023 20:09:19 +0200 Subject: [PATCH 38/51] test(utils): Update test utils for schema tests --- _types/user.ts | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 _types/user.ts diff --git a/_types/user.ts b/_types/user.ts new file mode 100644 index 00000000..c3ad19e9 --- /dev/null +++ b/_types/user.ts @@ -0,0 +1,38 @@ +export interface SignupResponse { + access_token: string; + token_type: string; + expires_in: number; + refresh_token: string; + user: User; +} +export interface User { + id: string; + aud: string; + role: string; + email: string; + email_confirmed_at: string; + phone: string; + last_sign_in_at: string; + app_metadata: AppMetadata; + user_metadata: Record; + identities?: IdentitiesEntity[] | null; + created_at: string; + updated_at: string; +} +export interface AppMetadata { + provider: string; + providers?: string[] | null; +} +export interface IdentitiesEntity { + id: string; + user_id: string; + identity_data: IdentityData; + provider: string; + last_sign_in_at: string; + created_at: string; + updated_at: string; +} +export interface IdentityData { + email: string; + sub: string; +} From 6f69042eaa63146e04fd4d6cb71e8f982e396270 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Wed, 19 Apr 2023 20:24:29 +0200 Subject: [PATCH 39/51] fix(schema): Update call to uuid generate The function was not able to find the uuid_generate_v4 function --- ...230419165714_fix_username_uuid_trigger.sql | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 supabase/migrations/20230419165714_fix_username_uuid_trigger.sql diff --git a/supabase/migrations/20230419165714_fix_username_uuid_trigger.sql b/supabase/migrations/20230419165714_fix_username_uuid_trigger.sql new file mode 100644 index 00000000..ba642211 --- /dev/null +++ b/supabase/migrations/20230419165714_fix_username_uuid_trigger.sql @@ -0,0 +1,21 @@ +SET check_function_bodies = OFF; + +CREATE OR REPLACE FUNCTION public.username_append_uuid() + RETURNS TRIGGER + LANGUAGE plpgsql + AS $function$ +BEGIN + IF EXISTS( + SELECT + 1 + FROM + public.profiles + WHERE + username = NEW.username) THEN + NEW.username := NEW.username || '-' || TRIM(BOTH FROM SUBSTRING( + LEFT(extensions.uuid_generate_v4()::text, 8), 1, 6)); +END IF; + RETURN NEW; +END; +$function$; + From 28110dd850e9000deadf435e78688443ca23d2cb Mon Sep 17 00:00:00 2001 From: ff6347 Date: Wed, 19 Apr 2023 20:24:52 +0200 Subject: [PATCH 40/51] chore(schema): Remove logging from delete function --- ...0230419180719_chore_make_delete_silent.sql | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 supabase/migrations/20230419180719_chore_make_delete_silent.sql diff --git a/supabase/migrations/20230419180719_chore_make_delete_silent.sql b/supabase/migrations/20230419180719_chore_make_delete_silent.sql new file mode 100644 index 00000000..1aac87c3 --- /dev/null +++ b/supabase/migrations/20230419180719_chore_make_delete_silent.sql @@ -0,0 +1,31 @@ +set check_function_bodies = off; + +CREATE OR REPLACE FUNCTION public.delete_user() + RETURNS trigger + LANGUAGE plpgsql + SECURITY DEFINER +AS $function$ +DECLARE + row_count int; +BEGIN + DELETE FROM public.profiles p + WHERE p.id = OLD.id; +-- IF found THEN +-- GET DIAGNOSTICS row_count = ROW_COUNT; +-- RAISE NOTICE 'DELETEd % row(s) FROM profiles', row_count; +-- END IF; + UPDATE + trees_watered + SET + uuid = NULL, + username = NULL + WHERE + uuid = OLD.id::text; + DELETE FROM trees_adopted ta + WHERE ta.uuid = OLD.id::text; + RETURN OLD; +END; +$function$ +; + + From c238268adcc9a5841f43433f6cfc3b8118994a72 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Wed, 19 Apr 2023 20:27:52 +0200 Subject: [PATCH 41/51] test(schema): Verify functions in schema These test check some beahaviour in the schema - adding uuid to username on duplicates (needed 4 migration from auth0) - cascading removal of users data from the database - change of username on trees_watered on change of username in profile --- __tests__/schema.test.ts | 187 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 __tests__/schema.test.ts diff --git a/__tests__/schema.test.ts b/__tests__/schema.test.ts new file mode 100644 index 00000000..91463d48 --- /dev/null +++ b/__tests__/schema.test.ts @@ -0,0 +1,187 @@ +import { + deleteSupabaseUser, + truncateTreesWaterd, +} from "../__test-utils/postgres"; +import { + SUPABASE_ANON_KEY, + SUPABASE_URL, + supabaseAnonClient, + supabaseServiceRoleClient, +} from "../__test-utils/supabase"; +describe("misc test testing the schema function of the database", () => { + test("inserting an existing username should alter the new name and add a uuid at end", async () => { + const email1 = "someone@email.com"; + const email2 = "someone@foo.com"; + await deleteSupabaseUser(email1); + await deleteSupabaseUser(email2); + const password = "12345678"; + const { data: user1, error } = await supabaseAnonClient.auth.signUp({ + email: email1, + password: password, + }); + const { data: user2, error: error2 } = await supabaseAnonClient.auth.signUp( + { + email: email2, + password: password, + } + ); + expect(error).toBeNull(); + expect(user1).toBeDefined(); + expect(error2).toBeNull(); + expect(user2).toBeDefined(); + + const { data: users, error: usersError } = await supabaseAnonClient + .from("profiles") + .select("*") + .in("id", [user1?.user?.id, user2?.user?.id]); + + expect(usersError).toBeNull(); + expect(users).toHaveLength(2); + expect(users?.[0].username).toBe("someone"); + expect(users?.[1].username).not.toBe("someone"); + expect(users?.[1].username).toContain("someone-"); + expect(users?.[1].username).toMatch(/^someone-[a-zA-Z0-9]{6}$/); + await deleteSupabaseUser(email1); + await deleteSupabaseUser(email2); + }); + + test("a user should be able to remove its account and his associated data", async () => { + const email = "user@email.com"; + await deleteSupabaseUser(email); // clean up before running + const { data, error } = await supabaseAnonClient.auth.signUp({ + email: email, + password: "12345678", + }); + expect(error).toBeNull(); + expect(data).toBeDefined(); + const { data: trees, error: treesError } = await supabaseAnonClient + .from("trees") + .select("*") + .limit(10); + expect(treesError).toBeNull(); + expect(trees).toHaveLength(10); + + const { data: adoptedTrees, error: adoptedTreesError } = + await supabaseServiceRoleClient + .from("trees_adopted") + .insert( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + trees!.map((tree) => ({ + uuid: data.user?.id, + tree_id: tree.id, + })) + ) + .select("*"); + expect(adoptedTreesError).toBeNull(); + expect(adoptedTrees).toHaveLength(10); + const { data: userTrees, error: userTreesError } = + await supabaseServiceRoleClient + .from("trees_watered") + .insert( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + trees!.map((tree) => ({ + uuid: data.user?.id, + amount: 1, + timestamp: new Date().toISOString(), + username: "user", + tree_id: tree.id, + })) + ) + .select("*"); + expect(userTreesError).toBeNull(); + expect(userTrees).toHaveLength(10); + + // since wa can not pass the token to our supabase client, we need to use fetch directly + const response = await fetch(`${SUPABASE_URL}/rest/v1/rpc/remove_account`, { + method: "POST", + headers: { + apikey: SUPABASE_ANON_KEY, + "Content-Type": "application/json", + Authorization: `Bearer ${data.session?.access_token}`, + }, + }); + expect(response.ok).toBeTruthy(); + expect(response.status).toBe(204); + const { data: treesAfter, error: treesAfterError } = + await supabaseAnonClient + .from("trees_watered") + .select("*") + .eq("uuid", data.user?.id); + expect(treesAfterError).toBeNull(); + expect(treesAfter).toHaveLength(0); + + const { data: adoptedTreesAfter, error: adoptedTreesAfterError } = + await supabaseAnonClient + .from("trees_adopted") + .select("*") + .eq("uuid", data.user?.id); + expect(adoptedTreesAfterError).toBeNull(); + expect(adoptedTreesAfter).toHaveLength(0); + await truncateTreesWaterd(); + }); + + test("if a user changes his username all the usernames on the trees_watered table should change too", async () => { + const email = "foo@bar.com"; + await deleteSupabaseUser(email); + await truncateTreesWaterd(); + const { data, error } = await supabaseAnonClient.auth.signUp({ + email: email, + password: "12345678", + }); + expect(error).toBeNull(); + expect(data).toBeDefined(); + const { data: trees, error: treesError } = await supabaseAnonClient + .from("trees") + .select("*") + .limit(10); + expect(treesError).toBeNull(); + expect(trees).toHaveLength(10); + + const { data: adoptedTrees, error: adoptedTreesError } = + await supabaseServiceRoleClient + .from("trees_watered") + .insert( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + trees!.map((tree) => ({ + uuid: data.user?.id, + tree_id: tree.id, + amount: 1, + timestamp: new Date().toISOString(), + username: "foo", + })) + ) + .select("*"); + expect(adoptedTreesError).toBeNull(); + expect(adoptedTrees).toHaveLength(10); + + // since we cant pass our access token to change our username to our anon client we use fetch directly + const changeResponse = await fetch( + `${SUPABASE_URL}/rest/v1/profiles?id=eq.${data?.user?.id}`, + { + method: "PATCH", + headers: { + apikey: SUPABASE_ANON_KEY, + "Content-Type": "application/json", + Authorization: `Bearer ${data.session?.access_token}`, + }, + body: JSON.stringify({ + username: "bar", + }), + } + ); + + expect(changeResponse.ok).toBeTruthy(); + expect(changeResponse.status).toBe(204); + + const { data: treesAfter, error: treesAfterError } = + await supabaseServiceRoleClient + .from("trees_watered") + .select("*") + .eq("username", "bar"); + + expect(treesAfterError).toBeNull(); + expect(treesAfter).toHaveLength(10); + await deleteSupabaseUser(email); + await truncateTreesWaterd(); + }); +}); From 0c2096f1a3afef01339cebc3887b58f05030537e Mon Sep 17 00:00:00 2001 From: ff6347 Date: Wed, 19 Apr 2023 20:28:16 +0200 Subject: [PATCH 42/51] test: Boilerplate of schema tests. WIP --- supabase/tests/database/unique_names.test.sql | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 supabase/tests/database/unique_names.test.sql diff --git a/supabase/tests/database/unique_names.test.sql b/supabase/tests/database/unique_names.test.sql new file mode 100644 index 00000000..2f97e1ba --- /dev/null +++ b/supabase/tests/database/unique_names.test.sql @@ -0,0 +1,6 @@ +begin; +select plan(1); -- only one statement to run + + +SELECT * FROM finish(); +rollback; From f0f3e78a14e17844b78445c756c43b32451b9f99 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Wed, 19 Apr 2023 20:28:44 +0200 Subject: [PATCH 43/51] chore: Used extension on this project --- .vscode/extensions.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 1600801f..07132bed 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,7 @@ { - "recommendations": ["humao.rest-client", "mikestead.dotenv"] -} + "recommendations": [ + "humao.rest-client", + "mikestead.dotenv", + "mkhl.direnv" + ] +} \ No newline at end of file From df78e61cc1e7bca4ddcb4ba751e5aef6d8379414 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Thu, 20 Apr 2023 14:37:25 +0200 Subject: [PATCH 44/51] test(schema): Make number of trees dynamic --- __tests__/schema.test.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/__tests__/schema.test.ts b/__tests__/schema.test.ts index 91463d48..932f7d71 100644 --- a/__tests__/schema.test.ts +++ b/__tests__/schema.test.ts @@ -46,6 +46,7 @@ describe("misc test testing the schema function of the database", () => { }); test("a user should be able to remove its account and his associated data", async () => { + const numberOfTrees = 10; const email = "user@email.com"; await deleteSupabaseUser(email); // clean up before running const { data, error } = await supabaseAnonClient.auth.signUp({ @@ -57,9 +58,9 @@ describe("misc test testing the schema function of the database", () => { const { data: trees, error: treesError } = await supabaseAnonClient .from("trees") .select("*") - .limit(10); + .limit(numberOfTrees); expect(treesError).toBeNull(); - expect(trees).toHaveLength(10); + expect(trees).toHaveLength(numberOfTrees); const { data: adoptedTrees, error: adoptedTreesError } = await supabaseServiceRoleClient @@ -73,7 +74,7 @@ describe("misc test testing the schema function of the database", () => { ) .select("*"); expect(adoptedTreesError).toBeNull(); - expect(adoptedTrees).toHaveLength(10); + expect(adoptedTrees).toHaveLength(numberOfTrees); const { data: userTrees, error: userTreesError } = await supabaseServiceRoleClient .from("trees_watered") @@ -89,7 +90,7 @@ describe("misc test testing the schema function of the database", () => { ) .select("*"); expect(userTreesError).toBeNull(); - expect(userTrees).toHaveLength(10); + expect(userTrees).toHaveLength(numberOfTrees); // since wa can not pass the token to our supabase client, we need to use fetch directly const response = await fetch(`${SUPABASE_URL}/rest/v1/rpc/remove_account`, { @@ -122,6 +123,7 @@ describe("misc test testing the schema function of the database", () => { test("if a user changes his username all the usernames on the trees_watered table should change too", async () => { const email = "foo@bar.com"; + const numberOfTrees = 10; await deleteSupabaseUser(email); await truncateTreesWaterd(); const { data, error } = await supabaseAnonClient.auth.signUp({ @@ -133,9 +135,9 @@ describe("misc test testing the schema function of the database", () => { const { data: trees, error: treesError } = await supabaseAnonClient .from("trees") .select("*") - .limit(10); + .limit(numberOfTrees); expect(treesError).toBeNull(); - expect(trees).toHaveLength(10); + expect(trees).toHaveLength(numberOfTrees); const { data: adoptedTrees, error: adoptedTreesError } = await supabaseServiceRoleClient @@ -152,7 +154,7 @@ describe("misc test testing the schema function of the database", () => { ) .select("*"); expect(adoptedTreesError).toBeNull(); - expect(adoptedTrees).toHaveLength(10); + expect(adoptedTrees).toHaveLength(numberOfTrees); // since we cant pass our access token to change our username to our anon client we use fetch directly const changeResponse = await fetch( @@ -180,7 +182,7 @@ describe("misc test testing the schema function of the database", () => { .eq("username", "bar"); expect(treesAfterError).toBeNull(); - expect(treesAfter).toHaveLength(10); + expect(treesAfter).toHaveLength(numberOfTrees); await deleteSupabaseUser(email); await truncateTreesWaterd(); }); From 773be99c3db69a98b8c702510a80f2503b4ad662 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 20 Apr 2023 17:03:51 +0000 Subject: [PATCH 45/51] chore(deps): update node.js to v18.16.0 --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index 0e9dc6b5..6d80269a 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v18.13.0 +18.16.0 From a3f9ec0deadef4b2f349365fd05fa8537c8f31b3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 20 Apr 2023 17:04:13 +0000 Subject: [PATCH 46/51] chore(deps): update actions/setup-node action to v3 --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ba38e471..b084fc8f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -38,7 +38,7 @@ jobs: password: ${{ secrets.DOCKER_PASSWORD }} - uses: actions/checkout@v3 - name: Use Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version-file: ".nvmrc" - uses: supabase/setup-cli@v1 From 5fcc5f64533cc021d1c925898069c039f5ea19c3 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Wed, 10 May 2023 10:49:07 +0200 Subject: [PATCH 47/51] fix(schema): Add public schema to table names @JensWinter had problems deleting users from the dashboard see #219 --- ...0230510084821_fix_delete_user_function.sql | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 supabase/migrations/20230510084821_fix_delete_user_function.sql diff --git a/supabase/migrations/20230510084821_fix_delete_user_function.sql b/supabase/migrations/20230510084821_fix_delete_user_function.sql new file mode 100644 index 00000000..4fc114e6 --- /dev/null +++ b/supabase/migrations/20230510084821_fix_delete_user_function.sql @@ -0,0 +1,28 @@ +SET check_function_bodies = OFF; + +CREATE OR REPLACE FUNCTION public.delete_user() + RETURNS TRIGGER + LANGUAGE plpgsql + SECURITY DEFINER + AS $function$ +DECLARE + row_count int; +BEGIN + DELETE FROM public.profiles p + WHERE p.id = OLD.id; + -- IF found THEN + -- GET DIAGNOSTICS row_count = ROW_COUNT; + -- RAISE NOTICE 'DELETEd % row(s) FROM profiles', row_count; + -- END IF; + UPDATE + public.trees_watered + SET + uuid = NULL, + username = NULL + WHERE + uuid = OLD.id::text; + DELETE FROM public.trees_adopted ta + WHERE ta.uuid = OLD.id::text; + RETURN OLD; +END; +$function$ From 6ac271b9d8dc0c658eccce1bd69ba1de76db27c2 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Thu, 11 May 2023 17:14:09 +0200 Subject: [PATCH 48/51] fix(schema): Remove deprecated time column Is not used anywhere in the frontend --- supabase/migrations/20230511150457_remove_time_column.sql | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 supabase/migrations/20230511150457_remove_time_column.sql diff --git a/supabase/migrations/20230511150457_remove_time_column.sql b/supabase/migrations/20230511150457_remove_time_column.sql new file mode 100644 index 00000000..f4bc2d54 --- /dev/null +++ b/supabase/migrations/20230511150457_remove_time_column.sql @@ -0,0 +1,3 @@ +alter table "public"."trees_watered" drop column "time"; + + From 7311c9a6296ec6de80410a51ceeb4628d09963a4 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Thu, 11 May 2023 17:14:38 +0200 Subject: [PATCH 49/51] test(schema): Remove time column from trees_watered --- __tests__/__snapshots__/post-routes-v3.test.ts.snap | 1 - __tests__/__snapshots__/post-routes.test.ts.snap | 1 - __tests__/delete-v3.test.ts | 1 - __tests__/delete.test.ts | 1 - 4 files changed, 4 deletions(-) diff --git a/__tests__/__snapshots__/post-routes-v3.test.ts.snap b/__tests__/__snapshots__/post-routes-v3.test.ts.snap index 5c1e9642..9e02e895 100644 --- a/__tests__/__snapshots__/post-routes-v3.test.ts.snap +++ b/__tests__/__snapshots__/post-routes-v3.test.ts.snap @@ -4,7 +4,6 @@ exports[`posting data should return 201 on water route invalid body missing uuid { "amount": Any, "id": Any, - "time": null, "timestamp": Any, "tree_id": Any, "username": Any, diff --git a/__tests__/__snapshots__/post-routes.test.ts.snap b/__tests__/__snapshots__/post-routes.test.ts.snap index 5c1e9642..9e02e895 100644 --- a/__tests__/__snapshots__/post-routes.test.ts.snap +++ b/__tests__/__snapshots__/post-routes.test.ts.snap @@ -4,7 +4,6 @@ exports[`posting data should return 201 on water route invalid body missing uuid { "amount": Any, "id": Any, - "time": null, "timestamp": Any, "tree_id": Any, "username": Any, diff --git a/__tests__/delete-v3.test.ts b/__tests__/delete-v3.test.ts index 00d8a0d6..8b256e3d 100644 --- a/__tests__/delete-v3.test.ts +++ b/__tests__/delete-v3.test.ts @@ -101,7 +101,6 @@ describe("api/v3/delete/[type]", () => { uuid, amount, timestamp, - time: timestamp, username: uuid, }) .select("id"); diff --git a/__tests__/delete.test.ts b/__tests__/delete.test.ts index 5b8f2dee..92a17366 100644 --- a/__tests__/delete.test.ts +++ b/__tests__/delete.test.ts @@ -89,7 +89,6 @@ describe("api/delete/[type]", () => { uuid, amount, timestamp, - time: timestamp, username: uuid, }) .select("id"); From f797717a0a00e3f3c1bfbf1b41680f6ea207298a Mon Sep 17 00:00:00 2001 From: ff6347 Date: Thu, 11 May 2023 17:14:58 +0200 Subject: [PATCH 50/51] chore: Update types remove time column from trees_watered --- _types/database.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/_types/database.ts b/_types/database.ts index 5c6cbd13..d0c664e7 100644 --- a/_types/database.ts +++ b/_types/database.ts @@ -235,7 +235,6 @@ export interface Database { Row: { amount: number id: number - time: string | null timestamp: string tree_id: string username: string | null @@ -244,7 +243,6 @@ export interface Database { Insert: { amount: number id?: number - time?: string | null timestamp: string tree_id: string username?: string | null @@ -253,7 +251,6 @@ export interface Database { Update: { amount?: number id?: number - time?: string | null timestamp?: string tree_id?: string username?: string | null From 39d641dc74b06e1f3bd29c83d33b304f68ed63b9 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Fri, 12 May 2023 10:54:08 +0200 Subject: [PATCH 51/51] chore(config): Adds recovery url to config related to PR https://github.com/technologiestiftung/giessdenkiez-de/pull/583 --- supabase/config.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/supabase/config.toml b/supabase/config.toml index f5f73065..a6361c6a 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -42,7 +42,10 @@ file_size_limit = "50MiB" # in emails. site_url = "http://localhost:3000" # A list of *exact* URLs that auth providers are permitted to redirect to post authentication. -additional_redirect_urls = ["https://localhost:3000"] +additional_redirect_urls = [ + "https://localhost:3000", + "https://localhost:3000/reset-password" +] # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one # week). jwt_expiry = 3600