瀏覽代碼

Merge pull request #202 from fosrl/dev

hotfixes coming from beta13
Milo Schwartz 5 月之前
父節點
當前提交
489f6bed17

+ 28 - 4
server/auth/sessions/app.ts

@@ -11,7 +11,7 @@ import {
     users
 } from "@server/db/schema";
 import db from "@server/db";
-import { eq } from "drizzle-orm";
+import { eq, inArray } from "drizzle-orm";
 import config from "@server/lib/config";
 import type { RandomReader } from "@oslojs/crypto/random";
 import { generateRandomString } from "@oslojs/crypto/random";
@@ -95,12 +95,36 @@ export async function validateSessionToken(
 }
 
 export async function invalidateSession(sessionId: string): Promise<void> {
-    await db.delete(resourceSessions).where(eq(resourceSessions.userSessionId, sessionId));
-    await db.delete(sessions).where(eq(sessions.sessionId, sessionId));
+    try {
+        await db.transaction(async (trx) => {
+            await trx
+            .delete(resourceSessions)
+            .where(eq(resourceSessions.userSessionId, sessionId));
+            await trx.delete(sessions).where(eq(sessions.sessionId, sessionId));
+        });
+    } catch (e) {
+        logger.error("Failed to invalidate session", e);
+    }
 }
 
 export async function invalidateAllSessions(userId: string): Promise<void> {
-    await db.delete(sessions).where(eq(sessions.userId, userId));
+    try {
+        await db.transaction(async (trx) => {
+            const userSessions = await trx
+            .select()
+            .from(sessions)
+            .where(eq(sessions.userId, userId));
+            await trx.delete(resourceSessions).where(
+                inArray(
+                    resourceSessions.userSessionId,
+                    userSessions.map((s) => s.sessionId)
+                )
+            );
+            await trx.delete(sessions).where(eq(sessions.userId, userId));
+        });
+    } catch (e) {
+        logger.error("Failed to all invalidate user sessions", e);
+    }
 }
 
 export function serializeSessionCookie(

+ 1 - 1
server/lib/consts.ts

@@ -2,7 +2,7 @@ import path from "path";
 import { fileURLToPath } from "url";
 
 // This is a placeholder value replaced by the build process
-export const APP_VERSION = "1.0.0-beta.13";
+export const APP_VERSION = "1.0.0-beta.14";
 
 export const __FILENAME = fileURLToPath(import.meta.url);
 export const __DIRNAME = path.dirname(__FILENAME);

+ 183 - 0
server/lib/ip.test.ts

@@ -0,0 +1,183 @@
+import { cidrToRange, findNextAvailableCidr } from "./ip";
+
+/**
+ * Compares two objects for deep equality
+ * @param actual The actual value to test
+ * @param expected The expected value to compare against
+ * @param message The message to display if assertion fails
+ * @throws Error if objects are not equal
+ */
+export function assertEqualsObj<T>(actual: T, expected: T, message: string): void {
+    const actualStr = JSON.stringify(actual);
+    const expectedStr = JSON.stringify(expected);
+    if (actualStr !== expectedStr) {
+        throw new Error(`${message}\nExpected: ${expectedStr}\nActual: ${actualStr}`);
+    }
+}
+
+/**
+ * Compares two primitive values for equality
+ * @param actual The actual value to test
+ * @param expected The expected value to compare against
+ * @param message The message to display if assertion fails
+ * @throws Error if values are not equal
+ */
+export function assertEquals<T>(actual: T, expected: T, message: string): void {
+    if (actual !== expected) {
+        throw new Error(`${message}\nExpected: ${expected}\nActual: ${actual}`);
+    }
+}
+
+/**
+ * Tests if a function throws an expected error
+ * @param fn The function to test
+ * @param expectedError The expected error message or part of it
+ * @param message The message to display if assertion fails
+ * @throws Error if function doesn't throw or throws unexpected error
+ */
+export function assertThrows(
+    fn: () => void,
+    expectedError: string,
+    message: string
+): void {
+    try {
+        fn();
+        throw new Error(`${message}: Expected to throw "${expectedError}"`);
+    } catch (error) {
+        if (!(error instanceof Error)) {
+            throw new Error(`${message}\nUnexpected error type: ${typeof error}`);
+        }
+        
+        if (!error.message.includes(expectedError)) {
+            throw new Error(
+                `${message}\nExpected error: ${expectedError}\nActual error: ${error.message}`
+            );
+        }
+    }
+}
+
+
+// Test cases
+function testFindNextAvailableCidr() {
+    console.log("Running findNextAvailableCidr tests...");
+    
+    // Test 1: Basic IPv4 allocation
+    {
+        const existing = ["10.0.0.0/16", "10.1.0.0/16"];
+        const result = findNextAvailableCidr(existing, 16, "10.0.0.0/8");
+        assertEquals(result, "10.2.0.0/16", "Basic IPv4 allocation failed");
+    }
+
+    // Test 2: Finding gap between allocations
+    {
+        const existing = ["10.0.0.0/16", "10.2.0.0/16"];
+        const result = findNextAvailableCidr(existing, 16, "10.0.0.0/8");
+        assertEquals(result, "10.1.0.0/16", "Finding gap between allocations failed");
+    }
+
+    // Test 3: No available space
+    {
+        const existing = ["10.0.0.0/8"];
+        const result = findNextAvailableCidr(existing, 8, "10.0.0.0/8");
+        assertEquals(result, null, "No available space test failed");
+    }
+
+    // // Test 4: IPv6 allocation
+    // {
+    //     const existing = ["2001:db8::/32", "2001:db8:1::/32"];
+    //     const result = findNextAvailableCidr(existing, 32, "2001:db8::/16");
+    //     assertEquals(result, "2001:db8:2::/32", "Basic IPv6 allocation failed");
+    // }
+
+    // // Test 5: Mixed IP versions
+    // {
+    //     const existing = ["10.0.0.0/16", "2001:db8::/32"];
+    //     assertThrows(
+    //         () => findNextAvailableCidr(existing, 16),
+    //         "All CIDRs must be of the same IP version",
+    //         "Mixed IP versions test failed"
+    //     );
+    // }
+
+    // Test 6: Empty input
+    {
+        const existing: string[] = [];
+        const result = findNextAvailableCidr(existing, 16);
+        assertEquals(result, null, "Empty input test failed");
+    }
+
+    // Test 7: Block size alignment
+    {
+        const existing = ["10.0.0.0/24"];
+        const result = findNextAvailableCidr(existing, 24, "10.0.0.0/16");
+        assertEquals(result, "10.0.1.0/24", "Block size alignment test failed");
+    }
+
+    // Test 8: Block size alignment
+    {
+        const existing: string[] = [];
+        const result = findNextAvailableCidr(existing, 24, "10.0.0.0/16");
+        assertEquals(result, "10.0.0.0/24", "Block size alignment test failed");
+    }
+
+    // Test 9: Large block size request
+    {
+        const existing = ["10.0.0.0/24", "10.0.1.0/24"];
+        const result = findNextAvailableCidr(existing, 16, "10.0.0.0/16");
+        assertEquals(result, null, "Large block size request test failed");
+    }
+
+    console.log("All findNextAvailableCidr tests passed!");
+}
+
+// function testCidrToRange() {
+//     console.log("Running cidrToRange tests...");
+
+//     // Test 1: Basic IPv4 conversion
+//     {
+//         const result = cidrToRange("192.168.0.0/24");
+//         assertEqualsObj(result, {
+//             start: BigInt("3232235520"),
+//             end: BigInt("3232235775")
+//         }, "Basic IPv4 conversion failed");
+//     }
+
+//     // Test 2: IPv6 conversion
+//     {
+//         const result = cidrToRange("2001:db8::/32");
+//         assertEqualsObj(result, {
+//             start: BigInt("42540766411282592856903984951653826560"),
+//             end: BigInt("42540766411282592875350729025363378175")
+//         }, "IPv6 conversion failed");
+//     }
+
+//     // Test 3: Invalid prefix length
+//     {
+//         assertThrows(
+//             () => cidrToRange("192.168.0.0/33"),
+//             "Invalid prefix length for IPv4",
+//             "Invalid IPv4 prefix test failed"
+//         );
+//     }
+
+//     // Test 4: Invalid IPv6 prefix
+//     {
+//         assertThrows(
+//             () => cidrToRange("2001:db8::/129"),
+//             "Invalid prefix length for IPv6",
+//             "Invalid IPv6 prefix test failed"
+//         );
+//     }
+
+//     console.log("All cidrToRange tests passed!");
+// }
+
+// Run all tests
+try {
+    // testCidrToRange();
+    testFindNextAvailableCidr();
+    console.log("All tests passed successfully!");
+} catch (error) {
+    console.error("Test failed:", error);
+    process.exit(1);
+}

+ 132 - 22
server/lib/ip.ts

@@ -3,58 +3,162 @@ interface IPRange {
     end: bigint;
 }
 
+type IPVersion = 4 | 6;
+
 /**
- * Converts IP address string to BigInt for numerical operations
+ * Detects IP version from address string
+ */
+function detectIpVersion(ip: string): IPVersion {
+    return ip.includes(':') ? 6 : 4;
+}
+
+/**
+ * Converts IPv4 or IPv6 address string to BigInt for numerical operations
  */
 function ipToBigInt(ip: string): bigint {
-    return ip.split('.')
-        .reduce((acc, octet) => BigInt.asUintN(64, (acc << BigInt(8)) + BigInt(parseInt(octet))), BigInt(0));
+    const version = detectIpVersion(ip);
+    
+    if (version === 4) {
+        return ip.split('.')
+            .reduce((acc, octet) => {
+                const num = parseInt(octet);
+                if (isNaN(num) || num < 0 || num > 255) {
+                    throw new Error(`Invalid IPv4 octet: ${octet}`);
+                }
+                return BigInt.asUintN(64, (acc << BigInt(8)) + BigInt(num));
+            }, BigInt(0));
+    } else {
+        // Handle IPv6
+        // Expand :: notation
+        let fullAddress = ip;
+        if (ip.includes('::')) {
+            const parts = ip.split('::');
+            if (parts.length > 2) throw new Error('Invalid IPv6 address: multiple :: found');
+            const missing = 8 - (parts[0].split(':').length + parts[1].split(':').length);
+            const padding = Array(missing).fill('0').join(':');
+            fullAddress = `${parts[0]}:${padding}:${parts[1]}`;
+        }
+
+        return fullAddress.split(':')
+            .reduce((acc, hextet) => {
+                const num = parseInt(hextet || '0', 16);
+                if (isNaN(num) || num < 0 || num > 65535) {
+                    throw new Error(`Invalid IPv6 hextet: ${hextet}`);
+                }
+                return BigInt.asUintN(128, (acc << BigInt(16)) + BigInt(num));
+            }, BigInt(0));
+    }
 }
 
 /**
  * Converts BigInt to IP address string
  */
-function bigIntToIp(num: bigint): string {
-    const octets: number[] = [];
-    for (let i = 0; i < 4; i++) {
-        octets.unshift(Number(num & BigInt(255)));
-        num = num >> BigInt(8);
+function bigIntToIp(num: bigint, version: IPVersion): string {
+    if (version === 4) {
+        const octets: number[] = [];
+        for (let i = 0; i < 4; i++) {
+            octets.unshift(Number(num & BigInt(255)));
+            num = num >> BigInt(8);
+        }
+        return octets.join('.');
+    } else {
+        const hextets: string[] = [];
+        for (let i = 0; i < 8; i++) {
+            hextets.unshift(Number(num & BigInt(65535)).toString(16).padStart(4, '0'));
+            num = num >> BigInt(16);
+        }
+        // Compress zero sequences
+        let maxZeroStart = -1;
+        let maxZeroLength = 0;
+        let currentZeroStart = -1;
+        let currentZeroLength = 0;
+
+        for (let i = 0; i < hextets.length; i++) {
+            if (hextets[i] === '0000') {
+                if (currentZeroStart === -1) currentZeroStart = i;
+                currentZeroLength++;
+                if (currentZeroLength > maxZeroLength) {
+                    maxZeroLength = currentZeroLength;
+                    maxZeroStart = currentZeroStart;
+                }
+            } else {
+                currentZeroStart = -1;
+                currentZeroLength = 0;
+            }
+        }
+
+        if (maxZeroLength > 1) {
+            hextets.splice(maxZeroStart, maxZeroLength, '');
+            if (maxZeroStart === 0) hextets.unshift('');
+            if (maxZeroStart + maxZeroLength === 8) hextets.push('');
+        }
+
+        return hextets.map(h => h === '0000' ? '0' : h.replace(/^0+/, '')).join(':');
     }
-    return octets.join('.');
 }
 
 /**
  * Converts CIDR to IP range
  */
-function cidrToRange(cidr: string): IPRange {
+export function cidrToRange(cidr: string): IPRange {
     const [ip, prefix] = cidr.split('/');
+    const version = detectIpVersion(ip);
     const prefixBits = parseInt(prefix);
     const ipBigInt = ipToBigInt(ip);
-    const mask = BigInt.asUintN(64, (BigInt(1) << BigInt(32 - prefixBits)) - BigInt(1));
+    
+    // Validate prefix length
+    const maxPrefix = version === 4 ? 32 : 128;
+    if (prefixBits < 0 || prefixBits > maxPrefix) {
+        throw new Error(`Invalid prefix length for IPv${version}: ${prefix}`);
+    }
+
+    const shiftBits = BigInt(maxPrefix - prefixBits);
+    const mask = BigInt.asUintN(version === 4 ? 64 : 128, (BigInt(1) << shiftBits) - BigInt(1));
     const start = ipBigInt & ~mask;
     const end = start | mask;
+    
     return { start, end };
 }
 
 /**
  * Finds the next available CIDR block given existing allocations
  * @param existingCidrs Array of existing CIDR blocks
- * @param blockSize Desired prefix length for the new block (e.g., 24 for /24)
- * @param startCidr Optional CIDR to start searching from (default: "0.0.0.0/0")
+ * @param blockSize Desired prefix length for the new block
+ * @param startCidr Optional CIDR to start searching from
  * @returns Next available CIDR block or null if none found
  */
 export function findNextAvailableCidr(
     existingCidrs: string[],
     blockSize: number,
-    startCidr: string = "0.0.0.0/0"
+    startCidr?: string
 ): string | null {
+
+    if (!startCidr && existingCidrs.length === 0) {
+        return null;
+    }
+        
+    // If no existing CIDRs, use the IP version from startCidr
+    const version = startCidr 
+        ? detectIpVersion(startCidr.split('/')[0])
+        : 4; // Default to IPv4 if no startCidr provided
+    
+    // Use appropriate default startCidr if none provided
+    startCidr = startCidr || (version === 4 ? "0.0.0.0/0" : "::/0");
+    
+    // If there are existing CIDRs, ensure all are same version
+    if (existingCidrs.length > 0 && 
+        existingCidrs.some(cidr => detectIpVersion(cidr.split('/')[0]) !== version)) {
+        throw new Error('All CIDRs must be of the same IP version');
+    }
+
     // Convert existing CIDRs to ranges and sort them
     const existingRanges = existingCidrs
         .map(cidr => cidrToRange(cidr))
         .sort((a, b) => (a.start < b.start ? -1 : 1));
 
     // Calculate block size
-    const blockSizeBigInt = BigInt(1) << BigInt(32 - blockSize);
+    const maxPrefix = version === 4 ? 32 : 128;
+    const blockSizeBigInt = BigInt(1) << BigInt(maxPrefix - blockSize);
 
     // Start from the beginning of the given CIDR
     let current = cidrToRange(startCidr).start;
@@ -63,7 +167,6 @@ export function findNextAvailableCidr(
     // Iterate through existing ranges
     for (let i = 0; i <= existingRanges.length; i++) {
         const nextRange = existingRanges[i];
-
         // Align current to block size
         const alignedCurrent = current + ((blockSizeBigInt - (current % blockSizeBigInt)) % blockSizeBigInt);
 
@@ -74,7 +177,7 @@ export function findNextAvailableCidr(
 
         // If we're at the end of existing ranges or found a gap
         if (!nextRange || alignedCurrent + blockSizeBigInt - BigInt(1) < nextRange.start) {
-            return `${bigIntToIp(alignedCurrent)}/${blockSize}`;
+            return `${bigIntToIp(alignedCurrent, version)}/${blockSize}`;
         }
 
         // Move current pointer to after the current range
@@ -85,12 +188,19 @@ export function findNextAvailableCidr(
 }
 
 /**
-* Checks if a given IP address is within a CIDR range
-* @param ip IP address to check
-* @param cidr CIDR range to check against
-* @returns boolean indicating if IP is within the CIDR range
-*/
+ * Checks if a given IP address is within a CIDR range
+ * @param ip IP address to check
+ * @param cidr CIDR range to check against
+ * @returns boolean indicating if IP is within the CIDR range
+ */
 export function isIpInCidr(ip: string, cidr: string): boolean {
+    const ipVersion = detectIpVersion(ip);
+    const cidrVersion = detectIpVersion(cidr.split('/')[0]);
+    
+    if (ipVersion !== cidrVersion) {
+        throw new Error('IP address and CIDR must be of the same version');
+    }
+
     const ipBigInt = ipToBigInt(ip);
     const range = cidrToRange(cidr);
     return ipBigInt >= range.start && ipBigInt <= range.end;

+ 56 - 4
server/lib/validators.ts

@@ -24,7 +24,7 @@ export function isValidUrlGlobPattern(pattern: string): boolean {
     for (let i = 0; i < segments.length; i++) {
         const segment = segments[i];
 
-        // Empty segments are not allowed (double slashes)
+        // Empty segments are not allowed (double slashes), except at the end
         if (!segment && i !== segments.length - 1) {
             return false;
         }
@@ -34,11 +34,63 @@ export function isValidUrlGlobPattern(pattern: string): boolean {
             return false;
         }
 
-        // Check for invalid characters
-        if (!/^[a-zA-Z0-9_*-]*$/.test(segment)) {
-            return false;
+        // Check each character in the segment
+        for (let j = 0; j < segment.length; j++) {
+            const char = segment[j];
+
+            // Check for percent-encoded sequences
+            if (char === "%" && j + 2 < segment.length) {
+                const hex1 = segment[j + 1];
+                const hex2 = segment[j + 2];
+                if (
+                    !/^[0-9A-Fa-f]$/.test(hex1) ||
+                    !/^[0-9A-Fa-f]$/.test(hex2)
+                ) {
+                    return false;
+                }
+                j += 2; // Skip the next two characters
+                continue;
+            }
+
+            // Allow:
+            // - unreserved (A-Z a-z 0-9 - . _ ~)
+            // - sub-delims (! $ & ' ( ) * + , ; =)
+            // - @ : for compatibility with some systems
+            if (!/^[A-Za-z0-9\-._~!$&'()*+,;=@:]$/.test(char)) {
+                return false;
+            }
         }
     }
 
     return true;
 }
+
+export function isUrlValid(url: string | undefined) {
+    if (!url) return true; // the link is optional in the schema so if it's empty it's valid
+    var pattern = new RegExp(
+        "^(https?:\\/\\/)?" + // protocol
+            "((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // domain name
+            "((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address
+            "(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path
+            "(\\?[;&a-z\\d%_.~+=-]*)?" + // query string
+            "(\\#[-a-z\\d_]*)?$",
+        "i"
+    );
+    return !!pattern.test(url);
+}
+
+export function isTargetValid(value: string | undefined) {
+    if (!value) return true;
+
+    const DOMAIN_REGEX =
+        /^[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9_])?(?:\.[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9_])?)*$/;
+    const IPV4_REGEX =
+        /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
+    const IPV6_REGEX = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i;
+
+    if (IPV4_REGEX.test(value) || IPV6_REGEX.test(value)) {
+        return true;
+    }
+
+    return DOMAIN_REGEX.test(value);
+}

+ 15 - 7
server/routers/auth/resetPassword.ts

@@ -149,8 +149,6 @@ export async function resetPassword(
 
         const passwordHash = await hashPassword(newPassword);
 
-        await invalidateAllSessions(resetRequest[0].userId);
-
         await db.transaction(async (trx) => {
             await trx
                 .update(users)
@@ -162,11 +160,21 @@ export async function resetPassword(
                 .where(eq(passwordResetTokens.email, email));
         });
 
-        await sendEmail(ConfirmPasswordReset({ email }), {
-            from: config.getNoReplyEmail(),
-            to: email,
-            subject: "Password Reset Confirmation"
-        });
+        try {
+            await invalidateAllSessions(resetRequest[0].userId);
+        } catch (e) {
+            logger.error("Failed to invalidate user sessions", e);
+        }
+
+        try {
+            await sendEmail(ConfirmPasswordReset({ email }), {
+                from: config.getNoReplyEmail(),
+                to: email,
+                subject: "Password Reset Confirmation"
+            });
+        } catch (e) {
+            logger.error("Failed to send password reset confirmation email", e);
+        }
 
         return response<ResetPasswordResponse>(res, {
             data: null,

+ 2 - 29
server/routers/target/createTarget.ts

@@ -12,34 +12,7 @@ import { fromError } from "zod-validation-error";
 import { addTargets } from "../newt/targets";
 import { eq } from "drizzle-orm";
 import { pickPort } from "./helpers";
-
-// Regular expressions for validation
-const DOMAIN_REGEX =
-    /^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
-const IPV4_REGEX =
-    /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
-const IPV6_REGEX = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i;
-
-// Schema for domain names and IP addresses
-const domainSchema = z
-    .string()
-    .min(1, "Domain cannot be empty")
-    .max(255, "Domain name too long")
-    .refine(
-        (value) => {
-            // Check if it's a valid IP address (v4 or v6)
-            if (IPV4_REGEX.test(value) || IPV6_REGEX.test(value)) {
-                return true;
-            }
-
-            // Check if it's a valid domain name
-            return DOMAIN_REGEX.test(value);
-        },
-        {
-            message: "Invalid domain name or IP address format",
-            path: ["domain"]
-        }
-    );
+import { isTargetValid } from "@server/lib/validators";
 
 const createTargetParamsSchema = z
     .object({
@@ -52,7 +25,7 @@ const createTargetParamsSchema = z
 
 const createTargetSchema = z
     .object({
-        ip: domainSchema,
+        ip: z.string().refine(isTargetValid),
         method: z.string().optional().nullable(),
         port: z.number().int().min(1).max(65535),
         enabled: z.boolean().default(true)

+ 2 - 29
server/routers/target/updateTarget.ts

@@ -11,34 +11,7 @@ import { fromError } from "zod-validation-error";
 import { addPeer } from "../gerbil/peers";
 import { addTargets } from "../newt/targets";
 import { pickPort } from "./helpers";
-
-// Regular expressions for validation
-const DOMAIN_REGEX =
-    /^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
-const IPV4_REGEX =
-    /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
-const IPV6_REGEX = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i;
-
-// Schema for domain names and IP addresses
-const domainSchema = z
-    .string()
-    .min(1, "Domain cannot be empty")
-    .max(255, "Domain name too long")
-    .refine(
-        (value) => {
-            // Check if it's a valid IP address (v4 or v6)
-            if (IPV4_REGEX.test(value) || IPV6_REGEX.test(value)) {
-                return true;
-            }
-
-            // Check if it's a valid domain name
-            return DOMAIN_REGEX.test(value);
-        },
-        {
-            message: "Invalid domain name or IP address format",
-            path: ["domain"]
-        }
-    );
+import { isTargetValid } from "@server/lib/validators";
 
 const updateTargetParamsSchema = z
     .object({
@@ -48,7 +21,7 @@ const updateTargetParamsSchema = z
 
 const updateTargetBodySchema = z
     .object({
-        ip: domainSchema.optional(),
+        ip: z.string().refine(isTargetValid),
         method: z.string().min(1).max(10).optional().nullable(),
         port: z.number().int().min(1).max(65535).optional(),
         enabled: z.boolean().optional()

+ 6 - 0
src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx

@@ -49,6 +49,7 @@ import {
 } from "@app/components/Settings";
 import { SwitchInput } from "@app/components/SwitchInput";
 import { InfoPopup } from "@app/components/ui/info-popup";
+import { useRouter } from "next/navigation";
 
 const UsersRolesFormSchema = z.object({
     roles: z.array(
@@ -82,6 +83,7 @@ export default function ResourceAuthenticationPage() {
     const { env } = useEnvContext();
 
     const api = createApiClient({ env });
+    const router = useRouter();
 
     const [pageLoading, setPageLoading] = useState(true);
 
@@ -236,6 +238,7 @@ export default function ResourceAuthenticationPage() {
                 title: "Saved successfully",
                 description: "Whitelist settings have been saved"
             });
+            router.refresh();
         } catch (e) {
             console.error(e);
             toast({
@@ -283,6 +286,7 @@ export default function ResourceAuthenticationPage() {
                 title: "Saved successfully",
                 description: "Authentication settings have been saved"
             });
+            router.refresh();
         } catch (e) {
             console.error(e);
             toast({
@@ -314,6 +318,7 @@ export default function ResourceAuthenticationPage() {
                 updateAuthInfo({
                     password: false
                 });
+                router.refresh();
             })
             .catch((e) => {
                 toast({
@@ -344,6 +349,7 @@ export default function ResourceAuthenticationPage() {
                 updateAuthInfo({
                     pincode: false
                 });
+                router.refresh();
             })
             .catch((e) => {
                 toast({

+ 6 - 31
src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx

@@ -62,39 +62,11 @@ import {
     SettingsSectionFooter
 } from "@app/components/Settings";
 import { SwitchInput } from "@app/components/SwitchInput";
-import { useSiteContext } from "@app/hooks/useSiteContext";
-import { InfoPopup } from "@app/components/ui/info-popup";
-
-// Regular expressions for validation
-const DOMAIN_REGEX =
-    /^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
-const IPV4_REGEX =
-    /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
-const IPV6_REGEX = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i;
-
-// Schema for domain names and IP addresses
-const domainSchema = z
-    .string()
-    .min(1, "Domain cannot be empty")
-    .max(255, "Domain name too long")
-    .refine(
-        (value) => {
-            // Check if it's a valid IP address (v4 or v6)
-            if (IPV4_REGEX.test(value) || IPV6_REGEX.test(value)) {
-                return true;
-            }
-
-            // Check if it's a valid domain name
-            return DOMAIN_REGEX.test(value);
-        },
-        {
-            message: "Invalid domain name or IP address format",
-            path: ["domain"]
-        }
-    );
+import { useRouter } from "next/navigation";
+import { isTargetValid } from "@server/lib/validators";
 
 const addTargetSchema = z.object({
-    ip: domainSchema,
+    ip: z.string().refine(isTargetValid),
     method: z.string().nullable(),
     port: z.coerce.number().int().positive()
     // protocol: z.string(),
@@ -125,6 +97,7 @@ export default function ReverseProxyTargets(props: {
     const [loading, setLoading] = useState(false);
 
     const [pageLoading, setPageLoading] = useState(true);
+    const router = useRouter();
 
     const addTargetForm = useForm({
         resolver: zodResolver(addTargetSchema),
@@ -299,6 +272,7 @@ export default function ReverseProxyTargets(props: {
             });
 
             setTargetsToRemove([]);
+            router.refresh();
         } catch (err) {
             console.error(err);
             toast({
@@ -339,6 +313,7 @@ export default function ReverseProxyTargets(props: {
                 title: "SSL Configuration",
                 description: "SSL configuration updated successfully"
             });
+            router.refresh();
         }
     }
 

+ 5 - 1
src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx

@@ -71,6 +71,7 @@ import {
     isValidUrlGlobPattern
 } from "@server/lib/validators";
 import { Switch } from "@app/components/ui/switch";
+import { useRouter } from "next/navigation";
 
 // Schema for rule validation
 const addRuleSchema = z.object({
@@ -107,6 +108,7 @@ export default function ResourceRules(props: {
     const [loading, setLoading] = useState(false);
     const [pageLoading, setPageLoading] = useState(true);
     const [rulesEnabled, setRulesEnabled] = useState(resource.applyRules);
+    const router = useRouter();
 
     const addRuleForm = useForm({
         resolver: zodResolver(addRuleSchema),
@@ -253,6 +255,7 @@ export default function ResourceRules(props: {
                 title: "Enable Rules",
                 description: "Rule evaluation has been updated"
             });
+            router.refresh();
         }
     }
 
@@ -370,6 +373,7 @@ export default function ResourceRules(props: {
             });
 
             setRulesToRemove([]);
+            router.refresh();
         } catch (err) {
             console.error(err);
             toast({
@@ -590,7 +594,7 @@ export default function ResourceRules(props: {
                     <SwitchInput
                         id="rules-toggle"
                         label="Enable Rules"
-                        defaultChecked={resource.applyRules}
+                        defaultChecked={rulesEnabled}
                         onCheckedChange={async (val) => {
                             await saveApplyRules(val);
                         }}