feat: allow wildcard web origins for local tunnel testing

This commit is contained in:
陈煜 committed 2026-10-01 17:58:28 +08:00
1 parent 2a650853ee
commit e4f4ff42e1
9 files changed
+115 -70

No files matched your search

+5 -4
View File
@@ -1,4 +1,5 @@
DATABASE_URL="mysql://USER:PASSWORD@HOST:3306/worthpath"
PORT=3100
WEB_ORIGIN=http://localhost:5173
COOKIE_SECURE=false
DATABASE_URL="mysql://USER:PASSWORD@HOST:3306/worthpath"
PORT=3100
# Local tunnel testing may use WEB_ORIGIN=*. Production requires an exact origin.
WEB_ORIGIN=http://localhost:5173
COOKIE_SECURE=false
+19 -1
View File
@@ -22,6 +22,20 @@ import { credentials } from './validation';
export type UserRequest = Request & { userId: string; sessionId: string; revealed: boolean };
const Public = () => SetMetadata('public', true);
const digest = (s: string) => createHash('sha256').update(s).digest('hex');
export function allowedOrigin(
origin: string | undefined,
configured: string | undefined,
production = false,
) {
if (configured !== '*') return !!origin && origin === configured;
if (production || !origin) return false;
try {
const url = new URL(origin);
return ['http:', 'https:'].includes(url.protocol) && url.origin === origin;
} catch {
return false;
}
}
@Injectable()
export class AuthService {
private attempts = new Map<string, { count: number; until: number }>();
@@ -87,7 +101,11 @@ export class AuthGuard implements CanActivate {
const req = ctx.switchToHttp().getRequest<UserRequest>();
if (
!['GET', 'HEAD', 'OPTIONS'].includes(req.method) &&
req.headers.origin !== process.env.WEB_ORIGIN
!allowedOrigin(
req.headers.origin,
process.env.WEB_ORIGIN,
process.env.NODE_ENV === 'production',
)
)
throw new ForbiddenException('请求来源不受信任');
if (this.reflector.get<boolean>('public', ctx.getHandler())) return true;
+2
View File
@@ -48,6 +48,8 @@ class AppModule {}
async function bootstrap() {
if (!process.env.DATABASE_URL || !process.env.WEB_ORIGIN)
throw Error('Missing local environment configuration');
if (process.env.NODE_ENV === 'production' && process.env.WEB_ORIGIN === '*')
throw Error('Production requires an explicit web origin');
if (process.env.NODE_ENV === 'production' && process.env.COOKIE_SECURE !== 'true')
throw Error('Production requires secure cookies');
const app = await NestFactory.create(AppModule, { logger: false, bodyParser: false });
+16
View File
@@ -6,6 +6,22 @@ import { positionInput, date, amount, businessDate, toBusinessDate } from '../sr
import { validateBackup } from '../src/backup';
import { RatesService } from '../src/rates';
import { Database } from '../src/database';
import { allowedOrigin } from '../src/auth';
test('development wildcard accepts HTTP origins while exact and production checks stay strict', () => {
assert.equal(allowedOrigin('https://tunnel.example', '*'), true);
assert.equal(allowedOrigin('http://example.test:8080', '*'), true);
for (const origin of [
undefined,
'null',
'file://host',
'https://example.test/path',
'https://name:secret@example.test',
])
assert.equal(allowedOrigin(origin, '*'), false);
assert.equal(allowedOrigin('https://tunnel.example', '*', true), false);
assert.equal(allowedOrigin('https://tunnel.example', 'http://localhost:5173'), false);
assert.equal(allowedOrigin('http://localhost:5173', 'http://localhost:5173'), true);
});
const rev = (amount: string, day: string) => ({
id: randomUUID(),
amount,
+3 -3
View File
@@ -6,7 +6,7 @@ import { PrismaClient } from '@prisma/client';
import { today } from '../src/validation';
import { readBackupZip } from '../src/zip';
const base = process.env.TEST_API_URL || 'http://127.0.0.1:3100/api',
origin = process.env.WEB_ORIGIN!;
origin = process.env.WEB_ORIGIN === '*' ? 'http://localhost:5173' : process.env.WEB_ORIGIN!;
test('real MySQL: authentication, isolation, history, backup and atomic failures', async () => {
const db = new PrismaClient(),
created: { id: string; username: string }[] = [];
@@ -53,10 +53,10 @@ test('real MySQL: authentication, isolation, history, backup and atomic failures
'Content-Type': 'application/json',
Origin: 'https://untrusted.invalid',
},
body: JSON.stringify({ baseCurrency: 'USD' }),
body: JSON.stringify({ showSidebar: true }),
})
).status,
403,
process.env.WEB_ORIGIN === '*' ? 200 : 403,
);
const make = async (
kind: string,
+6 -2
View File
@@ -12,7 +12,7 @@ test('privacy, minute history, backup-gated clear and idle sessions remain user
const res = await fetch(base + path, {
method,
headers: {
Origin: process.env.WEB_ORIGIN!,
Origin: process.env.WEB_ORIGIN === '*' ? 'http://localhost:5173' : process.env.WEB_ORIGIN!,
Cookie: cookie,
...(data ? { 'Content-Type': 'application/json' } : {}),
},
@@ -131,7 +131,11 @@ test('privacy, minute history, backup-gated clear and idle sessions remain user
form.append('file', new Blob([content]), 'backup.zip');
const res = await fetch(base + '/backup/upload', {
method: 'POST',
headers: { Origin: process.env.WEB_ORIGIN!, Cookie: cookie },
headers: {
Origin:
process.env.WEB_ORIGIN === '*' ? 'http://localhost:5173' : process.env.WEB_ORIGIN!,
Cookie: cookie,
},
body: form,
});
return { status: res.status, data: await res.json() };