mirror of
https://github.com/itdoginfo/podkop.git
synced 2025-12-07 20:16:53 +03:00
feat: finalize first modular pack
This commit is contained in:
@@ -7,11 +7,20 @@ export default [
|
|||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
...tseslint.configs.recommended,
|
...tseslint.configs.recommended,
|
||||||
{
|
{
|
||||||
ignores: ['dist', 'node_modules'],
|
ignores: ['node_modules'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rules: {
|
rules: {
|
||||||
'no-console': 'warn',
|
'no-console': 'off',
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_',
|
||||||
|
caughtErrorsIgnorePattern: '^_',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
prettier,
|
prettier,
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
"lint:fix": "eslint src --ext .ts,.tsx --fix",
|
"lint:fix": "eslint src --ext .ts,.tsx --fix",
|
||||||
"build": "tsup src/main.ts",
|
"build": "tsup src/main.ts",
|
||||||
"dev": "tsup src/main.ts --watch",
|
"dev": "tsup src/main.ts --watch",
|
||||||
"test": "vitest"
|
"test": "vitest",
|
||||||
|
"ci": "yarn format && yarn lint --max-warnings=0 && yarn test --run && yarn build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "8.45.0",
|
"@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 './getBaseUrl';
|
||||||
export * from './parseValueList';
|
export * from './parseValueList';
|
||||||
export * from './injectGlobalStyles';
|
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';
|
'use strict';
|
||||||
'require baseclass';
|
'require baseclass';
|
||||||
|
'require fs';
|
||||||
|
|
||||||
export * from './validators';
|
export * from './validators';
|
||||||
export * from './helpers';
|
export * from './helpers';
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function validateShadowsocksUrl(url: string): ValidationResult {
|
|||||||
'Invalid Shadowsocks URL: decoded credentials must contain method:password',
|
'Invalid Shadowsocks URL: decoded credentials must contain method:password',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
if (!encryptedPart.includes(':') && !encryptedPart.includes('-')) {
|
if (!encryptedPart.includes(':') && !encryptedPart.includes('-')) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
@@ -73,7 +73,7 @@ export function validateShadowsocksUrl(url: string): ValidationResult {
|
|||||||
message: 'Invalid port number. Must be between 1 and 65535',
|
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: 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',
|
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: false, message: 'Invalid Trojan URL: parsing failed' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function validateUrl(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { valid: true, message: 'Valid' };
|
return { valid: true, message: 'Valid' };
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
return { valid: false, message: 'Invalid URL format' };
|
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' };
|
return { valid: false, message: 'Invalid VLESS URL: parsing failed' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// This file is autogenerated, please don't change manually
|
// This file is autogenerated, please don't change manually
|
||||||
"use strict";
|
"use strict";
|
||||||
"require baseclass";
|
"require baseclass";
|
||||||
|
"require fs";
|
||||||
|
|
||||||
// src/validators/validateIp.ts
|
// src/validators/validateIp.ts
|
||||||
function validateIPV4(ip) {
|
function validateIPV4(ip) {
|
||||||
@@ -54,7 +55,7 @@ function validateUrl(url, protocols = ["http:", "https:"]) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { valid: true, message: "Valid" };
|
return { valid: true, message: "Valid" };
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
return { valid: false, message: "Invalid URL format" };
|
return { valid: false, message: "Invalid URL format" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,7 +144,7 @@ function validateShadowsocksUrl(url) {
|
|||||||
message: "Invalid Shadowsocks URL: decoded credentials must contain method:password"
|
message: "Invalid Shadowsocks URL: decoded credentials must contain method:password"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
if (!encryptedPart.includes(":") && !encryptedPart.includes("-")) {
|
if (!encryptedPart.includes(":") && !encryptedPart.includes("-")) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
@@ -176,7 +177,7 @@ function validateShadowsocksUrl(url) {
|
|||||||
message: "Invalid port number. Must be between 1 and 65535"
|
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: false, message: "Invalid Shadowsocks URL: parsing failed" };
|
||||||
}
|
}
|
||||||
return { valid: true, message: "Valid" };
|
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: false, message: "Invalid VLESS URL: parsing failed" };
|
||||||
}
|
}
|
||||||
return { valid: true, message: "Valid" };
|
return { valid: true, message: "Valid" };
|
||||||
@@ -293,7 +294,7 @@ function validateTrojanUrl(url) {
|
|||||||
message: "Invalid Trojan URL: must contain username, hostname and port"
|
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: false, message: "Invalid Trojan URL: parsing failed" };
|
||||||
}
|
}
|
||||||
return { valid: true, message: "Valid" };
|
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
|
// src/constants.ts
|
||||||
var STATUS_COLORS = {
|
var STATUS_COLORS = {
|
||||||
SUCCESS: "#4caf50",
|
SUCCESS: "#4caf50",
|
||||||
@@ -475,6 +492,53 @@ var COMMAND_SCHEDULING = {
|
|||||||
P10_PRIORITY: 1900
|
P10_PRIORITY: 1900
|
||||||
// Lowest priority
|
// 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({
|
return baseclass.extend({
|
||||||
ALLOWED_WITH_RUSSIA_INSIDE,
|
ALLOWED_WITH_RUSSIA_INSIDE,
|
||||||
BOOTSTRAP_DNS_SERVER_OPTIONS,
|
BOOTSTRAP_DNS_SERVER_OPTIONS,
|
||||||
@@ -494,8 +558,11 @@ return baseclass.extend({
|
|||||||
STATUS_COLORS,
|
STATUS_COLORS,
|
||||||
UPDATE_INTERVAL_OPTIONS,
|
UPDATE_INTERVAL_OPTIONS,
|
||||||
bulkValidate,
|
bulkValidate,
|
||||||
|
copyToClipboard,
|
||||||
|
executeShellCommand,
|
||||||
getBaseUrl,
|
getBaseUrl,
|
||||||
injectGlobalStyles,
|
injectGlobalStyles,
|
||||||
|
maskIP,
|
||||||
parseValueList,
|
parseValueList,
|
||||||
validateDNS,
|
validateDNS,
|
||||||
validateDomain,
|
validateDomain,
|
||||||
@@ -507,5 +574,6 @@ return baseclass.extend({
|
|||||||
validateSubnet,
|
validateSubnet,
|
||||||
validateTrojanUrl,
|
validateTrojanUrl,
|
||||||
validateUrl,
|
validateUrl,
|
||||||
validateVlessUrl
|
validateVlessUrl,
|
||||||
|
withTimeout
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user