mirror of
https://github.com/itdoginfo/podkop.git
synced 2025-12-06 11:36:50 +03:00
feat: finalize first modular pack
This commit is contained in:
@@ -7,11 +7,20 @@ export default [
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
ignores: ['dist', 'node_modules'],
|
||||
ignores: ['node_modules'],
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'no-console': 'warn',
|
||||
'no-console': 'off',
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
prettier,
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
"lint:fix": "eslint src --ext .ts,.tsx --fix",
|
||||
"build": "tsup src/main.ts",
|
||||
"dev": "tsup src/main.ts --watch",
|
||||
"test": "vitest"
|
||||
"test": "vitest",
|
||||
"ci": "yarn format && yarn lint --max-warnings=0 && yarn test --run && yarn build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.45.0",
|
||||
|
||||
29
fe-app-podkop/src/helpers/copyToClipboard.ts
Normal file
29
fe-app-podkop/src/helpers/copyToClipboard.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
interface CopyToClipboardResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export function copyToClipboard(text: string): CopyToClipboardResponse {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Copied!',
|
||||
};
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to copy: ${error.message}`,
|
||||
};
|
||||
} finally {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
}
|
||||
32
fe-app-podkop/src/helpers/executeShellCommand.ts
Normal file
32
fe-app-podkop/src/helpers/executeShellCommand.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { COMMAND_TIMEOUT } from '../constants';
|
||||
import { withTimeout } from './withTimeout';
|
||||
|
||||
interface ExecuteShellCommandParams {
|
||||
command: string;
|
||||
args: string[];
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
interface ExecuteShellCommandResponse {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
code?: number;
|
||||
}
|
||||
|
||||
export async function executeShellCommand({
|
||||
command,
|
||||
args,
|
||||
timeout = COMMAND_TIMEOUT,
|
||||
}: ExecuteShellCommandParams): Promise<ExecuteShellCommandResponse> {
|
||||
try {
|
||||
return withTimeout(
|
||||
fs.exec(command, args),
|
||||
timeout,
|
||||
[command, ...args].join(' '),
|
||||
);
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
|
||||
return { stdout: '', stderr: error?.message, code: 0 };
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
export * from './getBaseUrl';
|
||||
export * from './parseValueList';
|
||||
export * from './injectGlobalStyles';
|
||||
export * from './withTimeout';
|
||||
export * from './executeShellCommand';
|
||||
export * from './copyToClipboard';
|
||||
export * from './maskIP';
|
||||
|
||||
5
fe-app-podkop/src/helpers/maskIP.ts
Normal file
5
fe-app-podkop/src/helpers/maskIP.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function maskIP(ip: string = ''): string {
|
||||
const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
|
||||
|
||||
return ip.replace(ipv4Regex, (_match, _p1, _p2, _p3, p4) => `XX.XX.XX.${p4}`);
|
||||
}
|
||||
42
fe-app-podkop/src/helpers/tests/maskIp.test.js
Normal file
42
fe-app-podkop/src/helpers/tests/maskIp.test.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { maskIP } from '../maskIP';
|
||||
|
||||
export const validIPs = [
|
||||
['Standard private IP', '192.168.0.1', 'XX.XX.XX.1'],
|
||||
['Public IP', '8.8.8.8', 'XX.XX.XX.8'],
|
||||
['Mixed digits', '10.0.255.99', 'XX.XX.XX.99'],
|
||||
['Edge values', '255.255.255.255', 'XX.XX.XX.255'],
|
||||
['Zeros', '0.0.0.0', 'XX.XX.XX.0'],
|
||||
];
|
||||
|
||||
export const invalidIPs = [
|
||||
['Empty string', '', ''],
|
||||
['Missing octets', '192.168.1', '192.168.1'],
|
||||
['Extra octets', '1.2.3.4.5', '1.2.3.4.5'],
|
||||
['Letters inside', 'abc.def.ghi.jkl', 'abc.def.ghi.jkl'],
|
||||
['Spaces inside', '1. 2.3.4', '1. 2.3.4'],
|
||||
['Just dots', '...', '...'],
|
||||
['IP with port', '127.0.0.1:8080', '127.0.0.1:8080'],
|
||||
['IP with text', 'ip=192.168.0.1', 'ip=192.168.0.1'],
|
||||
];
|
||||
|
||||
describe('maskIP', () => {
|
||||
describe.each(validIPs)('Valid IPv4: %s', (_desc, ip, expected) => {
|
||||
it(`masks "${ip}" → "${expected}"`, () => {
|
||||
expect(maskIP(ip)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(invalidIPs)(
|
||||
'Invalid or malformed IP: %s',
|
||||
(_desc, ip, expected) => {
|
||||
it(`returns original string for "${ip}"`, () => {
|
||||
expect(maskIP(ip)).toBe(expected);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('defaults to empty string if no param passed', () => {
|
||||
expect(maskIP()).toBe('');
|
||||
});
|
||||
});
|
||||
21
fe-app-podkop/src/helpers/withTimeout.ts
Normal file
21
fe-app-podkop/src/helpers/withTimeout.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export async function withTimeout<T>(
|
||||
promise: Promise<T>,
|
||||
timeoutMs: number,
|
||||
operationName: string,
|
||||
timeoutMessage = 'Operation timed out',
|
||||
): Promise<T> {
|
||||
let timeoutId;
|
||||
const start = performance.now();
|
||||
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
timeoutId = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
return await Promise.race([promise, timeoutPromise]);
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
const elapsed = performance.now() - start;
|
||||
console.log(`[${operationName}] Execution time: ${elapsed.toFixed(2)} ms`);
|
||||
}
|
||||
}
|
||||
15
fe-app-podkop/src/luci.d.ts
vendored
Normal file
15
fe-app-podkop/src/luci.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
declare global {
|
||||
const fs: {
|
||||
exec(
|
||||
command: string,
|
||||
args?: string[],
|
||||
env?: Record<string, string>,
|
||||
): Promise<{
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
code?: number;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -1,5 +1,6 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require fs';
|
||||
|
||||
export * from './validators';
|
||||
export * from './helpers';
|
||||
|
||||
@@ -31,7 +31,7 @@ export function validateShadowsocksUrl(url: string): ValidationResult {
|
||||
'Invalid Shadowsocks URL: decoded credentials must contain method:password',
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
if (!encryptedPart.includes(':') && !encryptedPart.includes('-')) {
|
||||
return {
|
||||
valid: false,
|
||||
@@ -73,7 +73,7 @@ export function validateShadowsocksUrl(url: string): ValidationResult {
|
||||
message: 'Invalid port number. Must be between 1 and 65535',
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
return { valid: false, message: 'Invalid Shadowsocks URL: parsing failed' };
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export function validateTrojanUrl(url: string): ValidationResult {
|
||||
message: 'Invalid Trojan URL: must contain username, hostname and port',
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
return { valid: false, message: 'Invalid Trojan URL: parsing failed' };
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export function validateUrl(
|
||||
};
|
||||
}
|
||||
return { valid: true, message: 'Valid' };
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
return { valid: false, message: 'Invalid URL format' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ export function validateVlessUrl(url: string): ValidationResult {
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
return { valid: false, message: 'Invalid VLESS URL: parsing failed' };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// This file is autogenerated, please don't change manually
|
||||
"use strict";
|
||||
"require baseclass";
|
||||
"require fs";
|
||||
|
||||
// src/validators/validateIp.ts
|
||||
function validateIPV4(ip) {
|
||||
@@ -54,7 +55,7 @@ function validateUrl(url, protocols = ["http:", "https:"]) {
|
||||
};
|
||||
}
|
||||
return { valid: true, message: "Valid" };
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
return { valid: false, message: "Invalid URL format" };
|
||||
}
|
||||
}
|
||||
@@ -143,7 +144,7 @@ function validateShadowsocksUrl(url) {
|
||||
message: "Invalid Shadowsocks URL: decoded credentials must contain method:password"
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
if (!encryptedPart.includes(":") && !encryptedPart.includes("-")) {
|
||||
return {
|
||||
valid: false,
|
||||
@@ -176,7 +177,7 @@ function validateShadowsocksUrl(url) {
|
||||
message: "Invalid port number. Must be between 1 and 65535"
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
return { valid: false, message: "Invalid Shadowsocks URL: parsing failed" };
|
||||
}
|
||||
return { valid: true, message: "Valid" };
|
||||
@@ -255,7 +256,7 @@ function validateVlessUrl(url) {
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
return { valid: false, message: "Invalid VLESS URL: parsing failed" };
|
||||
}
|
||||
return { valid: true, message: "Valid" };
|
||||
@@ -293,7 +294,7 @@ function validateTrojanUrl(url) {
|
||||
message: "Invalid Trojan URL: must contain username, hostname and port"
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
return { valid: false, message: "Invalid Trojan URL: parsing failed" };
|
||||
}
|
||||
return { valid: true, message: "Valid" };
|
||||
@@ -366,6 +367,22 @@ function injectGlobalStyles() {
|
||||
);
|
||||
}
|
||||
|
||||
// src/helpers/withTimeout.ts
|
||||
async function withTimeout(promise, timeoutMs, operationName, timeoutMessage = "Operation timed out") {
|
||||
let timeoutId;
|
||||
const start = performance.now();
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
timeoutId = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
|
||||
});
|
||||
try {
|
||||
return await Promise.race([promise, timeoutPromise]);
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
const elapsed = performance.now() - start;
|
||||
console.log(`[${operationName}] Execution time: ${elapsed.toFixed(2)} ms`);
|
||||
}
|
||||
}
|
||||
|
||||
// src/constants.ts
|
||||
var STATUS_COLORS = {
|
||||
SUCCESS: "#4caf50",
|
||||
@@ -475,6 +492,53 @@ var COMMAND_SCHEDULING = {
|
||||
P10_PRIORITY: 1900
|
||||
// Lowest priority
|
||||
};
|
||||
|
||||
// src/helpers/executeShellCommand.ts
|
||||
async function executeShellCommand({
|
||||
command,
|
||||
args,
|
||||
timeout = COMMAND_TIMEOUT
|
||||
}) {
|
||||
try {
|
||||
return withTimeout(
|
||||
fs.exec(command, args),
|
||||
timeout,
|
||||
[command, ...args].join(" ")
|
||||
);
|
||||
} catch (err) {
|
||||
const error = err;
|
||||
return { stdout: "", stderr: error?.message, code: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
// src/helpers/copyToClipboard.ts
|
||||
function copyToClipboard(text) {
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
document.execCommand("copy");
|
||||
return {
|
||||
success: true,
|
||||
message: "Copied!"
|
||||
};
|
||||
} catch (err) {
|
||||
const error = err;
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to copy: ${error.message}`
|
||||
};
|
||||
} finally {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
}
|
||||
|
||||
// src/helpers/maskIP.ts
|
||||
function maskIP(ip = "") {
|
||||
const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
|
||||
return ip.replace(ipv4Regex, (_match, _p1, _p2, _p3, p4) => `XX.XX.XX.${p4}`);
|
||||
}
|
||||
return baseclass.extend({
|
||||
ALLOWED_WITH_RUSSIA_INSIDE,
|
||||
BOOTSTRAP_DNS_SERVER_OPTIONS,
|
||||
@@ -494,8 +558,11 @@ return baseclass.extend({
|
||||
STATUS_COLORS,
|
||||
UPDATE_INTERVAL_OPTIONS,
|
||||
bulkValidate,
|
||||
copyToClipboard,
|
||||
executeShellCommand,
|
||||
getBaseUrl,
|
||||
injectGlobalStyles,
|
||||
maskIP,
|
||||
parseValueList,
|
||||
validateDNS,
|
||||
validateDomain,
|
||||
@@ -507,5 +574,6 @@ return baseclass.extend({
|
||||
validateSubnet,
|
||||
validateTrojanUrl,
|
||||
validateUrl,
|
||||
validateVlessUrl
|
||||
validateVlessUrl,
|
||||
withTimeout
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user