tunnel: clear stale kill-switch key on subdomain rename
When a user renames their tunnel subdomain, the upsert (onConflict=user_id) overwrote the tunnels row in place, but the old subdomain's Redis tunnel:active:<sub> flag was never cleared. The edge tunnel-active gate kept serving the stale flag for up to its ~30s TTL, after which it fell back to a Postgres miss and (currently) returned a misleading suspended response. Capture the user's previous subdomain before the upsert and, when it changes, DEL the old kill-switch key (new clearTunnelActive helper) so the edge gate immediately stops treating the old subdomain as a live tunnel.
This commit is contained in:
@@ -3,7 +3,7 @@ import { randomBytes } from 'node:crypto';
|
|||||||
import { getSupabaseAdmin } from '@/lib/supabase/admin';
|
import { getSupabaseAdmin } from '@/lib/supabase/admin';
|
||||||
import { jsonNoStore } from '@/lib/admin/response';
|
import { jsonNoStore } from '@/lib/admin/response';
|
||||||
import { validateSubdomain } from '@/lib/validation';
|
import { validateSubdomain } from '@/lib/validation';
|
||||||
import { setTunnelActive } from '@/lib/redis';
|
import { setTunnelActive, clearTunnelActive } from '@/lib/redis';
|
||||||
import { hashToken, FRP_SERVER_ADDR, FRP_SERVER_PORT } from '@/lib/device';
|
import { hashToken, FRP_SERVER_ADDR, FRP_SERVER_PORT } from '@/lib/device';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
@@ -96,6 +96,16 @@ export async function POST(req: NextRequest) {
|
|||||||
return jsonNoStore({ error: 'subdomain taken' }, { status: 409 });
|
return jsonNoStore({ error: 'subdomain taken' }, { status: 409 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Capture the user's CURRENT subdomain (if any) before the upsert overwrites
|
||||||
|
// it. On a rename we must clear the old subdomain's edge kill-switch key,
|
||||||
|
// otherwise it lingers as "active" until its ~30s TTL expires.
|
||||||
|
const { data: current } = await admin
|
||||||
|
.from('tunnels')
|
||||||
|
.select('subdomain')
|
||||||
|
.eq('user_id', device.user_id)
|
||||||
|
.maybeSingle<{ subdomain: string }>();
|
||||||
|
const previousSubdomain = current?.subdomain ?? null;
|
||||||
|
|
||||||
const token = randomBytes(32).toString('hex');
|
const token = randomBytes(32).toString('hex');
|
||||||
const { data, error } = await admin
|
const { data, error } = await admin
|
||||||
.from('tunnels')
|
.from('tunnels')
|
||||||
@@ -114,6 +124,15 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
await setTunnelActive(subdomain, true);
|
await setTunnelActive(subdomain, true);
|
||||||
|
|
||||||
|
// Rename: drop the old subdomain's kill-switch key so the edge gate stops
|
||||||
|
// treating it as a live tunnel. With the key gone the gate falls through to a
|
||||||
|
// Postgres lookup, finds no row, and returns a clean "tunnel not found"
|
||||||
|
// instead of leaving a stale "active" flag pointing at a subdomain that no
|
||||||
|
// longer exists.
|
||||||
|
if (previousSubdomain && previousSubdomain !== data.subdomain) {
|
||||||
|
await clearTunnelActive(previousSubdomain);
|
||||||
|
}
|
||||||
|
|
||||||
return jsonNoStore({
|
return jsonNoStore({
|
||||||
subdomain: data.subdomain,
|
subdomain: data.subdomain,
|
||||||
token: data.token,
|
token: data.token,
|
||||||
|
|||||||
@@ -169,3 +169,21 @@ export async function setTunnelActive(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the live kill-switch flag for a subdomain (DEL tunnel:active:<sub>).
|
||||||
|
* Used when a subdomain is renamed/released so its stale flag does not linger:
|
||||||
|
* with the key gone, the edge gate falls through to its Postgres lookup, finds
|
||||||
|
* no tunnel row, and returns a clean "tunnel not found" instead of the
|
||||||
|
* "suspended: quota exceeded or inactive" page. No-op (false) when REDIS_URL is
|
||||||
|
* unset. Never throws.
|
||||||
|
*/
|
||||||
|
export async function clearTunnelActive(subdomain: string): Promise<boolean> {
|
||||||
|
if (!process.env.REDIS_URL) return false;
|
||||||
|
if (!subdomain) return false;
|
||||||
|
try {
|
||||||
|
return await redisDel(`tunnel:active:${subdomain}`);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user