mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-12 10:21:30 +00:00
Added UI to manage cookies
This commit is contained in:
14
package-lock.json
generated
14
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
62
packages/bruno-app/src/components/Accordion/index.js
Normal file
62
packages/bruno-app/src/components/Accordion/index.js
Normal 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;
|
||||
29
packages/bruno-app/src/components/Accordion/styledWrapper.js
Normal file
29
packages/bruno-app/src/components/Accordion/styledWrapper.js
Normal 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 };
|
||||
@@ -0,0 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div``;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
`;
|
||||
15
packages/bruno-app/src/components/ToggleSwitch/index.js
Normal file
15
packages/bruno-app/src/components/ToggleSwitch/index.js
Normal 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;
|
||||
@@ -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');
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user