Skip to content
Open
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
28 changes: 9 additions & 19 deletions apps/processor/src/utils/security.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import path from 'path';
// Import canonical file security utilities from @pagespace/lib
import {
DANGEROUS_MIME_TYPES as LIB_DANGEROUS_MIME_TYPES,
isDangerousMimeType as libIsDangerousMimeType
} from '@pagespace/lib';

export const SAFE_EXTENSION_PATTERN = /^[a-z0-9]{1,8}$/i;
export const DEFAULT_EXTENSION = '.bin';
Expand Down Expand Up @@ -96,22 +101,7 @@ export function sanitizeFilename(filename: string | null | undefined): string {
|| 'file';
}

/**
* Dangerous MIME types that can execute JavaScript
*/
export const DANGEROUS_MIME_TYPES = [
'text/html',
'application/xhtml+xml',
'image/svg+xml',
'application/xml',
'text/xml',
] as const;

/**
* Check if MIME type is dangerous (can execute scripts)
*/
export function isDangerousMimeType(mimeType: string | null | undefined): boolean {
if (!mimeType) return false;
const normalized = mimeType.toLowerCase().split(';')[0].trim();
return DANGEROUS_MIME_TYPES.includes(normalized as any);
}
// Re-export file security utilities from @pagespace/lib (canonical source)
// These were previously duplicated here but are now consolidated
export const DANGEROUS_MIME_TYPES = LIB_DANGEROUS_MIME_TYPES;
export const isDangerousMimeType = libIsDangerousMimeType;
4 changes: 2 additions & 2 deletions apps/web/src/app/api/account/devices/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { users, db, eq, deviceTokens, sql, and, isNull } from '@pagespace/db';
import { loggers } from '@pagespace/lib/server';
import { loggers, getClientIP } from '@pagespace/lib/server';
import { authenticateRequestWithOptions, isAuthError } from '@/lib/auth';
import { getUserDeviceTokens, revokeAllUserDeviceTokens, decodeDeviceToken, createDeviceTokenRecord, revokeExpiredDeviceTokens } from '@pagespace/lib/device-auth-utils';

Expand Down Expand Up @@ -142,7 +142,7 @@ export async function DELETE(req: Request) {
{
deviceName: currentDeviceInfo.deviceName || undefined,
userAgent: req.headers.get('user-agent') ?? undefined,
ipAddress: req.headers.get('x-forwarded-for')?.split(',')[0] || req.headers.get('x-real-ip') || undefined,
ipAddress: getClientIP(req) !== 'unknown' ? getClientIP(req) : undefined,
}
);

