Added UI to manage cookies

This commit is contained in:
sanish-bruno
2025-02-19 11:28:41 +05:30
committed by Anoop M D
parent 253cb8b315
commit 51c86bc0e9
13 changed files with 1133 additions and 43 deletions

14
package-lock.json generated
View File

@@ -17161,6 +17161,18 @@
"node": "*"
}
},
"node_modules/moment-timezone": {
"version": "0.5.47",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.47.tgz",
"integrity": "sha512-UbNt/JAWS0m/NJOebR0QMRHBk0hu03r5dx9GK8Cs0AS3I81yDcOc9k+DytPItgVvBP7J6Mf6U2n3BPAacAV9oA==",
"license": "MIT",
"dependencies": {
"moment": "^2.29.4"
},
"engines": {
"node": "*"
}
},
"node_modules/mousetrap": {
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz",
@@ -24446,6 +24458,8 @@
"lodash": "^4.17.21",
"markdown-it": "^13.0.2",
"markdown-it-replace-link": "^1.2.0",
"moment": "^2.30.1",
"moment-timezone": "^0.5.47",
"mousetrap": "^1.6.5",
"nanoid": "3.3.8",
"path": "^0.12.7",

View File

@@ -47,6 +47,8 @@
"lodash": "^4.17.21",
"markdown-it": "^13.0.2",
"markdown-it-replace-link": "^1.2.0",
"moment": "^2.30.1",
"moment-timezone": "^0.5.47",
"mousetrap": "^1.6.5",
"nanoid": "3.3.8",
"path": "^0.12.7",

View File

@@ -0,0 +1,62 @@
import React, { createContext, useContext, useState } from 'react';
import { IconChevronDown } from '@tabler/icons';
import { AccordionItem, AccordionHeader, AccordionContent } from './styledWrapper';
const AccordionContext = createContext();
const Accordion = ({ children, defaultIndex }) => {
const [openIndex, setOpenIndex] = useState(defaultIndex);
const toggleItem = (index) => {
setOpenIndex(openIndex === index ? null : index);
};
return (
<AccordionContext.Provider value={{ openIndex, toggleItem }}>
<div>{children}</div>
</AccordionContext.Provider>
);
};
const Item = ({ index, children, ...props }) => {
return (
<AccordionItem {...props}>
{React.Children.map(children, (child) => React.cloneElement(child, { index }))}
</AccordionItem>
);
};
export const Header = ({ index, children, ...props }) => {
const { openIndex, toggleItem } = useContext(AccordionContext);
const isOpen = openIndex === index;
return (
<AccordionHeader onClick={() => toggleItem(index)} {...props}>
<div className="w-full">{children}</div>
<IconChevronDown
className="w-5 h-5 ml-auto"
style={{
transform: `rotate(${isOpen ? '180deg' : '0deg'})`,
transition: 'transform 0.3s ease-in-out'
}}
/>
</AccordionHeader>
);
};
const Content = ({ index, children, ...props }) => {
const { openIndex } = useContext(AccordionContext);
const isOpen = openIndex === index;
return (
<AccordionContent isOpen={isOpen} {...props}>
{children}
</AccordionContent>
);
};
Accordion.Item = Item;
Accordion.Header = Header;
Accordion.Content = Content;
export default Accordion;

View File

@@ -0,0 +1,29 @@
import styled from 'styled-components';
const AccordionItem = styled.div`
border: 1px solid ${(props) => props.theme.input.border};
border-radius: 4px;
overflow: hidden;
margin-bottom: 1rem;
`;
const AccordionHeader = styled.button`
width: 100%;
display: flex;
padding: 1rem;
background: transparent;
cursor: pointer;
font-weight: 500;
&:hover {
background-color: ${(props) => props.theme.plainGrid.hoverBg};
}
`;
const AccordionContent = styled.div`
padding: ${(props) => (props.isOpen ? '1rem' : '0')};
max-height: ${(props) => (props.isOpen ? 'auto' : '0')};
transition: all 0.2s ease-in-out;
`;
export { AccordionItem, AccordionHeader, AccordionContent };

View File

@@ -0,0 +1,5 @@
import styled from 'styled-components';
const StyledWrapper = styled.div``;
export default StyledWrapper;

View File

@@ -0,0 +1,359 @@
import React, { useState, useRef, useEffect } from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import Modal from 'components/Modal/index';
import { modifyCookie, addCookie, getParsedCookie, createCookieString } from 'providers/ReduxStore/slices/app';
import { useDispatch } from 'react-redux';
import toast from 'react-hot-toast';
import ToggleSwitch from 'components/ToggleSwitch/index';
import { IconInfoCircle } from '@tabler/icons';
import moment from 'moment';
import 'moment-timezone';
import { Tooltip } from 'react-tooltip';
const removeEmptyValues = (obj) => {
return Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== null && value !== undefined));
};
const ModifyCookieModal = ({ onClose, domain, cookie }) => {
const dispatch = useDispatch();
const [isRawMode, setIsRawMode] = useState(false);
const [cookieString, setCookieString] = useState('');
const initialParseRef = useRef(false);
const formik = useFormik({
enableReinitialize: true,
initialValues: {
...(cookie ? cookie : {}),
key: cookie?.key || '',
value: cookie?.value || '',
path: cookie?.path || '/',
domain: cookie?.domain || domain || '',
expires: cookie?.expires ? moment(cookie.expires).format(moment.HTML5_FMT.DATETIME_LOCAL) : '',
secure: cookie?.secure || false,
httpOnly: cookie?.httpOnly || false
},
validationSchema: Yup.object({
key: Yup.string().required('Key is required'),
value: Yup.string().required('Value is required'),
domain: Yup.string().required('Domain is required'),
secure: Yup.boolean(),
httpOnly: Yup.boolean(),
expires: Yup.mixed()
.nullable()
.transform((value) => {
if (!value || value === '') return null;
return moment(value).isValid() ? moment(value).toDate() : null;
})
.test('future-date', 'Expiration date must be in the future', (value) => {
if (!value) return true;
return moment(value).isAfter(moment());
})
}),
onSubmit: (values) => {
const modValues = removeEmptyValues({
...(cookie ? cookie : {}),
...values,
expires: values.expires
? moment(values.expires).isValid()
? moment(values.expires).toDate()
: Infinity
: Infinity
});
handleCookieDispatch(cookie, domain, modValues, onClose);
}
});
const title = cookie ? 'Modify Cookie' : 'Add Cookie';
const handleCookieDispatch = (cookie, domain, modValues, onClose) => {
if (cookie) {
dispatch(modifyCookie(domain, cookie, cookie.path, cookie.key, modValues))
.then(() => {
toast.success('Cookie modified successfully');
onClose();
})
.catch((err) => {
toast.error('An error occurred while modifying cookie');
console.error(err);
});
} else {
dispatch(addCookie(domain, modValues))
.then(() => {
toast.success('Cookie added successfully');
onClose();
})
.catch((err) => {
toast.error('An error occurred while adding cookie');
console.error(err);
});
}
};
const onSubmit = async () => {
try {
if (isRawMode) {
const cookieObj = await dispatch(getParsedCookie(cookieString));
const modifiedCookie = removeEmptyValues({
...formik.values,
...cookieObj,
expires: cookieObj?.expires
? moment(cookieObj.expires).isValid()
? moment(cookieObj.expires).toDate()
: Infinity
: Infinity
});
if (!cookieObj) {
toast.error('Please enter a valid cookie string');
return;
}
formik.setValues(
(values) => ({
...values,
...modifiedCookie,
expires:
modifiedCookie?.expires && moment(modifiedCookie.expires).isValid()
? moment(new Date(modifiedCookie.expires)).format(moment.HTML5_FMT.DATETIME_LOCAL)
: ''
}),
true
);
handleCookieDispatch(cookie, domain, modifiedCookie, onClose);
} else {
formik.handleSubmit();
}
} catch (error) {
const errMsg = error.message || 'An error occurred while parsing cookie string';
toast.error(errMsg);
}
};
useEffect(() => {
if (!isRawMode) return;
const loadCookieString = async () => {
if (cookie) {
const str = await dispatch(createCookieString(cookie));
setCookieString(str);
}
return '';
};
loadCookieString();
}, [cookie, isRawMode]);
// create the cookieString when raw mode is enabled
useEffect(() => {
if (isRawMode) {
const createCookieStr = async () => {
const str = await dispatch(createCookieString(formik.values));
setCookieString(str);
};
createCookieStr();
}
}, [isRawMode, formik.values]);
useEffect(() => {
// Reset the ref when raw mode changes
if (isRawMode) {
initialParseRef.current = false;
return;
}
const setParsedCookie = async () => {
if (!isRawMode && cookieString && !initialParseRef.current) {
try {
const cookieObj = await dispatch(getParsedCookie(cookieString));
if (!cookieObj) return;
initialParseRef.current = true;
formik.setValues(
(values) => ({
...values,
...cookieObj,
expires:
cookieObj?.expires && moment(cookieObj.expires).isValid()
? moment(new Date(cookieObj.expires)).format(moment.HTML5_FMT.DATETIME_LOCAL)
: ''
}),
true
);
} catch (error) {
const errMsg = error.message || 'An error occurred while parsing cookie string';
toast.error(errMsg);
}
}
};
setParsedCookie();
}, [isRawMode, cookieString, dispatch, formik]);
return (
<Modal
size="lg"
title={title}
onClose={onClose}
handleCancel={onClose}
handleConfirm={onSubmit}
customHeader={
<div className="flex items-center justify-between w-full">
<h2 className="text-sm font-bold">{title}</h2>
<div className="ml-auto flex items-center ">
<ToggleSwitch
className="mr-2"
isOn={isRawMode}
size="2xs"
handleToggle={(e) => {
setIsRawMode(e.target.checked);
}}
/>
<label className="text-sm font-normal mr-4 normal-case">Edit Raw</label>
</div>
</div>
}
>
<form onSubmit={(e) => e.preventDefault()} className="p-6">
{isRawMode ? (
<div>
<div className="flex items-center gap-2 mb-1">
<label className="block text-sm">Set-Cookie String</label>
<IconInfoCircle id="cookie-raw-info" size={16} strokeWidth={1.5} className="text-gray-400" />
<Tooltip
anchorId="cookie-raw-info"
className="tooltip-mod"
html="Key, Path, and Domain are immutable properties and cannot be modified for existing cookies"
/>
</div>
<textarea
value={cookieString}
onChange={(e) => setCookieString(e.target.value)}
className="block textbox w-full h-24"
placeholder="key=value; key2=value2"
/>
</div>
) : (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm mb-1">
Key<span className="text-red-600">*</span>{' '}
</label>
<input
type="text"
name="key"
value={formik.values.key}
onChange={formik.handleChange}
className="block textbox non-passphrase-input w-full disabled:opacity-50"
disabled={!!cookie}
/>
{formik.touched.key && formik.errors.key && (
<div className="text-red-500 text-sm mt-1">{formik.errors.key}</div>
)}
</div>
<div>
<label className="block text-sm mb-1">
Value<span className="text-red-600">*</span>{' '}
</label>
<input
type="text"
name="value"
value={formik.values.value}
onChange={formik.handleChange}
className="block textbox non-passphrase-input w-full"
/>
{formik.touched.value && formik.errors.value && (
<div className="text-red-500 text-sm mt-1">{formik.errors.value}</div>
)}
</div>
<div>
<label className="block text-sm mb-1">
Domain<span className="text-red-600">*</span>{' '}
</label>
<input
type="text"
name="domain"
value={formik.values.domain}
onChange={formik.handleChange}
className="block textbox non-passphrase-input w-full disabled:opacity-50"
disabled={!!cookie}
/>
{formik.touched.domain && formik.errors.domain && (
<div className="text-red-500 text-sm mt-1">{formik.errors.domain}</div>
)}
</div>
<div>
<label className="block text-sm mb-1">Path</label>
<input
type="text"
name="path"
value={formik.values.path}
onChange={formik.handleChange}
className="block textbox non-passphrase-input w-full disabled:opacity-50"
disabled={!!cookie}
/>
{formik.touched.path && formik.errors.path && (
<div className="text-red-500 text-sm mt-1">{formik.errors.path}</div>
)}
</div>
</div>
{/* Date Picker */}
<div className="w-full flex items-end">
<div>
<label className="block text-sm mb-1">Expiration ({moment.tz.guess()})</label>
<input
type="datetime-local"
name="expires"
value={formik.values.expires}
onChange={(e) => {
formik.handleChange(e);
}}
className="block textbox non-passphrase-input w-full"
min={moment().format(moment.HTML5_FMT.DATETIME_LOCAL)}
/>
{formik.touched.expires && formik.errors.expires && (
<div className="text-red-500 text-sm mt-1">{formik.errors.expires}</div>
)}
</div>
{/* Checkboxes */}
<div className="flex space-x-4 ml-auto">
<label className="flex items-center">
<input
type="checkbox"
name="secure"
checked={formik.values.secure}
onChange={formik.handleChange}
className="mr-2"
/>
<span className="text-sm">Secure</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
name="httpOnly"
checked={formik.values.httpOnly}
onChange={formik.handleChange}
className="mr-2"
/>
<span className="text-sm">HTTP Only</span>
</label>
</div>
</div>
</div>
)}
</form>
</Modal>
);
};
export default ModifyCookieModal;

