From 0447b22dd2a97efd65837cac9642ed3a5993d1d4 Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Tue, 16 Sep 2025 17:54:13 +0200 Subject: [PATCH] Restrict internal/send-search-notifications with API key --- backend/api/README.md | 33 +++++++++++++++++--- backend/api/src/app.ts | 25 +++++++++++++-- backend/api/src/send-search-notifications.ts | 3 +- common/src/api/schema.ts | 8 ----- common/src/api/utils.ts | 2 +- common/src/secrets.ts | 1 + common/src/util/api.ts | 1 + scripts/curl.sh | 18 ++++++++++- 8 files changed, 72 insertions(+), 19 deletions(-) diff --git a/backend/api/README.md b/backend/api/README.md index 359a22e2..a5dae269 100644 --- a/backend/api/README.md +++ b/backend/api/README.md @@ -8,26 +8,31 @@ It runs in a docker inside a Google Cloud virtual machine. You must have the `gcloud` CLI. On MacOS: + ```bash brew install --cask google-cloud-sdk ``` On Linux: + ```bash sudo apt-get update && sudo apt-get install google-cloud-sdk ``` Then: + ```bash gcloud init gcloud auth login gcloud config set project YOUR_PROJECT_ID ``` + ### Setup This section is only for the people who are creating a server from scratch, for instance for a forked project. One-time commands you may need to run: + ```bash gcloud artifacts repositories create builds \ --repository-format=docker \ @@ -51,6 +56,20 @@ gcloud projects add-iam-policy-binding compass-130ba \ gcloud run services list ``` +Set up the saved search notifications job: + +```bash +gcloud scheduler jobs create http daily-saved-search-notifications \ + --schedule="0 19 * * *" \ + --uri="https://api.compassmeet.com/internal/send-search-notifications" \ + --http-method=POST \ + --headers="x-api-key=" \ + --time-zone="UTC" \ + --location=us-west1 +``` + +View it [here](https://console.cloud.google.com/cloudscheduler). + ##### DNS * After deployment, Terraform assigns a static external IP to this resource. @@ -60,6 +79,7 @@ gcloud run services list gcloud compute addresses describe api-lb-ip-2 --global --format="get(address)" 34.117.20.215 ``` + Since Vercel manages your domain (`compassmeet.com`): 1. Log in to [Vercel dashboard](https://vercel.com/dashboard). @@ -67,7 +87,7 @@ Since Vercel manages your domain (`compassmeet.com`): 3. Add an **A record** for your API subdomain: | Type | Name | Value | TTL | -| ---- | ---- | ------------ | ----- | +|------|------|--------------|-------| | A | api | 34.123.45.67 | 600 s | * `Name` is just the subdomain: `api` → `api.compassmeet.com`. @@ -85,7 +105,6 @@ curl -I https://api.compassmeet.com * `nslookup` should return the LB IP (`34.123.45.67`). * `curl -I` should return `200 OK` from your service. - If SSL isn’t ready (may take 15 mins), check LB logs: ```bash @@ -96,7 +115,9 @@ gcloud compute ssl-certificates describe api-lb-cert-2 Secrets are strings that shouldn't be checked into Git (eg API keys, passwords). -Add the secrets for your specific project in [Google Cloud Secrets manager](https://console.cloud.google.com/security/secret-manager), so that the virtual machine can access them. +Add the secrets for your specific project +in [Google Cloud Secrets manager](https://console.cloud.google.com/security/secret-manager), so that the virtual machine +can access them. For Compass, the name of the secrets are in [secrets.ts](../../common/src/secrets.ts). @@ -111,13 +132,16 @@ In root directory, run the local api with hot reload, along with all the other b ### Deploy Run in this directory to deploy your code to the server. + ```bash ./deploy-api.sh prod ``` ### Connect to the server -Run in this directory to connect to the API server running as virtual machine in Google Cloud. You can access logs, files, debug, etc. +Run in this directory to connect to the API server running as virtual machine in Google Cloud. You can access logs, +files, debug, etc. + ```bash ./ssh-api.sh prod ``` @@ -130,5 +154,4 @@ sudo docker logs -f $(sudo docker ps -alq) docker exec -it $(sudo docker ps -alq) sh docker run -it --rm $(docker images -q | head -n 1) sh docker rmi -f $(docker images -aq) - ``` diff --git a/backend/api/src/app.ts b/backend/api/src/app.ts index 736e47ac..f070dda7 100644 --- a/backend/api/src/app.ts +++ b/backend/api/src/app.ts @@ -23,7 +23,6 @@ import {getDisplayUser, getUser} from './get-user' import {getMe} from './get-me' import {hasFreeLike} from './has-free-like' import {health} from './health' -import {sendSearchNotifications} from './send-search-notifications' import {type APIHandler, typedEndpoint} from './helpers/endpoint' import {hideComment} from './hide-comment' import {likeLover} from './like-lover' @@ -53,6 +52,7 @@ import {getNotifications} from './get-notifications' import {updateNotifSettings} from './update-notif-setting' import swaggerUi from "swagger-ui-express" import * as fs from "fs" +import {sendSearchNotifications} from "api/send-search-notifications"; const allowCorsUnrestricted: RequestHandler = cors({}) @@ -166,7 +166,6 @@ const handlers: { [k in APIPath]: APIHandler } = { 'get-channel-messages': getChannelMessages, 'get-channel-seen-time': getLastSeenChannelTime, 'set-channel-seen-time': setChannelLastSeenTime, - 'send-search-notifications': sendSearchNotifications, } Object.entries(handlers).forEach(([path, handler]) => { @@ -194,6 +193,28 @@ Object.entries(handlers).forEach(([path, handler]) => { } }) +console.log('COMPASS_API_KEY:', process.env.COMPASS_API_KEY) + +// Internal Endpoints +app.post( + '/' + pathWithPrefix("internal/send-search-notifications"), + async (req, res) => { + const apiKey = req.header("x-api-key"); + if (apiKey !== process.env.COMPASS_API_KEY) { + return res.status(401).json({error: "Unauthorized"}); + } + + try { + const result = await sendSearchNotifications() + return res.status(200).json(result) + } catch (err) { + console.error("Failed to send notifications:", err); + return res.status(500).json({error: "Internal server error"}); + } + } +); + + app.use(allowCorsUnrestricted, (req, res) => { if (req.method === 'OPTIONS') { res.status(200).send() diff --git a/backend/api/src/send-search-notifications.ts b/backend/api/src/send-search-notifications.ts index d2c53f25..fcb8ce1a 100644 --- a/backend/api/src/send-search-notifications.ts +++ b/backend/api/src/send-search-notifications.ts @@ -1,4 +1,3 @@ -import {APIHandler} from './helpers/endpoint' import {createSupabaseDirectClient} from "shared/supabase/init"; import {from, renderSql, select} from "shared/supabase/sql-builder"; import {loadProfiles, profileQueryType} from "api/get-profiles"; @@ -18,7 +17,7 @@ export const notifyBookmarkedSearch = async (matches: MatchesByUserType) => { } } -export const sendSearchNotifications: APIHandler<'send-search-notifications'> = async (_, auth) => { +export const sendSearchNotifications = async () => { const pg = createSupabaseDirectClient() const search_query = renderSql( diff --git a/common/src/api/schema.ts b/common/src/api/schema.ts index 080c2415..e992f86f 100644 --- a/common/src/api/schema.ts +++ b/common/src/api/schema.ts @@ -51,14 +51,6 @@ export const API = (_apiTypeCheck = { props: z.object({}), returns: {} as { jwt: string }, }, - 'send-search-notifications': { - method: 'POST', - authed: false, - props: z.object({}), - returns: {} as { - status: 'success' | 'fail' - }, - }, 'mark-all-notifs-read': { method: 'POST', authed: true, diff --git a/common/src/api/utils.ts b/common/src/api/utils.ts index 9e450637..22640a6d 100644 --- a/common/src/api/utils.ts +++ b/common/src/api/utils.ts @@ -20,7 +20,7 @@ export class APIError extends Error { } } -export function pathWithPrefix(path: APIPath) { +export function pathWithPrefix(path: string) { return `v0/${path}` } diff --git a/common/src/secrets.ts b/common/src/secrets.ts index 010bebbb..ab02215b 100644 --- a/common/src/secrets.ts +++ b/common/src/secrets.ts @@ -15,6 +15,7 @@ export const secrets = ( 'TEST_CREATE_USER_KEY', 'GEODB_API_KEY', 'RESEND_KEY', + 'COMPASS_API_KEY', 'NEXT_PUBLIC_FIREBASE_API_KEY', // Some typescript voodoo to keep the string literal types while being not readonly. ] as const diff --git a/common/src/util/api.ts b/common/src/util/api.ts index 9005cd3b..05c1e365 100644 --- a/common/src/util/api.ts +++ b/common/src/util/api.ts @@ -67,6 +67,7 @@ export async function baseApiCall(props: { body: params == null || method === 'GET' ? undefined : JSON.stringify(params), }) + // console.log(req) return fetch(req).then(async (resp) => { const json = (await resp.json()) as { [k: string]: any } if (!resp.ok) { diff --git a/scripts/curl.sh b/scripts/curl.sh index d9e7d35c..fa722562 100755 --- a/scripts/curl.sh +++ b/scripts/curl.sh @@ -1,5 +1,21 @@ #!/bin/bash -curl -X POST http://localhost:8088/v0/send-search-notifications +set -e +cd "$(dirname "$0")"/.. + +source .env + +#export url=http://localhost:8088/v0 +export url=https://api.compassmeet.com + +export endpoint=/internal/send-search-notifications + +curl -X POST ${url}${endpoint} \ + -H "x-api-key: ${COMPASS_API_KEY}" \ + -H "Content-Type: application/json" \ + -d '{}' + +echo +