Expand Down
6 changes: 2 additions & 4 deletions apps/web/src/app/api/auth/device/refresh/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
getRefreshTokenMaxAge,
generateCSRFToken,
getSessionIdFromJWT,
getClientIP,
} from '@pagespace/lib/server';
import { createId } from '@paralleldrive/cuid2';
import { loggers, logAuthEvent } from '@pagespace/lib/server';
Expand All @@ -35,10 +36,7 @@ export async function POST(req: Request) {

const { deviceToken, deviceId, userAgent, appVersion } = validation.data;

const clientIP =
req.headers.get('x-forwarded-for')?.split(',')[0] ||
req.headers.get('x-real-ip') ||
'unknown';
const clientIP = getClientIP(req);

const deviceRecord = await validateDeviceToken(deviceToken);
if (!deviceRecord) {
Expand Down
6 changes: 2 additions & 4 deletions apps/web/src/app/api/auth/google/callback/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { users, refreshTokens } from '@pagespace/db';
import { db, eq, or } from '@pagespace/db';
import { z } from 'zod/v4';
import { generateAccessToken, generateRefreshToken, getRefreshTokenMaxAge, checkRateLimit, resetRateLimit, RATE_LIMIT_CONFIGS, decodeToken, generateCSRFToken, getSessionIdFromJWT, validateOrCreateDeviceToken } from '@pagespace/lib/server';
import { generateAccessToken, generateRefreshToken, getRefreshTokenMaxAge, checkRateLimit, resetRateLimit, RATE_LIMIT_CONFIGS, decodeToken, generateCSRFToken, getSessionIdFromJWT, validateOrCreateDeviceToken, getClientIP } from '@pagespace/lib/server';
import { serialize } from 'cookie';
import { createId } from '@paralleldrive/cuid2';
import { loggers, logAuthEvent } from '@pagespace/lib/server';
Expand Down Expand Up @@ -94,9 +94,7 @@ export async function GET(req: Request) {
}

// Rate limiting by IP address
const clientIP = req.headers.get('x-forwarded-for')?.split(',')[0] ||
req.headers.get('x-real-ip') ||
'unknown';
const clientIP = getClientIP(req);

const ipRateLimit = checkRateLimit(clientIP, RATE_LIMIT_CONFIGS.LOGIN);
if (!ipRateLimit.allowed) {
Expand Down
6 changes: 2 additions & 4 deletions apps/web/src/app/api/auth/google/signin/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from 'zod/v4';
import { checkRateLimit, RATE_LIMIT_CONFIGS } from '@pagespace/lib/server';
import { checkRateLimit, RATE_LIMIT_CONFIGS, getClientIP } from '@pagespace/lib/server';
import { loggers } from '@pagespace/lib/server';
import crypto from 'crypto';

Expand All @@ -19,9 +19,7 @@ export async function POST(req: Request) {
}

// Rate limiting by IP address
const clientIP = req.headers.get('x-forwarded-for')?.split(',')[0] ||
req.headers.get('x-real-ip') ||
'unknown';
const clientIP = getClientIP(req);

const ipRateLimit = checkRateLimit(clientIP, RATE_LIMIT_CONFIGS.LOGIN);
if (!ipRateLimit.allowed) {
Expand Down
5 changes: 2 additions & 3 deletions apps/web/src/app/api/auth/login/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
RATE_LIMIT_CONFIGS,
decodeToken,
validateOrCreateDeviceToken,
getClientIP,
} from '@pagespace/lib/server';
import { serialize } from 'cookie';
import { createId } from '@paralleldrive/cuid2';
Expand Down Expand Up @@ -41,9 +42,7 @@ export async function POST(req: Request) {
const { email, password, deviceId, deviceName, deviceToken: existingDeviceToken } = validation.data;

// Rate limiting by IP address and email
const clientIP = req.headers.get('x-forwarded-for')?.split(',')[0] ||
req.headers.get('x-real-ip') ||
'unknown';
const clientIP = getClientIP(req);

const ipRateLimit = checkRateLimit(clientIP, RATE_LIMIT_CONFIGS.LOGIN);
const emailRateLimit = checkRateLimit(email.toLowerCase(), RATE_LIMIT_CONFIGS.LOGIN);
Expand Down
6 changes: 2 additions & 4 deletions apps/web/src/app/api/auth/logout/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { refreshTokens } from '@pagespace/db';
import { db, eq } from '@pagespace/db';
import { parse, serialize } from 'cookie';
import { loggers, logAuthEvent } from '@pagespace/lib/server';
import { loggers, logAuthEvent, getClientIP } from '@pagespace/lib/server';
import { trackAuthEvent } from '@pagespace/lib/activity-tracker';
import { revokeDeviceTokenByValue, revokeDeviceTokensByDevice } from '@pagespace/lib/device-auth-utils';
import { authenticateRequestWithOptions, isAuthError } from '@/lib/auth';
Expand All @@ -16,9 +16,7 @@ export async function POST(req: Request) {
const cookies = parse(cookieHeader || '');
const refreshTokenValue = cookies.refreshToken;

const clientIP = req.headers.get('x-forwarded-for')?.split(',')[0] ||
req.headers.get('x-real-ip') ||
'unknown';
const clientIP = getClientIP(req);

// Revoke device token to ensure proper device separation
// This prevents token reuse when logging back in on different devices
Expand Down
5 changes: 2 additions & 3 deletions apps/web/src/app/api/auth/mobile/login/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
RATE_LIMIT_CONFIGS,
decodeToken,
validateOrCreateDeviceToken,
getClientIP,
} from '@pagespace/lib/server';
import { generateCSRFToken, getSessionIdFromJWT } from '@pagespace/lib/server';
import { loggers, logAuthEvent } from '@pagespace/lib/server';
Expand Down Expand Up @@ -38,9 +39,7 @@ export async function POST(req: Request) {
const { email, password, deviceId, platform, deviceName, appVersion, deviceToken: existingDeviceToken } = validation.data;

// Rate limiting by IP address and email
const clientIP = req.headers.get('x-forwarded-for')?.split(',')[0] ||
req.headers.get('x-real-ip') ||
'unknown';
const clientIP = getClientIP(req);

const ipRateLimit = checkRateLimit(clientIP, RATE_LIMIT_CONFIGS.LOGIN);
const emailRateLimit = checkRateLimit(email.toLowerCase(), RATE_LIMIT_CONFIGS.LOGIN);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
generateCSRFToken,
getSessionIdFromJWT,
validateOrCreateDeviceToken,
getClientIP,
} from '@pagespace/lib/server';
import { loggers, logAuthEvent } from '@pagespace/lib/server';
import { trackAuthEvent } from '@pagespace/lib/activity-tracker';
Expand Down Expand Up @@ -101,10 +102,7 @@ export async function POST(req: Request) {
platform = requestPlatform;

// Rate limiting by IP address
const clientIP =
req.headers.get('x-forwarded-for')?.split(',')[0] ||
req.headers.get('x-real-ip') ||
'unknown';
const clientIP = getClientIP(req);

const ipRateLimit = checkRateLimit(clientIP, RATE_LIMIT_CONFIGS.LOGIN);
if (!ipRateLimit.allowed) {
Expand Down
6 changes: 2 additions & 4 deletions apps/web/src/app/api/auth/mobile/refresh/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
RATE_LIMIT_CONFIGS,
generateCSRFToken,
getSessionIdFromJWT,
getClientIP,
} from '@pagespace/lib/server';
import { z } from 'zod/v4';
import { loggers } from '@pagespace/lib/server';
Expand All @@ -31,10 +32,7 @@ export async function POST(req: Request) {

const { deviceToken, deviceId } = validation.data;

const clientIP =
req.headers.get('x-forwarded-for')?.split(',')[0] ||
req.headers.get('x-real-ip') ||
'unknown';
const clientIP = getClientIP(req);

// Rate limiting by IP address for refresh attempts
const rateLimit = checkRateLimit(`refresh:device:${clientIP}`, RATE_LIMIT_CONFIGS.REFRESH);
Expand Down
5 changes: 2 additions & 3 deletions apps/web/src/app/api/auth/mobile/signup/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
createNotification,
decodeToken,
validateOrCreateDeviceToken,
getClientIP,
} from '@pagespace/lib/server';
import { generateCSRFToken, getSessionIdFromJWT } from '@pagespace/lib/server';
import { createId } from '@paralleldrive/cuid2';
Expand Down Expand Up @@ -42,9 +43,7 @@ const signupSchema = z.object({
});

export async function POST(req: Request) {
const clientIP = req.headers.get('x-forwarded-for')?.split(',')[0] ||
req.headers.get('x-real-ip') ||
'unknown';
const clientIP = getClientIP(req);

let email: string | undefined;

Expand Down
6 changes: 2 additions & 4 deletions apps/web/src/app/api/auth/refresh/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { users, refreshTokens, deviceTokens } from '@pagespace/db';
import { db, eq, sql, and, isNull } from '@pagespace/db';
import { decodeToken, generateAccessToken, generateRefreshToken, getRefreshTokenMaxAge, checkRateLimit, RATE_LIMIT_CONFIGS } from '@pagespace/lib/server';
import { decodeToken, generateAccessToken, generateRefreshToken, getRefreshTokenMaxAge, checkRateLimit, RATE_LIMIT_CONFIGS, getClientIP } from '@pagespace/lib/server';
import { validateDeviceToken } from '@pagespace/lib/device-auth-utils';
import { serialize } from 'cookie';
import { parse } from 'cookie';
Expand All @@ -16,9 +16,7 @@ export async function POST(req: Request) {
}

// Rate limiting by IP address for refresh attempts
const clientIP = req.headers.get('x-forwarded-for')?.split(',')[0] ||
req.headers.get('x-real-ip') ||
'unknown';
const clientIP = getClientIP(req);

const rateLimit = checkRateLimit(`refresh:${clientIP}`, RATE_LIMIT_CONFIGS.REFRESH);

Expand Down
6 changes: 2 additions & 4 deletions apps/web/src/app/api/auth/signup/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { users, userAiSettings, refreshTokens, db, eq } from '@pagespace/db';
import bcrypt from 'bcryptjs';
import { z } from 'zod/v4';
import { generateAccessToken, generateRefreshToken, getRefreshTokenMaxAge, checkRateLimit, resetRateLimit, RATE_LIMIT_CONFIGS, createNotification, decodeToken, validateOrCreateDeviceToken } from '@pagespace/lib/server';
import { generateAccessToken, generateRefreshToken, getRefreshTokenMaxAge, checkRateLimit, resetRateLimit, RATE_LIMIT_CONFIGS, createNotification, decodeToken, validateOrCreateDeviceToken, getClientIP } from '@pagespace/lib/server';
import { createId } from '@paralleldrive/cuid2';
import { loggers, logAuthEvent } from '@pagespace/lib/server';
import { trackAuthEvent } from '@pagespace/lib/activity-tracker';
Expand Down Expand Up @@ -38,9 +38,7 @@ const signupSchema = z.object({
});

export async function POST(req: Request) {
const clientIP = req.headers.get('x-forwarded-for')?.split(',')[0] ||
req.headers.get('x-real-ip') ||
'unknown';
const clientIP = getClientIP(req);

let email: string | undefined;

Expand Down
6 changes: 2 additions & 4 deletions apps/web/src/app/api/contact/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { contactSubmissions, db } from '@pagespace/db';
import { z } from 'zod/v4';
import { createId } from '@paralleldrive/cuid2';
import { loggers } from '@pagespace/lib/server';
import { loggers, getClientIP } from '@pagespace/lib/server';
import { authenticateRequestWithOptions, isAuthError } from '@/lib/auth';

const AUTH_OPTIONS = { allow: ['jwt'] as const, requireCSRF: true };
Expand All @@ -14,9 +14,7 @@ const contactSchema = z.object({
});

export async function POST(request: Request) {
const clientIP = request.headers.get('x-forwarded-for')?.split(',')[0] ||
request.headers.get('x-real-ip') ||
'unknown';
const clientIP = getClientIP(request);

try {
// Authenticate request (optional - allows unauthenticated contact forms)
Expand Down
5 changes: 2 additions & 3 deletions apps/web/src/app/api/mcp-ws/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
isToolExecuteMessage,
isToolResultMessage,
} from '@/lib/websocket';
import { decodeToken } from '@pagespace/lib/server';
import { decodeToken, getClientIP } from '@pagespace/lib/server';
import { getCookieValueFromHeader } from '@/lib/utils/get-cookie-value';

// Initialize cleanup interval on module load
Expand Down Expand Up @@ -72,8 +72,7 @@ export async function UPGRADE(
request: NextRequest
) {
const requestUrl = request.url;
const clientIp =
request.headers.get('x-forwarded-for')?.split(',')[0].trim() || 'unknown';
const clientIp = getClientIP(request);

// SECURITY CHECK 1: Verify secure connection in production
if (!isSecureConnection(requestUrl, request)) {
Expand Down
5 changes: 2 additions & 3 deletions apps/web/src/app/api/track/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { NextResponse } from 'next/server';
import { trackActivity, trackFeature, trackError } from '@pagespace/lib/activity-tracker';
import { getClientIP } from '@pagespace/lib/server';
import { authenticateRequestWithOptions, isAuthError } from '@/lib/auth';

const AUTH_OPTIONS = { allow: ['jwt'] as const, requireCSRF: false };
Expand All @@ -19,9 +20,7 @@ export async function POST(request: Request) {
}

// Get client IP and user agent
const ip = request.headers.get('x-forwarded-for')?.split(',')[0] ||
request.headers.get('x-real-ip') ||
'unknown';
const ip = getClientIP(request);
const userAgent = request.headers.get('user-agent') || 'unknown';

// Parse tracking data
Expand Down
8 changes: 1 addition & 7 deletions apps/web/src/app/dashboard/storage/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
import { formatDistanceToNow } from "date-fns";
import { toast } from "sonner";
import { fetchWithAuth } from "@/lib/auth/auth-fetch";
import { formatBytes } from "@pagespace/lib/client-safe";

interface StorageInfo {
quota: {
Expand Down Expand Up @@ -100,13 +101,6 @@ const getFileIcon = (mimeType: string) => {
return File;
};

const formatBytes = (bytes: number): string => {
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
if (bytes === 0) return '0 B';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${Math.round(bytes / Math.pow(1024, i) * 100) / 100} ${sizes[i]}`;
};

export default function StorageDashboard() {
const router = useRouter();
const [storageInfo, setStorageInfo] = useState<StorageInfo | null>(null);
Expand Down
Loading
Loading