mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
feat(pagination): add pagination utility functions for handling page size and continuation tokens
- Introduced `PaginationRequest` interface to define pagination parameters. - Implemented `parsePagination` function to extract and validate pagination parameters from a URL. - Added `encodeContinuationToken` and `decodeContinuationToken` functions for managing continuation tokens. - Ensured that pagination respects maximum page size limits defined in configuration.
This commit is contained in:
+4
-3
@@ -1,4 +1,5 @@
|
||||
import { JWTPayload } from '../types';
|
||||
import { LIMITS } from '../config/limits';
|
||||
|
||||
// Base64 URL encode
|
||||
function base64UrlEncode(data: Uint8Array): string {
|
||||
@@ -19,7 +20,7 @@ function base64UrlDecode(str: string): Uint8Array {
|
||||
}
|
||||
|
||||
// Create JWT
|
||||
export async function createJWT(payload: Omit<JWTPayload, 'iat' | 'exp' | 'iss' | 'premium' | 'email_verified' | 'amr'>, secret: string, expiresIn: number = 7200): Promise<string> {
|
||||
export async function createJWT(payload: Omit<JWTPayload, 'iat' | 'exp' | 'iss' | 'premium' | 'email_verified' | 'amr'>, secret: string, expiresIn: number = LIMITS.auth.accessTokenTtlSeconds): Promise<string> {
|
||||
const header = { alg: 'HS256', typ: 'JWT' };
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
@@ -90,7 +91,7 @@ export async function verifyJWT(token: string, secret: string): Promise<JWTPaylo
|
||||
|
||||
// Create refresh token (simple random string)
|
||||
export function createRefreshToken(): string {
|
||||
const bytes = new Uint8Array(32);
|
||||
const bytes = new Uint8Array(LIMITS.auth.refreshTokenRandomBytes);
|
||||
crypto.getRandomValues(bytes);
|
||||
return base64UrlEncode(bytes);
|
||||
}
|
||||
@@ -116,7 +117,7 @@ export async function createFileDownloadToken(
|
||||
cipherId,
|
||||
attachmentId,
|
||||
jti: createRefreshToken(),
|
||||
exp: now + 300, // 5 minutes
|
||||
exp: now + LIMITS.auth.fileDownloadTokenTtlSeconds, // 5 minutes
|
||||
};
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { LIMITS } from '../config/limits';
|
||||
|
||||
const MAX_PAGE_SIZE = LIMITS.pagination.maxPageSize;
|
||||
|
||||
export interface PaginationRequest {
|
||||
limit: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
export function parsePagination(url: URL): PaginationRequest | null {
|
||||
const pageSizeRaw = url.searchParams.get('pageSize');
|
||||
const continuationToken = url.searchParams.get('continuationToken');
|
||||
if (!pageSizeRaw && !continuationToken) return null;
|
||||
|
||||
const pageSize = pageSizeRaw ? Number(pageSizeRaw) : LIMITS.pagination.defaultPageSize;
|
||||
if (!Number.isInteger(pageSize) || pageSize <= 0) return null;
|
||||
|
||||
const limit = Math.min(pageSize, MAX_PAGE_SIZE);
|
||||
const offset = decodeContinuationToken(continuationToken);
|
||||
|
||||
return { limit, offset };
|
||||
}
|
||||
|
||||
export function encodeContinuationToken(offset: number): string {
|
||||
return btoa(String(offset));
|
||||
}
|
||||
|
||||
export function decodeContinuationToken(token: string | null): number {
|
||||
if (!token) return 0;
|
||||
try {
|
||||
const decoded = atob(token);
|
||||
const offset = Number(decoded);
|
||||
if (!Number.isInteger(offset) || offset < 0) return 0;
|
||||
return offset;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+69
-14
@@ -1,10 +1,68 @@
|
||||
import { LIMITS } from '../config/limits';
|
||||
|
||||
const CORS_METHODS = 'GET, POST, PUT, DELETE, PATCH, OPTIONS';
|
||||
const CORS_HEADERS = 'Content-Type, Authorization, Accept, Device-Type, Bitwarden-Client-Name, Bitwarden-Client-Version';
|
||||
|
||||
function isTrustedClientOrigin(origin: string): boolean {
|
||||
// Official browser extension / desktop-webview common origins.
|
||||
if (origin === 'null') return true;
|
||||
if (origin.startsWith('chrome-extension://')) return true;
|
||||
if (origin.startsWith('moz-extension://')) return true;
|
||||
if (origin.startsWith('safari-web-extension://')) return true;
|
||||
if (origin.startsWith('app://')) return true;
|
||||
if (origin.startsWith('capacitor://')) return true;
|
||||
if (origin.startsWith('ionic://')) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function getAllowedOrigin(request: Request): string | null {
|
||||
const origin = request.headers.get('Origin');
|
||||
if (!origin) return null;
|
||||
|
||||
const targetOrigin = new URL(request.url).origin;
|
||||
if (origin === targetOrigin) return origin;
|
||||
if (isTrustedClientOrigin(origin)) return origin;
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildCorsHeaders(request: Request): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Access-Control-Allow-Methods': CORS_METHODS,
|
||||
'Access-Control-Allow-Headers': CORS_HEADERS,
|
||||
'Access-Control-Max-Age': String(LIMITS.cors.preflightMaxAgeSeconds),
|
||||
};
|
||||
|
||||
const allowedOrigin = getAllowedOrigin(request);
|
||||
if (allowedOrigin) {
|
||||
headers['Access-Control-Allow-Origin'] = allowedOrigin;
|
||||
headers['Vary'] = 'Origin';
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
export function applyCors(
|
||||
request: Request,
|
||||
response: Response
|
||||
): Response {
|
||||
const headers = new Headers(response.headers);
|
||||
const corsHeaders = buildCorsHeaders(request);
|
||||
for (const [k, v] of Object.entries(corsHeaders)) {
|
||||
headers.set(k, v);
|
||||
}
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
// JSON response helper
|
||||
export function jsonResponse(data: any, status: number = 200, headers: Record<string, string> = {}): Response {
|
||||
return new Response(JSON.stringify(data), {
|
||||
status,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...getCorsHeaders(),
|
||||
...headers,
|
||||
},
|
||||
});
|
||||
@@ -40,21 +98,19 @@ export function identityErrorResponse(message: string, error: string = 'invalid_
|
||||
);
|
||||
}
|
||||
|
||||
// CORS headers
|
||||
export function getCorsHeaders(): Record<string, string> {
|
||||
return {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization, Accept, Device-Type, Bitwarden-Client-Name, Bitwarden-Client-Version',
|
||||
'Access-Control-Max-Age': '86400',
|
||||
};
|
||||
}
|
||||
|
||||
// Handle CORS preflight
|
||||
export function handleCors(): Response {
|
||||
export function handleCors(request: Request): Response {
|
||||
const origin = request.headers.get('Origin');
|
||||
if (origin) {
|
||||
const allowedOrigin = getAllowedOrigin(request);
|
||||
if (!allowedOrigin) {
|
||||
return new Response(null, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
headers: getCorsHeaders(),
|
||||
headers: buildCorsHeaders(request),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -64,7 +120,6 @@ export function htmlResponse(html: string, status: number = 200): Response {
|
||||
status,
|
||||
headers: {
|
||||
'Content-Type': 'text/html; charset=utf-8',
|
||||
...getCorsHeaders(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user