feat: introducing Auth for admin dashboard (HBE-138) (#32)
This commit is contained in:
15
packages/hoppscotch-sh-admin/assets/icons/auth/email.svg
Normal file
15
packages/hoppscotch-sh-admin/assets/icons/auth/email.svg
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-label="Email"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
>
|
||||||
|
<rect width="512" height="512" rx="15%" fill="teal" />
|
||||||
|
<rect width="356" height="256" x="78" y="128" fill="#fff" rx="8%" />
|
||||||
|
<path
|
||||||
|
fill="none"
|
||||||
|
stroke="teal"
|
||||||
|
stroke-width="20"
|
||||||
|
d="M434 128L269 292c-7 8-19 8-26 0L78 128m0 256l129-128m227 128L305 256"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 383 B |
12
packages/hoppscotch-sh-admin/assets/icons/auth/github.svg
Normal file
12
packages/hoppscotch-sh-admin/assets/icons/auth/github.svg
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-label="GitHub"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
>
|
||||||
|
<rect width="512" height="512" rx="15%" fill="#181717" />
|
||||||
|
<path
|
||||||
|
fill="#fff"
|
||||||
|
d="M335 499c14 0 12 17 12 17H165s-2-17 12-17c13 0 16-6 16-12l-1-44c-71 16-86-34-86-34-12-30-28-37-28-37-24-16 1-16 1-16 26 2 40 26 40 26 22 39 59 28 74 22 2-17 9-28 16-35-57-6-116-28-116-126 0-28 10-51 26-69-3-6-11-32 3-67 0 0 21-7 70 26 42-12 86-12 128 0 49-33 70-26 70-26 14 35 6 61 3 67 16 18 26 41 26 69 0 98-60 120-117 126 10 8 18 24 18 48l-1 70c0 6 3 12 16 12z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 571 B |
24
packages/hoppscotch-sh-admin/assets/icons/auth/google.svg
Normal file
24
packages/hoppscotch-sh-admin/assets/icons/auth/google.svg
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-label="Google"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
>
|
||||||
|
<rect width="512" height="512" rx="15%" fill="#fff" />
|
||||||
|
<path
|
||||||
|
fill="#4285f4"
|
||||||
|
d="M386 400c45-42 65-112 53-179H260v74h102c-4 24-18 44-38 57z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#34a853"
|
||||||
|
d="M90 341a192 192 0 0 0 296 59l-62-48c-53 35-141 22-171-60z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#fbbc02"
|
||||||
|
d="M153 292c-8-25-8-48 0-73l-63-49c-23 46-30 111 0 171z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#ea4335"
|
||||||
|
d="M153 219c22-69 116-109 179-50l55-54c-78-75-230-72-297 55z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 555 B |
12
packages/hoppscotch-sh-admin/assets/icons/auth/microsoft.svg
Normal file
12
packages/hoppscotch-sh-admin/assets/icons/auth/microsoft.svg
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-label="Microsoft"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
>
|
||||||
|
<rect width="512" height="512" rx="15%" fill="#fff" />
|
||||||
|
<path d="M75 75v171h171v-171z" fill="#f25022" />
|
||||||
|
<path d="M266 75v171h171v-171z" fill="#7fba00" />
|
||||||
|
<path d="M75 266v171h171v-171z" fill="#00a4ef" />
|
||||||
|
<path d="M266 266v171h171v-171z" fill="#ffb900" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 378 B |
59
packages/hoppscotch-sh-admin/assets/icons/logo.svg
Normal file
59
packages/hoppscotch-sh-admin/assets/icons/logo.svg
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" fill="none">
|
||||||
|
<path fill="#10B981" d="M0 0h512v512H0z" />
|
||||||
|
<circle cx="197.76" cy="157.84" r="10" fill="#fff" fill-opacity=".75" />
|
||||||
|
<circle cx="259.76" cy="161.84" r="12" fill="#fff" fill-opacity=".75" />
|
||||||
|
<circle cx="319.76" cy="177.84" r="10" fill="#fff" fill-opacity=".75" />
|
||||||
|
<path
|
||||||
|
d="M344.963 235.676c2.075-12.698-38.872-29.804-90.967-38.094-52.09-8.296-96.404-4.665-98.48 8.033-.257 1.035 0 1.812.263 2.853-1.298-.521-76.714 211.212-76.714 211.212H364.14s-17.621-181.414-20.211-181.414c.515-.772 1.035-1.549 1.035-2.59Z"
|
||||||
|
fill="url(#a)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M314.902 227.386c-1.298 8.033-30.839 9.845-66.343 4.402-35.247-5.7-62.982-16.843-61.684-24.618.521-2.59 3.888-4.665 9.331-5.7-18.141.777-30.062 4.145-31.096 9.845-1.555 10.628 34.726 25.139 81.373 32.657 46.647 7.512 85.782 4.665 87.594-5.7 1.041-6.226-9.33-12.961-26.431-19.439 4.923 2.847 7.513 5.957 7.256 8.553Z"
|
||||||
|
fill="#A7F3D0"
|
||||||
|
fill-opacity=".5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M333.557 157.413c-3.104-32.137-27.729-59.351-60.9-64.53-33.172-5.186-64.531 12.954-77.749 42.238 21.251 1.298 44.057 3.631 67.904 7.518 25.396 3.888 49.237 9.074 70.745 14.774Z"
|
||||||
|
fill="url(#b)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M74.142 158.002c-2.59 15.808 30.319 35.247 81.894 51.055-.257-1.04-.257-1.818-.257-2.853 2.07-12.698 46.127-16.328 98.48-8.032 52.347 8.29 93.037 25.396 90.961 38.094-.257 1.04-.514 1.818-1.035 2.589 53.645.778 90.968-7.512 93.557-23.32 3.625-24.104-74.638-56.498-174.93-72.306-100.555-15.808-185.045-9.331-188.67 14.773Zm115.586-1.298c.778-4.145 4.665-7.255 8.81-6.477 4.145.777 7.256 4.665 6.478 8.81-.52 4.145-4.665 6.998-8.81 6.478-4.145-.778-7.255-4.666-6.478-8.811Zm59.866 4.145c.777-5.7 6.22-9.587 11.92-8.547 5.7.778 9.588 6.215 8.553 11.921-1.041 5.442-6.478 9.33-11.92 8.553-5.706-.778-9.594-6.221-8.553-11.927Zm62.975 15.294c.778-4.145 4.665-7.255 8.81-6.478 4.145.778 7.255 4.666 6.478 8.811-.515 4.145-4.665 7.255-8.81 6.477-4.145-.777-7.256-4.665-6.478-8.81Z"
|
||||||
|
fill="url(#c)"
|
||||||
|
/>
|
||||||
|
<defs>
|
||||||
|
<radialGradient
|
||||||
|
id="b"
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="1"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(0 32.7063 -69.3245 0 264.232 124.706)"
|
||||||
|
>
|
||||||
|
<stop stop-color="#047857" />
|
||||||
|
<stop offset="1" stop-color="#064E3B" />
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient
|
||||||
|
id="c"
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="1"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="translate(255.837 186.754) scale(1389.61)"
|
||||||
|
>
|
||||||
|
<stop stop-color="#047857" />
|
||||||
|
<stop offset=".115" stop-color="#064E3B" />
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="a"
|
||||||
|
x1="224.998"
|
||||||
|
y1="157.606"
|
||||||
|
x2="224.998"
|
||||||
|
y2="403.696"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stop-color="#86EFAC" stop-opacity=".75" />
|
||||||
|
<stop offset=".635" stop-color="#fff" stop-opacity=".2" />
|
||||||
|
<stop offset="1" stop-color="#fff" stop-opacity="0" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -1,249 +1,191 @@
|
|||||||
@mixin base-theme {
|
@mixin base-theme {
|
||||||
--font-sans: "Inter", sans-serif;
|
--font-sans: 'Inter', sans-serif;
|
||||||
--font-mono: "Roboto Mono", monospace;
|
--font-mono: 'Roboto Mono', monospace;
|
||||||
--font-icon: "Material Icons";
|
--font-icon: 'Material Icons';
|
||||||
--font-size-tiny: calc(var(--font-size-body) - 0.062rem);
|
--font-size-tiny: calc(var(--font-size-body) - 0.062rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin dark-theme {
|
@mixin dark-theme {
|
||||||
--primary-color: theme("colors.neutral.900");
|
--primary-color: theme('colors.neutral.900');
|
||||||
--primary-light-color: theme("colors.dark.600");
|
--primary-light-color: theme('colors.dark.600');
|
||||||
--primary-dark-color: theme("colors.neutral.800");
|
--primary-dark-color: theme('colors.neutral.800');
|
||||||
--primary-contrast-color: #161616;
|
--primary-contrast-color: #161616;
|
||||||
--secondary-color: theme("colors.neutral.400");
|
--secondary-color: theme('colors.neutral.400');
|
||||||
--secondary-light-color: theme("colors.neutral.500");
|
--secondary-light-color: theme('colors.neutral.500');
|
||||||
--secondary-dark-color: theme("colors.neutral.100");
|
--secondary-dark-color: theme('colors.neutral.100');
|
||||||
--divider-color: theme("colors.neutral.800");
|
--divider-color: theme('colors.neutral.800');
|
||||||
--divider-light-color: theme("colors.dark.500");
|
--divider-light-color: theme('colors.dark.500');
|
||||||
--divider-dark-color: theme("colors.dark.300");
|
--divider-dark-color: theme('colors.dark.300');
|
||||||
--error-color: theme("colors.stone.800");
|
--error-color: theme('colors.stone.800');
|
||||||
--tooltip-color: theme("colors.neutral.100");
|
--tooltip-color: theme('colors.neutral.100');
|
||||||
--popover-color: theme("colors.dark.700");
|
--popover-color: theme('colors.dark.700');
|
||||||
--editor-theme: "merbivore_soft";
|
--editor-theme: 'merbivore_soft';
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin light-theme {
|
@mixin light-theme {
|
||||||
--primary-color: theme("colors.white");
|
--primary-color: theme('colors.white');
|
||||||
--primary-light-color: theme("colors.neutral.50");
|
--primary-light-color: theme('colors.neutral.50');
|
||||||
--primary-dark-color: theme("colors.neutral.100");
|
--primary-dark-color: theme('colors.neutral.100');
|
||||||
--primary-contrast-color: #fefefe;
|
--primary-contrast-color: #fefefe;
|
||||||
--secondary-color: theme("colors.neutral.500");
|
--secondary-color: theme('colors.neutral.500');
|
||||||
--secondary-light-color: theme("colors.neutral.400");
|
--secondary-light-color: theme('colors.neutral.400');
|
||||||
--secondary-dark-color: theme("colors.neutral.900");
|
--secondary-dark-color: theme('colors.neutral.900');
|
||||||
--divider-color: theme("colors.gray.100");
|
--divider-color: theme('colors.gray.100');
|
||||||
--divider-light-color: theme("colors.neutral.100");
|
--divider-light-color: theme('colors.neutral.100');
|
||||||
--divider-dark-color: theme("colors.neutral.300");
|
--divider-dark-color: theme('colors.neutral.300');
|
||||||
--error-color: theme("colors.yellow.100");
|
--error-color: theme('colors.yellow.100');
|
||||||
--tooltip-color: theme("colors.neutral.800");
|
--tooltip-color: theme('colors.neutral.800');
|
||||||
--popover-color: theme("colors.white");
|
--popover-color: theme('colors.white');
|
||||||
--editor-theme: "textmate";
|
--editor-theme: 'textmate';
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin black-theme {
|
@mixin black-theme {
|
||||||
--primary-color: theme("colors.dark.900");
|
--primary-color: theme('colors.dark.900');
|
||||||
--primary-light-color: theme("colors.neutral.900");
|
--primary-light-color: theme('colors.neutral.900');
|
||||||
--primary-dark-color: theme("colors.dark.800");
|
--primary-dark-color: theme('colors.dark.800');
|
||||||
--primary-contrast-color: #0e0e0e;
|
--primary-contrast-color: #0e0e0e;
|
||||||
--secondary-color: theme("colors.neutral.400");
|
--secondary-color: theme('colors.neutral.400');
|
||||||
--secondary-light-color: theme("colors.neutral.500");
|
--secondary-light-color: theme('colors.neutral.500');
|
||||||
--secondary-dark-color: theme("colors.neutral.100");
|
--secondary-dark-color: theme('colors.neutral.100');
|
||||||
--divider-color: theme("colors.neutral.800");
|
--divider-color: theme('colors.neutral.800');
|
||||||
--divider-light-color: theme("colors.dark.800");
|
--divider-light-color: theme('colors.dark.800');
|
||||||
--divider-dark-color: theme("colors.dark.300");
|
--divider-dark-color: theme('colors.dark.300');
|
||||||
--error-color: theme("colors.stone.900");
|
--error-color: theme('colors.stone.900');
|
||||||
--tooltip-color: theme("colors.neutral.100");
|
--tooltip-color: theme('colors.neutral.100');
|
||||||
--popover-color: theme("colors.dark.600");
|
--popover-color: theme('colors.dark.600');
|
||||||
--editor-theme: "twilight";
|
--editor-theme: 'twilight';
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin dark-editor-theme {
|
@mixin dark-editor-theme {
|
||||||
--editor-type-color: theme("colors.purple.400");
|
--editor-type-color: theme('colors.purple.400');
|
||||||
--editor-name-color: theme("colors.blue.400");
|
--editor-name-color: theme('colors.blue.400');
|
||||||
--editor-operator-color: theme("colors.indigo.400");
|
--editor-operator-color: theme('colors.indigo.400');
|
||||||
--editor-invalid-color: theme("colors.red.400");
|
--editor-invalid-color: theme('colors.red.400');
|
||||||
--editor-separator-color: theme("colors.gray.400");
|
--editor-separator-color: theme('colors.gray.400');
|
||||||
--editor-meta-color: theme("colors.gray.400");
|
--editor-meta-color: theme('colors.gray.400');
|
||||||
--editor-variable-color: theme("colors.green.400");
|
--editor-variable-color: theme('colors.green.400');
|
||||||
--editor-link-color: theme("colors.cyan.400");
|
--editor-link-color: theme('colors.cyan.400');
|
||||||
--editor-process-color: theme("colors.fuchsia.400");
|
--editor-process-color: theme('colors.fuchsia.400');
|
||||||
--editor-constant-color: theme("colors.violet.400");
|
--editor-constant-color: theme('colors.violet.400');
|
||||||
--editor-keyword-color: theme("colors.pink.400");
|
--editor-keyword-color: theme('colors.pink.400');
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin light-editor-theme {
|
@mixin light-editor-theme {
|
||||||
--editor-type-color: theme("colors.purple.600");
|
--editor-type-color: theme('colors.purple.600');
|
||||||
--editor-name-color: theme("colors.red.600");
|
--editor-name-color: theme('colors.red.600');
|
||||||
--editor-operator-color: theme("colors.indigo.600");
|
--editor-operator-color: theme('colors.indigo.600');
|
||||||
--editor-invalid-color: theme("colors.red.600");
|
--editor-invalid-color: theme('colors.red.600');
|
||||||
--editor-separator-color: theme("colors.gray.600");
|
--editor-separator-color: theme('colors.gray.600');
|
||||||
--editor-meta-color: theme("colors.gray.600");
|
--editor-meta-color: theme('colors.gray.600');
|
||||||
--editor-variable-color: theme("colors.green.600");
|
--editor-variable-color: theme('colors.green.600');
|
||||||
--editor-link-color: theme("colors.cyan.600");
|
--editor-link-color: theme('colors.cyan.600');
|
||||||
--editor-process-color: theme("colors.blue.600");
|
--editor-process-color: theme('colors.blue.600');
|
||||||
--editor-constant-color: theme("colors.fuchsia.600");
|
--editor-constant-color: theme('colors.fuchsia.600');
|
||||||
--editor-keyword-color: theme("colors.pink.600");
|
--editor-keyword-color: theme('colors.pink.600');
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin black-editor-theme {
|
@mixin black-editor-theme {
|
||||||
--editor-type-color: theme("colors.purple.400");
|
--editor-type-color: theme('colors.purple.400');
|
||||||
--editor-name-color: theme("colors.fuchsia.400");
|
--editor-name-color: theme('colors.fuchsia.400');
|
||||||
--editor-operator-color: theme("colors.indigo.400");
|
--editor-operator-color: theme('colors.indigo.400');
|
||||||
--editor-invalid-color: theme("colors.red.400");
|
--editor-invalid-color: theme('colors.red.400');
|
||||||
--editor-separator-color: theme("colors.gray.400");
|
--editor-separator-color: theme('colors.gray.400');
|
||||||
--editor-meta-color: theme("colors.gray.400");
|
--editor-meta-color: theme('colors.gray.400');
|
||||||
--editor-variable-color: theme("colors.green.400");
|
--editor-variable-color: theme('colors.green.400');
|
||||||
--editor-link-color: theme("colors.cyan.400");
|
--editor-link-color: theme('colors.cyan.400');
|
||||||
--editor-process-color: theme("colors.violet.400");
|
--editor-process-color: theme('colors.violet.400');
|
||||||
--editor-constant-color: theme("colors.blue.400");
|
--editor-constant-color: theme('colors.blue.400');
|
||||||
--editor-keyword-color: theme("colors.pink.400");
|
--editor-keyword-color: theme('colors.pink.400');
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin green-theme {
|
@mixin green-theme {
|
||||||
--accent-color: theme("colors.green.500");
|
--accent-color: theme('colors.green.500');
|
||||||
--accent-light-color: theme("colors.green.400");
|
--accent-light-color: theme('colors.green.400');
|
||||||
--accent-dark-color: theme("colors.green.600");
|
--accent-dark-color: theme('colors.green.600');
|
||||||
--accent-contrast-color: theme("colors.white");
|
--accent-contrast-color: theme('colors.white');
|
||||||
--gradient-from-color: theme("colors.green.200");
|
--gradient-from-color: theme('colors.green.200');
|
||||||
--gradient-via-color: theme("colors.green.400");
|
--gradient-via-color: theme('colors.green.400');
|
||||||
--gradient-to-color: theme("colors.green.600");
|
--gradient-to-color: theme('colors.green.600');
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin teal-theme {
|
@mixin teal-theme {
|
||||||
--accent-color: theme("colors.teal.500");
|
--accent-color: theme('colors.teal.500');
|
||||||
--accent-light-color: theme("colors.teal.400");
|
--accent-light-color: theme('colors.teal.400');
|
||||||
--accent-dark-color: theme("colors.teal.600");
|
--accent-dark-color: theme('colors.teal.600');
|
||||||
--accent-contrast-color: theme("colors.white");
|
--accent-contrast-color: theme('colors.white');
|
||||||
--gradient-from-color: theme("colors.teal.200");
|
--gradient-from-color: theme('colors.teal.200');
|
||||||
--gradient-via-color: theme("colors.teal.400");
|
--gradient-via-color: theme('colors.teal.400');
|
||||||
--gradient-to-color: theme("colors.teal.600");
|
--gradient-to-color: theme('colors.teal.600');
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin blue-theme {
|
@mixin blue-theme {
|
||||||
--accent-color: theme("colors.blue.500");
|
--accent-color: theme('colors.blue.500');
|
||||||
--accent-light-color: theme("colors.blue.400");
|
--accent-light-color: theme('colors.blue.400');
|
||||||
--accent-dark-color: theme("colors.blue.600");
|
--accent-dark-color: theme('colors.blue.600');
|
||||||
--accent-contrast-color: theme("colors.white");
|
--accent-contrast-color: theme('colors.white');
|
||||||
--gradient-from-color: theme("colors.blue.200");
|
--gradient-from-color: theme('colors.blue.200');
|
||||||
--gradient-via-color: theme("colors.blue.400");
|
--gradient-via-color: theme('colors.blue.400');
|
||||||
--gradient-to-color: theme("colors.blue.600");
|
--gradient-to-color: theme('colors.blue.600');
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin indigo-theme {
|
@mixin indigo-theme {
|
||||||
--accent-color: theme("colors.indigo.500");
|
--accent-color: theme('colors.indigo.500');
|
||||||
--accent-light-color: theme("colors.indigo.400");
|
--accent-light-color: theme('colors.indigo.400');
|
||||||
--accent-dark-color: theme("colors.indigo.600");
|
--accent-dark-color: theme('colors.indigo.600');
|
||||||
--accent-contrast-color: theme("colors.white");
|
--accent-contrast-color: theme('colors.white');
|
||||||
--gradient-from-color: theme("colors.indigo.200");
|
--gradient-from-color: theme('colors.indigo.200');
|
||||||
--gradient-via-color: theme("colors.indigo.400");
|
--gradient-via-color: theme('colors.indigo.400');
|
||||||
--gradient-to-color: theme("colors.indigo.600");
|
--gradient-to-color: theme('colors.indigo.600');
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin purple-theme {
|
@mixin purple-theme {
|
||||||
--accent-color: theme("colors.purple.500");
|
--accent-color: theme('colors.purple.500');
|
||||||
--accent-light-color: theme("colors.purple.400");
|
--accent-light-color: theme('colors.purple.400');
|
||||||
--accent-dark-color: theme("colors.purple.600");
|
--accent-dark-color: theme('colors.purple.600');
|
||||||
--accent-contrast-color: theme("colors.white");
|
--accent-contrast-color: theme('colors.white');
|
||||||
--gradient-from-color: theme("colors.purple.200");
|
--gradient-from-color: theme('colors.purple.200');
|
||||||
--gradient-via-color: theme("colors.purple.400");
|
--gradient-via-color: theme('colors.purple.400');
|
||||||
--gradient-to-color: theme("colors.purple.600");
|
--gradient-to-color: theme('colors.purple.600');
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin yellow-theme {
|
@mixin yellow-theme {
|
||||||
--accent-color: theme("colors.yellow.500");
|
--accent-color: theme('colors.yellow.500');
|
||||||
--accent-light-color: theme("colors.yellow.400");
|
--accent-light-color: theme('colors.yellow.400');
|
||||||
--accent-dark-color: theme("colors.yellow.600");
|
--accent-dark-color: theme('colors.yellow.600');
|
||||||
--accent-contrast-color: theme("colors.white");
|
--accent-contrast-color: theme('colors.white');
|
||||||
--gradient-from-color: theme("colors.yellow.200");
|
--gradient-from-color: theme('colors.yellow.200');
|
||||||
--gradient-via-color: theme("colors.yellow.400");
|
--gradient-via-color: theme('colors.yellow.400');
|
||||||
--gradient-to-color: theme("colors.yellow.600");
|
--gradient-to-color: theme('colors.yellow.600');
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin orange-theme {
|
@mixin orange-theme {
|
||||||
--accent-color: theme("colors.orange.500");
|
--accent-color: theme('colors.orange.500');
|
||||||
--accent-light-color: theme("colors.orange.400");
|
--accent-light-color: theme('colors.orange.400');
|
||||||
--accent-dark-color: theme("colors.orange.600");
|
--accent-dark-color: theme('colors.orange.600');
|
||||||
--accent-contrast-color: theme("colors.white");
|
--accent-contrast-color: theme('colors.white');
|
||||||
--gradient-from-color: theme("colors.orange.200");
|
--gradient-from-color: theme('colors.orange.200');
|
||||||
--gradient-via-color: theme("colors.orange.400");
|
--gradient-via-color: theme('colors.orange.400');
|
||||||
--gradient-to-color: theme("colors.orange.600");
|
--gradient-to-color: theme('colors.orange.600');
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin red-theme {
|
@mixin red-theme {
|
||||||
--accent-color: theme("colors.red.500");
|
--accent-color: theme('colors.red.500');
|
||||||
--accent-light-color: theme("colors.red.400");
|
--accent-light-color: theme('colors.red.400');
|
||||||
--accent-dark-color: theme("colors.red.600");
|
--accent-dark-color: theme('colors.red.600');
|
||||||
--accent-contrast-color: theme("colors.white");
|
--accent-contrast-color: theme('colors.white');
|
||||||
--gradient-from-color: theme("colors.red.200");
|
--gradient-from-color: theme('colors.red.200');
|
||||||
--gradient-via-color: theme("colors.red.400");
|
--gradient-via-color: theme('colors.red.400');
|
||||||
--gradient-to-color: theme("colors.red.600");
|
--gradient-to-color: theme('colors.red.600');
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin pink-theme {
|
@mixin pink-theme {
|
||||||
--accent-color: theme("colors.pink.500");
|
--accent-color: theme('colors.pink.500');
|
||||||
--accent-light-color: theme("colors.pink.400");
|
--accent-light-color: theme('colors.pink.400');
|
||||||
--accent-dark-color: theme("colors.pink.600");
|
--accent-dark-color: theme('colors.pink.600');
|
||||||
--accent-contrast-color: theme("colors.white");
|
--accent-contrast-color: theme('colors.white');
|
||||||
--gradient-from-color: theme("colors.pink.200");
|
--gradient-from-color: theme('colors.pink.200');
|
||||||
--gradient-via-color: theme("colors.pink.400");
|
--gradient-via-color: theme('colors.pink.400');
|
||||||
--gradient-to-color: theme("colors.pink.600");
|
--gradient-to-color: theme('colors.pink.600');
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
@include base-theme;
|
|
||||||
@include dark-theme;
|
|
||||||
@include green-theme;
|
|
||||||
@include dark-editor-theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root.light {
|
|
||||||
@include light-theme;
|
|
||||||
@include light-editor-theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root.dark {
|
|
||||||
@include dark-theme;
|
|
||||||
@include dark-editor-theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root.black {
|
|
||||||
@include black-theme;
|
|
||||||
@include black-editor-theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-accent="blue"] {
|
|
||||||
@include blue-theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-accent="green"] {
|
|
||||||
@include green-theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-accent="teal"] {
|
|
||||||
@include teal-theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-accent="indigo"] {
|
|
||||||
@include indigo-theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-accent="purple"] {
|
|
||||||
@include purple-theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-accent="orange"] {
|
|
||||||
@include orange-theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-accent="pink"] {
|
|
||||||
@include pink-theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-accent="red"] {
|
|
||||||
@include red-theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-accent="yellow"] {
|
|
||||||
@include yellow-theme;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin font-small {
|
@mixin font-small {
|
||||||
@@ -294,14 +236,73 @@
|
|||||||
--sidebar-primary-sticky-fold: 2.5rem;
|
--sidebar-primary-sticky-fold: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-font-size="small"] {
|
:root {
|
||||||
@include font-small;
|
@include base-theme;
|
||||||
}
|
@include dark-theme;
|
||||||
|
@include green-theme;
|
||||||
:root[data-font-size="medium"] {
|
@include dark-editor-theme;
|
||||||
@include font-medium;
|
@include font-medium;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-font-size="large"] {
|
:root.light {
|
||||||
|
@include light-theme;
|
||||||
|
@include light-editor-theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.dark {
|
||||||
|
@include dark-theme;
|
||||||
|
@include dark-editor-theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.black {
|
||||||
|
@include black-theme;
|
||||||
|
@include black-editor-theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-accent='blue'] {
|
||||||
|
@include blue-theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-accent='green'] {
|
||||||
|
@include green-theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-accent='teal'] {
|
||||||
|
@include teal-theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-accent='indigo'] {
|
||||||
|
@include indigo-theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-accent='purple'] {
|
||||||
|
@include purple-theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-accent='orange'] {
|
||||||
|
@include orange-theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-accent='pink'] {
|
||||||
|
@include pink-theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-accent='red'] {
|
||||||
|
@include red-theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-accent='yellow'] {
|
||||||
|
@include yellow-theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-font-size='small'] {
|
||||||
|
@include font-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-font-size='medium'] {
|
||||||
|
@include font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-font-size='large'] {
|
||||||
@include font-large;
|
@include font-large;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
||||||
"dev:vite": "vite",
|
"dev:vite": "vite",
|
||||||
"dev:gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml --watch dotenv_config_path=\"../../.env\"",
|
"dev:gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml --watch dotenv_config_path=\".env\"",
|
||||||
"build": "vue-tsc && vite build",
|
"build": "vue-tsc && vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
@@ -18,11 +18,15 @@
|
|||||||
"@types/express": "^4.17.15",
|
"@types/express": "^4.17.15",
|
||||||
"@urql/vue": "^1.0.4",
|
"@urql/vue": "^1.0.4",
|
||||||
"@vueuse/core": "^9.10.0",
|
"@vueuse/core": "^9.10.0",
|
||||||
|
"axios": "^0.21.4",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-graphql": "^0.12.0",
|
"express-graphql": "^0.12.0",
|
||||||
|
"fp-ts": "^2.13.1",
|
||||||
"graphql": "^16.6.0",
|
"graphql": "^16.6.0",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"rxjs": "^7.8.0",
|
||||||
"ts-node-dev": "^2.0.0",
|
"ts-node-dev": "^2.0.0",
|
||||||
"unplugin-icons": "^0.14.9",
|
"unplugin-icons": "^0.14.9",
|
||||||
"unplugin-vue-components": "^0.21.0",
|
"unplugin-vue-components": "^0.21.0",
|
||||||
@@ -38,14 +42,14 @@
|
|||||||
"@graphql-codegen/typescript-document-nodes": "3.0.0",
|
"@graphql-codegen/typescript-document-nodes": "3.0.0",
|
||||||
"@graphql-codegen/typescript-operations": "3.0.0",
|
"@graphql-codegen/typescript-operations": "3.0.0",
|
||||||
"@graphql-codegen/urql-introspection": "2.2.1",
|
"@graphql-codegen/urql-introspection": "2.2.1",
|
||||||
"@vitejs/plugin-vue": "^1.6.0",
|
"@vitejs/plugin-vue": "^3.1.0",
|
||||||
"@vue/compiler-sfc": "^3.2.6",
|
"@vue/compiler-sfc": "^3.2.6",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"sass": "^1.57.1",
|
"sass": "^1.57.1",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^4.9.3",
|
"typescript": "^4.9.3",
|
||||||
"vite": "^2.5.1",
|
"vite": "^3.1.4",
|
||||||
"vite-plugin-pages": "^0.26.0",
|
"vite-plugin-pages": "^0.26.0",
|
||||||
"vite-plugin-vue-layouts": "^0.7.0",
|
"vite-plugin-vue-layouts": "^0.7.0",
|
||||||
"vite-plugin-windicss": "^1.8.8",
|
"vite-plugin-windicss": "^1.8.8",
|
||||||
|
|||||||
@@ -5,9 +5,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useDark, useToggle } from '@vueuse/core';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useDark, useToggle } from '@vueuse/core';
|
import { HOPP_MODULES } from './modules';
|
||||||
|
|
||||||
const defaultLayout = 'default';
|
const defaultLayout = 'default';
|
||||||
|
|
||||||
@@ -18,7 +19,10 @@ const layout = computed(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isDark = useDark();
|
const isDark = useDark();
|
||||||
const toggleDark = useToggle(isDark);
|
useToggle(isDark);
|
||||||
|
|
||||||
|
// Run module root component setup code
|
||||||
|
HOPP_MODULES.forEach((mod) => mod.onRootSetup?.());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
34
packages/hoppscotch-sh-admin/src/components.d.ts
vendored
34
packages/hoppscotch-sh-admin/src/components.d.ts
vendored
@@ -8,51 +8,31 @@ export {}
|
|||||||
declare module '@vue/runtime-core' {
|
declare module '@vue/runtime-core' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
AppHeader: typeof import('./components/app/Header.vue')['default']
|
AppHeader: typeof import('./components/app/Header.vue')['default']
|
||||||
|
AppLogin: typeof import('./components/app/Login.vue')['default']
|
||||||
|
AppLogout: typeof import('./components/app/Logout.vue')['default']
|
||||||
AppModal: typeof import('./components/app/Modal.vue')['default']
|
AppModal: typeof import('./components/app/Modal.vue')['default']
|
||||||
AppSidebar: typeof import('./components/app/Sidebar.vue')['default']
|
AppSidebar: typeof import('./components/app/Sidebar.vue')['default']
|
||||||
AppToast: typeof import('./components/app/Toast.vue')['default']
|
AppToast: typeof import('./components/app/Toast.vue')['default']
|
||||||
ButtonPrimary: typeof import('./../../hoppscotch-ui/src/components/button/Primary.vue')['default']
|
|
||||||
ButtonSecondary: typeof import('./../../hoppscotch-ui/src/components/button/Secondary.vue')['default']
|
|
||||||
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
|
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
|
||||||
|
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
|
||||||
|
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
|
||||||
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
|
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
|
||||||
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
|
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
|
||||||
IconLucideBell: typeof import('~icons/lucide/bell')['default']
|
IconLucideBell: typeof import('~icons/lucide/bell')['default']
|
||||||
IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default']
|
IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
|
||||||
IconLucideChevronLeft: typeof import('~icons/lucide/chevron-left')['default']
|
|
||||||
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
|
||||||
IconLucideLayoutDashboard: typeof import('~icons/lucide/layout-dashboard')['default']
|
IconLucideLayoutDashboard: typeof import('~icons/lucide/layout-dashboard')['default']
|
||||||
IconLucideLineChart: typeof import('~icons/lucide/line-chart')['default']
|
IconLucideLineChart: typeof import('~icons/lucide/line-chart')['default']
|
||||||
IconLucideLock: typeof import('~icons/lucide/lock')['default']
|
IconLucideLock: typeof import('~icons/lucide/lock')['default']
|
||||||
IconLucideMenu: typeof import('~icons/lucide/menu')['default']
|
IconLucideMenu: typeof import('~icons/lucide/menu')['default']
|
||||||
IconLucideMoreHorizontal: typeof import('~icons/lucide/more-horizontal')['default']
|
|
||||||
IconLucideSettings: typeof import('~icons/lucide/settings')['default']
|
IconLucideSettings: typeof import('~icons/lucide/settings')['default']
|
||||||
IconLucideSidebarClose: typeof import('~icons/lucide/sidebar-close')['default']
|
IconLucideSidebarClose: typeof import('~icons/lucide/sidebar-close')['default']
|
||||||
IconLucideSidebarOpen: typeof import('~icons/lucide/sidebar-open')['default']
|
IconLucideSidebarOpen: typeof import('~icons/lucide/sidebar-open')['default']
|
||||||
IconLucideUser: typeof import('~icons/lucide/user')['default']
|
IconLucideUser: typeof import('~icons/lucide/user')['default']
|
||||||
IconLucideUserCog: typeof import('~icons/lucide/user-cog')['default']
|
IconLucideUserCog: typeof import('~icons/lucide/user-cog')['default']
|
||||||
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
||||||
|
ProfilePicture: typeof import('./components/profile/Picture.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default']
|
|
||||||
SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default']
|
|
||||||
SmartCheckbox: typeof import('./../../hoppscotch-ui/src/components/smart/Checkbox.vue')['default']
|
|
||||||
SmartConfirmModal: typeof import('./../../hoppscotch-ui/src/components/smart/ConfirmModal.vue')['default']
|
|
||||||
SmartExpand: typeof import('./../../hoppscotch-ui/src/components/smart/Expand.vue')['default']
|
|
||||||
SmartFileChip: typeof import('./../../hoppscotch-ui/src/components/smart/FileChip.vue')['default']
|
|
||||||
SmartIntersection: typeof import('./../../hoppscotch-ui/src/components/smart/Intersection.vue')['default']
|
|
||||||
SmartItem: typeof import('./../../hoppscotch-ui/src/components/smart/Item.vue')['default']
|
|
||||||
SmartLink: typeof import('./../../hoppscotch-ui/src/components/smart/Link.vue')['default']
|
|
||||||
SmartModal: typeof import('./../../hoppscotch-ui/src/components/smart/Modal.vue')['default']
|
|
||||||
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
|
|
||||||
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
|
|
||||||
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
|
|
||||||
SmartSlideOver: typeof import('./../../hoppscotch-ui/src/components/smart/SlideOver.vue')['default']
|
|
||||||
SmartSpinner: typeof import('./../../hoppscotch-ui/src/components/smart/Spinner.vue')['default']
|
|
||||||
SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default']
|
|
||||||
SmartTabs: typeof import('./../../hoppscotch-ui/src/components/smart/Tabs.vue')['default']
|
|
||||||
SmartToggle: typeof import('./../../hoppscotch-ui/src/components/smart/Toggle.vue')['default']
|
|
||||||
SmartWindow: typeof import('./../../hoppscotch-ui/src/components/smart/Window.vue')['default']
|
|
||||||
SmartWindows: typeof import('./../../hoppscotch-ui/src/components/smart/Windows.vue')['default']
|
|
||||||
TeamsAddMembers: typeof import('./components/teams/AddMembers.vue')['default']
|
TeamsAddMembers: typeof import('./components/teams/AddMembers.vue')['default']
|
||||||
UsersDetails: typeof import('./components/users/Details.vue')['default']
|
UsersDetails: typeof import('./components/users/Details.vue')['default']
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,15 +26,25 @@
|
|||||||
<icon-lucide-bell class="text-gray-300 w-6" />
|
<icon-lucide-bell class="text-gray-300 w-6" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="relative">
|
<div v-if="currentUser" class="relative">
|
||||||
<button
|
<button
|
||||||
@click="dropdownOpen = !dropdownOpen"
|
@click="dropdownOpen = !dropdownOpen"
|
||||||
class="relative z-10 block w-8 h-8 overflow-hidden rounded-full shadow focus:outline-none"
|
class="relative z-10 block w-8 h-8 overflow-hidden rounded-full shadow focus:outline-none"
|
||||||
>
|
>
|
||||||
<img
|
<ProfilePicture
|
||||||
class="object-cover w-full h-full"
|
v-if="currentUser.photoURL"
|
||||||
src="https://media.licdn.com/dms/image/C5603AQHMCx72bNN1MA/profile-displayphoto-shrink_800_800/0/1630736416611?e=2147483647&v=beta&t=McWCdK_7t_NLeU4ze3JPB8xvwg5w50Okuj2JDBekqjw"
|
v-tippy="{
|
||||||
alt="Your avatar"
|
theme: 'tooltip',
|
||||||
|
}"
|
||||||
|
:url="currentUser.photoURL"
|
||||||
|
:alt="currentUser.displayName ?? 'No Name'"
|
||||||
|
:title="currentUser.displayName ?? currentUser.email ?? 'No Name'"
|
||||||
|
/>
|
||||||
|
<ProfilePicture
|
||||||
|
v-else
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="currentUser.displayName ?? currentUser.email ?? 'No Name'"
|
||||||
|
:initial="currentUser.displayName ?? currentUser.email"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -56,21 +66,13 @@
|
|||||||
v-show="dropdownOpen"
|
v-show="dropdownOpen"
|
||||||
class="absolute right-0 z-20 w-48 py-2 mt-2 bg-zinc-200 dark:bg-zinc-800 rounded-md shadow-xl"
|
class="absolute right-0 z-20 w-48 py-2 mt-2 bg-zinc-200 dark:bg-zinc-800 rounded-md shadow-xl"
|
||||||
>
|
>
|
||||||
<a
|
<HoppSmartItem to="/profile" :icon="IconUser" :label="'Profile'" />
|
||||||
href="#"
|
<HoppSmartItem
|
||||||
class="block px-4 py-2 text-sm text-gray-800 dark:text-gray-200 hover:bg-emerald-700 hover:text-white"
|
to="/settings"
|
||||||
>Profile</a
|
:icon="IconSettings"
|
||||||
>
|
:label="'Settings'"
|
||||||
<a
|
/>
|
||||||
href="#"
|
<AppLogout ref="logout" />
|
||||||
class="block px-4 py-2 text-sm text-gray-800 dark:text-gray-200 hover:bg-emerald-700 hover:text-white"
|
|
||||||
>Settings</a
|
|
||||||
>
|
|
||||||
<router-link
|
|
||||||
to="/"
|
|
||||||
class="block px-4 py-2 text-sm text-gray-800 dark:text-gray-200 hover:bg-emerald-700 hover:text-white"
|
|
||||||
>Log out</router-link
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,11 +81,20 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import IconSettings from '~icons/lucide/settings';
|
||||||
|
import IconUser from '~icons/lucide/user';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useSidebar } from '../../composables/useSidebar';
|
import { useReadonlyStream } from '~/composables/stream';
|
||||||
|
import { useSidebar } from '~/composables/useSidebar';
|
||||||
|
import { auth } from '~/helpers/auth';
|
||||||
|
|
||||||
const { isOpen, isExpanded } = useSidebar();
|
const { isOpen, isExpanded } = useSidebar();
|
||||||
|
|
||||||
|
const currentUser = useReadonlyStream(
|
||||||
|
auth.getProbableUserStream(),
|
||||||
|
auth.getProbableUser()
|
||||||
|
);
|
||||||
|
|
||||||
const expandSidebar = () => {
|
const expandSidebar = () => {
|
||||||
isExpanded.value = !isExpanded.value;
|
isExpanded.value = !isExpanded.value;
|
||||||
};
|
};
|
||||||
|
|||||||
236
packages/hoppscotch-sh-admin/src/components/app/Login.vue
Normal file
236
packages/hoppscotch-sh-admin/src/components/app/Login.vue
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="nonAdminUser" class="text-center">
|
||||||
|
Logged in as non admin user. Please
|
||||||
|
<span @click="logout()" class="text-red-500 cursor-pointer underline"
|
||||||
|
>sign out</span
|
||||||
|
>
|
||||||
|
and login with an admin account.
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div v-if="mode === 'sign-in'" class="flex flex-col space-y-2">
|
||||||
|
<HoppSmartItem
|
||||||
|
:loading="signingInWithGitHub"
|
||||||
|
:icon="IconGithub"
|
||||||
|
:label="`Continue with GitHub`"
|
||||||
|
@click="signInWithGithub"
|
||||||
|
/>
|
||||||
|
<HoppSmartItem
|
||||||
|
:loading="signingInWithGoogle"
|
||||||
|
:icon="IconGoogle"
|
||||||
|
:label="`Continue with Google`"
|
||||||
|
@click="signInWithGoogle"
|
||||||
|
/>
|
||||||
|
<HoppSmartItem
|
||||||
|
:loading="signingInWithMicrosoft"
|
||||||
|
:icon="IconMicrosoft"
|
||||||
|
:label="`Continue with Microsoft`"
|
||||||
|
@click="signInWithMicrosoft"
|
||||||
|
/>
|
||||||
|
<HoppSmartItem
|
||||||
|
:icon="IconEmail"
|
||||||
|
:label="`Continue with Email`"
|
||||||
|
@click="mode = 'email'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
v-if="mode === 'email'"
|
||||||
|
class="flex flex-col space-y-2"
|
||||||
|
@submit.prevent="signInWithEmail"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
v-model="form.email"
|
||||||
|
class="input floating-input"
|
||||||
|
placeholder=" "
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
spellcheck="false"
|
||||||
|
v-focus
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
<label for="email"> Email </label>
|
||||||
|
</div>
|
||||||
|
<HoppButtonPrimary
|
||||||
|
:loading="signingInWithEmail"
|
||||||
|
type="submit"
|
||||||
|
:label="`Send magic link`"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
<div v-if="mode === 'email-sent'" class="flex flex-col px-4">
|
||||||
|
<div class="flex flex-col items-center justify-center max-w-md">
|
||||||
|
<icon-lucide-inbox class="w-6 h-6 text-accent" />
|
||||||
|
<h3 class="my-2 text-lg text-center">
|
||||||
|
We sent a magic link to {{ form.email }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-center">
|
||||||
|
We sent a magic link to {{ form.email }}. Click on the link to sign
|
||||||
|
in.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<section class="text-center mt-10">
|
||||||
|
<div v-if="mode === 'sign-in'" class="text-secondaryLight text-tiny">
|
||||||
|
By signing in, you are agreeing to our
|
||||||
|
<HoppSmartAnchor
|
||||||
|
class="link"
|
||||||
|
to="https://docs.hoppscotch.io/terms"
|
||||||
|
blank
|
||||||
|
label="Terms of Service"
|
||||||
|
/>
|
||||||
|
and
|
||||||
|
<HoppSmartAnchor
|
||||||
|
class="link"
|
||||||
|
to="https://docs.hoppscotch.io/privacy"
|
||||||
|
blank
|
||||||
|
label="Privacy Policy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="mode === 'email'">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:label="'All sign in option'"
|
||||||
|
:icon="IconArrowLeft"
|
||||||
|
class="!p-0"
|
||||||
|
@click="mode = 'sign-in'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="mode === 'email-sent'"
|
||||||
|
class="flex justify-between flex-1 text-secondaryLight"
|
||||||
|
>
|
||||||
|
<HoppSmartAnchor
|
||||||
|
class="link"
|
||||||
|
:label="'Re enter email'"
|
||||||
|
:icon="IconArrowLeft"
|
||||||
|
@click="mode = 'email'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import IconGithub from '~icons/auth/github';
|
||||||
|
import IconGoogle from '~icons/auth/google';
|
||||||
|
import IconEmail from '~icons/auth/email';
|
||||||
|
import IconMicrosoft from '~icons/auth/microsoft';
|
||||||
|
import IconArrowLeft from '~icons/lucide/arrow-left';
|
||||||
|
import { setLocalConfig } from '~/helpers/localpersistence';
|
||||||
|
import { useStreamSubscriber } from '~/composables/stream';
|
||||||
|
import { useToast } from '~/composables/toast';
|
||||||
|
import { auth } from '~/helpers/auth';
|
||||||
|
|
||||||
|
const { subscribeToStream } = useStreamSubscriber();
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
// DATA
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
email: '',
|
||||||
|
});
|
||||||
|
const signingInWithGoogle = ref(false);
|
||||||
|
const signingInWithGitHub = ref(false);
|
||||||
|
const signingInWithMicrosoft = ref(false);
|
||||||
|
const signingInWithEmail = ref(false);
|
||||||
|
const mode = ref('sign-in');
|
||||||
|
|
||||||
|
const nonAdminUser = ref(false);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const currentUser$ = auth.getCurrentUserStream();
|
||||||
|
|
||||||
|
subscribeToStream(currentUser$, (user) => {
|
||||||
|
if (user && !user.isAdmin) {
|
||||||
|
nonAdminUser.value = true;
|
||||||
|
toast.error(`You are logged in. But you're not an admin`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function signInWithGoogle() {
|
||||||
|
signingInWithGoogle.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await auth.signInUserWithGoogle();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
/*
|
||||||
|
A auth/account-exists-with-different-credential Firebase error wont happen between Google and any other providers
|
||||||
|
Seems Google account overwrites accounts of other providers https://github.com/firebase/firebase-android-sdk/issues/25
|
||||||
|
*/
|
||||||
|
toast.error(`Failed to sign in with Google`);
|
||||||
|
}
|
||||||
|
|
||||||
|
signingInWithGoogle.value = false;
|
||||||
|
}
|
||||||
|
async function signInWithGithub() {
|
||||||
|
signingInWithGitHub.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await auth.signInUserWithGithub();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
/*
|
||||||
|
A auth/account-exists-with-different-credential Firebase error wont happen between Google and any other providers
|
||||||
|
Seems Google account overwrites accounts of other providers https://github.com/firebase/firebase-android-sdk/issues/25
|
||||||
|
*/
|
||||||
|
toast.error(`Failed to sign in with GitHub`);
|
||||||
|
}
|
||||||
|
|
||||||
|
signingInWithGitHub.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signInWithMicrosoft() {
|
||||||
|
signingInWithMicrosoft.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await auth.signInUserWithMicrosoft();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
/*
|
||||||
|
A auth/account-exists-with-different-credential Firebase error wont happen between MS with Google or Github
|
||||||
|
If a Github account exists and user then logs in with MS email we get a "Something went wrong toast" and console errors and MS replaces GH as only provider.
|
||||||
|
The error messages are as follows:
|
||||||
|
FirebaseError: Firebase: Error (auth/popup-closed-by-user).
|
||||||
|
@firebase/auth: Auth (9.6.11): INTERNAL ASSERTION FAILED: Pending promise was never set
|
||||||
|
They may be related to https://github.com/firebase/firebaseui-web/issues/947
|
||||||
|
*/
|
||||||
|
toast.error(`Something went wrong`);
|
||||||
|
}
|
||||||
|
|
||||||
|
signingInWithMicrosoft.value = false;
|
||||||
|
}
|
||||||
|
async function signInWithEmail() {
|
||||||
|
signingInWithEmail.value = true;
|
||||||
|
|
||||||
|
await auth
|
||||||
|
.signInWithEmail(form.value.email)
|
||||||
|
.then(() => {
|
||||||
|
mode.value = 'email-sent';
|
||||||
|
setLocalConfig('emailForSignIn', form.value.email);
|
||||||
|
})
|
||||||
|
.catch((e: any) => {
|
||||||
|
console.error(e);
|
||||||
|
toast.error(e.message);
|
||||||
|
signingInWithEmail.value = false;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
signingInWithEmail.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
try {
|
||||||
|
await auth.signOutUser();
|
||||||
|
window.location.reload();
|
||||||
|
toast.success(`Logged out`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
toast.error(`Something went wrong`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
62
packages/hoppscotch-sh-admin/src/components/app/Logout.vue
Normal file
62
packages/hoppscotch-sh-admin/src/components/app/Logout.vue
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex" @click="openLogoutModal()">
|
||||||
|
<HoppSmartItem
|
||||||
|
:icon="IconLogOut"
|
||||||
|
:label="'Logout'"
|
||||||
|
:outline="outline"
|
||||||
|
:shortcut="shortcut"
|
||||||
|
@click="openLogoutModal()"
|
||||||
|
/>
|
||||||
|
<HoppSmartConfirmModal
|
||||||
|
:show="confirmLogout"
|
||||||
|
:title="`Confirm Logout`"
|
||||||
|
@hide-modal="confirmLogout = false"
|
||||||
|
@resolve="logout"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import IconLogOut from '~icons/lucide/log-out';
|
||||||
|
import { useToast } from '~/composables/toast';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { auth } from '~/helpers/auth';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
outline: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
shortcut: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'confirm-logout'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const confirmLogout = ref(false);
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
try {
|
||||||
|
await auth.signOutUser();
|
||||||
|
router.push(`/`);
|
||||||
|
toast.success(`Logged out`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
toast.error(`Something went wrong`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openLogoutModal = () => {
|
||||||
|
emit('confirm-logout');
|
||||||
|
confirmLogout.value = true;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
tabindex="0"
|
||||||
|
class="relative flex items-center justify-center cursor-pointer focus:outline-none focus-visible:ring focus-visible:ring-primaryDark"
|
||||||
|
:class="[`rounded-${rounded}`, `w-${size} h-${size}`]"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="url"
|
||||||
|
class="absolute object-cover object-center transition bg-primaryDark"
|
||||||
|
:class="[`rounded-${rounded}`, `w-${size} h-${size}`]"
|
||||||
|
:src="url"
|
||||||
|
:alt="alt"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="absolute flex items-center justify-center object-cover object-center transition bg-primaryDark text-accentContrast"
|
||||||
|
:class="[`rounded-${rounded}`, `w-${size} h-${size}`]"
|
||||||
|
:style="`background-color: ${initial ? toHex(initial) : '#480000'}`"
|
||||||
|
>
|
||||||
|
<template v-if="initial && initial.charAt(0).toUpperCase()">
|
||||||
|
{{ initial.charAt(0).toUpperCase() }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<icon-lucide-user v-else></icon-lucide-user>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
v-if="indicator"
|
||||||
|
class="border-primary border-2 h-2.5 -top-0.5 -right-0.5 w-2.5 absolute"
|
||||||
|
:class="[`rounded-${rounded}`, indicatorStyles]"
|
||||||
|
></span>
|
||||||
|
<!-- w-5 h-5 rounded-lg -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, PropType } from 'vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
alt: {
|
||||||
|
type: String,
|
||||||
|
default: 'Profile picture',
|
||||||
|
},
|
||||||
|
indicator: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
indicatorStyles: {
|
||||||
|
type: String,
|
||||||
|
default: 'bg-green-500',
|
||||||
|
},
|
||||||
|
rounded: {
|
||||||
|
type: String,
|
||||||
|
default: 'full',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: '5',
|
||||||
|
},
|
||||||
|
initial: {
|
||||||
|
type: String as PropType<string | undefined | null>,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toHex(initial: string) {
|
||||||
|
let hash = 0;
|
||||||
|
if (initial.length === 0) return hash;
|
||||||
|
for (let i = 0; i < initial.length; i++) {
|
||||||
|
hash = initial.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
|
hash = hash & hash;
|
||||||
|
}
|
||||||
|
let color = '#';
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const value = (hash >> (i * 8)) & 255;
|
||||||
|
color += `00${value.toString(16)}`.slice(-2);
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
62
packages/hoppscotch-sh-admin/src/composables/auth.ts
Normal file
62
packages/hoppscotch-sh-admin/src/composables/auth.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { platform } from '~/platform';
|
||||||
|
import { AuthEvent, HoppUser } from '~/platform/auth';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import { onBeforeUnmount, onMounted, watch, WatchStopHandle } from 'vue';
|
||||||
|
import { useReadonlyStream } from './stream';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Vue composable function that is called when the auth status
|
||||||
|
* is being updated to being logged in (fired multiple times),
|
||||||
|
* this is also called on component mount if the login
|
||||||
|
* was already resolved before mount.
|
||||||
|
*/
|
||||||
|
export function onLoggedIn(exec: (user: HoppUser) => void) {
|
||||||
|
const currentUser = useReadonlyStream(
|
||||||
|
platform.auth.getCurrentUserStream(),
|
||||||
|
platform.auth.getCurrentUser()
|
||||||
|
);
|
||||||
|
|
||||||
|
let watchStop: WatchStopHandle | null = null;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (currentUser.value) exec(currentUser.value);
|
||||||
|
|
||||||
|
watchStop = watch(currentUser, (newVal, prev) => {
|
||||||
|
if (prev === null && newVal !== null) {
|
||||||
|
exec(newVal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
watchStop?.();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Vue composable function that calls its param function
|
||||||
|
* when a new event (login, logout etc.) happens in
|
||||||
|
* the auth system.
|
||||||
|
*
|
||||||
|
* NOTE: Unlike `onLoggedIn` for which the callback will be called once on mount with the current state,
|
||||||
|
* here the callback will only be called on authentication event occurances.
|
||||||
|
* You might want to check the auth state from an `onMounted` hook or something
|
||||||
|
* if you want to access the initial state
|
||||||
|
*
|
||||||
|
* @param func A function which accepts an event
|
||||||
|
*/
|
||||||
|
export function onAuthEvent(func: (ev: AuthEvent) => void) {
|
||||||
|
const authEvents$ = platform.auth.getAuthEventsStream();
|
||||||
|
|
||||||
|
let sub: Subscription | null = null;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
sub = authEvents$.subscribe((ev) => {
|
||||||
|
func(ev);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
sub?.unsubscribe();
|
||||||
|
});
|
||||||
|
}
|
||||||
174
packages/hoppscotch-sh-admin/src/composables/stream.ts
Normal file
174
packages/hoppscotch-sh-admin/src/composables/stream.ts
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import { clone, cloneDeep } from 'lodash-es';
|
||||||
|
import { Observable, Subscription } from 'rxjs';
|
||||||
|
import { customRef, onBeforeUnmount, readonly, Ref } from 'vue';
|
||||||
|
|
||||||
|
type CloneMode = 'noclone' | 'shallow' | 'deep';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a readonly (no writes) ref for an RxJS Observable
|
||||||
|
* @param stream$ The RxJS Observable to listen to
|
||||||
|
* @param initialValue The initial value to apply until the stream emits a value
|
||||||
|
* @param cloneMode Determines whether or not and how deep to clone the emitted value.
|
||||||
|
* Useful for issues in reactivity due to reference sharing. Defaults to shallow clone
|
||||||
|
* @returns A readonly ref which has the latest value from the stream
|
||||||
|
*/
|
||||||
|
export function useReadonlyStream<T>(
|
||||||
|
stream$: Observable<T>,
|
||||||
|
initialValue: T,
|
||||||
|
cloneMode: CloneMode = 'shallow'
|
||||||
|
): Ref<T> {
|
||||||
|
let sub: Subscription | null = null;
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (sub) {
|
||||||
|
sub.unsubscribe();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const r = customRef((track, trigger) => {
|
||||||
|
let val = initialValue;
|
||||||
|
|
||||||
|
sub = stream$.subscribe((value) => {
|
||||||
|
if (cloneMode === 'noclone') {
|
||||||
|
val = value;
|
||||||
|
} else if (cloneMode === 'shallow') {
|
||||||
|
val = clone(value);
|
||||||
|
} else if (cloneMode === 'deep') {
|
||||||
|
val = cloneDeep(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
trigger();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
get() {
|
||||||
|
track();
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
set() {
|
||||||
|
trigger(); // <- Not exactly needed here
|
||||||
|
throw new Error('Cannot write to a ref from useReadonlyStream');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Casting to still maintain the proper type signature for ease of use
|
||||||
|
return readonly(r) as Ref<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useStream<T>(
|
||||||
|
stream$: Observable<T>,
|
||||||
|
initialValue: T,
|
||||||
|
setter: (val: T) => void
|
||||||
|
) {
|
||||||
|
let sub: Subscription | null = null;
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (sub) {
|
||||||
|
sub.unsubscribe();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return customRef((track, trigger) => {
|
||||||
|
let value = initialValue;
|
||||||
|
|
||||||
|
sub = stream$.subscribe((val) => {
|
||||||
|
value = val;
|
||||||
|
trigger();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
get() {
|
||||||
|
track();
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
set(value: T) {
|
||||||
|
trigger();
|
||||||
|
setter(value);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A static (doesn't cleanup on itself and does
|
||||||
|
* not require component instace) version of useStream
|
||||||
|
*/
|
||||||
|
export function useStreamStatic<T>(
|
||||||
|
stream$: Observable<T>,
|
||||||
|
initialValue: T,
|
||||||
|
setter: (val: T) => void
|
||||||
|
): [Ref<T>, () => void] {
|
||||||
|
let sub: Subscription | null = null;
|
||||||
|
|
||||||
|
const stopper = () => {
|
||||||
|
if (sub) {
|
||||||
|
sub.unsubscribe();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
customRef((track, trigger) => {
|
||||||
|
let value = initialValue;
|
||||||
|
|
||||||
|
sub = stream$.subscribe((val) => {
|
||||||
|
value = val;
|
||||||
|
trigger();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
get() {
|
||||||
|
track();
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
set(value: T) {
|
||||||
|
trigger();
|
||||||
|
setter(value);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
stopper,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StreamSubscriberFunc = <T>(
|
||||||
|
stream: Observable<T>,
|
||||||
|
next?: ((value: T) => void) | undefined,
|
||||||
|
error?: ((e: any) => void) | undefined,
|
||||||
|
complete?: (() => void) | undefined
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A composable that provides the ability to run streams
|
||||||
|
* and subscribe to them and respect the component lifecycle.
|
||||||
|
*/
|
||||||
|
export function useStreamSubscriber(): {
|
||||||
|
subscribeToStream: StreamSubscriberFunc;
|
||||||
|
} {
|
||||||
|
const subs: Subscription[] = [];
|
||||||
|
|
||||||
|
const runAndSubscribe = <T>(
|
||||||
|
stream: Observable<T>,
|
||||||
|
next?: (value: T) => void,
|
||||||
|
error?: (e: any) => void,
|
||||||
|
complete?: () => void
|
||||||
|
) => {
|
||||||
|
const sub = stream.subscribe({
|
||||||
|
next,
|
||||||
|
error,
|
||||||
|
complete: () => {
|
||||||
|
if (complete) complete();
|
||||||
|
subs.splice(subs.indexOf(sub), 1);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
subs.push(sub);
|
||||||
|
};
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
subs.forEach((sub) => sub.unsubscribe());
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribeToStream: runAndSubscribe,
|
||||||
|
};
|
||||||
|
}
|
||||||
385
packages/hoppscotch-sh-admin/src/helpers/auth.ts
Normal file
385
packages/hoppscotch-sh-admin/src/helpers/auth.ts
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { BehaviorSubject, Subject } from 'rxjs';
|
||||||
|
import {
|
||||||
|
getLocalConfig,
|
||||||
|
removeLocalConfig,
|
||||||
|
setLocalConfig,
|
||||||
|
} from './localpersistence';
|
||||||
|
import { Ref, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A common (and required) set of fields that describe a user.
|
||||||
|
*/
|
||||||
|
export type HoppUser = {
|
||||||
|
/** A unique ID identifying the user */
|
||||||
|
uid: string;
|
||||||
|
|
||||||
|
/** The name to be displayed as the user's */
|
||||||
|
displayName: string | null;
|
||||||
|
|
||||||
|
/** The user's email address */
|
||||||
|
email: string | null;
|
||||||
|
|
||||||
|
/** URL to the profile picture of the user */
|
||||||
|
photoURL: string | null;
|
||||||
|
|
||||||
|
// Regarding `provider` and `accessToken`:
|
||||||
|
// The current implementation and use case for these 2 fields are super weird due to legacy.
|
||||||
|
// Currrently these fields are only basically populated for Github Auth as we need the access token issued
|
||||||
|
// by it to implement Gist submission. I would really love refactor to make this thing more sane.
|
||||||
|
|
||||||
|
/** Name of the provider authenticating (NOTE: See notes on `platform/auth.ts`) */
|
||||||
|
provider?: string;
|
||||||
|
/** Access Token for the auth of the user against the given `provider`. */
|
||||||
|
accessToken?: string;
|
||||||
|
emailVerified: boolean;
|
||||||
|
|
||||||
|
isAdmin: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AuthEvent =
|
||||||
|
| { event: 'probable_login'; user: HoppUser } // We have previous login state, but the app is waiting for authentication
|
||||||
|
| { event: 'login'; user: HoppUser } // We are authenticated
|
||||||
|
| { event: 'logout' } // No authentication and we have no previous state
|
||||||
|
| { event: 'token_refresh' }; // We have previous login state, but the app is waiting for authentication
|
||||||
|
|
||||||
|
export type GithubSignInResult =
|
||||||
|
| { type: 'success'; user: HoppUser } // The authentication was a success
|
||||||
|
| { type: 'account-exists-with-different-cred'; link: () => Promise<void> } // We authenticated correctly, but the provider didn't match, so we give the user the opportunity to link to continue completing auth
|
||||||
|
| { type: 'error'; err: unknown }; // Auth failed completely and we don't know why
|
||||||
|
|
||||||
|
export const authEvents$ = new Subject<
|
||||||
|
AuthEvent | { event: 'token_refresh' }
|
||||||
|
>();
|
||||||
|
const currentUser$ = new BehaviorSubject<HoppUser | null>(null);
|
||||||
|
export const probableUser$ = new BehaviorSubject<HoppUser | null>(null);
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
await axios.get(`${import.meta.env.VITE_BACKEND_API_URL}/auth/logout`, {
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signInUserWithGithubFB() {
|
||||||
|
window.location.href = `${import.meta.env.VITE_BACKEND_API_URL}/auth/github`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signInUserWithGoogleFB() {
|
||||||
|
window.location.href = `${import.meta.env.VITE_BACKEND_API_URL}/auth/google`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signInUserWithMicrosoftFB() {
|
||||||
|
window.location.href = `${
|
||||||
|
import.meta.env.VITE_BACKEND_API_URL
|
||||||
|
}/auth/microsoft`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getInitialUserDetails() {
|
||||||
|
const res = await axios.post<{
|
||||||
|
data?: {
|
||||||
|
me?: {
|
||||||
|
uid: string;
|
||||||
|
displayName: string;
|
||||||
|
email: string;
|
||||||
|
photoURL: string;
|
||||||
|
isAdmin: boolean;
|
||||||
|
createdOn: string;
|
||||||
|
// emailVerified: boolean
|
||||||
|
};
|
||||||
|
};
|
||||||
|
errors?: Array<{
|
||||||
|
message: string;
|
||||||
|
}>;
|
||||||
|
}>(
|
||||||
|
`${import.meta.env.VITE_BACKEND_GQL_URL}`,
|
||||||
|
{
|
||||||
|
query: `query Me {
|
||||||
|
me {
|
||||||
|
uid
|
||||||
|
displayName
|
||||||
|
email
|
||||||
|
photoURL
|
||||||
|
isAdmin
|
||||||
|
createdOn
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
withCredentials: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isGettingInitialUser: Ref<null | boolean> = ref(null);
|
||||||
|
|
||||||
|
function setUser(user: HoppUser | null) {
|
||||||
|
currentUser$.next(user);
|
||||||
|
probableUser$.next(user);
|
||||||
|
|
||||||
|
setLocalConfig('login_state', JSON.stringify(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setInitialUser() {
|
||||||
|
isGettingInitialUser.value = true;
|
||||||
|
const res = await getInitialUserDetails();
|
||||||
|
|
||||||
|
const error = res.errors && res.errors[0];
|
||||||
|
|
||||||
|
// no cookies sent. so the user is not logged in
|
||||||
|
if (error && error.message === 'auth/cookies_not_found') {
|
||||||
|
setUser(null);
|
||||||
|
isGettingInitialUser.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cookies sent, but it is expired, we need to refresh the token
|
||||||
|
if (error && error.message === 'Unauthorized') {
|
||||||
|
const isRefreshSuccess = await refreshToken();
|
||||||
|
|
||||||
|
if (isRefreshSuccess) {
|
||||||
|
setInitialUser();
|
||||||
|
} else {
|
||||||
|
setUser(null);
|
||||||
|
isGettingInitialUser.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// no errors, we have a valid user
|
||||||
|
if (res.data && res.data.me) {
|
||||||
|
const hoppBackendUser = res.data.me;
|
||||||
|
|
||||||
|
const hoppUser: HoppUser = {
|
||||||
|
uid: hoppBackendUser.uid,
|
||||||
|
displayName: hoppBackendUser.displayName,
|
||||||
|
email: hoppBackendUser.email,
|
||||||
|
photoURL: hoppBackendUser.photoURL,
|
||||||
|
// all our signin methods currently guarantees the email is verified
|
||||||
|
emailVerified: true,
|
||||||
|
isAdmin: hoppBackendUser.isAdmin,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!hoppUser.isAdmin) {
|
||||||
|
const isAdmin = await elevateUser();
|
||||||
|
hoppUser.isAdmin = isAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUser(hoppUser);
|
||||||
|
|
||||||
|
isGettingInitialUser.value = false;
|
||||||
|
|
||||||
|
authEvents$.next({
|
||||||
|
event: 'login',
|
||||||
|
user: hoppUser,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshToken() {
|
||||||
|
const res = await axios.get(
|
||||||
|
`${import.meta.env.VITE_BACKEND_API_URL}/auth/refresh`,
|
||||||
|
{
|
||||||
|
withCredentials: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const isSuccessful = res.status === 200;
|
||||||
|
|
||||||
|
if (isSuccessful) {
|
||||||
|
authEvents$.next({
|
||||||
|
event: 'token_refresh',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return isSuccessful;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function elevateUser() {
|
||||||
|
const res = await axios.get(
|
||||||
|
`${import.meta.env.VITE_BACKEND_API_URL}/auth/verify/admin`,
|
||||||
|
{
|
||||||
|
withCredentials: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return !!res.data?.isAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendMagicLink(email: string) {
|
||||||
|
const res = await axios.post(
|
||||||
|
`${import.meta.env.VITE_BACKEND_API_URL}/auth/signin`,
|
||||||
|
{
|
||||||
|
email,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
withCredentials: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.data && res.data.deviceIdentifier) {
|
||||||
|
setLocalConfig('deviceIdentifier', res.data.deviceIdentifier);
|
||||||
|
} else {
|
||||||
|
throw new Error('test: does not get device identifier');
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const auth = {
|
||||||
|
getCurrentUserStream: () => currentUser$,
|
||||||
|
getAuthEventsStream: () => authEvents$,
|
||||||
|
getProbableUserStream: () => probableUser$,
|
||||||
|
|
||||||
|
getCurrentUser: () => currentUser$.value,
|
||||||
|
getProbableUser: () => probableUser$.value,
|
||||||
|
|
||||||
|
getBackendHeaders() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
getGQLClientOptions() {
|
||||||
|
return {
|
||||||
|
fetchOptions: {
|
||||||
|
credentials: 'include',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* it is not possible for us to know if the current cookie is expired because we cannot access http-only cookies from js
|
||||||
|
* hence just returning if the currentUser$ has a value associated with it
|
||||||
|
*/
|
||||||
|
willBackendHaveAuthError() {
|
||||||
|
return !currentUser$.value;
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
onBackendGQLClientShouldReconnect(func: () => void) {
|
||||||
|
authEvents$.subscribe((event) => {
|
||||||
|
if (
|
||||||
|
event.event == 'login' ||
|
||||||
|
event.event == 'logout' ||
|
||||||
|
event.event == 'token_refresh'
|
||||||
|
) {
|
||||||
|
func();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* we cannot access our auth cookies from javascript, so leaving this as null
|
||||||
|
*/
|
||||||
|
getDevOptsBackendIDToken() {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
async performAuthInit() {
|
||||||
|
const probableUser = JSON.parse(getLocalConfig('login_state') ?? 'null');
|
||||||
|
probableUser$.next(probableUser);
|
||||||
|
await setInitialUser();
|
||||||
|
},
|
||||||
|
|
||||||
|
waitProbableLoginToConfirm() {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
if (this.getCurrentUser()) {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!probableUser$.value) reject(new Error('no_probable_user'));
|
||||||
|
|
||||||
|
const unwatch = watch(isGettingInitialUser, (val) => {
|
||||||
|
if (val === true || val === false) {
|
||||||
|
resolve();
|
||||||
|
unwatch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async signInWithEmail(email: string) {
|
||||||
|
await sendMagicLink(email);
|
||||||
|
},
|
||||||
|
|
||||||
|
isSignInWithEmailLink(url: string) {
|
||||||
|
const urlObject = new URL(url);
|
||||||
|
const searchParams = new URLSearchParams(urlObject.search);
|
||||||
|
|
||||||
|
return !!searchParams.get('token');
|
||||||
|
},
|
||||||
|
|
||||||
|
async verifyEmailAddress() {
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
async signInUserWithGoogle() {
|
||||||
|
await signInUserWithGoogleFB();
|
||||||
|
},
|
||||||
|
async signInUserWithGithub() {
|
||||||
|
await signInUserWithGithubFB();
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
async signInUserWithMicrosoft() {
|
||||||
|
await signInUserWithMicrosoftFB();
|
||||||
|
},
|
||||||
|
async signInWithEmailLink(email: string, url: string) {
|
||||||
|
const urlObject = new URL(url);
|
||||||
|
const searchParams = new URLSearchParams(urlObject.search);
|
||||||
|
|
||||||
|
const token = searchParams.get('token');
|
||||||
|
const deviceIdentifier = getLocalConfig('deviceIdentifier');
|
||||||
|
|
||||||
|
await axios.post(
|
||||||
|
`${import.meta.env.VITE_BACKEND_API_URL}/auth/verify`,
|
||||||
|
{
|
||||||
|
token: token,
|
||||||
|
deviceIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
withCredentials: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async setEmailAddress(_email: string) {
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async setDisplayName(name: string) {
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
|
||||||
|
async signOutUser() {
|
||||||
|
// if (!currentUser$.value) throw new Error("No user has logged in")
|
||||||
|
|
||||||
|
await logout();
|
||||||
|
|
||||||
|
probableUser$.next(null);
|
||||||
|
currentUser$.next(null);
|
||||||
|
removeLocalConfig('login_state');
|
||||||
|
|
||||||
|
authEvents$.next({
|
||||||
|
event: 'logout',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async processMagicLink() {
|
||||||
|
if (this.isSignInWithEmailLink(window.location.href)) {
|
||||||
|
const deviceIdentifier = getLocalConfig('deviceIdentifier');
|
||||||
|
|
||||||
|
if (!deviceIdentifier) {
|
||||||
|
throw new Error(
|
||||||
|
'Device Identifier not found, you can only signin from the browser you generated the magic link'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.signInWithEmailLink(deviceIdentifier, window.location.href);
|
||||||
|
|
||||||
|
removeLocalConfig('deviceIdentifier');
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
query Me {
|
||||||
|
me {
|
||||||
|
uid
|
||||||
|
displayName
|
||||||
|
photoURL
|
||||||
|
isAdmin
|
||||||
|
createdOn
|
||||||
|
}
|
||||||
|
}
|
||||||
27
packages/hoppscotch-sh-admin/src/helpers/localpersistence.ts
Normal file
27
packages/hoppscotch-sh-admin/src/helpers/localpersistence.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Gets a value in LocalStorage.
|
||||||
|
*
|
||||||
|
* NOTE: Use LocalStorage to only store non-reactive simple data
|
||||||
|
* For more complex data, use stores and connect it to localpersistence
|
||||||
|
*/
|
||||||
|
export function getLocalConfig(name: string) {
|
||||||
|
return window.localStorage.getItem(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a value in LocalStorage.
|
||||||
|
*
|
||||||
|
* NOTE: Use LocalStorage to only store non-reactive simple data
|
||||||
|
* For more complex data, use stores and connect it to localpersistence
|
||||||
|
*/
|
||||||
|
export function setLocalConfig(key: string, value: string) {
|
||||||
|
window.localStorage.setItem(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear config value in LocalStorage.
|
||||||
|
* @param key Key to be cleared
|
||||||
|
*/
|
||||||
|
export function removeLocalConfig(key: string) {
|
||||||
|
window.localStorage.removeItem(key);
|
||||||
|
}
|
||||||
@@ -1,33 +1,16 @@
|
|||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue';
|
||||||
import urql, { createClient } from '@urql/vue';
|
import urql, { createClient } from '@urql/vue';
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
import Toasted from '@hoppscotch/vue-toasted';
|
|
||||||
import type { ToastOptions } from '@hoppscotch/vue-toasted';
|
|
||||||
|
|
||||||
// STYLES
|
// STYLES
|
||||||
import 'virtual:windi.css';
|
import 'virtual:windi.css';
|
||||||
import '@hoppscotch/vue-toasted/style.css';
|
|
||||||
import '@hoppscotch/ui/style.css';
|
import '@hoppscotch/ui/style.css';
|
||||||
import '../assets/scss/themes.scss';
|
import '../assets/scss/themes.scss';
|
||||||
import '../assets/scss/styles.scss';
|
import '../assets/scss/styles.scss';
|
||||||
// END STYLES
|
// END STYLES
|
||||||
|
|
||||||
import {
|
import { HOPP_MODULES } from './modules';
|
||||||
createRouter,
|
import { auth } from './helpers/auth';
|
||||||
createWebHashHistory,
|
|
||||||
createWebHistory,
|
|
||||||
} from 'vue-router';
|
|
||||||
|
|
||||||
import { setupLayouts } from 'virtual:generated-layouts';
|
|
||||||
import generatedRoutes from 'virtual:generated-pages';
|
|
||||||
|
|
||||||
import { plugin as HoppUIPlugin, HoppUIPluginOptions } from '@hoppscotch/ui';
|
|
||||||
|
|
||||||
const options: HoppUIPluginOptions = {
|
|
||||||
/* Define options here */
|
|
||||||
};
|
|
||||||
|
|
||||||
const routes = setupLayouts(generatedRoutes);
|
|
||||||
|
|
||||||
const app = createApp(App).use(
|
const app = createApp(App).use(
|
||||||
urql,
|
urql,
|
||||||
@@ -41,21 +24,10 @@ const app = createApp(App).use(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// We are using a fork of Vue Toasted (github.com/clayzar/vue-toasted) which is a bit of
|
// Initialize auth
|
||||||
// an untrusted fork, we will either want to make our own fork or move to a more stable one
|
await auth.performAuthInit();
|
||||||
// The original Vue Toasted doesn't support Vue 3 and the OP has been irresponsive.
|
|
||||||
app.use(Toasted, <ToastOptions>{
|
|
||||||
position: 'bottom-center',
|
|
||||||
duration: 3000,
|
|
||||||
keepOnHover: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use(HoppUIPlugin, options);
|
// Initialize modules
|
||||||
app.use(
|
HOPP_MODULES.forEach((mod) => mod.onVueAppInit?.(app));
|
||||||
createRouter({
|
|
||||||
history: createWebHistory(),
|
|
||||||
routes,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
app.mount('#app');
|
app.mount('#app');
|
||||||
|
|||||||
19
packages/hoppscotch-sh-admin/src/modules/admin.ts
Normal file
19
packages/hoppscotch-sh-admin/src/modules/admin.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { auth } from '~/helpers/auth';
|
||||||
|
import { HoppModule } from '.';
|
||||||
|
|
||||||
|
const isAdmin = () => {
|
||||||
|
const user = auth.getCurrentUser();
|
||||||
|
return user ? user.isAdmin : false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default <HoppModule>{
|
||||||
|
onBeforeRouteChange(to, from, next) {
|
||||||
|
if (to.name !== 'index' && !isAdmin()) {
|
||||||
|
next({ name: 'index' });
|
||||||
|
} else if (to.name === 'index' && isAdmin()) {
|
||||||
|
next({ name: 'dashboard' });
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
57
packages/hoppscotch-sh-admin/src/modules/index.ts
Normal file
57
packages/hoppscotch-sh-admin/src/modules/index.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { App } from 'vue';
|
||||||
|
import { pipe } from 'fp-ts/function';
|
||||||
|
import * as A from 'fp-ts/Array';
|
||||||
|
import {
|
||||||
|
NavigationGuardNext,
|
||||||
|
RouteLocationNormalized,
|
||||||
|
Router,
|
||||||
|
} from 'vue-router';
|
||||||
|
|
||||||
|
export type HoppModule = {
|
||||||
|
/**
|
||||||
|
* Define this function to get access to Vue App instance and augment
|
||||||
|
* it (installing components, directives and plugins). Also useful for
|
||||||
|
* early generic initializations. This function should be called first
|
||||||
|
*/
|
||||||
|
onVueAppInit?: (app: App) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the router is done initializing.
|
||||||
|
* Used if a module requires access to the router instance
|
||||||
|
*/
|
||||||
|
onRouterInit?: (app: App, router: Router) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the root component (App.vue) is running setup.
|
||||||
|
* This function is generally called last in the lifecycle.
|
||||||
|
* This function executes with a component setup context, so you can
|
||||||
|
* run composables within this and it should just be scoped to the
|
||||||
|
* root component
|
||||||
|
*/
|
||||||
|
onRootSetup?: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by the router to tell all the modules before a route navigation
|
||||||
|
* is made.
|
||||||
|
*/
|
||||||
|
onBeforeRouteChange?: (
|
||||||
|
to: RouteLocationNormalized,
|
||||||
|
from: RouteLocationNormalized,
|
||||||
|
next: NavigationGuardNext,
|
||||||
|
router: Router
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by the router to tell all the modules that a route navigation has completed
|
||||||
|
*/
|
||||||
|
onAfterRouteChange?: (to: RouteLocationNormalized, router: Router) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All the modules Hoppscotch loads into the app
|
||||||
|
*/
|
||||||
|
export const HOPP_MODULES = pipe(
|
||||||
|
import.meta.glob('@modules/*.ts', { eager: true }),
|
||||||
|
Object.values,
|
||||||
|
A.map(({ default: defaultVal }) => defaultVal as HoppModule)
|
||||||
|
);
|
||||||
72
packages/hoppscotch-sh-admin/src/modules/router.ts
Normal file
72
packages/hoppscotch-sh-admin/src/modules/router.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { HoppModule, HOPP_MODULES } from '.';
|
||||||
|
import {
|
||||||
|
createRouter,
|
||||||
|
createWebHistory,
|
||||||
|
RouteLocationNormalized,
|
||||||
|
} from 'vue-router';
|
||||||
|
import { setupLayouts } from 'virtual:generated-layouts';
|
||||||
|
import generatedRoutes from 'virtual:generated-pages';
|
||||||
|
import { readonly, ref } from 'vue';
|
||||||
|
|
||||||
|
const routes = setupLayouts(generatedRoutes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reactive value signifying whether we are currently navigating
|
||||||
|
* into the first route the application is routing into.
|
||||||
|
* Useful, if you want to do stuff for the initial page load (for example splash screens!)
|
||||||
|
*/
|
||||||
|
const _isLoadingInitialRoute = ref(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Says whether a given route looks like an initial route which
|
||||||
|
* is loaded as the first route.
|
||||||
|
*
|
||||||
|
* NOTE: This function assumes Vue Router represents that initial route
|
||||||
|
* in the way we expect (fullPath == "/" and name == undefined). If this
|
||||||
|
* function breaks later on, most probs vue-router updated its semantics
|
||||||
|
* and we have to correct this function.
|
||||||
|
*/
|
||||||
|
function isInitialRoute(route: RouteLocationNormalized) {
|
||||||
|
return route.fullPath === '/' && route.name === undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reactive value signifying whether we are currently navigating
|
||||||
|
* into the first route the application is routing into.
|
||||||
|
* Useful, if you want to do stuff for the initial page load (for example splash screens!)
|
||||||
|
*
|
||||||
|
* NOTE: This reactive value is READONLY
|
||||||
|
*/
|
||||||
|
export const isLoadingInitialRoute = readonly(_isLoadingInitialRoute);
|
||||||
|
|
||||||
|
export default <HoppModule>{
|
||||||
|
onVueAppInit(app) {
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes,
|
||||||
|
});
|
||||||
|
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
_isLoadingInitialRoute.value = isInitialRoute(from);
|
||||||
|
|
||||||
|
HOPP_MODULES.forEach((mod) => {
|
||||||
|
mod.onBeforeRouteChange?.(to, from, next, router);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Instead of this a better architecture is for the router
|
||||||
|
// module to expose a stream of router events that can be independently
|
||||||
|
// subbed to
|
||||||
|
router.afterEach((to) => {
|
||||||
|
_isLoadingInitialRoute.value = false;
|
||||||
|
|
||||||
|
HOPP_MODULES.forEach((mod) => {
|
||||||
|
mod.onAfterRouteChange?.(to, router);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(router);
|
||||||
|
|
||||||
|
HOPP_MODULES.forEach((mod) => mod.onRouterInit?.(app, router));
|
||||||
|
},
|
||||||
|
};
|
||||||
19
packages/hoppscotch-sh-admin/src/modules/toast.ts
Normal file
19
packages/hoppscotch-sh-admin/src/modules/toast.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import Toasted from '@hoppscotch/vue-toasted';
|
||||||
|
import type { ToastOptions } from '@hoppscotch/vue-toasted';
|
||||||
|
import { HoppModule } from '.';
|
||||||
|
|
||||||
|
import '@hoppscotch/vue-toasted/style.css';
|
||||||
|
|
||||||
|
// We are using a fork of Vue Toasted (github.com/clayzar/vue-toasted) which is a bit of
|
||||||
|
// an untrusted fork, we will either want to make our own fork or move to a more stable one
|
||||||
|
// The original Vue Toasted doesn't support Vue 3 and the OP has been irresponsive.
|
||||||
|
|
||||||
|
export default <HoppModule>{
|
||||||
|
onVueAppInit(app) {
|
||||||
|
app.use(Toasted, <ToastOptions>{
|
||||||
|
position: 'bottom-center',
|
||||||
|
duration: 3000,
|
||||||
|
keepOnHover: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
13
packages/hoppscotch-sh-admin/src/modules/ui.ts
Normal file
13
packages/hoppscotch-sh-admin/src/modules/ui.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { HoppModule } from '.';
|
||||||
|
import { plugin as HoppUI, HoppUIPluginOptions } from '@hoppscotch/ui';
|
||||||
|
|
||||||
|
const HoppUIOptions: HoppUIPluginOptions = {};
|
||||||
|
|
||||||
|
export default <HoppModule>{
|
||||||
|
onVueAppInit(app) {
|
||||||
|
// disable eslint for this line. it's a hack because there's some unknown type error
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
app.use(HoppUI, HoppUIOptions);
|
||||||
|
},
|
||||||
|
};
|
||||||
15
packages/hoppscotch-sh-admin/src/modules/v-focus.ts
Normal file
15
packages/hoppscotch-sh-admin/src/modules/v-focus.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { nextTick } from "vue"
|
||||||
|
import { HoppModule } from "."
|
||||||
|
|
||||||
|
/*
|
||||||
|
Declares a `v-focus` directive that can be used for components
|
||||||
|
to acquire focus instantly once mounted
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default <HoppModule>{
|
||||||
|
onVueAppInit(app) {
|
||||||
|
app.directive("focus", {
|
||||||
|
mounted: (el) => nextTick(() => el.focus()),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,113 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-center h-screen px-6 bg-gray-200 dark:bg-gradient-to-r dark:from-zinc-700 dark:to-gray-900"
|
class="flex items-center justify-center h-screen bg-gray-200 dark:bg-gradient-to-r dark:from-zinc-700 dark:to-gray-900 p-6"
|
||||||
>
|
>
|
||||||
|
<div>
|
||||||
|
<div class="flex flex-col items-center justify-center mb-10">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
class="tracking-wide !font-bold !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark uppercase"
|
||||||
|
:label="'Hoppscotch'"
|
||||||
|
to="/"
|
||||||
|
/>
|
||||||
|
<span> Dashboard </span>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="w-full max-w-lg p-8 bg-white dark:bg-gradient-to-r dark:from-zinc-800 dark:to-gray-900 rounded-lg shadow-md"
|
class="bg-primary xs:w-xs sm:w-sm md:w-lg p-10 rounded-md border border-secondaryLight"
|
||||||
>
|
|
||||||
<div class="flex items-center justify-center mt-6">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<img src="/public/cover.jpg" alt="" class="h-12" />
|
|
||||||
|
|
||||||
<span
|
|
||||||
class="mx-2 text-2xl font-semibold text-gray-600 dark:text-gray-300"
|
|
||||||
>Hoppscotch</span
|
|
||||||
>
|
>
|
||||||
|
<AppLogin />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="mt-4" @submit.prevent="login">
|
|
||||||
<label class="block">
|
|
||||||
<span class="text-sm text-gray-400">Email</span>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
class="block w-full p-2 mt-1 bg-gray-700 border-gray-700 rounded-md focus:border-emerald-600 focus:ring focus:ring-opacity-40 focus:ring-emerald-500"
|
|
||||||
v-model="email"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="block mt-3">
|
|
||||||
<span class="text-sm text-gray-400">Password</span>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
class="block w-full p-2 mt-1 bg-gray-700 border-gray-700 rounded-md focus:border-emerald-600 focus:ring focus:ring-opacity-40 focus:ring-emerald-500"
|
|
||||||
v-model="password"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-between mt-4">
|
|
||||||
<div>
|
|
||||||
<label class="inline-flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
class="text-indigo-600 border-gray-200 rounded-md focus:border-emerald-600 focus:ring focus:ring-opacity-40 focus:ring-emerald-500"
|
|
||||||
/>
|
|
||||||
<span class="mx-2 text-sm text-gray-400">Remember me</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
class="block text-sm text-emerald-700 fontme hover:underline"
|
|
||||||
href="#"
|
|
||||||
>Forgot your password?</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-6">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="w-full px-4 py-2 text-sm text-center text-white bg-emerald-600 rounded-md focus:outline-none hover:bg-emerald-500"
|
|
||||||
>
|
|
||||||
Sign in
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-6">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="w-full px-4 py-2 text-sm text-center text-white border border-gray-500 rounded-md focus:outline-none hover:bg-gray-700"
|
|
||||||
>
|
|
||||||
Sign in with Google
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-6">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="w-full px-4 py-2 text-sm text-center text-white border border-gray-500 rounded-md focus:outline-none hover:bg-gray-700"
|
|
||||||
>
|
|
||||||
Sign in with Microsoft
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-6">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="w-full px-4 py-2 text-sm text-center text-white border border-gray-500 rounded-md focus:outline-none hover:bg-gray-700"
|
|
||||||
>
|
|
||||||
Sign in with Github
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const email = ref('joel@gmail.com');
|
|
||||||
const password = ref('@#!@#asdf1231!_!@#');
|
|
||||||
|
|
||||||
function login() {
|
|
||||||
router.push('/dashboard');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<route lang="yaml">
|
<route lang="yaml">
|
||||||
meta:
|
meta:
|
||||||
layout: empty
|
layout: empty
|
||||||
|
|||||||
@@ -1,91 +1,43 @@
|
|||||||
// {
|
|
||||||
// "compilerOptions": {
|
|
||||||
// "target": "ESNext",
|
|
||||||
// "useDefineForClassFields": true,
|
|
||||||
// "module": "ESNext",
|
|
||||||
// "moduleResolution": "Node",
|
|
||||||
// "strict": true,
|
|
||||||
// "jsx": "preserve",
|
|
||||||
// "resolveJsonModule": true,
|
|
||||||
// "isolatedModules": true,
|
|
||||||
// "esModuleInterop": true,
|
|
||||||
// "lib": ["ESNext", "DOM"],
|
|
||||||
// "skipLibCheck": true,
|
|
||||||
// "noEmit": true
|
|
||||||
// },
|
|
||||||
// "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
|
||||||
// "references": [{ "path": "./tsconfig.node.json" }]
|
|
||||||
// }
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
/* Basic Options */
|
"module": "ESNext",
|
||||||
// "incremental": true, /* Enable incremental compilation */
|
"moduleResolution": "node",
|
||||||
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
|
"strict": true,
|
||||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
"jsx": "preserve",
|
||||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
"sourceMap": true,
|
||||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
"resolveJsonModule": true,
|
||||||
// "checkJs": true, /* Report errors in .js files. */
|
"isolatedModules": true,
|
||||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
|
"esModuleInterop": true,
|
||||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
"lib": ["ESNext", "DOM"],
|
||||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
"skipLibCheck": true,
|
||||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
"noUnusedLocals": true,
|
||||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
"paths": {
|
||||||
// "outDir": "./", /* Redirect output structure to the directory. */
|
"~/*": ["./src/*"],
|
||||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
"@composables/*": ["./src/composables/*"],
|
||||||
// "composite": true, /* Enable project compilation */
|
"@components/*": ["./src/components/*"],
|
||||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
"@helpers/*": ["./src/helpers/*"],
|
||||||
// "removeComments": true, /* Do not emit comments to output. */
|
"@modules/*": ["./src/modules/*"],
|
||||||
// "noEmit": true, /* Do not emit outputs. */
|
"@workers/*": ["./src/workers/*"],
|
||||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
"@functional/*": ["./src/helpers/functional/*"]
|
||||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
},
|
||||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
"types": [
|
||||||
|
"vite/client",
|
||||||
/* Strict Type-Checking Options */
|
"unplugin-icons/types/vue",
|
||||||
"strict": true, /* Enable all strict type-checking options. */
|
"vite-plugin-pages/client",
|
||||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
"vite-plugin-vue-layouts/client"
|
||||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
]
|
||||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
},
|
||||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
"include": [
|
||||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
"meta.ts",
|
||||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
"src/**/*.ts",
|
||||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
"src/**/*.d.ts",
|
||||||
|
"src/**/*.tsx",
|
||||||
/* Additional Checks */
|
"src/**/*.vue"
|
||||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
],
|
||||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
"vueCompilerOptions": {
|
||||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
"jsxTemplates": true,
|
||||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
"experimentalRfc436": true
|
||||||
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
|
|
||||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
|
|
||||||
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
|
|
||||||
|
|
||||||
/* Module Resolution Options */
|
|
||||||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
|
||||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
|
||||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
|
||||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
|
||||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
|
||||||
// "types": [], /* Type declaration files to be included in compilation. */
|
|
||||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
|
||||||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
|
||||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
|
||||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
|
||||||
|
|
||||||
/* Source Map Options */
|
|
||||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
|
||||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
|
||||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
|
||||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
|
||||||
|
|
||||||
/* Experimental Options */
|
|
||||||
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
|
||||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
|
||||||
|
|
||||||
/* Advanced Options */
|
|
||||||
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
|
||||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,15 @@ import path from 'path';
|
|||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
envDir: path.resolve(__dirname, "../../"),
|
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
},
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'~': path.resolve(__dirname, '../hoppscotch-sh-admin/src'),
|
||||||
|
'@modules': path.resolve(__dirname, '../hoppscotch-sh-admin/src/modules'),
|
||||||
|
},
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
Pages({
|
Pages({
|
||||||
@@ -30,22 +35,25 @@ export default defineConfig({
|
|||||||
}),
|
}),
|
||||||
Components({
|
Components({
|
||||||
dts: './src/components.d.ts',
|
dts: './src/components.d.ts',
|
||||||
dirs: ['./src/components', '../hoppscotch-ui/src/components'],
|
dirs: ['./src/components'],
|
||||||
directoryAsNamespace: true,
|
directoryAsNamespace: true,
|
||||||
resolvers: [
|
resolvers: [
|
||||||
IconResolver({
|
IconResolver({
|
||||||
prefix: 'icon',
|
prefix: 'icon',
|
||||||
customCollections: ['hopp', 'auth', 'brands'],
|
customCollections: ['auth'],
|
||||||
}),
|
}),
|
||||||
(compName: string) => {
|
(compName: string) => {
|
||||||
if (compName.startsWith("Hopp"))
|
if (compName.startsWith('Hopp'))
|
||||||
return { name: compName, from: "@hoppscotch/ui" }
|
return { name: compName, from: '@hoppscotch/ui' };
|
||||||
else return undefined
|
else return undefined;
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
Icons({
|
Icons({
|
||||||
compiler: 'vue3',
|
compiler: 'vue3',
|
||||||
|
customCollections: {
|
||||||
|
auth: FileSystemIconLoader('../hoppscotch-sh-admin/assets/icons/auth'),
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
440
pnpm-lock.yaml
generated
440
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user