View File

@@ -11,6 +11,47 @@ const Wrapper = styled.div`
user-select: none;
}
}
.textbox {
line-height: 1.42857143;
border: 1px solid #ccc;
padding: 0.45rem;
box-shadow: none;
border-radius: 0px;
outline: none;
box-shadow: none;
transition: border-color ease-in-out 0.1s;
border-radius: 3px;
background-color: ${(props) => props.theme.modal.input.bg};
border: 1px solid ${(props) => props.theme.modal.input.border};
&:focus {
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
outline: none !important;
}
}
.scroll-box {
max-height: 500px;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
overflow-scrolling: touch;
background:
/* Shadow Cover TOP */ linear-gradient(
${(props) => props.theme.modal.body.bg} 20%,
rgba(255, 255, 255, 0)
)
center top,
/* Shadow Cover BOTTOM */ linear-gradient(rgba(255, 255, 255, 0), ${(props) => props.theme.modal.body.bg} 80%)
center bottom,
/* Shadow TOP */ linear-gradient(rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0) 100%) center top,
/* Shadow BOTTOM */ linear-gradient(rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.1) 100%) center bottom;
background-repeat: no-repeat;
background-size: 100% 30px, 100% 30px, 100% 10px, 100% 10px;
background-attachment: local, local, scroll, scroll;
}
`;
export default Wrapper;

