Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions apps/redis/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Local Redis Stack (Optional)

This folder wires up a Redis + Serverless Redis HTTP (SRH) stack for local testing. The platform defaults to an in-memory store, so you only start this stack when you want Redis-backed state and Upstash-compatible HTTP access.

## Quick start

```bash
# from apps/redis
bun redis:start
```

The `redis:start` script runs `docker compose up -d`, which launches Redis on port 6379 and SRH on port 8079.

## Configure your app

1. **Point your environment at SRH**

```env
UPSTASH_REDIS_REST_URL=http://localhost:8079
UPSTASH_REDIS_REST_TOKEN=example_token
```

With this configuration your app uses the local SRH proxy instead of the default local memory store.

2. **Connect to Redis directly (optional)**

First, find the Redis container name:

```bash
docker compose ps
```

Then connect using `docker exec`:

```bash
docker exec -it <container-name> redis-cli
```

Replace `<container-name>` with the name from `docker compose ps` (typically `redis-redis-1`). From there you can inspect keys, adjust TTLs, and flush state.

## CI and GitHub Actions

Use the same `bun redis:start`/`bun redis:stop` pair in CI jobs if you need a deterministic Redis backend. SRH waits until the first request before dialing Redis, so there is no race condition on startup.

## Tear down

```bash
bun redis:stop
```

This command runs `docker compose down` and removes the containers.

> **Optional reminder:** if you skip running this stack, the app keeps using the in-memory fallback. Running the stack swaps you over to Redis-based storage so your data lives in the containers instead of memory.
13 changes: 13 additions & 0 deletions apps/redis/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
services:
redis:
image: redis
ports:
- "6379:6379"
serverless-redis-http:
ports:
- "8079:80"
image: hiett/serverless-redis-http:latest
environment:
SRH_MODE: env
SRH_TOKEN: example_token
SRH_CONNECTION_STRING: "redis://redis:6379" # Using `redis` hostname since they're in the same Docker network.
14 changes: 14 additions & 0 deletions apps/redis/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "@tuturuuu/redis",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"redis:start": "docker compose up -d",
"redis:stop": "docker compose down"
},
"devDependencies": {
"@upstash/cli": "^0.3.0"
},
"packageManager": "[email protected]"
}
15 changes: 12 additions & 3 deletions apps/web/src/proxy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { match } from '@formatjs/intl-localematcher';
import { updateSession } from '@ncthub/supabase/next/proxy';
import type { SupabaseUser } from '@ncthub/supabase/next/user';
import { guardApiProxyRequest } from '@ncthub/utils/api-proxy-guard';
import Negotiator from 'negotiator';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
Expand All @@ -9,12 +10,20 @@ import { LOCALE_COOKIE_NAME, PUBLIC_PATHS } from './constants/common';
import { defaultLocale, type Locale, supportedLocales } from './i18n/routing';

export async function proxy(req: NextRequest): Promise<NextResponse> {
if (req.nextUrl.pathname.startsWith('/api')) {
const guardResponse = await guardApiProxyRequest(req, {
prefixBase: 'proxy:web:api',
});
if (guardResponse) {
return guardResponse;
}

return NextResponse.next();
}

// Make sure user session is always refreshed
const { res, user } = await updateSession(req);

// If current path starts with /api, return without redirecting
if (req.nextUrl.pathname.startsWith('/api')) return res;

// Handle special cases for public paths
const { res: nextRes, redirect } = handleRedirect({ req, res, user });
if (redirect) return nextRes;
Expand Down
182 changes: 155 additions & 27 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
"lint:fix": "biome lint --write",
"format-and-lint": "biome check",
"format-and-lint:fix": "biome check --write",
"redis:start": "cd apps/redis && bun redis:start",
"redis:stop": "cd apps/redis && bun redis:stop",
"stop": "cd apps/db && bun sb:stop",
"sb:status": "cd apps/db && bun sb:status",
"sb:start": "cd apps/db && bun sb:start",
Expand Down
1 change: 1 addition & 0 deletions packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"packageManager": "[email protected]",
"dependencies": {
"@ncthub/supabase": "workspace:*",
"@upstash/ratelimit": "^2.0.8",
"@upstash/redis": "^1.37.0",
"clsx": "^2.1.1",
"next": "16.2.1",
Expand Down
29 changes: 29 additions & 0 deletions packages/utils/src/abuse-protection/__tests__/edge.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest';
import { extractIPFromRequest } from '../edge';

describe('abuse-protection edge', () => {
describe('extractIPFromRequest', () => {
it('prefers cf-connecting-ip over x-forwarded-for', () => {
const headers = new Headers();
headers.set('cf-connecting-ip', '203.0.113.10');
headers.set('x-forwarded-for', '198.51.100.10, 10.0.0.1');

expect(extractIPFromRequest(headers)).toBe('203.0.113.10');
});

it('falls back to true-client-ip before generic proxy headers', () => {
const headers = new Headers();
headers.set('true-client-ip', '203.0.113.20');
headers.set('x-forwarded-for', '198.51.100.20, 10.0.0.1');

expect(extractIPFromRequest(headers)).toBe('203.0.113.20');
});

it('falls back to x-forwarded-for when cloud proxy headers are absent', () => {
const headers = new Headers();
headers.set('x-forwarded-for', '198.51.100.30, 10.0.0.1');

expect(extractIPFromRequest(headers)).toBe('198.51.100.30');
});
});
});
Loading
Loading