Restrict internal/send-search-notifications with API key

This commit is contained in:
MartinBraquet
2025-09-16 17:54:13 +02:00
parent cf125c1b48
commit 0447b22dd2
8 changed files with 72 additions and 19 deletions

View File

@@ -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=<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 isnt 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)
```

View File

@@ -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<k> } = {
'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()

View File

@@ -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(

View File

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

View File

@@ -20,7 +20,7 @@ export class APIError extends Error {
}
}
export function pathWithPrefix(path: APIPath) {
export function pathWithPrefix(path: string) {
return `v0/${path}`
}

View File

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

View File

@@ -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) {

View File

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