diff --git a/.all-contributorsrc b/.all-contributorsrc index f9559c99..264375f5 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -66,6 +66,24 @@ "contributions": [ "doc" ] + }, + { + "login": "dnsos", + "name": "Dennis Ostendorf", + "avatar_url": "https://avatars.githubusercontent.com/u/15640196?v=4", + "profile": "https://github.com/dnsos", + "contributions": [ + "review" + ] + }, + { + "login": "julizet", + "name": "Julia Zet", + "avatar_url": "https://avatars.githubusercontent.com/u/52455010?v=4", + "profile": "https://github.com/julizet", + "contributions": [ + "review" + ] } ], "contributorsPerLine": 7 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fd08ec9b..b084fc8f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -38,7 +38,7 @@ jobs: password: ${{ secrets.DOCKER_PASSWORD }} - uses: actions/checkout@v3 - name: Use Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version-file: ".nvmrc" - uses: supabase/setup-cli@v1 @@ -68,7 +68,7 @@ jobs: node-version-file: ".nvmrc" - run: npm ci - id: semantic-release - uses: cycjimmy/semantic-release-action@v2 + uses: cycjimmy/semantic-release-action@v3 with: semantic_version: 18 env: diff --git a/.nvmrc b/.nvmrc index 0e9dc6b5..6d80269a 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v18.13.0 +18.16.0 diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 1600801f..07132bed 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,7 @@ { - "recommendations": ["humao.rest-client", "mikestead.dotenv"] -} + "recommendations": [ + "humao.rest-client", + "mikestead.dotenv", + "mkhl.direnv" + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 2dacb966..aeacc705 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,26 @@ - [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) + - [Vercel Environment Variables](#vercel-environment-variables) + - [API Routes /v3](#api-routes-v3) + - [](#) - [API Authorization](#api-authorization) + - [Supabase](#supabase) + - [Auth0 (deprecated)](#auth0-deprecated-1) - [Tests](#tests) - - [Supabase](#supabase) + - [Supabase](#supabase-1) - [Migrations and Types](#migrations-and-types) - [Deployment](#deployment) - [Radolan Harvester](#radolan-harvester) + - [API Routes](#api-routes) + - [API Authorization](#api-authorization-1) + - [Supabase](#supabase-2) + - [Auth0 (deprecated)](#auth0-deprecated-2) + - [Tests](#tests-1) - [Contributors ✨](#contributors-) - [Credits](#credits) @@ -28,7 +37,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 +45,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 +85,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 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. @@ -98,9 +109,10 @@ vercel env add SUPABASE_ANON_KEY # the max rows allowed to fetch from supabase (default 1000) vercel env add SUPABASE_MAX_ROWS # below are all taken from auth0.com -vercel env add jwksuri -vercel env add audience -vercel env add issuer +# the v3 api does not need them anymore +# vercel env add jwksuri +# vercel env add audience +# vercel env add issuer ``` To let these variables take effect you need to deploy your application once more. @@ -109,11 +121,9 @@ To let these variables take effect you need to deploy your application once more vercel --prod ``` - +## API Routes /v3 -## API Routes - -There are 3 main routes `/get`, `/post` and `/delete`. +There are 3 main routes `/v3/get`, `/v3/post` and `/v3/delete`. On the `/get` route all actions are controlled by passing URL params. On the `/post` and `/delete` route you will have to work with additional POST bodies. For example to fetch a specific tree run the following command. @@ -127,20 +137,48 @@ You can see all the available routes in the [docs/api.http](./docs/api.http) fil Currently we have these routes -| `/get` | `/post` | `/delete` | -| -------------------- | -------- | ---------- | -| `/byid` | `/adopt` | `/unadopt` | -| `/treesbyids` | `/water` | `/unwater` | -| `/adopted` | | | -| `/istreeadopted` | | | -| `/wateredandadopted` | | | -| `/lastwatered` | | | -| `/wateredbyuser` | | | +| `/v3/get` | `/v3/post` | `/v3/delete` | +| :------------------- | :--------- | :----------- | +| `/byid` | `/adopt` | `/unadopt` | +| `/treesbyids` | `/water` | `/unwater` | +| `/adopted` | | | +| `/istreeadopted` | | | +| `/wateredandadopted` | | | +| `/lastwatered` | | | +| `/wateredbyuser` | | | + +### ### API Authorization Some of the request will need an authorization header. You can obtain a token by making a request to your auth0 token issuer. +### Supabase + +You can sign up with the request below. You will get an access token to use in your requests. + +```bash +curl --request POST \ + --url http://localhost:54321/auth/v1/signup \ + --header 'apikey: ' \ + --header 'content-type: application/json' \ + --header 'user-agent: vscode-restclient' \ + --data '{"email": "someone@email.com","password": "1234567890"}' +``` + +```bash +curl --request POST \ + --url http://localhost:8080/post/adopt \ + --header 'authorization: Bearer ' \ + --header 'content-type: application/json' \ + --data '{"tree_id":"_01","uuid": ""}' + +``` + +The user id will be removed in future versions since the supabase SDK can get the user id from the access token and each token is bound to a specific user. + +#### Auth0 (deprecated) + ```bash curl --request POST \ --url https://your-tenant.eu.auth0.com/oauth/token \ @@ -153,10 +191,10 @@ This will respond with an `access_token`. Use it to make authenticated requests. ```bash curl --request POST \ - --url http://localhost:3000/post \ + --url http://localhost:8080/post/adopt \ --header 'authorization: Bearer ' \ --header 'content-type: application/json' \ - --data '{"queryType":"adopt","tree_id":"_01","uuid": "auth0|123"}' + --data '{"tree_id":"_01","uuid": "auth0|123"}' ``` Take a look into [docs/api.http](./docs/api.http). The requests in this file can be run with the VSCode extension [REST Client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client). @@ -223,6 +261,99 @@ INSERT INTO "public"."radolan_harvester" ("id", "collection_date", "start_date", This process is actually a little blackbox we need to solve. +## API Routes + +There are 3 main routes `/get`, `/post` and `/delete`. + +On the `/get` route all actions are controlled by passing URL params. On the `/post` and `/delete` route you will have to work with additional POST bodies. For example to fetch a specific tree run the following command. + +```bash +curl --request GET \ + --url 'http://localhost:8080/get/byid&id=_123456789' \ + +``` + +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 (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 +curl --request POST \ + --url https://your-tenant.eu.auth0.com/oauth/token \ + --header 'content-type: application/json' \ + --data '{"client_id": "","client_secret": "","audience": "","grant_type": "client_credentials"}' +# fill in the fields +``` + +This will respond with an `access_token`. Use it to make authenticated requests. + +```bash +curl --request POST \ + --url http://localhost:8080/post \ + --header 'authorization: Bearer ' \ + --header 'content-type: application/json' \ + --data '{"queryType":"adopt","tree_id":"_01","uuid": "auth0|123"}' +``` + +Take a look into [docs/api.http](./docs/api.http). The requests in this file can be run with the VSCode extension [REST Client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client). + +## Tests + +Locally you will need supabase running and a `.env` file with the right values in it. + +```bash +cd giessdenkiez-de-postgres-api +supabase start +# Once the backaned is up and running, run the tests +# Make sure to you habe your .env file setup right +# with all the values from `supabase status` and your API from Auth0.com +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 + ## Contributors ✨ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): @@ -231,14 +362,18 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d - - - - - - - - + + + + + + + + + + + +

Fabian MorΓ³n Zirfas

πŸ’» πŸ“–

Fabian

πŸ’» πŸ“–

warenix

πŸ’» πŸ“–

Daniel Sippel

πŸ“–

Sebastian Meier

πŸ’»

Lucas Vogel

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

πŸ’» πŸ“–
Fabian
Fabian

πŸ’» πŸ“–
warenix
warenix

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

πŸ“–
Sebastian Meier
Sebastian Meier

πŸ’»
Lucas Vogel
Lucas Vogel

πŸ“–
Dennis Ostendorf
Dennis Ostendorf

πŸ‘€
Julia Zet
Julia Zet

πŸ‘€
@@ -277,5 +412,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/ - - diff --git a/__test-utils/postgres.ts b/__test-utils/postgres.ts index 0c306bc4..2a5e19a6 100644 --- a/__test-utils/postgres.ts +++ b/__test-utils/postgres.ts @@ -16,15 +16,28 @@ export async function truncateTreesAdopted() { sql.end(); } -export async function createWateredTrees() { +export async function createWateredTrees(userId?: string, userName?: string) { const sql = postgres(url); + const randomText = sql`md5(random()::text)`; await sql` - INSERT INTO trees_watered (uuid, 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 + ${userId ? userId : sql`extensions.uuid_generate_v4()::text`}, + random() * 10, + NOW() - (random() * INTERVAL '7 days'), + ${userName ? userName : randomText}, + 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 f8c768d0..7dfcc5e5 100644 --- a/__test-utils/req-test-token.ts +++ b/__test-utils/req-test-token.ts @@ -1,9 +1,11 @@ +import { SignupResponse } from "../_types/user"; +import { SUPABASE_ANON_KEY, SUPABASE_URL } from "./supabase"; const issuer = process.env.issuer || ""; const client_id = process.env.client_id || ""; const client_secret = process.env.client_secret || ""; const audience = process.env.audience || ""; -export async function requestTestToken() { +export async function requestAuth0TestToken() { const response = await fetch(`${issuer}oauth/token`, { method: "POST", headers: { @@ -23,3 +25,60 @@ export async function requestTestToken() { 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 SignupResponse; + return json.access_token; +} + +export async function createSupabaseUser( + email: string, + password: string, + opts?: { returnFullUser: boolean } +) { + 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) { + console.log(response.status); + 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 }; + }; + if (opts?.returnFullUser) { + return json; + } + return json.access_token; +} diff --git a/__test-utils/supabase.ts b/__test-utils/supabase.ts new file mode 100644 index 00000000..ffea419c --- /dev/null +++ b/__test-utils/supabase.ts @@ -0,0 +1,16 @@ +import { createClient } from "@supabase/supabase-js"; +import { Database } from "../_types/database"; +export const SUPABASE_URL = + process.env.SUPABASE_URL || "http://localhost:54321"; +export const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || ""; +export const SUPABASE_SERVICE_ROLE_KEY = + process.env.SUPABASE_SERVICE_ROLE_KEY || ""; + +export const supabaseAnonClient = createClient( + SUPABASE_URL, + SUPABASE_ANON_KEY +); +export const supabaseServiceRoleClient = createClient( + SUPABASE_URL, + SUPABASE_SERVICE_ROLE_KEY +); diff --git a/__tests__/__snapshots__/get-routes.test.ts.snap b/__tests__/__snapshots__/get-routes.test.ts.snap index 79c51c0d..2ecaecb5 100644 --- a/__tests__/__snapshots__/get-routes.test.ts.snap +++ b/__tests__/__snapshots__/get-routes.test.ts.snap @@ -1537,7 +1537,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", } `; @@ -1553,7 +1552,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", } `; @@ -1563,7 +1561,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", } `; @@ -1579,7 +1576,6 @@ exports[`GET routes snapshot tests default responses should return 200 on lastwa "total": 0, }, "url": "/?type=lastwatered&id=_210028b9c8", - "version": "2.0.0", } `; @@ -2352,7 +2348,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", } `; @@ -2368,7 +2363,6 @@ exports[`GET routes snapshot tests default responses should return 200 on watere "total": 0, }, "url": "/?type=wateredandadopted", - "version": "2.0.0", } `; @@ -2384,7 +2378,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", } `; @@ -2393,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__/__snapshots__/index.test.ts.snap b/__tests__/__snapshots__/index.test.ts.snap index 28ae249a..e574da90 100644 --- a/__tests__/__snapshots__/index.test.ts.snap +++ b/__tests__/__snapshots__/index.test.ts.snap @@ -281,7 +281,6 @@ exports[`GET/POST/DELETE routes index should list all routes on /delete 1`] = ` }, }, ], - "version": "2.0.0", } `; @@ -566,7 +565,6 @@ exports[`GET/POST/DELETE routes index should list all routes on /get 1`] = ` }, }, ], - "version": "2.0.0", } `; @@ -851,6 +849,5 @@ exports[`GET/POST/DELETE routes index should list all routes on /post 1`] = ` }, }, ], - "version": "2.0.0", } `; 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..9e02e895 --- /dev/null +++ b/__tests__/__snapshots__/post-routes-v3.test.ts.snap @@ -0,0 +1,12 @@ +// 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, + "timestamp": Any, + "tree_id": Any, + "username": Any, + "uuid": Any, +} +`; diff --git a/__tests__/__snapshots__/post-routes.test.ts.snap b/__tests__/__snapshots__/post-routes.test.ts.snap index 5c1e9642..9e02e895 100644 --- a/__tests__/__snapshots__/post-routes.test.ts.snap +++ b/__tests__/__snapshots__/post-routes.test.ts.snap @@ -4,7 +4,6 @@ exports[`posting data should return 201 on water route invalid body missing uuid { "amount": Any, "id": Any, - "time": null, "timestamp": Any, "tree_id": Any, "username": Any, diff --git a/__tests__/__snapshots__/route-listing.test.ts.snap b/__tests__/__snapshots__/route-listing.test.ts.snap index 40b585f6..7ea83c49 100644 --- a/__tests__/__snapshots__/route-listing.test.ts.snap +++ b/__tests__/__snapshots__/route-listing.test.ts.snap @@ -119,167 +119,3 @@ exports[`route listing should list all POST routes 1`] = ` }, } `; - -exports[`route listing should list all the GET routes 1`] = ` -{ - "method": "GET", - "routes": { - "adopted": { - "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", - }, - "uuid": { - "type": "string", - }, - }, - "required": [ - "uuid", - ], - "type": "object", - }, - "url": "get/adopted", - }, - "byid": { - "schema": { - "additionalProperties": false, - "properties": { - "id": { - "type": "string", - }, - "type": { - "description": "The type property is atomaticaly added by dynamic vercel api routes. You should not add it yourself", - "type": "string", - }, - }, - "required": [ - "id", - ], - "type": "object", - }, - "url": "get/byid", - }, - "istreeadopted": { - "schema": { - "additionalProperties": false, - "properties": { - "id": { - "type": "string", - }, - "type": { - "description": "The type property is atomaticaly added by dynamic vercel api routes. You should not add it yourself", - "type": "string", - }, - "uuid": { - "type": "string", - }, - }, - "required": [ - "uuid", - "id", - ], - "type": "object", - }, - "url": "get/istreeadopted", - }, - "lastwatered": { - "schema": { - "additionalProperties": false, - "properties": { - "id": { - "type": "string", - }, - "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": [ - "id", - ], - "type": "object", - }, - "url": "get/lastwatered", - }, - "treesbyids": { - "schema": { - "additionalProperties": false, - "properties": { - "limit": { - "type": "string", - }, - "offset": { - "type": "string", - }, - "tree_ids": { - "type": "string", - }, - "type": { - "description": "The type property is atomaticaly added by dynamic vercel api routes. You should not add it yourself", - "type": "string", - }, - }, - "required": [ - "tree_ids", - ], - "type": "object", - }, - "url": "get/treesbyids", - }, - "wateredandadopted": { - "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/wateredandadopted", - }, - "wateredbyuser": { - "schema": { - "additionalProperties": false, - "properties": { - "type": { - "description": "The type property is atomaticaly added by dynamic vercel api routes. You should not add it yourself", - "type": "string", - }, - "uuid": { - "type": "string", - }, - }, - "required": [ - "uuid", - ], - "type": "object", - }, - "url": "get/wateredbyuser", - }, - }, -} -`; 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/__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-v3.test.ts b/__tests__/delete-v3.test.ts new file mode 100644 index 00000000..8b256e3d --- /dev/null +++ b/__tests__/delete-v3.test.ts @@ -0,0 +1,180 @@ +// 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, + 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 fa2d8348..92a17366 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 { @@ -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,9 +32,9 @@ 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 requestTestToken(); + const token = await requestAuth0TestToken(); const response = await fetch(url, { method: "DELETE", headers: { @@ -49,9 +49,9 @@ 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 requestTestToken(); + const token = await requestAuth0TestToken(); const response = await fetch(url, { method: "DELETE", headers: { @@ -89,7 +89,6 @@ describe("api/delete/[type]", () => { uuid, amount, timestamp, - time: timestamp, username: uuid, }) .select("id"); @@ -100,9 +99,9 @@ describe("api/delete/[type]", () => { const watering_id = waterData[0].id; const { server, url } = await createTestServer( { type: "unwater" }, - deleteHandler + v2DeleteHandler ); - const token = await requestTestToken(); + const token = await requestAuth0TestToken(); const response = await fetch(url, { method: "DELETE", headers: { @@ -149,9 +148,9 @@ describe("api/delete/[type]", () => { const { server, url } = await createTestServer( { type: "unadopt" }, - deleteHandler + v2DeleteHandler ); - 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 cc675cfc..6fc38336 100644 --- a/__tests__/get-routes.test.ts +++ b/__tests__/get-routes.test.ts @@ -1,15 +1,20 @@ import each from "jest-each"; import fetch from "cross-fetch"; 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 { requestTestToken } from "../__test-utils/req-test-token"; +import { + createSupabaseUser, + requestAuth0TestToken, + requestSupabaseTestToken, +} from "../__test-utils/req-test-token"; // byid βœ“ - // treesbyids βœ“ // wateredandadopted βœ“ // lastwatered βœ“ @@ -22,10 +27,10 @@ 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 + v2handler ); // console.log(url); const response = await fetch(url, { @@ -41,10 +46,10 @@ 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 + v2handler ); const response = await fetch(url, { headers: { @@ -59,10 +64,10 @@ 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 + v2handler ); const response = await fetch(url, { headers: { @@ -79,7 +84,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(); @@ -92,7 +97,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(); @@ -104,7 +109,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(); @@ -116,7 +121,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(); @@ -128,7 +133,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(); @@ -174,16 +179,203 @@ 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 }, + 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(); + 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 }, - handler + v3handler ); 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-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(); + }); + } +); 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") diff --git a/__tests__/route-listing.test.ts b/__tests__/route-listing.test.ts index 064b5f59..9efdfd93 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.wateredandadopted.schema + getRoutesList.routes.lastwatered.schema ); expect(valid).toBe(false); @@ -22,6 +22,168 @@ describe("route listing", () => { }); test("should list all the GET routes", async () => { - expect(getRoutesList).toMatchSnapshot(); + expect(getRoutesList).toMatchInlineSnapshot(` + { + "method": "GET", + "routes": { + "adopted": { + "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", + }, + "uuid": { + "type": "string", + }, + }, + "required": [ + "uuid", + ], + "type": "object", + }, + "url": "get/adopted", + }, + "byid": { + "schema": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + }, + "type": { + "description": "The type property is atomaticaly added by dynamic vercel api routes. You should not add it yourself", + "type": "string", + }, + }, + "required": [ + "id", + ], + "type": "object", + }, + "url": "get/byid", + }, + "istreeadopted": { + "schema": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + }, + "type": { + "description": "The type property is atomaticaly added by dynamic vercel api routes. You should not add it yourself", + "type": "string", + }, + "uuid": { + "type": "string", + }, + }, + "required": [ + "uuid", + "id", + ], + "type": "object", + }, + "url": "get/istreeadopted", + }, + "lastwatered": { + "schema": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + }, + "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": [ + "id", + ], + "type": "object", + }, + "url": "get/lastwatered", + }, + "treesbyids": { + "schema": { + "additionalProperties": false, + "properties": { + "limit": { + "type": "string", + }, + "offset": { + "type": "string", + }, + "tree_ids": { + "type": "string", + }, + "type": { + "description": "The type property is atomaticaly added by dynamic vercel api routes. You should not add it yourself", + "type": "string", + }, + }, + "required": [ + "tree_ids", + ], + "type": "object", + }, + "url": "get/treesbyids", + }, + "wateredandadopted": { + "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/wateredandadopted", + }, + "wateredbyuser": { + "schema": { + "additionalProperties": false, + "properties": { + "type": { + "description": "The type property is atomaticaly added by dynamic vercel api routes. You should not add it yourself", + "type": "string", + }, + "uuid": { + "type": "string", + }, + }, + "required": [ + "uuid", + ], + "type": "object", + }, + "url": "get/wateredbyuser", + }, + }, + } + `); }); }); diff --git a/__tests__/schema.test.ts b/__tests__/schema.test.ts new file mode 100644 index 00000000..932f7d71 --- /dev/null +++ b/__tests__/schema.test.ts @@ -0,0 +1,189 @@ +import { + deleteSupabaseUser, + truncateTreesWaterd, +} from "../__test-utils/postgres"; +import { + SUPABASE_ANON_KEY, + SUPABASE_URL, + supabaseAnonClient, + supabaseServiceRoleClient, +} from "../__test-utils/supabase"; +describe("misc test testing the schema function of the database", () => { + test("inserting an existing username should alter the new name and add a uuid at end", async () => { + const email1 = "someone@email.com"; + const email2 = "someone@foo.com"; + await deleteSupabaseUser(email1); + await deleteSupabaseUser(email2); + const password = "12345678"; + const { data: user1, error } = await supabaseAnonClient.auth.signUp({ + email: email1, + password: password, + }); + const { data: user2, error: error2 } = await supabaseAnonClient.auth.signUp( + { + email: email2, + password: password, + } + ); + expect(error).toBeNull(); + expect(user1).toBeDefined(); + expect(error2).toBeNull(); + expect(user2).toBeDefined(); + + const { data: users, error: usersError } = await supabaseAnonClient + .from("profiles") + .select("*") + .in("id", [user1?.user?.id, user2?.user?.id]); + + expect(usersError).toBeNull(); + expect(users).toHaveLength(2); + expect(users?.[0].username).toBe("someone"); + expect(users?.[1].username).not.toBe("someone"); + expect(users?.[1].username).toContain("someone-"); + expect(users?.[1].username).toMatch(/^someone-[a-zA-Z0-9]{6}$/); + await deleteSupabaseUser(email1); + await deleteSupabaseUser(email2); + }); + + test("a user should be able to remove its account and his associated data", async () => { + const numberOfTrees = 10; + const email = "user@email.com"; + await deleteSupabaseUser(email); // clean up before running + const { data, error } = await supabaseAnonClient.auth.signUp({ + email: email, + password: "12345678", + }); + expect(error).toBeNull(); + expect(data).toBeDefined(); + const { data: trees, error: treesError } = await supabaseAnonClient + .from("trees") + .select("*") + .limit(numberOfTrees); + expect(treesError).toBeNull(); + expect(trees).toHaveLength(numberOfTrees); + + const { data: adoptedTrees, error: adoptedTreesError } = + await supabaseServiceRoleClient + .from("trees_adopted") + .insert( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + trees!.map((tree) => ({ + uuid: data.user?.id, + tree_id: tree.id, + })) + ) + .select("*"); + expect(adoptedTreesError).toBeNull(); + expect(adoptedTrees).toHaveLength(numberOfTrees); + const { data: userTrees, error: userTreesError } = + await supabaseServiceRoleClient + .from("trees_watered") + .insert( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + trees!.map((tree) => ({ + uuid: data.user?.id, + amount: 1, + timestamp: new Date().toISOString(), + username: "user", + tree_id: tree.id, + })) + ) + .select("*"); + expect(userTreesError).toBeNull(); + expect(userTrees).toHaveLength(numberOfTrees); + + // since wa can not pass the token to our supabase client, we need to use fetch directly + const response = await fetch(`${SUPABASE_URL}/rest/v1/rpc/remove_account`, { + method: "POST", + headers: { + apikey: SUPABASE_ANON_KEY, + "Content-Type": "application/json", + Authorization: `Bearer ${data.session?.access_token}`, + }, + }); + expect(response.ok).toBeTruthy(); + expect(response.status).toBe(204); + const { data: treesAfter, error: treesAfterError } = + await supabaseAnonClient + .from("trees_watered") + .select("*") + .eq("uuid", data.user?.id); + expect(treesAfterError).toBeNull(); + expect(treesAfter).toHaveLength(0); + + const { data: adoptedTreesAfter, error: adoptedTreesAfterError } = + await supabaseAnonClient + .from("trees_adopted") + .select("*") + .eq("uuid", data.user?.id); + expect(adoptedTreesAfterError).toBeNull(); + expect(adoptedTreesAfter).toHaveLength(0); + await truncateTreesWaterd(); + }); + + test("if a user changes his username all the usernames on the trees_watered table should change too", async () => { + const email = "foo@bar.com"; + const numberOfTrees = 10; + await deleteSupabaseUser(email); + await truncateTreesWaterd(); + const { data, error } = await supabaseAnonClient.auth.signUp({ + email: email, + password: "12345678", + }); + expect(error).toBeNull(); + expect(data).toBeDefined(); + const { data: trees, error: treesError } = await supabaseAnonClient + .from("trees") + .select("*") + .limit(numberOfTrees); + expect(treesError).toBeNull(); + expect(trees).toHaveLength(numberOfTrees); + + const { data: adoptedTrees, error: adoptedTreesError } = + await supabaseServiceRoleClient + .from("trees_watered") + .insert( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + trees!.map((tree) => ({ + uuid: data.user?.id, + tree_id: tree.id, + amount: 1, + timestamp: new Date().toISOString(), + username: "foo", + })) + ) + .select("*"); + expect(adoptedTreesError).toBeNull(); + expect(adoptedTrees).toHaveLength(numberOfTrees); + + // since we cant pass our access token to change our username to our anon client we use fetch directly + const changeResponse = await fetch( + `${SUPABASE_URL}/rest/v1/profiles?id=eq.${data?.user?.id}`, + { + method: "PATCH", + headers: { + apikey: SUPABASE_ANON_KEY, + "Content-Type": "application/json", + Authorization: `Bearer ${data.session?.access_token}`, + }, + body: JSON.stringify({ + username: "bar", + }), + } + ); + + expect(changeResponse.ok).toBeTruthy(); + expect(changeResponse.status).toBe(204); + + const { data: treesAfter, error: treesAfterError } = + await supabaseServiceRoleClient + .from("trees_watered") + .select("*") + .eq("username", "bar"); + + expect(treesAfterError).toBeNull(); + expect(treesAfter).toHaveLength(numberOfTrees); + await deleteSupabaseUser(email); + await truncateTreesWaterd(); + }); +}); 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, + }); + }); +}); diff --git a/_requests/delete/unadopt.ts b/_requests/delete/unadopt.ts new file mode 100644 index 00000000..15ef58bc --- /dev/null +++ b/_requests/delete/unadopt.ts @@ -0,0 +1,28 @@ +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, + user?: User +) { + 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() + .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..0893859a --- /dev/null +++ b/_requests/delete/unwater.ts @@ -0,0 +1,29 @@ +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, + user?: User +) { + 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() + .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 58% rename from api/get/_requests/adopted.ts rename to _requests/get/adopted.ts index 19e2803b..86dd4811 100644 --- a/api/get/_requests/adopted.ts +++ b/_requests/get/adopted.ts @@ -1,33 +1,37 @@ 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"; +import { urlContainsV3 } from "../../_utils/check-if-v3"; 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; + 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/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/istreeadopted.ts b/_requests/get/istreeadopted.ts similarity index 51% rename from api/get/_requests/istreeadopted.ts rename to _requests/get/istreeadopted.ts index c2793458..6796342b 100644 --- a/api/get/_requests/istreeadopted.ts +++ b/_requests/get/istreeadopted.ts @@ -1,17 +1,22 @@ +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 { urlContainsV3 } from "../../_utils/check-if-v3"; +import { setupResponseData } from "../../_utils/setup-response"; +import { supabase } from "../../_utils/supabase"; 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" }); + 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/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/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 56% rename from api/get/_requests/wateredbyuser.ts rename to _requests/get/wateredbyuser.ts index 4bef5b57..cbfa9986 100644 --- a/api/get/_requests/wateredbyuser.ts +++ b/_requests/get/wateredbyuser.ts @@ -1,33 +1,36 @@ 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 { 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 + 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; + 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 new file mode 100644 index 00000000..bdebe97f --- /dev/null +++ b/_requests/post/adopt.ts @@ -0,0 +1,34 @@ +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, + user?: User +) { + 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( + { + 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..5cea11c0 --- /dev/null +++ b/_requests/post/water.ts @@ -0,0 +1,54 @@ +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, + user?: User +) { + const body = request.body as TreesWatered; + 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({ + // 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/_types/database.ts b/_types/database.ts index a4e7c8d3..d0c664e7 100644 --- a/_types/database.ts +++ b/_types/database.ts @@ -1,407 +1,455 @@ export type Json = - | string - | number - | boolean - | null - | { [key: string]: Json } - | Json[]; + | string + | number + | boolean + | null + | { [key: string]: Json } + | Json[] export interface Database { - graphql_public: { - Tables: { - [_ in never]: never; - }; - Views: { - [_ in never]: never; - }; - Functions: { - graphql: { - Args: { - operationName: string; - query: string; - variables: Json; - extensions: Json; - }; - Returns: Json; - }; - }; - Enums: { - [_ in never]: never; - }; - }; - public: { - Tables: { - profiles: { - Row: { - id: string; - username: string | null; - }; - Insert: { - id: string; - username?: string | null; - }; - Update: { - id?: string; - username?: string | null; - }; - }; - radolan_data: { - Row: { - geom_id: number | null; - id: number; - measured_at: string | null; - value: number | null; - }; - Insert: { - geom_id?: number | null; - id?: number; - measured_at?: string | null; - value?: number | null; - }; - Update: { - geom_id?: number | null; - id?: number; - measured_at?: string | null; - value?: number | null; - }; - }; - radolan_geometry: { - Row: { - centroid: unknown | null; - geometry: unknown | null; - id: number; - }; - Insert: { - centroid?: unknown | null; - geometry?: unknown | null; - id?: number; - }; - Update: { - centroid?: unknown | null; - geometry?: unknown | null; - id?: number; - }; - }; - radolan_harvester: { - Row: { - collection_date: string | null; - end_date: string | null; - id: number; - start_date: string | null; - }; - Insert: { - collection_date?: string | null; - end_date?: string | null; - id?: number; - start_date?: string | null; - }; - Update: { - collection_date?: string | null; - end_date?: string | null; - id?: number; - start_date?: string | null; - }; - }; - radolan_temp: { - Row: { - geometry: unknown | null; - id: number; - measured_at: string | null; - value: number | null; - }; - Insert: { - geometry?: unknown | null; - id?: number; - measured_at?: string | null; - value?: number | null; - }; - Update: { - geometry?: unknown | null; - id?: number; - measured_at?: string | null; - value?: number | null; - }; - }; - trees: { - Row: { - adopted: string | null; - artbot: string | null; - artdtsch: string | null; - baumhoehe: string | null; - bezirk: string | null; - caretaker: string | null; - eigentuemer: string | null; - gattung: string | null; - gattungdeutsch: string | null; - geom: unknown | null; - gmlid: string | null; - hausnr: string | null; - id: string; - kennzeich: string | null; - kronedurch: string | null; - lat: string | null; - lng: string | null; - pflanzjahr: number | null; - radolan_days: number[] | null; - radolan_sum: number | null; - stammumfg: string | null; - standalter: string | null; - standortnr: string | null; - strname: string | null; - type: string | null; - watered: string | null; - zusatz: string | null; - }; - Insert: { - adopted?: string | null; - artbot?: string | null; - artdtsch?: string | null; - baumhoehe?: string | null; - bezirk?: string | null; - caretaker?: string | null; - eigentuemer?: string | null; - gattung?: string | null; - gattungdeutsch?: string | null; - geom?: unknown | null; - gmlid?: string | null; - hausnr?: string | null; - id: string; - kennzeich?: string | null; - kronedurch?: string | null; - lat?: string | null; - lng?: string | null; - pflanzjahr?: number | null; - radolan_days?: number[] | null; - radolan_sum?: number | null; - stammumfg?: string | null; - standalter?: string | null; - standortnr?: string | null; - strname?: string | null; - type?: string | null; - watered?: string | null; - zusatz?: string | null; - }; - Update: { - adopted?: string | null; - artbot?: string | null; - artdtsch?: string | null; - baumhoehe?: string | null; - bezirk?: string | null; - caretaker?: string | null; - eigentuemer?: string | null; - gattung?: string | null; - gattungdeutsch?: string | null; - geom?: unknown | null; - gmlid?: string | null; - hausnr?: string | null; - id?: string; - kennzeich?: string | null; - kronedurch?: string | null; - lat?: string | null; - lng?: string | null; - pflanzjahr?: number | null; - radolan_days?: number[] | null; - radolan_sum?: number | null; - stammumfg?: string | null; - standalter?: string | null; - standortnr?: string | null; - strname?: string | null; - type?: string | null; - watered?: string | null; - zusatz?: string | null; - }; - }; - trees_adopted: { - Row: { - id: number; - tree_id: string; - uuid: string | null; - }; - Insert: { - id?: number; - tree_id: string; - uuid?: string | null; - }; - Update: { - id?: number; - tree_id?: string; - uuid?: string | null; - }; - }; - trees_watered: { - Row: { - amount: number; - id: number; - time: string | null; - timestamp: string; - tree_id: string; - username: string | null; - uuid: string | null; - }; - Insert: { - amount: number; - id?: number; - time?: string | null; - timestamp: string; - tree_id: string; - username?: string | null; - uuid?: string | null; - }; - Update: { - amount?: number; - id?: number; - time?: string | null; - timestamp?: string; - tree_id?: string; - username?: string | null; - uuid?: string | null; - }; - }; - }; - Views: { - [_ in never]: never; - }; - Functions: { - count_by_age: { - Args: { start_year: number; end_year: number }; - Returns: number; - }; - get_watered_and_adopted: { - Args: Record; - Returns: { tree_id: string; adopted: number; watered: number }[]; - }; - }; - Enums: { - [_ in never]: never; - }; - }; - storage: { - Tables: { - buckets: { - Row: { - created_at: string | null; - id: string; - name: string; - owner: string | null; - public: boolean | null; - updated_at: string | null; - }; - Insert: { - created_at?: string | null; - id: string; - name: string; - owner?: string | null; - public?: boolean | null; - updated_at?: string | null; - }; - Update: { - created_at?: string | null; - id?: string; - name?: string; - owner?: string | null; - public?: boolean | null; - updated_at?: string | null; - }; - }; - migrations: { - Row: { - executed_at: string | null; - hash: string; - id: number; - name: string; - }; - Insert: { - executed_at?: string | null; - hash: string; - id: number; - name: string; - }; - Update: { - executed_at?: string | null; - hash?: string; - id?: number; - name?: string; - }; - }; - objects: { - Row: { - bucket_id: string | null; - created_at: string | null; - id: string; - last_accessed_at: string | null; - metadata: Json | null; - name: string | null; - owner: string | null; - path_tokens: string[] | null; - updated_at: string | null; - }; - Insert: { - bucket_id?: string | null; - created_at?: string | null; - id?: string; - last_accessed_at?: string | null; - metadata?: Json | null; - name?: string | null; - owner?: string | null; - path_tokens?: string[] | null; - updated_at?: string | null; - }; - Update: { - bucket_id?: string | null; - created_at?: string | null; - id?: string; - last_accessed_at?: string | null; - metadata?: Json | null; - name?: string | null; - owner?: string | null; - path_tokens?: string[] | null; - updated_at?: string | null; - }; - }; - }; - Views: { - [_ in never]: never; - }; - Functions: { - extension: { - Args: { name: string }; - Returns: string; - }; - filename: { - Args: { name: string }; - Returns: string; - }; - foldername: { - Args: { name: string }; - Returns: string[]; - }; - get_size_by_bucket: { - Args: Record; - Returns: { size: number; bucket_id: string }[]; - }; - search: { - Args: { - prefix: string; - bucketname: string; - limits: number; - levels: number; - offsets: number; - search: string; - sortcolumn: string; - sortorder: string; - }; - Returns: { - name: string; - id: string; - updated_at: string; - created_at: string; - last_accessed_at: string; - metadata: Json; - }[]; - }; - }; - Enums: { - [_ in never]: never; - }; - }; + graphql_public: { + Tables: { + [_ in never]: never + } + Views: { + [_ in never]: never + } + Functions: { + graphql: { + Args: { + operationName?: string + query?: string + variables?: Json + extensions?: Json + } + Returns: Json + } + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } + public: { + Tables: { + profiles: { + Row: { + id: string + username: string | null + } + Insert: { + id: string + username?: string | null + } + Update: { + id?: string + username?: string | null + } + } + radolan_data: { + Row: { + geom_id: number | null + id: number + measured_at: string | null + value: number | null + } + Insert: { + geom_id?: number | null + id?: number + measured_at?: string | null + value?: number | null + } + Update: { + geom_id?: number | null + id?: number + measured_at?: string | null + value?: number | null + } + } + radolan_geometry: { + Row: { + centroid: unknown | null + geometry: unknown | null + id: number + } + Insert: { + centroid?: unknown | null + geometry?: unknown | null + id?: number + } + Update: { + centroid?: unknown | null + geometry?: unknown | null + id?: number + } + } + radolan_harvester: { + Row: { + collection_date: string | null + end_date: string | null + id: number + start_date: string | null + } + Insert: { + collection_date?: string | null + end_date?: string | null + id?: number + start_date?: string | null + } + Update: { + collection_date?: string | null + end_date?: string | null + id?: number + start_date?: string | null + } + } + radolan_temp: { + Row: { + geometry: unknown | null + id: number + measured_at: string | null + value: number | null + } + Insert: { + geometry?: unknown | null + id?: number + measured_at?: string | null + value?: number | null + } + Update: { + geometry?: unknown | null + id?: number + measured_at?: string | null + value?: number | null + } + } + trees: { + Row: { + adopted: string | null + artbot: string | null + artdtsch: string | null + baumhoehe: string | null + bezirk: string | null + caretaker: string | null + eigentuemer: string | null + gattung: string | null + gattungdeutsch: string | null + geom: unknown | null + gmlid: string | null + hausnr: string | null + id: string + kennzeich: string | null + kronedurch: string | null + lat: string | null + lng: string | null + pflanzjahr: number | null + radolan_days: number[] | null + radolan_sum: number | null + stammumfg: string | null + standalter: string | null + standortnr: string | null + strname: string | null + type: string | null + watered: string | null + zusatz: string | null + } + Insert: { + adopted?: string | null + artbot?: string | null + artdtsch?: string | null + baumhoehe?: string | null + bezirk?: string | null + caretaker?: string | null + eigentuemer?: string | null + gattung?: string | null + gattungdeutsch?: string | null + geom?: unknown | null + gmlid?: string | null + hausnr?: string | null + id: string + kennzeich?: string | null + kronedurch?: string | null + lat?: string | null + lng?: string | null + pflanzjahr?: number | null + radolan_days?: number[] | null + radolan_sum?: number | null + stammumfg?: string | null + standalter?: string | null + standortnr?: string | null + strname?: string | null + type?: string | null + watered?: string | null + zusatz?: string | null + } + Update: { + adopted?: string | null + artbot?: string | null + artdtsch?: string | null + baumhoehe?: string | null + bezirk?: string | null + caretaker?: string | null + eigentuemer?: string | null + gattung?: string | null + gattungdeutsch?: string | null + geom?: unknown | null + gmlid?: string | null + hausnr?: string | null + id?: string + kennzeich?: string | null + kronedurch?: string | null + lat?: string | null + lng?: string | null + pflanzjahr?: number | null + radolan_days?: number[] | null + radolan_sum?: number | null + stammumfg?: string | null + standalter?: string | null + standortnr?: string | null + strname?: string | null + type?: string | null + watered?: string | null + zusatz?: string | null + } + } + trees_adopted: { + Row: { + id: number + tree_id: string + uuid: string | null + } + Insert: { + id?: number + tree_id: string + uuid?: string | null + } + Update: { + id?: number + tree_id?: string + uuid?: string | null + } + } + trees_watered: { + Row: { + amount: number + id: number + timestamp: string + tree_id: string + username: string | null + uuid: string | null + } + Insert: { + amount: number + id?: number + timestamp: string + tree_id: string + username?: string | null + uuid?: string | null + } + Update: { + amount?: number + id?: number + timestamp?: string + tree_id?: string + username?: string | null + uuid?: string | null + } + } + } + Views: { + [_ in never]: never + } + Functions: { + count_by_age: { + Args: { + start_year: number + end_year: number + } + Returns: number + } + get_watered_and_adopted: { + Args: Record + Returns: { + tree_id: string + adopted: number + watered: number + }[] + } + remove_account: { + Args: Record + Returns: undefined + } + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } + storage: { + Tables: { + buckets: { + Row: { + allowed_mime_types: string[] | null + avif_autodetection: boolean | null + created_at: string | null + file_size_limit: number | null + id: string + name: string + owner: string | null + public: boolean | null + updated_at: string | null + } + Insert: { + allowed_mime_types?: string[] | null + avif_autodetection?: boolean | null + created_at?: string | null + file_size_limit?: number | null + id: string + name: string + owner?: string | null + public?: boolean | null + updated_at?: string | null + } + Update: { + allowed_mime_types?: string[] | null + avif_autodetection?: boolean | null + created_at?: string | null + file_size_limit?: number | null + id?: string + name?: string + owner?: string | null + public?: boolean | null + updated_at?: string | null + } + } + migrations: { + Row: { + executed_at: string | null + hash: string + id: number + name: string + } + Insert: { + executed_at?: string | null + hash: string + id: number + name: string + } + Update: { + executed_at?: string | null + hash?: string + id?: number + name?: string + } + } + objects: { + Row: { + bucket_id: string | null + created_at: string | null + id: string + last_accessed_at: string | null + metadata: Json | null + name: string | null + owner: string | null + path_tokens: string[] | null + updated_at: string | null + version: string | null + } + Insert: { + bucket_id?: string | null + created_at?: string | null + id?: string + last_accessed_at?: string | null + metadata?: Json | null + name?: string | null + owner?: string | null + path_tokens?: string[] | null + updated_at?: string | null + version?: string | null + } + Update: { + bucket_id?: string | null + created_at?: string | null + id?: string + last_accessed_at?: string | null + metadata?: Json | null + name?: string | null + owner?: string | null + path_tokens?: string[] | null + updated_at?: string | null + version?: string | null + } + } + } + Views: { + [_ in never]: never + } + Functions: { + can_insert_object: { + Args: { + bucketid: string + name: string + owner: string + metadata: Json + } + Returns: undefined + } + extension: { + Args: { + name: string + } + Returns: string + } + filename: { + Args: { + name: string + } + Returns: string + } + foldername: { + Args: { + name: string + } + Returns: string[] + } + get_size_by_bucket: { + Args: Record + Returns: { + size: number + bucket_id: string + }[] + } + search: { + Args: { + prefix: string + bucketname: string + limits?: number + levels?: number + offsets?: number + search?: string + sortcolumn?: string + sortorder?: string + } + Returns: { + name: string + id: string + updated_at: string + created_at: string + last_accessed_at: string + metadata: Json + }[] + } + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } } + diff --git a/_types/user.ts b/_types/user.ts new file mode 100644 index 00000000..c3ad19e9 --- /dev/null +++ b/_types/user.ts @@ -0,0 +1,38 @@ +export interface SignupResponse { + access_token: string; + token_type: string; + expires_in: number; + refresh_token: string; + user: User; +} +export interface User { + id: string; + aud: string; + role: string; + email: string; + email_confirmed_at: string; + phone: string; + last_sign_in_at: string; + app_metadata: AppMetadata; + user_metadata: Record; + identities?: IdentitiesEntity[] | null; + created_at: string; + updated_at: string; +} +export interface AppMetadata { + provider: string; + providers?: string[] | null; +} +export interface IdentitiesEntity { + id: string; + user_id: string; + identity_data: IdentityData; + provider: string; + last_sign_in_at: string; + created_at: string; + updated_at: string; +} +export interface IdentityData { + email: string; + sub: string; +} 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/_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/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/_utils/validation.ts b/_utils/validation.ts index f95c0f7a..74384295 100644 --- a/_utils/validation.ts +++ b/_utils/validation.ts @@ -62,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: { @@ -146,9 +111,6 @@ export const getSchemas: Record = { byid: byidSchema, treesbyids: treesbyidsSchema, wateredandadopted: wateredandadoptedSchemata, - all: allSchema, - countbyage: countbyageSchema, - byage: byageSchema, lastwatered: lastwateredSchema, adopted: adoptedSchema, istreeadopted: istreeadoptedSchema, 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..74aa8c36 --- /dev/null +++ b/_utils/verify-supabase-token.ts @@ -0,0 +1,24 @@ +// 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"; + +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/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 62b4dd89..128b21a8 100644 --- a/api/get/[type].ts +++ b/api/get/[type].ts @@ -3,13 +3,14 @@ import setHeaders from "../../_utils/set-headers"; import { queryTypes as queryTypesList } from "../../_utils/routes-listing"; import { getSchemas, paramsToObject, validate } from "../../_utils/validation"; -import byidHandler from "./_requests/byid"; -import treesbyidsHandler from "./_requests/treesbyids"; -import wateredandadoptedHandler from "./_requests/wateredandadopted"; -import lastwateredHandler from "./_requests/lastwatered"; -import adoptedHandler from "./_requests/adopted"; -import istreeadoptedHandler from "./_requests/istreeadopted"; -import wateredbyuserHandler from "./_requests/wateredbyuser"; +import byidHandler from "../../_requests/get/byid"; +import treesbyidsHandler from "../../_requests/get/treesbyids"; +import wateredandadoptedHandler from "../../_requests/get/wateredandadopted"; +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]); @@ -58,19 +59,32 @@ export default async function handler( case "wateredandadopted": { return await wateredandadoptedHandler(request, response); } + case "lastwatered": { return await lastwateredHandler(request, response); } // 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 938298c3..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,8 +19,21 @@ 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 + */ + const auth0RequestValid = await verifyAuth0Request(request); + if (!auth0RequestValid) { return response.status(401).json({ error: "unauthorized" }); } const { type } = request.query; @@ -56,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/delete/[type].ts b/api/v3/delete/[type].ts new file mode 100644 index 00000000..142f2355 --- /dev/null +++ b/api/v3/delete/[type].ts @@ -0,0 +1,62 @@ +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) { + return response.status(401).json({ error: "unauthorized" }); + } + if (!userData) { + 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/get/[type].ts b/api/v3/get/[type].ts new file mode 100644 index 00000000..9cf90c8f --- /dev/null +++ b/api/v3/get/[type].ts @@ -0,0 +1,95 @@ +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 byidHandler from "../../../_requests/get/byid"; +import treesbyidsHandler from "../../../_requests/get/treesbyids"; +import wateredandadoptedHandler from "../../../_requests/get/wateredandadopted"; +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(`/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) { + 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 "treesbyids": { + return await treesbyidsHandler(request, response); + } + case "wateredandadopted": { + return await wateredandadoptedHandler(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) { + return response.status(401).json({ error: "unauthorized" }); + } + if (!userData) { + 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/api/v3/post/[type].ts b/api/v3/post/[type].ts new file mode 100644 index 00000000..28729a3b --- /dev/null +++ b/api/v3/post/[type].ts @@ -0,0 +1,63 @@ +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) { + return response.status(401).json({ error: "unauthorized" }); + } + if (!userData) { + 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); + } + } +} diff --git a/docs/api.http b/docs/api.http index 0d3eafea..baef8fe2 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.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjc5NDE4Mzc1LCJzdWIiOiI2NmE2ZWNjZS1hZDNhLTRkODctOTIzZS02OTFhMzFhYTMyNjMiLCJlbWFpbCI6ImZhYmlhbm1vcm9uemlyZmFzQHByb3Rvbm1haWwuY2giLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7fSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTY3OTQxNDc3NX1dLCJzZXNzaW9uX2lkIjoiOWYxYWM4OWItYTU4NC00Mjg5LWIxYWItY2IxNTBmYWZlMmVkIn0.QKf89g4ASbyC4txyHBWh7A_-nQyL93uY694S1Wm8TIY + # @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,106 @@ 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 +240,9 @@ Content-Type: application/json + + + ##### ####### ####### # # # # # # # @@ -185,7 +300,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 { @@ -247,7 +362,6 @@ Content-Type: application/json } - # β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ # β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ # β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ @@ -298,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 + diff --git a/package-lock.json b/package-lock.json index 0fa53db6..83b4ec36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ }, "devDependencies": { "@faker-js/faker": "7.6.0", - "@saithodev/semantic-release-backmerge": "2.1.2", + "@saithodev/semantic-release-backmerge": "2.2.0", "@technologiestiftung/semantic-release-config": "1.1.0", "@types/geojson": "7946.0.10", "@types/jest": "29.4.0", @@ -3520,17 +3520,17 @@ ] }, "node_modules/@saithodev/semantic-release-backmerge": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@saithodev/semantic-release-backmerge/-/semantic-release-backmerge-2.1.2.tgz", - "integrity": "sha512-fNd8cmijjFIMp4GcdTAcug/7tr4k+8bAyvSsbLOnfyKCWyq42lg14vFZOryLiyLUAe8gpPlI7XzDPWyFTR5zug==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@saithodev/semantic-release-backmerge/-/semantic-release-backmerge-2.2.0.tgz", + "integrity": "sha512-ZPqFNtF84Y9GhoiWZnd59+Sg/PS7/KYfpwGC76xnZiZqieEhF5lcKkkYbee/riDPTxNhz2PXK20qs9duYDh+ig==", "dev": true, "dependencies": { "@semantic-release/error": "^2.2.0 || ^3.0.0", "aggregate-error": "^3.1.0", - "debug": "^4.3.2", + "debug": "^4.3.4", "execa": "^5.1.1", "lodash": "^4.17.21", - "semantic-release": ">=13.0.0" + "semantic-release": ">=13.0.0 <20" } }, "node_modules/@semantic-release/changelog": { @@ -23262,17 +23262,17 @@ "optional": true }, "@saithodev/semantic-release-backmerge": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@saithodev/semantic-release-backmerge/-/semantic-release-backmerge-2.1.2.tgz", - "integrity": "sha512-fNd8cmijjFIMp4GcdTAcug/7tr4k+8bAyvSsbLOnfyKCWyq42lg14vFZOryLiyLUAe8gpPlI7XzDPWyFTR5zug==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@saithodev/semantic-release-backmerge/-/semantic-release-backmerge-2.2.0.tgz", + "integrity": "sha512-ZPqFNtF84Y9GhoiWZnd59+Sg/PS7/KYfpwGC76xnZiZqieEhF5lcKkkYbee/riDPTxNhz2PXK20qs9duYDh+ig==", "dev": true, "requires": { "@semantic-release/error": "^2.2.0 || ^3.0.0", "aggregate-error": "^3.1.0", - "debug": "^4.3.2", + "debug": "^4.3.4", "execa": "^5.1.1", "lodash": "^4.17.21", - "semantic-release": ">=13.0.0" + "semantic-release": ">=13.0.0 <20" } }, "@semantic-release/changelog": { diff --git a/package.json b/package.json index 4e55ae17..f0274ad7 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" @@ -22,7 +22,7 @@ }, "devDependencies": { "@faker-js/faker": "7.6.0", - "@saithodev/semantic-release-backmerge": "2.1.2", + "@saithodev/semantic-release-backmerge": "2.2.0", "@technologiestiftung/semantic-release-config": "1.1.0", "@types/geojson": "7946.0.10", "@types/jest": "29.4.0", diff --git a/release.config.cjs b/release.config.cjs index e63a5461..44142757 100644 --- a/release.config.cjs +++ b/release.config.cjs @@ -1,16 +1,16 @@ module.exports = { - extends: "@technologiestiftung/semantic-release-config", - branches: [ - { name: "master" }, - { name: "staging", channel: "pre/rc", prerelease: "rc" }, - ], - plugins: [ - [ - "@saithodev/semantic-release-backmerge", - { - branch: [{ from: "master", to: "staging" }], - backmergeStrategy: "merge", - }, - ], - ], + extends: "@technologiestiftung/semantic-release-config", + branches: [ + { name: "master" }, + { name: "staging", channel: "pre/rc", prerelease: "rc" }, + ], + plugins: [ + [ + "@saithodev/semantic-release-backmerge", + { + backmergeBranches: [{ from: "master", to: "staging" }], + backmergeStrategy: "merge", + }, + ], + ], }; diff --git a/supabase/config.toml b/supabase/config.toml index c7b51dda..a6361c6a 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -12,7 +12,7 @@ schemas = [] extra_search_path = ["extensions"] # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size # for accidental or malicious requests. -max_rows = 100 +max_rows = 10000 [db] # Port to use for the local database URL. @@ -42,7 +42,10 @@ file_size_limit = "50MiB" # in emails. site_url = "http://localhost:3000" # A list of *exact* URLs that auth providers are permitted to redirect to post authentication. -additional_redirect_urls = ["https://localhost:3000"] +additional_redirect_urls = [ + "https://localhost:3000", + "https://localhost:3000/reset-password" +] # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one # week). jwt_expiry = 3600 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 (); + 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$; + 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"; + 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$; + 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)); + diff --git a/supabase/migrations/20230329185408_username_constraints.sql b/supabase/migrations/20230329185408_username_constraints.sql new file mode 100644 index 00000000..3fbeaffc --- /dev/null +++ b/supabase/migrations/20230329185408_username_constraints.sql @@ -0,0 +1,88 @@ +CREATE UNIQUE INDEX username_unique_constraint ON public.profiles USING btree (username); + +ALTER TABLE "public"."profiles" + ADD CONSTRAINT "username_length_constraint" CHECK (((length(username) >= 3) AND (length(username) <= 50))) NOT valid; + +ALTER TABLE "public"."profiles" validate CONSTRAINT "username_length_constraint"; + +ALTER TABLE "public"."profiles" + ADD CONSTRAINT "username_unique_constraint" UNIQUE USING INDEX "username_unique_constraint"; + +SET check_function_bodies = OFF; + +CREATE OR REPLACE FUNCTION public.username_append_uuid () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $function$ +BEGIN + IF EXISTS ( + SELECT + 1 + FROM + public.profiles + WHERE + username = NEW.username) THEN + NEW.username := NEW.username || '-' || TRIM(BOTH FROM SUBSTRING( + LEFT (CAST(uuid_generate_v4 () AS text), 8), 1, 6)); +END IF; + RETURN NEW; +END; +$function$; + +CREATE TRIGGER username_check_trigger + BEFORE INSERT OR UPDATE ON public.profiles + FOR EACH ROW + EXECUTE FUNCTION username_append_uuid (); + +CREATE OR REPLACE FUNCTION public.delete_user () + RETURNS TRIGGER + LANGUAGE plpgsql + SECURITY DEFINER + AS $function$ +DECLARE + row_count int; +BEGIN + DELETE FROM public.profiles p + WHERE p.id = OLD.id; + IF found THEN + GET DIAGNOSTICS row_count = ROW_COUNT; + RAISE NOTICE 'DELETEd % row(s) FROM profiles', row_count; + END IF; + UPDATE + trees_watered + SET + uuid = NULL, + username = NULL + WHERE + uuid = OLD.id::text; + DELETE FROM trees_adopted ta + WHERE ta.uuid = OLD.id::text; + RETURN OLD; +END; +$function$; + +CREATE OR REPLACE FUNCTION public.remove_account () + RETURNS void + LANGUAGE sql + SECURITY DEFINER + AS $function$ + DELETE FROM auth.users + WHERE id = auth.uid (); + +$function$; + +CREATE OR REPLACE FUNCTION public.update_username_on_trees_watered () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $function$ +BEGIN + UPDATE + trees_watered + SET + username = NEW.username + WHERE + uuid = OLD.id::text; + RETURN NEW; +END; +$function$; + diff --git a/supabase/migrations/20230419150648_unique_username_case_insensitive.sql b/supabase/migrations/20230419150648_unique_username_case_insensitive.sql new file mode 100644 index 00000000..4c96effc --- /dev/null +++ b/supabase/migrations/20230419150648_unique_username_case_insensitive.sql @@ -0,0 +1,13 @@ +CREATE EXTENSION IF NOT EXISTS "citext" WITH SCHEMA "extensions"; + +ALTER TABLE "public"."profiles" + DROP CONSTRAINT "username_length_constraint"; + +ALTER TABLE "public"."profiles" + ALTER COLUMN "username" SET data TYPE citext USING "username"::citext; + +ALTER TABLE "public"."profiles" + ADD CONSTRAINT "username_length_constraint" CHECK (((length((username)::text) >= 3) AND (length((username)::text) <= 50))) NOT valid; + +ALTER TABLE "public"."profiles" validate CONSTRAINT "username_length_constraint"; + diff --git a/supabase/migrations/20230419165714_fix_username_uuid_trigger.sql b/supabase/migrations/20230419165714_fix_username_uuid_trigger.sql new file mode 100644 index 00000000..ba642211 --- /dev/null +++ b/supabase/migrations/20230419165714_fix_username_uuid_trigger.sql @@ -0,0 +1,21 @@ +SET check_function_bodies = OFF; + +CREATE OR REPLACE FUNCTION public.username_append_uuid() + RETURNS TRIGGER + LANGUAGE plpgsql + AS $function$ +BEGIN + IF EXISTS( + SELECT + 1 + FROM + public.profiles + WHERE + username = NEW.username) THEN + NEW.username := NEW.username || '-' || TRIM(BOTH FROM SUBSTRING( + LEFT(extensions.uuid_generate_v4()::text, 8), 1, 6)); +END IF; + RETURN NEW; +END; +$function$; + diff --git a/supabase/migrations/20230419180719_chore_make_delete_silent.sql b/supabase/migrations/20230419180719_chore_make_delete_silent.sql new file mode 100644 index 00000000..1aac87c3 --- /dev/null +++ b/supabase/migrations/20230419180719_chore_make_delete_silent.sql @@ -0,0 +1,31 @@ +set check_function_bodies = off; + +CREATE OR REPLACE FUNCTION public.delete_user() + RETURNS trigger + LANGUAGE plpgsql + SECURITY DEFINER +AS $function$ +DECLARE + row_count int; +BEGIN + DELETE FROM public.profiles p + WHERE p.id = OLD.id; +-- IF found THEN +-- GET DIAGNOSTICS row_count = ROW_COUNT; +-- RAISE NOTICE 'DELETEd % row(s) FROM profiles', row_count; +-- END IF; + UPDATE + trees_watered + SET + uuid = NULL, + username = NULL + WHERE + uuid = OLD.id::text; + DELETE FROM trees_adopted ta + WHERE ta.uuid = OLD.id::text; + RETURN OLD; +END; +$function$ +; + + diff --git a/supabase/migrations/20230510084821_fix_delete_user_function.sql b/supabase/migrations/20230510084821_fix_delete_user_function.sql new file mode 100644 index 00000000..4fc114e6 --- /dev/null +++ b/supabase/migrations/20230510084821_fix_delete_user_function.sql @@ -0,0 +1,28 @@ +SET check_function_bodies = OFF; + +CREATE OR REPLACE FUNCTION public.delete_user() + RETURNS TRIGGER + LANGUAGE plpgsql + SECURITY DEFINER + AS $function$ +DECLARE + row_count int; +BEGIN + DELETE FROM public.profiles p + WHERE p.id = OLD.id; + -- IF found THEN + -- GET DIAGNOSTICS row_count = ROW_COUNT; + -- RAISE NOTICE 'DELETEd % row(s) FROM profiles', row_count; + -- END IF; + UPDATE + public.trees_watered + SET + uuid = NULL, + username = NULL + WHERE + uuid = OLD.id::text; + DELETE FROM public.trees_adopted ta + WHERE ta.uuid = OLD.id::text; + RETURN OLD; +END; +$function$ diff --git a/supabase/migrations/20230511150457_remove_time_column.sql b/supabase/migrations/20230511150457_remove_time_column.sql new file mode 100644 index 00000000..f4bc2d54 --- /dev/null +++ b/supabase/migrations/20230511150457_remove_time_column.sql @@ -0,0 +1,3 @@ +alter table "public"."trees_watered" drop column "time"; + + diff --git a/supabase/tests/database/unique_names.test.sql b/supabase/tests/database/unique_names.test.sql new file mode 100644 index 00000000..2f97e1ba --- /dev/null +++ b/supabase/tests/database/unique_names.test.sql @@ -0,0 +1,6 @@ +begin; +select plan(1); -- only one statement to run + + +SELECT * FROM finish(); +rollback; 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"