View File

@@ -1,53 +1,330 @@
import React from 'react';
import React, { useState, useRef, useEffect, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Modal from 'components/Modal';
import { IconTrash } from '@tabler/icons';
import { deleteCookiesForDomain } from 'providers/ReduxStore/slices/app';
import Accordion from 'components/Accordion/index';
import { IconTrash, IconEdit, IconCirclePlus, IconCookieOff, IconAlertTriangle, IconSearch } from '@tabler/icons';
import { deleteCookiesForDomain, deleteCookie } from 'providers/ReduxStore/slices/app';
import toast from 'react-hot-toast';
import ModifyCookieModal from 'components/Cookies/ModifyCookieModal/index';
import StyledWrapper from './StyledWrapper';
import moment from 'moment';
import { Tooltip } from 'react-tooltip';
const CollectionProperties = ({ onClose }) => {
const dispatch = useDispatch();
const cookies = useSelector((state) => state.app.cookies) || [];
const [isModifyCookieModalOpen, setIsModifyCookieModalOpen] = useState(false);
const [currentDomain, setCurrentDomain] = useState('');
const [cookieToEdit, setCookieToEdit] = useState(null);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [deleteModalContent, setDeleteModalContent] = useState(null);
const [deleteModalTitle, setDeleteModalTitle] = useState('');
const [onDeleteAction, setOnDeleteAction] = useState(() => {});
const [searchText, setSearchText] = useState('');
const handleDeleteDomain = (domain) => {
dispatch(deleteCookiesForDomain(domain))
.then(() => {
toast.success('Domain deleted successfully');
})
.catch((err) => console.log(err) && toast.error('Failed to delete domain'));
const handleAddCookie = (domain) => {
setCurrentDomain(domain);
setIsModifyCookieModalOpen(true);
};
const handleEditCookie = (domain, cookie) => {
setCurrentDomain(domain);
setCookieToEdit(cookie);
setIsModifyCookieModalOpen(true);
};
const openModal = (title, content, onDelete) => {
setDeleteModalTitle(title);
setDeleteModalContent(content);
setOnDeleteAction(() => onDelete);
setIsDeleteModalOpen(true);
};
const closeDeleteModal = () => {
setIsDeleteModalOpen(false);
};
const handleDeleteDomain = (domain) => {
openModal('Delete Domain', `Are you sure you want to delete the domain ${domain}?`, () => {
dispatch(deleteCookiesForDomain(domain))
.then(() => {
toast.success('Domain deleted successfully');
})
.catch((err) => console.log(err) && toast.error('Failed to delete domain'));
closeDeleteModal();
});
};
const handleDeleteCookie = (domain, path, key) => {
openModal('Delete Cookie', `Are you sure you want to delete the cookie ${key}?`, () => {
dispatch(deleteCookie(domain, path, key))
.then(() => {
toast.success('Cookie deleted successfully');
})
.catch((err) => console.log(err) && toast.error('Failed to delete cookie'));
closeDeleteModal();
});
};
const filteredCookies = useMemo(() => {
return cookies.filter((cookie) => cookie.domain.toLowerCase().includes(searchText.toLowerCase()));
}, [cookies, searchText]);
if (!cookies || !cookies.length) {
return (
<>
<Modal size="xl" title="Cookies" hideFooter={true} handleCancel={onClose}>
<StyledWrapper>
<div className="flex items-center justify-center flex-col">
<IconCookieOff size={48} />
<h2 className="text-lg font-semibold mt-4">No cookies found</h2>
<p className="text-gray-500 mt-2">Add cookies to get started</p>
<button
type="submit"
className="submit btn btn-sm btn-secondary flex items-center gap-1 mt-4"
onClick={() => {
handleAddCookie('');
}}
>
<IconCirclePlus strokeWidth={1.5} size={16} />
<span>Add cookie</span>
</button>
</div>
</StyledWrapper>
</Modal>
{isModifyCookieModalOpen && (
<ModifyCookieModal
onClose={() => {
setCookieToEdit(null);
setCurrentDomain('');
setIsModifyCookieModalOpen(false);
}}
domain={currentDomain}
cookie={cookieToEdit}
/>
)}
</>
);
}
if (cookies.length && !filteredCookies.length) {
return (
<>
<Modal
size="xl"
title="Cookies"
hideFooter={true}
handleCancel={onClose}
customHeader={
<StyledWrapper className="flex items-center justify-between w-full">
<h2 className="text-sm font-semibold">Cookies</h2>
<input
type="search"
placeholder="Search by domain"
value={searchText}
onChange={(e) => {
setSearchText(e.target.value);
}}
className="block textbox non-passphrase-input h-9 ml-auto"
/>
<button
type="submit"
className="submit btn btn-sm h-9 btn-secondary flex items-center gap-1 mx-4"
onClick={() => {
handleAddCookie('');
}}
>
<IconCirclePlus strokeWidth={1.5} size={16} />
<span>Add New</span>
</button>
</StyledWrapper>
}
>
<StyledWrapper>
<div className="flex items-center justify-center flex-col">
<IconSearch size={48} />
<h2 className="text-lg font-semibold mt-4">No search results</h2>
<p className="text-gray-500 mt-2">Try a different search term</p>
</div>
</StyledWrapper>
</Modal>
</>
);
}
return (
<Modal size="md" title="Cookies" hideFooter={true} handleCancel={onClose}>
<StyledWrapper>
<table className="w-full border-collapse" style={{ marginTop: '-1rem' }}>
<thead>
<tr>
<th className="py-2 px-2 text-left">Domain</th>
<th className="py-2 px-2 text-left">Cookie</th>
<th className="py-2 px-2 text-center" style={{ width: 80 }}>
Actions
</th>
</tr>
</thead>
<tbody>
{cookies.map((cookie) => (
<tr key={cookie.domain}>
<td className="py-2 px-2">{cookie.domain}</td>
<td className="py-2 px-2 break-all">{cookie.cookieString}</td>
<td className="text-center">
<button tabIndex="-1" onClick={() => handleDeleteDomain(cookie.domain)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</td>
</tr>
))}
</tbody>
</table>
</StyledWrapper>
</Modal>
<>
<Modal
size="xl"
title="Cookies"
hideFooter={true}
handleCancel={onClose}
customHeader={
<StyledWrapper className="flex items-center justify-between w-full">
<h2 className="text-sm font-semibold">Cookies</h2>
<input
type="search"
placeholder="Search by domain"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="block textbox non-passphrase-input h-9 ml-auto"
/>
<button
type="submit"
className="submit btn btn-sm h-9 btn-secondary flex items-center gap-1 mx-4"
onClick={() => {
handleAddCookie('');
}}
>
<IconCirclePlus strokeWidth={1.5} size={16} />
<span>Add New</span>
</button>
</StyledWrapper>
}
>
<StyledWrapper>
<div className="scroll-box">
<Accordion defaultIndex={0}>
{filteredCookies.map((domainWithCookies, i) => (
<Accordion.Item key={i} index={i}>
<Accordion.Header index={i} className="flex items-center">
<div className="flex items-center">
<span>{domainWithCookies.domain}</span>
<span className="ml-2 text-xs dark:text-gray-300 text-gray-500">
({domainWithCookies.cookies.length}{' '}
{domainWithCookies.cookies.length === 1 ? 'cookie' : 'cookies'})
</span>
<div className="ml-auto flex items-center gap-2">
<button
type="submit"
className="flex items-center gap-1 text-gray-500 hover:text-gray-950 dark:text-white dark:hover:text-gray-300"
onClick={(e) => {
e.stopPropagation();
handleAddCookie(domainWithCookies.domain);
}}
>
<IconCirclePlus strokeWidth={1.5} size={16} />
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteDomain(domainWithCookies.domain);
}}
className="text-gray-950 dark:text-white dark:hover:hover:text-red-600 hover:text-red-600 mr-2"
>
<IconTrash strokeWidth={1.5} size={16} />
</button>
</div>
</div>
</Accordion.Header>
<Accordion.Content index={i}>
<div className="flex items-center justify-between">
<table className="w-full">
<thead>
<tr className="text-left border-b border-gray-300 dark:border-gray-500">
<th className="py-2 px-4 font-medium w-32">Name</th>
<th className="py-2 px-4 font-medium w-52">Value</th>
<th className="py-2 px-4 font-medium">Path</th>
<th className="py-2 px-4 font-medium">Expires</th>
<th className="py-2 px-4 font-medium text-center">Secure</th>
<th className="py-2 px-4 font-medium text-center">HTTP Only</th>
<th className="py-2 px-4 font-medium text-right w-24">Actions</th>
</tr>
</thead>
<tbody>
{domainWithCookies.cookies.map((cookie) => (
<tr key={cookie.key} className="border-b border-gray-300 dark:border-gray-500">
<td className="py-2 px-4 truncate">{cookie.key}</td>
<td className="py-2 px-4 truncate">
<span id={`cookie-value-${cookie.key}`}>{cookie.value}</span>
<Tooltip
anchorId={`cookie-value-${cookie.key}`}
className="tooltip-mod"
html={cookie.value}
/>
</td>
<td className="py-2 px-4 truncate">{cookie.path || '/'}</td>
<td className="py-2 px-4 truncate">
<span id={`cookie-expires-${cookie.key}`}>
{cookie.expires && moment(cookie.expires).isValid()
? new Date(cookie.expires).toLocaleString()
: 'Session Cookie'}
</span>
{cookie.expires && moment(cookie.expires).isValid() && (
<Tooltip
anchorId={`cookie-expires-${cookie.key}`}
className="tooltip-mod"
html={new Date(cookie.expires).toLocaleString()}
/>
)}
</td>
<td className="py-2 px-4 text-center">{cookie.secure ? '✓' : ''}</td>
<td className="py-2 px-4 text-center">{cookie.httpOnly ? '✓' : ''}</td>
<td className="py-2 px-4">
<div className="flex items-center justify-end gap-2">
<button
onClick={() => handleEditCookie(domainWithCookies.domain, cookie)}
className="text-gray-700 hover:text-gray-950
dark:text-white dark:hover:text-gray-300"
>
<IconEdit strokeWidth={1.5} size={16} />
</button>
<button
onClick={() =>
handleDeleteCookie(domainWithCookies.domain, cookie.path, cookie.key)
}
className="text-gray-950 dark:text-white dark:hover:hover:text-red-600 hover:text-red-600"
>
<IconTrash strokeWidth={1.5} size={16} />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Accordion.Content>
</Accordion.Item>
))}
</Accordion>
</div>
</StyledWrapper>
</Modal>
{isDeleteModalOpen && (
<Modal onClose={closeDeleteModal} handleCancel={closeDeleteModal} title={deleteModalTitle} hideFooter={true}>
<div className="flex items-center font-normal">
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
<h1 className="ml-2 text-lg font-semibold">Hold on..</h1>
</div>
<div className="font-normal mt-4">{deleteModalContent}</div>
<div className="flex justify-between mt-6">
<div>
<button className="btn btn-sm btn-close" onClick={closeDeleteModal}>
Close
</button>
</div>
<div>
<button className="btn btn-sm btn-danger" onClick={onDeleteAction}>
Delete
</button>
</div>
</div>
</Modal>
)}
{isModifyCookieModalOpen && (
<ModifyCookieModal
onClose={() => {
setCookieToEdit(null);
setCurrentDomain('');
setIsModifyCookieModalOpen(false);
}}
domain={currentDomain}
cookie={cookieToEdit}
/>
)}
</>
);
};

View File

@@ -0,0 +1,91 @@
import styled from 'styled-components';
const switchSizes = {
'2xs': { width: 32, height: 16, buttonSize: 14 },
xs: { width: 40, height: 20, buttonSize: 18 },
s: { width: 44, height: 22, buttonSize: 20 },
m: { width: 50, height: 24, buttonSize: 22 }, // default size
l: { width: 56, height: 28, buttonSize: 26 },
xl: { width: 64, height: 32, buttonSize: 30 },
'2xl': { width: 72, height: 36, buttonSize: 34 }
};
const getSizeValues = (size = 'm') => switchSizes[size] || switchSizes.m;
export const Switch = styled.div`
position: relative;
display: inline-block;
width: ${(props) => getSizeValues(props.size).width}px;
height: ${(props) => getSizeValues(props.size).height}px;
border-radius: ${(props) => getSizeValues(props.size).height}px;
`;
export const Checkbox = styled.input`
opacity: 0;
width: 0;
height: 0;
&:checked + label div {
background-color: ${(props) => props.theme.textLink};
}
&:checked + label div:before {
transform: translateX(${(props) => getSizeValues(props.size).width - getSizeValues(props.size).buttonSize - 2}px);
}
`;
export const Label = styled.label`
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: pointer;
background-color: ${(props) => props.theme.input.bg};
border-radius: 24px;
div {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: ${(props) => props.theme.colors.text.muted};
border-radius: 24px;
transition: transform 0.2s;
}
`;
export const Inner = styled.div`
position: absolute;
top: 2px;
left: 2px;
right: 2px;
bottom: 2px;
background-color: #fafafa;
transition: 0.4s;
border-radius: ${(props) => getSizeValues(props.size).height - 2}px;
`;
export const SwitchButton = styled.div`
position: absolute;
height: ${(props) => getSizeValues(props.size).buttonSize}px;
width: ${(props) => getSizeValues(props.size).buttonSize}px;
left: 2px;
bottom: 2px;
background-color: white;
transition: 0.4s;
border-radius: 50%;
&:before {
content: '';
position: absolute;
height: ${(props) => getSizeValues(props.size).buttonSize - 2}px;
width: ${(props) => getSizeValues(props.size).buttonSize - 2}px;
background-color: white;
top: 2px;
left: 2px;
transition: 0.4s;
border-radius: 50%;
}
`;

View File

@@ -0,0 +1,15 @@
import { Checkbox, Inner, Label, Switch, SwitchButton } from './StyledWrapper';
const ToggleSwitch = ({ isOn, handleToggle, size = 'm', ...props }) => {
return (
<Switch size={size} {...props}>
<Checkbox checked={isOn} onChange={handleToggle} id="toggle-switch" type="checkbox" size={size} />
<Label htmlFor="toggle-switch">
<Inner size={size} />
<SwitchButton size={size} />
</Label>
</Switch>
);
};
export default ToggleSwitch;

View File

@@ -122,6 +122,44 @@ export const deleteCookiesForDomain = (domain) => (dispatch, getState) => {
});
};
export const deleteCookie = (domain, path, cookieKey) => (dispatch, getState) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('renderer:delete-cookie', domain, path, cookieKey).then(resolve).catch(reject);
});
};
export const addCookie = (domain, cookie) => (dispatch, getState) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('renderer:add-cookie', domain, cookie).then(resolve).catch(reject);
});
};
export const modifyCookie = (domain, oldCookie, path, key, cookie) => (dispatch, getState) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('renderer:modify-cookie', domain, oldCookie, cookie).then(resolve).catch(reject);
});
};
export const getParsedCookie = (cookieStr) => () => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('renderer:get-parsed-cookie', cookieStr).then(resolve).catch(reject);
});
};
export const createCookieString = (cookieObj) => () => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('renderer:create-cookie-string', cookieObj).then(resolve).catch(reject);
});
};
export const completeQuitFlow = () => (dispatch, getState) => {
const { ipcRenderer } = window;
return ipcRenderer.invoke('main:complete-quit-flow');

View File

@@ -29,7 +29,7 @@ const {
const { openCollectionDialog } = require('../app/collections');
const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common');
const { moveRequestUid, deleteRequestUid } = require('../cache/requestUids');
const { deleteCookiesForDomain, getDomainsWithCookies } = require('../utils/cookies');
const { deleteCookiesForDomain, getDomainsWithCookies, addCookieForDomain, modifyCookieForDomain, parseCookieString, createCookieString, deleteCookie } = require('../utils/cookies');
const EnvironmentSecretsStore = require('../store/env-secrets');
const CollectionSecurityStore = require('../store/collection-security');
const UiStateSnapshotStore = require('../store/ui-state-snapshot');
@@ -395,7 +395,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
* And it is not WSL path (meaning its not linux running on windows using WSL)
* And it has sub directories
* Only then we need to use the temp dir approach to rename the folder
*
*
* Windows OS would sometimes throw error when renaming a folder with sub directories
* This is a alternative approach to avoid that error
*/
@@ -770,6 +770,54 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
});
ipcMain.handle('renderer:delete-cookie', async (event, domain, path, cookieKey) => {
try {
await deleteCookie(domain, path, cookieKey);
const domainsWithCookies = await getDomainsWithCookies();
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
} catch (error) {
return Promise.reject(error);
}
});
// add cookie
ipcMain.handle('renderer:add-cookie', async (event, domain, cookie) => {
try {
await addCookieForDomain(domain, cookie);
const domainsWithCookies = await getDomainsWithCookies();
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
} catch (error) {
return Promise.reject(error);
}
});
// modify cookie
ipcMain.handle('renderer:modify-cookie', async (event, domain, oldCookie, cookie) => {
try {
await modifyCookieForDomain(domain, oldCookie, cookie);
const domainsWithCookies = await getDomainsWithCookies();
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
} catch (error) {
return Promise.reject(error);
}
});
ipcMain.handle('renderer:get-parsed-cookie', async (event, cookieStr) => {
try {
return parseCookieString(cookieStr);
} catch (error) {
return Promise.reject(error);
}
});
ipcMain.handle('renderer:create-cookie-string', async (event, cookie) => {
try {
return createCookieString(cookie);
} catch (error) {
return Promise.reject(error);
}
});
ipcMain.handle('renderer:save-collection-security-config', async (event, collectionPath, securityConfig) => {
try {
collectionSecurityStore.setSecurityConfigForCollection(collectionPath, {

View File

@@ -1,5 +1,6 @@
const { Cookie, CookieJar } = require('tough-cookie');
const each = require('lodash/each');
const moment = require('moment');
const cookieJar = new CookieJar();
@@ -64,22 +65,130 @@ const getDomainsWithCookies = () => {
});
};
const deleteCookiesForDomain = (domain) => {
const deleteCookie = (domain, path, cookieKey) => {
return new Promise((resolve, reject) => {
cookieJar.store.removeCookie(domain, path, cookieKey, (err) => {
if (err) {
return reject(err);
}
return resolve();
});
});
};
const deleteCookiesForDomain = (domain) => {
return new Promise((resolve, reject) => {
cookieJar.store.removeCookies(domain, null, (err) => {
if (err) {
return reject(err);
}
return resolve();
});
});
};
const updateCookieObj = (cookieObj, oldCookie) => {
return {
...cookieObj,
// Preserve immutable properties from old cookie
path: oldCookie.path,
key: oldCookie.key,
domain: oldCookie.domain,
// Handle other mutable properties
expires: cookieObj?.expires && moment(cookieObj.expires).isValid() ? new Date(cookieObj.expires) : Infinity,
creation: oldCookie?.creation && moment(oldCookie.creation).isValid() ? new Date(oldCookie.creation) : new Date(),
lastAccessed:
oldCookie?.lastAccessed && moment(oldCookie.lastAccessed).isValid()
? new Date(oldCookie.lastAccessed)
: new Date()
};
};
const createCookieObj = (cookieObj) => {
return {
...cookieObj,
path: cookieObj.path || '/',
expires: cookieObj?.expires && moment(cookieObj.expires).isValid() ? new Date(cookieObj.expires) : Infinity,
creation: cookieObj?.creation && moment(cookieObj.creation).isValid() ? new Date(cookieObj.creation) : new Date(),
lastAccessed:
cookieObj?.lastAccessed && moment(cookieObj.lastAccessed).isValid()
? new Date(cookieObj.lastAccessed)
: new Date()
};
};
const addCookieForDomain = (domain, cookieObj) => {
return new Promise((resolve, reject) => {
try {
const cookie = new Cookie(createCookieObj(cookieObj));
cookieJar.store.putCookie(cookie, (err) => {
if (err) {
return reject(err);
}
return resolve();
});
} catch (err) {
reject(err);
}
});
};
const modifyCookieForDomain = (domain, oldCookieObj, cookieObj) => {
return new Promise((resolve, reject) => {
try {
const oldCookie = new Cookie(createCookieObj(oldCookieObj));
const newCookie = new Cookie(updateCookieObj(cookieObj, oldCookie));
cookieJar.store.updateCookie(oldCookie, newCookie, (removeErr) => {
if (removeErr) {
return reject(removeErr);
}
return resolve();
});
} catch (err) {
reject(err);
}
});
};
const parseCookieString = (cookieStr) => {
try {
const cookie = Cookie.parse(cookieStr);
if (!cookie) return null;
return {
...cookie,
expires: cookie.expires === Infinity ? null : cookie.expires
};
} catch (err) {
throw new Error(err);
}
};
const createCookieString = (cookieObj) => {
const cookie = new Cookie(createCookieObj(cookieObj));
// cookie.toString() omits the domain
let cookieString = cookie.toString();
// Manually append domain and hostOnly if they exist
if (cookieObj.hostOnly && !cookieString.includes('Domain=')) {
cookieString += `; Domain=${cookieObj.domain}`;
}
return cookieString;
};
module.exports = {
addCookieToJar,
getCookiesForUrl,
getCookieStringForUrl,
getDomainsWithCookies,
deleteCookiesForDomain
deleteCookie,
deleteCookiesForDomain,
addCookieForDomain,
modifyCookieForDomain,
parseCookieString,
createCookieString,
updateCookieObj,
createCookieObj
};