From aad4073c952c746514e34f47e9e921929dad947a Mon Sep 17 00:00:00 2001 From: ff6347 Date: Thu, 16 Mar 2023 20:25:00 +0100 Subject: [PATCH 01/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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 f35b95688cf286394908523e36603b6e5442fe6f Mon Sep 17 00:00:00 2001 From: ff6347 Date: Tue, 21 Mar 2023 16:26:32 +0100 Subject: [PATCH 17/25] 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 18/25] 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 19/25] 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 20/25] 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 21/25] 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 22/25] 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 23/25] 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 24/25] 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 25/25] 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/ - -