mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-06-12 01:07:57 +02:00
Proxy visual mode
Proxy Import / Export
This commit is contained in:
@@ -57,6 +57,8 @@ See [production docker compose example](https://github.com/phishingclub/phishing
|
||||
- **Browser impersonation** - Impersonate JA4 fingerprints in proxied requests
|
||||
- **Response overwriting** - Shortcut proxying with custom responses
|
||||
- **Forward proxying** - Use HTTP and SOCKS5 proxies to ensure requests originate from the right location
|
||||
- **Visual Editor** - Use the visual editor to easily setup a proxy
|
||||
- **Import compromised oauth token** - Use compromised tokens to send more phishing via. oauth enabled endpoints
|
||||
|
||||
### Blogs & Resources
|
||||
- [Covert red team phishing with Phishing Club](http://phishing.club/blog/covert-red-team-phishing-with-phishing-club/)
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
export let bindTo = null;
|
||||
export let resetTabFocus = () => {};
|
||||
export let noAutoFocus = false;
|
||||
export let fullscreen = false;
|
||||
|
||||
let modalElement;
|
||||
let previousActiveElement;
|
||||
@@ -25,7 +26,7 @@
|
||||
// only handle focus when modal visibility actually changes and not during submission
|
||||
if (visible && !modalInitialized) {
|
||||
window.addEventListener('keydown', keyHandler);
|
||||
// Prevent body scrolling when modal is open
|
||||
// prevent body scrolling when modal is open
|
||||
document.body.style.overflow = 'hidden';
|
||||
if (!isSubmitting) {
|
||||
handleModalOpen();
|
||||
@@ -34,7 +35,7 @@
|
||||
wasVisible = true;
|
||||
} else if (!visible && modalInitialized) {
|
||||
window.removeEventListener('keydown', keyHandler);
|
||||
// Restore body scrolling when modal is closed
|
||||
// restore body scrolling when modal is closed
|
||||
document.body.style.overflow = 'auto';
|
||||
handleModalClose();
|
||||
modalInitialized = false;
|
||||
@@ -49,13 +50,13 @@
|
||||
close();
|
||||
}
|
||||
} else if (e.key === 'Tab') {
|
||||
// Check if the focused element is a dropdown option button
|
||||
// check if the focused element is a dropdown option button
|
||||
const focusedElement = document.activeElement;
|
||||
const isDropdownOption =
|
||||
focusedElement?.closest('[role="listbox"]') && focusedElement?.role === 'option';
|
||||
|
||||
if (isDropdownOption) {
|
||||
// Don't intercept tab from dropdown options - let them handle it first
|
||||
// don't intercept tab from dropdown options - let them handle it first
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -64,62 +65,62 @@
|
||||
};
|
||||
|
||||
const handleTabKey = (e) => {
|
||||
// Store the current focused element before updating the list
|
||||
// store the current focused element before updating the list
|
||||
const currentlyFocused = document.activeElement;
|
||||
|
||||
// Update focusable elements before handling tab to account for dynamic changes
|
||||
// update focusable elements before handling tab to account for dynamic changes
|
||||
updateFocusableElements();
|
||||
|
||||
if (focusableElements.length === 0) return;
|
||||
|
||||
// Always prevent default tab behavior to keep focus within modal
|
||||
// always prevent default tab behavior to keep focus within modal
|
||||
e.preventDefault();
|
||||
|
||||
let currentIndex = focusableElements.indexOf(currentlyFocused);
|
||||
|
||||
// If current element is not found (-1), try to find a related element
|
||||
// if current element is not found (-1), try to find a related element
|
||||
if (currentIndex === -1) {
|
||||
// Check if the current element is inside a TextFieldSelect or similar component
|
||||
// check if the current element is inside a TextFieldSelect or similar component
|
||||
const parentComponent = currentlyFocused?.closest('.textfield-select-container');
|
||||
if (parentComponent) {
|
||||
// Look for the input element within the same component
|
||||
// look for the input element within the same component
|
||||
const inputInComponent = parentComponent.querySelector('input');
|
||||
if (inputInComponent) {
|
||||
const inputIndex = focusableElements.indexOf(inputInComponent);
|
||||
if (inputIndex !== -1) {
|
||||
// Use the input's position for navigation and continue tab flow
|
||||
// use the input's position for navigation and continue tab flow
|
||||
currentIndex = inputIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we still can't find the element, handle it gracefully
|
||||
// if we still can't find the element, handle it gracefully
|
||||
if (currentIndex === -1) {
|
||||
// Check if the current element is inside a TextFieldSelect or similar component
|
||||
// check if the current element is inside a TextFieldSelect or similar component
|
||||
const parentComponent = currentlyFocused?.closest('.textfield-select-container');
|
||||
if (parentComponent) {
|
||||
// Look for the input element within the same component
|
||||
// look for the input element within the same component
|
||||
const inputInComponent = parentComponent.querySelector('input');
|
||||
if (inputInComponent) {
|
||||
const inputIndex = focusableElements.indexOf(inputInComponent);
|
||||
if (inputIndex !== -1) {
|
||||
// Use the input's position for navigation
|
||||
// use the input's position for navigation
|
||||
currentIndex = inputIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we still can't find the element, handle it gracefully
|
||||
// if we still can't find the element, handle it gracefully
|
||||
if (currentIndex === -1) {
|
||||
// If we can't find the element or related element, try to be smarter
|
||||
// Check if we should go forward or backward based on the shift key
|
||||
// if we can't find the element or related element, try to be smarter
|
||||
// check if we should go forward or backward based on the shift key
|
||||
if (e.shiftKey) {
|
||||
// Shift+Tab: go to last element
|
||||
// shift+Tab: go to last element
|
||||
lastFocusableElement?.focus();
|
||||
} else {
|
||||
// Tab: try to find the first input in the form, or fallback to first element
|
||||
// tab: try to find the first input in the form, or fallback to first element
|
||||
const firstInput = focusableElements.find((el) => el.tagName === 'INPUT');
|
||||
if (firstInput) {
|
||||
firstInput.focus();
|
||||
@@ -130,23 +131,23 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Now handle normal tab navigation
|
||||
// now handle normal tab navigation
|
||||
if (e.shiftKey) {
|
||||
// Shift + Tab - go to previous element
|
||||
// shift + Tab - go to previous element
|
||||
if (currentIndex <= 0) {
|
||||
// If at first element, go to last
|
||||
// if at first element, go to last
|
||||
lastFocusableElement?.focus();
|
||||
} else {
|
||||
// Go to previous element
|
||||
// go to previous element
|
||||
focusableElements[currentIndex - 1]?.focus();
|
||||
}
|
||||
} else {
|
||||
// Tab - go to next element
|
||||
// tab - go to next element
|
||||
if (currentIndex >= focusableElements.length - 1) {
|
||||
// If at last element, go to first
|
||||
// if at last element, go to first
|
||||
firstFocusableElement?.focus();
|
||||
} else {
|
||||
// Go to next element
|
||||
// go to next element
|
||||
focusableElements[currentIndex + 1]?.focus();
|
||||
}
|
||||
}
|
||||
@@ -206,7 +207,7 @@
|
||||
const updateFocusableElements = () => {
|
||||
focusableElements = getFocusableElements();
|
||||
|
||||
// Reorder elements: form controls first, then navigation buttons, then close button
|
||||
// reorder elements: form controls first, then navigation buttons, then close button
|
||||
const formElements = [];
|
||||
const navigationButtons = [];
|
||||
const closeButton = modalElement?.querySelector('[data-close-button]');
|
||||
@@ -226,7 +227,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Rebuild focusable elements in desired order: form controls, then navigation, then close
|
||||
// rebuild focusable elements in desired order: form controls, then navigation, then close
|
||||
focusableElements = [...formElements, ...navigationButtons];
|
||||
if (closeButton) {
|
||||
focusableElements.push(closeButton);
|
||||
@@ -242,37 +243,37 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the currently focused element
|
||||
// store the currently focused element
|
||||
previousActiveElement = document.activeElement;
|
||||
|
||||
// Wait for the DOM to update
|
||||
// wait for the DOM to update
|
||||
await tick();
|
||||
|
||||
updateFocusableElements();
|
||||
|
||||
// Focus the first focusable element (excluding close button)
|
||||
// focus the first focusable element (excluding close button)
|
||||
if (!noAutoFocus && firstFocusableElement) {
|
||||
firstFocusableElement.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleModalClose = () => {
|
||||
// Restore focus to the previously focused element
|
||||
// restore focus to the previously focused element
|
||||
if (previousActiveElement && typeof previousActiveElement.focus === 'function') {
|
||||
previousActiveElement.focus();
|
||||
}
|
||||
};
|
||||
|
||||
// Exposed function to reset tab focus when modal content changes
|
||||
// exposed function to reset tab focus when modal content changes
|
||||
const handleResetTabFocus = async () => {
|
||||
// Check if a TextFieldSelect is currently selecting an option
|
||||
// check if a TextFieldSelect is currently selecting an option
|
||||
const isTextFieldSelectActive = modalElement?.querySelector('[data-selecting="true"]');
|
||||
|
||||
await tick();
|
||||
updateFocusableElements();
|
||||
};
|
||||
|
||||
// Call resetTabFocus when it changes
|
||||
// call resetTabFocus when it changes
|
||||
$: if (resetTabFocus) {
|
||||
resetTabFocus = handleResetTabFocus;
|
||||
}
|
||||
@@ -280,9 +281,9 @@
|
||||
onMount(() => {
|
||||
return () => {
|
||||
window.removeEventListener('keydown', keyHandler);
|
||||
// Ensure body scrolling is restored when component is destroyed
|
||||
// ensure body scrolling is restored when component is destroyed
|
||||
document.body.style.overflow = 'auto';
|
||||
// Restore focus if modal was open when component was destroyed
|
||||
// restore focus if modal was open when component was destroyed
|
||||
if (visible && previousActiveElement && typeof previousActiveElement.focus === 'function') {
|
||||
previousActiveElement.focus();
|
||||
}
|
||||
@@ -306,7 +307,7 @@
|
||||
}
|
||||
visible = false;
|
||||
window.removeEventListener('keydown', keyHandler);
|
||||
// Restore body scrolling when modal is closed
|
||||
// restore body scrolling when modal is closed
|
||||
document.body.style.overflow = 'auto';
|
||||
onClose();
|
||||
};
|
||||
@@ -324,11 +325,15 @@
|
||||
>
|
||||
<section
|
||||
bind:this={modalElement}
|
||||
class="shadow-xl dark:shadow-gray-900/70 w-auto ml-20 mr-8 max-h-[90vh] bg-white dark:bg-gray-800 opacity-100 rounded-md flex flex-col transition-colors duration-200"
|
||||
class="shadow-xl dark:shadow-gray-900/70 bg-white dark:bg-gray-800 opacity-100 rounded-md flex flex-col transition-all duration-200
|
||||
{fullscreen
|
||||
? 'fixed inset-0 w-full h-full max-w-none max-h-none rounded-none'
|
||||
: 'w-auto ml-20 mr-8 max-h-[90vh]'}"
|
||||
>
|
||||
<div
|
||||
class:opacity-20={isSubmitting}
|
||||
class="bg-cta-blue dark:bg-blue-800 text-white rounded-t-md py-4 px-8 flex justify-between flex-shrink-0 transition-colors duration-200"
|
||||
class="bg-cta-blue dark:bg-blue-800 text-white py-4 px-8 flex justify-between flex-shrink-0 transition-colors duration-200
|
||||
{fullscreen ? '' : 'rounded-t-md'}"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<h1 id="modal-title" class="uppercase mr-8 font-semibold text-2xl">{headerText}</h1>
|
||||
@@ -346,7 +351,11 @@
|
||||
<img class="w-full" src="/close-white.svg" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="px-8 overflow-y-auto overflow-x-visible {scrollBarClassesVertical}">
|
||||
<div
|
||||
class="px-8 overflow-y-auto overflow-x-visible {scrollBarClassesVertical} {fullscreen
|
||||
? 'flex-1'
|
||||
: ''}"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
export let pattern = null;
|
||||
export let id = null;
|
||||
export let onBlur = () => {};
|
||||
export let error = false;
|
||||
// type can only be set initially
|
||||
export let type = 'text';
|
||||
let inputType = 'text';
|
||||
@@ -97,7 +98,9 @@
|
||||
{required}
|
||||
{placeholder}
|
||||
{pattern}
|
||||
class="text-ellipsis row-start-1 row-span-3 justify-self-center rounded-md py-2 pl-2 text-gray-600 dark:text-gray-300 border border-transparent dark:border-gray-700/60 focus:outline-none focus:border-solid focus:border-slate-400 dark:focus:border-highlight-blue/80 focus:bg-gray-100 dark:focus:bg-gray-700/60 bg-grayblue-light dark:bg-gray-900/60 font-normal transition-colors duration-200"
|
||||
class="text-ellipsis row-start-1 row-span-3 justify-self-center rounded-md py-2 pl-2 text-gray-600 dark:text-gray-300 border focus:outline-none focus:border-solid focus:bg-gray-100 dark:focus:bg-gray-700/60 bg-grayblue-light dark:bg-gray-900/60 font-normal transition-colors duration-200 {error
|
||||
? 'border-red-500 dark:border-red-400'
|
||||
: 'border-transparent dark:border-gray-700/60 focus:border-slate-400 dark:focus:border-highlight-blue/80'}"
|
||||
class:w-24={width === 'small'}
|
||||
class:w-60={width === 'medium'}
|
||||
class:w-95={width === 'large'}
|
||||
|
||||
@@ -21,6 +21,31 @@
|
||||
// Ensure options is always an array
|
||||
$: optionsArray = Array.isArray(options) ? options : Array.from(options);
|
||||
|
||||
// helper to get display label for an option (supports both string and {value, label} objects)
|
||||
const getOptionLabel = (opt) => {
|
||||
if (opt && typeof opt === 'object' && 'label' in opt) {
|
||||
return opt.label;
|
||||
}
|
||||
return opt;
|
||||
};
|
||||
|
||||
// helper to get value for an option (supports both string and {value, label} objects)
|
||||
const getOptionValue = (opt) => {
|
||||
if (opt && typeof opt === 'object' && 'value' in opt) {
|
||||
return opt.value;
|
||||
}
|
||||
return opt;
|
||||
};
|
||||
|
||||
// get display label for current value
|
||||
const getDisplayLabel = (val) => {
|
||||
const found = optionsArray.find((opt) => getOptionValue(opt) === val);
|
||||
if (found) {
|
||||
return getOptionLabel(found);
|
||||
}
|
||||
return val;
|
||||
};
|
||||
|
||||
let allOptions = [];
|
||||
let showDropdown = false;
|
||||
let inputElement;
|
||||
@@ -33,9 +58,10 @@
|
||||
if (!searchValue) {
|
||||
return [...optionsArray];
|
||||
}
|
||||
return optionsArray.filter(
|
||||
(opt) => opt && opt.toLowerCase && opt.toLowerCase().includes(searchValue.toLowerCase())
|
||||
);
|
||||
return optionsArray.filter((opt) => {
|
||||
const label = getOptionLabel(opt);
|
||||
return label && label.toLowerCase && label.toLowerCase().includes(searchValue.toLowerCase());
|
||||
});
|
||||
};
|
||||
|
||||
// Track if user has typed (for filtering) vs just focused (show all)
|
||||
@@ -68,11 +94,11 @@
|
||||
|
||||
// Select an option
|
||||
const selectOption = (option) => {
|
||||
value = option;
|
||||
value = getOptionValue(option);
|
||||
showDropdown = false;
|
||||
hasTyped = false; // Reset typing flag after selection
|
||||
justSelected = true; // Set flag to prevent dropdown reopening
|
||||
onSelect(option);
|
||||
onSelect(value);
|
||||
// Focus the input field after selection without reopening dropdown
|
||||
setTimeout(() => {
|
||||
if (inputElement) {
|
||||
@@ -244,6 +270,7 @@
|
||||
const labelId = `${comboboxId}-label`;
|
||||
|
||||
// Reactive statements for accessibility
|
||||
$: displayValue = getDisplayLabel(value);
|
||||
$: hasValue = value && value !== '';
|
||||
$: ariaExpanded = showDropdown;
|
||||
</script>
|
||||
@@ -292,7 +319,7 @@
|
||||
aria-controls={listboxId}
|
||||
aria-autocomplete="list"
|
||||
aria-haspopup="listbox"
|
||||
bind:value
|
||||
value={showDropdown ? value : displayValue}
|
||||
on:focus={handleFocus}
|
||||
on:blur={handleBlur}
|
||||
on:input={handleInput}
|
||||
@@ -358,7 +385,7 @@
|
||||
<button
|
||||
id="{listboxId}-option-{index}"
|
||||
role="option"
|
||||
aria-selected={value === option}
|
||||
aria-selected={value === getOptionValue(option)}
|
||||
class="w-full text-left bg-slate-100 dark:bg-gray-900 rounded-md text-gray-600 dark:text-gray-300 hover:bg-grayblue-dark dark:hover:bg-highlight-blue/40 hover:text-white py-2 px-2 cursor-pointer focus:bg-grayblue-dark dark:focus:bg-highlight-blue/40 focus:text-white focus:outline-none transition-colors duration-200"
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
@@ -368,7 +395,7 @@
|
||||
on:keydown={(e) => handleOptionKeyDown(e, option)}
|
||||
on:blur={handleBlur}
|
||||
>
|
||||
{option}
|
||||
{getOptionLabel(option)}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
// ES module wrapper for js-yaml UMD bundle
|
||||
import './js-yaml.js';
|
||||
|
||||
// the UMD bundle assigns to globalThis.jsyaml
|
||||
const jsyaml = globalThis.jsyaml;
|
||||
|
||||
export const load = jsyaml.load;
|
||||
export const dump = jsyaml.dump;
|
||||
export const loadAll = jsyaml.loadAll;
|
||||
export const Schema = jsyaml.Schema;
|
||||
export const Type = jsyaml.Type;
|
||||
export const YAMLException = jsyaml.YAMLException;
|
||||
export const CORE_SCHEMA = jsyaml.CORE_SCHEMA;
|
||||
export const DEFAULT_SCHEMA = jsyaml.DEFAULT_SCHEMA;
|
||||
export const FAILSAFE_SCHEMA = jsyaml.FAILSAFE_SCHEMA;
|
||||
export const JSON_SCHEMA = jsyaml.JSON_SCHEMA;
|
||||
|
||||
export default jsyaml;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -29,12 +29,14 @@
|
||||
import SimpleCodeEditor from '$lib/components/editor/SimpleCodeEditor.svelte';
|
||||
import AutoRefresh from '$lib/components/AutoRefresh.svelte';
|
||||
import TableCellScope from '$lib/components/table/TableCellScope.svelte';
|
||||
import ProxyConfigBuilder from '$lib/components/proxy/ProxyConfigBuilder.svelte';
|
||||
|
||||
// services
|
||||
const appStateService = AppStateService.instance;
|
||||
|
||||
// bindings
|
||||
let form = null;
|
||||
let proxyConfigBuilder = null;
|
||||
let formValues = {
|
||||
id: null,
|
||||
name: null,
|
||||
@@ -66,6 +68,147 @@
|
||||
let selectedProxyForIPList = null;
|
||||
let isLoadingIPAllowList = false;
|
||||
|
||||
// counter to force ProxyConfigBuilder recreation when modal opens
|
||||
let modalOpenCounter = 0;
|
||||
|
||||
// file input reference for YAML mode import
|
||||
let yamlFileInput = null;
|
||||
|
||||
// import configuration from YAML string with metadata (for YAML mode)
|
||||
function importYamlConfig(yamlStr) {
|
||||
if (!yamlStr || yamlStr.trim() === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// dynamically import js-yaml
|
||||
import('$lib/components/yaml/index.js').then((jsyaml) => {
|
||||
const parsed = jsyaml.default.load(yamlStr);
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
console.warn('Invalid YAML: not an object');
|
||||
return;
|
||||
}
|
||||
|
||||
// extract and apply general section
|
||||
if (parsed._general) {
|
||||
if (parsed._general.name) {
|
||||
formValues.name = parsed._general.name;
|
||||
}
|
||||
if (parsed._general.description) {
|
||||
formValues.description = parsed._general.description;
|
||||
}
|
||||
if (parsed._general.start_url) {
|
||||
formValues.startURL = parsed._general.start_url;
|
||||
}
|
||||
// remove _general from parsed object before serializing back
|
||||
delete parsed._general;
|
||||
}
|
||||
|
||||
// serialize back to YAML without _meta for the config
|
||||
const cleanYaml = jsyaml.default.dump(parsed, {
|
||||
indent: 2,
|
||||
lineWidth: -1,
|
||||
quotingType: "'",
|
||||
forceQuotes: false,
|
||||
noRefs: true
|
||||
});
|
||||
formValues.proxyConfig = cleanYaml;
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse imported YAML config:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// export configuration to YAML file with metadata (for YAML mode)
|
||||
function exportYamlConfig() {
|
||||
import('$lib/components/yaml/index.js').then((jsyaml) => {
|
||||
try {
|
||||
// parse current config
|
||||
const parsed = formValues.proxyConfig
|
||||
? jsyaml.default.load(formValues.proxyConfig) || {}
|
||||
: {};
|
||||
|
||||
// build output with _general first
|
||||
const output = {};
|
||||
|
||||
// add general section with proxy metadata
|
||||
output._general = {};
|
||||
if (formValues.name) {
|
||||
output._general.name = formValues.name;
|
||||
}
|
||||
if (formValues.description) {
|
||||
output._general.description = formValues.description;
|
||||
}
|
||||
if (formValues.startURL) {
|
||||
output._general.start_url = formValues.startURL;
|
||||
}
|
||||
|
||||
// merge rest of config
|
||||
Object.assign(output, parsed);
|
||||
|
||||
// serialize to YAML
|
||||
const yamlContent = jsyaml.default.dump(output, {
|
||||
indent: 2,
|
||||
lineWidth: -1,
|
||||
quotingType: "'",
|
||||
forceQuotes: false,
|
||||
noRefs: true
|
||||
});
|
||||
|
||||
// create blob and download
|
||||
const blob = new Blob([yamlContent], { type: 'application/x-yaml' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
const safeName = (formValues.name || 'proxy-config').replace(/[^a-zA-Z0-9-_]/g, '_');
|
||||
a.download = `${safeName}.yaml`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (e) {
|
||||
console.warn('Failed to export YAML config:', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// trigger file input for YAML mode import
|
||||
function triggerYamlImport() {
|
||||
yamlFileInput?.click();
|
||||
}
|
||||
|
||||
// handle file selection for YAML mode import
|
||||
function handleYamlImportFile(event) {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const content = e.target?.result;
|
||||
if (typeof content === 'string') {
|
||||
importYamlConfig(content);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
|
||||
// reset file input so same file can be imported again
|
||||
event.target.value = '';
|
||||
}
|
||||
|
||||
// editor mode: 'yaml' or 'gui' - restore from localStorage, default to yaml for safety
|
||||
const EDITOR_MODE_STORAGE_KEY = 'proxy-editor-mode';
|
||||
let editorMode =
|
||||
(typeof localStorage !== 'undefined' && localStorage.getItem(EDITOR_MODE_STORAGE_KEY)) ||
|
||||
'yaml';
|
||||
|
||||
// save editor mode to localStorage when it changes
|
||||
$: if (typeof localStorage !== 'undefined' && editorMode) {
|
||||
localStorage.setItem(EDITOR_MODE_STORAGE_KEY, editorMode);
|
||||
}
|
||||
|
||||
// fullscreen mode for modal - automatically true when in GUI mode
|
||||
$: isModalFullscreen = editorMode === 'gui';
|
||||
|
||||
const currentExample = `version: "0.0"
|
||||
|
||||
# optional: forward proxy for outbound requests
|
||||
@@ -220,6 +363,16 @@ portal.example.com:
|
||||
try {
|
||||
isSubmitting = true;
|
||||
const saveOnly = event?.detail?.saveOnly || false;
|
||||
|
||||
// validate config when in GUI mode
|
||||
if (editorMode === 'gui' && proxyConfigBuilder) {
|
||||
const validation = proxyConfigBuilder.validate();
|
||||
if (!validation.valid) {
|
||||
isSubmitting = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (modalMode === 'create' || modalMode === 'copy') {
|
||||
await create();
|
||||
return;
|
||||
@@ -305,6 +458,7 @@ portal.example.com:
|
||||
|
||||
const openCreateModal = () => {
|
||||
modalMode = 'create';
|
||||
modalOpenCounter++;
|
||||
isModalVisible = true;
|
||||
};
|
||||
|
||||
@@ -322,6 +476,7 @@ portal.example.com:
|
||||
/** @param {string} id */
|
||||
const openUpdateModal = async (id) => {
|
||||
modalMode = 'update';
|
||||
modalOpenCounter++;
|
||||
showIsLoading();
|
||||
|
||||
// reset form values first
|
||||
@@ -353,6 +508,7 @@ portal.example.com:
|
||||
|
||||
const openCopyModal = async (id) => {
|
||||
modalMode = 'copy';
|
||||
modalOpenCounter++;
|
||||
showIsLoading();
|
||||
|
||||
// reset form values first
|
||||
@@ -511,48 +667,140 @@ portal.example.com:
|
||||
</TableRow>
|
||||
{/each}
|
||||
</Table>
|
||||
<Modal headerText={modalText} visible={isModalVisible} onClose={closeModal} {isSubmitting}>
|
||||
<Modal
|
||||
headerText={modalText}
|
||||
visible={isModalVisible}
|
||||
onClose={closeModal}
|
||||
{isSubmitting}
|
||||
fullscreen={isModalFullscreen}
|
||||
>
|
||||
<FormGrid on:submit={onSubmit} bind:bindTo={form} {isSubmitting} {modalMode}>
|
||||
<div class="col-span-3 w-full overflow-y-auto px-6 py-4 space-y-8">
|
||||
<!-- Basic Information Section -->
|
||||
<div class="w-full">
|
||||
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">
|
||||
Basic Information
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-[1fr_2fr_2fr] gap-4">
|
||||
<div>
|
||||
<TextField
|
||||
required
|
||||
minLength={1}
|
||||
maxLength={64}
|
||||
bind:value={formValues.name}
|
||||
placeholder="Company Auth Proxy">Name</TextField
|
||||
>
|
||||
<div
|
||||
class="col-span-3 w-full px-6 py-4 {isModalFullscreen
|
||||
? 'flex flex-col min-h-0 overflow-hidden'
|
||||
: 'overflow-y-auto space-y-8'}"
|
||||
>
|
||||
{#if editorMode === 'yaml'}
|
||||
<!-- Basic Information Section - only shown in YAML mode -->
|
||||
<div class="w-full mb-6 pt-4 pb-2 border-b border-gray-200 dark:border-gray-600">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<h3 class="text-base font-medium text-pc-darkblue dark:text-white">
|
||||
Basic Information
|
||||
</h3>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="file"
|
||||
accept=".yaml,.yml"
|
||||
bind:this={yamlFileInput}
|
||||
on:change={handleYamlImportFile}
|
||||
class="hidden"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-slate-600 dark:text-gray-400 bg-slate-100 dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-md hover:bg-slate-200 dark:hover:bg-gray-700 transition-colors"
|
||||
on:click={triggerYamlImport}
|
||||
title="Import configuration from YAML file"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
|
||||
<polyline points="7 10 12 15 17 10" />
|
||||
<line x1="12" y1="15" x2="12" y2="3" />
|
||||
</svg>
|
||||
Import
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-slate-600 dark:text-gray-400 bg-slate-100 dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-md hover:bg-slate-200 dark:hover:bg-gray-700 transition-colors"
|
||||
on:click={exportYamlConfig}
|
||||
title="Export configuration to YAML file"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
|
||||
<polyline points="17 8 12 3 7 8" />
|
||||
<line x1="12" y1="3" x2="12" y2="15" />
|
||||
</svg>
|
||||
Export
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<TextField optional maxLength={255} bind:value={formValues.description}
|
||||
>Description</TextField
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<TextField
|
||||
required
|
||||
minLength={3}
|
||||
bind:value={formValues.startURL}
|
||||
placeholder="https://login.example.com/auth"
|
||||
toolTipText="Domain must be in proxy configuration">Start URL</TextField
|
||||
>
|
||||
<div class="grid grid-cols-1 md:grid-cols-[1fr_2fr_2fr] gap-4">
|
||||
<div>
|
||||
<TextField
|
||||
required
|
||||
minLength={1}
|
||||
maxLength={64}
|
||||
bind:value={formValues.name}
|
||||
placeholder="Company Auth Proxy">Name</TextField
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<TextField optional maxLength={255} bind:value={formValues.description}
|
||||
>Description</TextField
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<TextField
|
||||
required
|
||||
minLength={3}
|
||||
bind:value={formValues.startURL}
|
||||
placeholder="https://login.example.com/auth"
|
||||
toolTipText="Domain must match a phishing domain in the hosts configuration"
|
||||
>Start URL</TextField
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Proxy Configuration Section -->
|
||||
<div class="w-full">
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col py-2 w-full">
|
||||
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">
|
||||
<div
|
||||
class="w-full {isModalFullscreen ? 'flex-1 flex flex-col min-h-0 overflow-hidden' : ''}"
|
||||
>
|
||||
<div class={isModalFullscreen ? 'flex flex-col h-full min-h-0' : 'space-y-4'}>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-base font-medium text-pc-darkblue dark:text-white">
|
||||
Proxy Configuration
|
||||
</h3>
|
||||
<!-- Editor Mode Tabs -->
|
||||
<div
|
||||
class="flex border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 text-sm font-medium transition-colors duration-200 {editorMode ===
|
||||
'yaml'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'}"
|
||||
on:click={() => (editorMode = 'yaml')}
|
||||
>
|
||||
YAML
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 text-sm font-medium transition-colors duration-200 {editorMode ===
|
||||
'gui'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'}"
|
||||
on:click={() => (editorMode = 'gui')}
|
||||
>
|
||||
Visual
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if editorMode === 'yaml'}
|
||||
<div class="w-80vw">
|
||||
<SimpleCodeEditor
|
||||
bind:value={formValues.proxyConfig}
|
||||
@@ -562,7 +810,23 @@ portal.example.com:
|
||||
enableProxyCompletion={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex-1 min-h-0 overflow-hidden">
|
||||
{#key modalOpenCounter}
|
||||
<ProxyConfigBuilder
|
||||
bind:this={proxyConfigBuilder}
|
||||
config={formValues.proxyConfig}
|
||||
name={formValues.name}
|
||||
description={formValues.description}
|
||||
startURL={formValues.startURL}
|
||||
on:change={(e) => (formValues.proxyConfig = e.detail)}
|
||||
on:nameChange={(e) => (formValues.name = e.detail)}
|
||||
on:descriptionChange={(e) => (formValues.description = e.detail)}
|
||||
on:startURLChange={(e) => (formValues.startURL = e.detail)}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user