standalone: about section, design changes (2.0.8)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cap-standalone",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.8",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "bun run --watch src/index.js",
|
||||
|
||||
+124
-53
@@ -3,7 +3,7 @@
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #181818;
|
||||
background-color: #111113;
|
||||
color: white;
|
||||
display: flex;
|
||||
max-height: 100vh;
|
||||
@@ -31,11 +31,10 @@ nav .logo {
|
||||
border: none;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
padding: 0px;
|
||||
margin-right: auto;
|
||||
transition: opacity 0.2s;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
nav .logo img {
|
||||
@@ -49,7 +48,7 @@ nav .logo p {
|
||||
color: rgb(248, 248, 248);
|
||||
opacity: 0;
|
||||
margin-left: -5px;
|
||||
transition: margin-left 0.2s, opacity 0.2s;
|
||||
transition: margin-left 0.15s, opacity 0.15s;
|
||||
pointer-events: none;
|
||||
filter: blur(1px);
|
||||
}
|
||||
@@ -76,7 +75,7 @@ nav .settings {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: opacity 0.2s;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
nav .settings:hover {
|
||||
@@ -93,7 +92,7 @@ nav .logout {
|
||||
border: none;
|
||||
color: rgb(248, 248, 248);
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.2s;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
nav .logout:hover {
|
||||
@@ -161,13 +160,13 @@ nav .logout:hover {
|
||||
margin: 4px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
background-color: #0076d8;
|
||||
background-color: #007aff;
|
||||
color: white;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
padding: 0px 6px;
|
||||
transition: transform 0.2s, opacity 0.2s;
|
||||
transition: transform 0.15s, opacity 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.state-homepage .keys-top .create-key svg {
|
||||
@@ -184,6 +183,10 @@ nav .logout:hover {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.state-homepage .keys-top .create-key:focus {
|
||||
box-shadow: 0px 0px 0px 2px rgba(0, 122, 255, 0.5);
|
||||
}
|
||||
|
||||
.keys-list.no-keys p {
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
@@ -212,13 +215,13 @@ nav .logout:hover {
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #282828;
|
||||
background-color: #212121;
|
||||
border-radius: 10px;
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
color: white;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
|
||||
animation: scaleIn 0.2s ease-in-out;
|
||||
animation: scaleIn 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
@@ -256,6 +259,7 @@ nav .logout:hover {
|
||||
.modal .content {
|
||||
padding: 12px 16px;
|
||||
padding-top: 0px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.modal-content input[type="text"] {
|
||||
@@ -264,21 +268,19 @@ nav .logout:hover {
|
||||
margin-top: 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
color: white;
|
||||
border: 1px solid #e3e4e810;
|
||||
background-color: #e3e4e807;
|
||||
box-shadow: inset 0 0 0 1px color(display-p3 0.9608 0.9608 1 / 0.242);
|
||||
color: #eee;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
font-family: inherit;
|
||||
margin-bottom: 8px;
|
||||
text-overflow: ellipsis;
|
||||
transition: box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.modal-content input[type="text"]::focus {
|
||||
text-overflow: unset;
|
||||
}
|
||||
|
||||
.modal-content input[type="text"]:focus {
|
||||
box-shadow: 0px 0px 0px 3px rgba(255, 255, 255, 0.37);
|
||||
text-overflow: unset;
|
||||
box-shadow: 0px 0px 0px 2px rgba(255, 255, 255);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -290,11 +292,10 @@ nav .logout:hover {
|
||||
.modal-content button.primary {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background-color: white;
|
||||
color: black;
|
||||
background-color: #007aff;
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
font-family: inherit;
|
||||
height: 36px;
|
||||
font-weight: 500;
|
||||
@@ -304,7 +305,7 @@ nav .logout:hover {
|
||||
margin: 16px;
|
||||
width: calc(100% - 32px);
|
||||
margin-top: 0px;
|
||||
transition: opacity 0.2s;
|
||||
transition: opacity 0.15s, background-color 0.15s;
|
||||
}
|
||||
|
||||
.modal-content button.primary.secondary {
|
||||
@@ -420,7 +421,7 @@ nav .logout:hover {
|
||||
.key:focus,
|
||||
.key:active,
|
||||
.key:has(button:focus) {
|
||||
box-shadow: 0px 0px 0px 3px rgba(255, 255, 255, 0.37);
|
||||
box-shadow: 0px 0px 0px 3px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.state-key-page {
|
||||
@@ -462,7 +463,6 @@ nav .logout:hover {
|
||||
}
|
||||
|
||||
.state-key-page .topbar .left button {
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
@@ -480,8 +480,8 @@ nav .logout:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.state-key-page .topbar .left button:active {
|
||||
opacity: 0.6;
|
||||
.state-key-page .topbar .left button:focus {
|
||||
box-shadow: 0px 0px 0px 2px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.state-key-page .topbar .right {
|
||||
@@ -492,7 +492,7 @@ nav .logout:hover {
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
background-color: #181818;
|
||||
background-color: #111113;
|
||||
padding: 0px;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
@@ -540,9 +540,10 @@ nav .logout:hover {
|
||||
margin-top: 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
color: white;
|
||||
border: 1px solid #e3e4e810;
|
||||
background-color: #e3e4e807;
|
||||
border: none;
|
||||
box-shadow: inset 0 0 0 1px color(display-p3 0.9608 0.9608 1 / 0.242);
|
||||
color: #eee;
|
||||
background-color: transparent;
|
||||
font-family: inherit;
|
||||
margin-bottom: 8px;
|
||||
text-overflow: ellipsis;
|
||||
@@ -550,12 +551,9 @@ nav .logout:hover {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.config input::focus {
|
||||
text-overflow: unset;
|
||||
}
|
||||
|
||||
.config input:focus {
|
||||
box-shadow: 0px 0px 0px 3px rgba(255, 255, 255, 0.37);
|
||||
text-overflow: unset;
|
||||
box-shadow: 0px 0px 0px 2px white;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -575,7 +573,7 @@ nav .logout:hover {
|
||||
color: black;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
transition: background-color 0.15s;
|
||||
font-family: inherit;
|
||||
height: 36px;
|
||||
font-weight: 500;
|
||||
@@ -612,7 +610,7 @@ nav .logout:hover {
|
||||
margin-left: -6px;
|
||||
cursor: pointer;
|
||||
width: fit-content;
|
||||
transition: opacity 0.2s;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.keypage-back:hover {
|
||||
@@ -624,7 +622,7 @@ nav .logout:hover {
|
||||
}
|
||||
|
||||
.keypage-back svg {
|
||||
transition: margin-left 0.2s, margin-right 0.2s;
|
||||
transition: margin-left 0.15s, margin-right 0.15s;
|
||||
}
|
||||
|
||||
.keypage-back:hover svg {
|
||||
@@ -653,22 +651,27 @@ nav .logout:hover {
|
||||
.state-settings h1 {
|
||||
margin-top: 0px;
|
||||
font-size: 27px;
|
||||
margin-bottom: 0px;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.state-settings h2 {
|
||||
font-weight: 500;
|
||||
font-size: 22px;
|
||||
margin-top: 14px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.sessions-list, .apikeys-list {
|
||||
.sessions-list,
|
||||
.apikeys-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: 13px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.sessions-list .session, .apikeys-list .apikey {
|
||||
.sessions-list .session,
|
||||
.apikeys-list .apikey {
|
||||
border: 1px solid #1f1f1f;
|
||||
border-radius: 8px;
|
||||
background-color: #161616;
|
||||
@@ -678,24 +681,90 @@ nav .logout:hover {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.session .session-info, .apikey .apikey-info {
|
||||
.apikeys-list p {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.about-info {
|
||||
line-height: 1.5;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.about-info b {
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.about-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
width: fit-content;
|
||||
padding: 0.4em 0.6em;
|
||||
transition: opacity 0.15s;
|
||||
border-radius: 6px;
|
||||
animation: abtbtn 1s ease-in-out;
|
||||
box-shadow: rgb(0 0 0 / 9%) 0px 8px 24px;
|
||||
}
|
||||
|
||||
.about-button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.about-button:focus {
|
||||
box-shadow: 0px 0px 0px 2px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.nano-github-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
opacity: 0.2;
|
||||
text-decoration: none;
|
||||
width: fit-content;
|
||||
transition: opacity 0.15s;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
margin-top: 2em;
|
||||
border-radius: 6px;
|
||||
animation: abtbtn 1s ease-in-out;
|
||||
}
|
||||
|
||||
.nano-github-button:hover {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.nano-github-button:focus {
|
||||
box-shadow: 0px 0px 0px 2px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.session .session-info,
|
||||
.apikey .apikey-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.session-token, .apikey-token {
|
||||
.session-token,
|
||||
.apikey-token {
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.session-expires, .apikey-created {
|
||||
.session-expires,
|
||||
.apikey-created {
|
||||
opacity: 0.93;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.session .session-delete-button, .apikey .apikey-delete-button {
|
||||
.session .session-delete-button,
|
||||
.apikey .apikey-delete-button {
|
||||
border-radius: 6px;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
@@ -707,12 +776,14 @@ nav .logout:hover {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.session .session-delete-button:hover, .apikey .apikey-delete-button:hover {
|
||||
.session .session-delete-button:hover,
|
||||
.apikey .apikey-delete-button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.session .session-delete-button:active, .apikey .apikey-delete-button:active {
|
||||
opacity: 0.6;
|
||||
.session .session-delete-button:focus,
|
||||
.apikey .apikey-delete-button:focus {
|
||||
box-shadow: 0px 0px 0px 2px white;
|
||||
}
|
||||
|
||||
.apikeys-title {
|
||||
@@ -723,7 +794,7 @@ nav .logout:hover {
|
||||
.apikeys-title button {
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background-color: #0076d8;
|
||||
background-color: #007aff;
|
||||
color: white;
|
||||
padding: 0.4em 0.6em;
|
||||
font-family: inherit;
|
||||
@@ -733,13 +804,13 @@ nav .logout:hover {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: opacity 0.2s;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.apikeys-title button:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.apikeys-title button:active {
|
||||
opacity: 0.5;
|
||||
.apikeys-title button:focus {
|
||||
box-shadow: 0px 0px 0px 2px rgba(0, 122, 255, 0.5);
|
||||
}
|
||||
|
||||
@@ -92,6 +92,26 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="keys-list"></div>
|
||||
<a
|
||||
href="https://github.com/tiagozip/cap"
|
||||
target="_blank"
|
||||
class="nano-github-button"
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 17 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M8.5 2.22168C5.23312 2.22168 2.58496 4.87398 2.58496 8.14677C2.58496 10.7642 4.27962 12.9853 6.63026 13.7684C6.92601 13.8228 7.03366 13.6401 7.03366 13.4827C7.03366 13.3425 7.02893 12.9693 7.02597 12.4754C5.38041 12.8333 5.0332 11.681 5.0332 11.681C4.76465 10.996 4.37663 10.8139 4.37663 10.8139C3.83954 10.4471 4.41744 10.4542 4.41744 10.4542C5.01072 10.4956 5.32303 11.0647 5.32303 11.0647C5.85065 11.9697 6.70774 11.7082 7.04431 11.5568C7.09873 11.1741 7.25134 10.9132 7.42051 10.7654C6.10737 10.6157 4.72621 10.107 4.72621 7.83683C4.72621 7.19031 4.95689 6.66092 5.33486 6.24686C5.27394 6.09721 5.07105 5.49447 5.39283 4.67938C5.39283 4.67938 5.88969 4.51967 7.01947 5.28626C7.502 5.15466 7.99985 5.08763 8.5 5.08692C9.00278 5.08929 9.50851 5.15495 9.98113 5.28626C11.1103 4.51967 11.606 4.67879 11.606 4.67879C11.9289 5.49447 11.7255 6.09721 11.6651 6.24686C12.0437 6.66092 12.2732 7.19031 12.2732 7.83683C12.2732 10.1129 10.8897 10.6139 9.5724 10.7606C9.78475 10.9434 9.97344 11.3048 9.97344 11.8579C9.97344 12.6493 9.96634 13.2887 9.96634 13.4827C9.96634 13.6413 10.0728 13.8258 10.3733 13.7678C11.5512 13.3728 12.5751 12.6175 13.3003 11.6089C14.0256 10.6002 14.4155 9.38912 14.415 8.14677C14.415 4.87398 11.7663 2.22168 8.5 2.22168Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="state-key-page" style="display: none">
|
||||
@@ -190,6 +210,30 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="apikeys-list"></div>
|
||||
|
||||
<h2>About</h2>
|
||||
<p class="about-info"></p>
|
||||
<a
|
||||
href="https://github.com/tiagozip/cap"
|
||||
target="_blank"
|
||||
class="about-button"
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 17 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M8.5 2.22168C5.23312 2.22168 2.58496 4.87398 2.58496 8.14677C2.58496 10.7642 4.27962 12.9853 6.63026 13.7684C6.92601 13.8228 7.03366 13.6401 7.03366 13.4827C7.03366 13.3425 7.02893 12.9693 7.02597 12.4754C5.38041 12.8333 5.0332 11.681 5.0332 11.681C4.76465 10.996 4.37663 10.8139 4.37663 10.8139C3.83954 10.4471 4.41744 10.4542 4.41744 10.4542C5.01072 10.4956 5.32303 11.0647 5.32303 11.0647C5.85065 11.9697 6.70774 11.7082 7.04431 11.5568C7.09873 11.1741 7.25134 10.9132 7.42051 10.7654C6.10737 10.6157 4.72621 10.107 4.72621 7.83683C4.72621 7.19031 4.95689 6.66092 5.33486 6.24686C5.27394 6.09721 5.07105 5.49447 5.39283 4.67938C5.39283 4.67938 5.88969 4.51967 7.01947 5.28626C7.502 5.15466 7.99985 5.08763 8.5 5.08692C9.00278 5.08929 9.50851 5.15495 9.98113 5.28626C11.1103 4.51967 11.606 4.67879 11.606 4.67879C11.9289 5.49447 11.7255 6.09721 11.6651 6.24686C12.0437 6.66092 12.2732 7.19031 12.2732 7.83683C12.2732 10.1129 10.8897 10.6139 9.5724 10.7606C9.78475 10.9434 9.97344 11.3048 9.97344 11.8579C9.97344 12.6493 9.96634 13.2887 9.96634 13.4827C9.96634 13.6413 10.0728 13.8258 10.3733 13.7678C11.5512 13.3728 12.5751 12.6175 13.3003 11.6089C14.0256 10.6002 14.4155 9.38912 14.415 8.14677C14.415 4.87398 11.7663 2.22168 8.5 2.22168Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
Enjoying Cap? Star us on GitHub!
|
||||
</a>
|
||||
</div>
|
||||
</body>
|
||||
<script src="./public/js/index.js" type="module"></script>
|
||||
|
||||
+356
-362
@@ -1,439 +1,433 @@
|
||||
import { createModal } from "./utils.js";
|
||||
import makeApiRequest from "./api.js";
|
||||
import setState from "./state.js";
|
||||
import initHomepage from "./homepage.js";
|
||||
import setState from "./state.js";
|
||||
import { createModal } from "./utils.js";
|
||||
|
||||
let currentChart = null;
|
||||
let currentSiteKey = null;
|
||||
|
||||
export default async function loadKeyPage(siteKey) {
|
||||
setState("loading");
|
||||
currentSiteKey = siteKey;
|
||||
setState("loading");
|
||||
currentSiteKey = siteKey;
|
||||
|
||||
try {
|
||||
const response = await makeApiRequest("GET", `/keys/${siteKey}`);
|
||||
try {
|
||||
const response = await makeApiRequest("GET", `/keys/${siteKey}`);
|
||||
|
||||
if (!response.success && response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
if (!response.success && response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
displayKeyData(response);
|
||||
await loadChartData();
|
||||
setupEventListeners();
|
||||
setState("keyPage");
|
||||
} catch (error) {
|
||||
createModal(
|
||||
"Error",
|
||||
`<div class="content"><p>Failed to load key data: ${error.message}</p></div>`
|
||||
);
|
||||
setState("homepage");
|
||||
}
|
||||
displayKeyData(response);
|
||||
await loadChartData();
|
||||
setupEventListeners();
|
||||
setState("keyPage");
|
||||
} catch (error) {
|
||||
createModal(
|
||||
"Error",
|
||||
`<div class="content"><p>Failed to load key data: ${error.message}</p></div>`,
|
||||
);
|
||||
setState("homepage");
|
||||
}
|
||||
}
|
||||
|
||||
function displayKeyData(data) {
|
||||
const { key } = data;
|
||||
const { key } = data;
|
||||
|
||||
const titleElement = document.querySelector(".state-key-page .topbar h1");
|
||||
titleElement.innerText = key.name;
|
||||
const titleElement = document.querySelector(".state-key-page .topbar h1");
|
||||
titleElement.innerText = key.name;
|
||||
|
||||
const configElement = document.querySelector(".state-key-page .config");
|
||||
configElement.querySelector("#keyName").value = key.name;
|
||||
configElement.querySelector("#difficulty").value = key.config.difficulty;
|
||||
configElement.querySelector("#challengeCount").value =
|
||||
key.config.challengeCount;
|
||||
configElement.querySelector("#saltSize").value = key.config.saltSize;
|
||||
const configElement = document.querySelector(".state-key-page .config");
|
||||
configElement.querySelector("#keyName").value = key.name;
|
||||
configElement.querySelector("#difficulty").value = key.config.difficulty;
|
||||
configElement.querySelector("#challengeCount").value =
|
||||
key.config.challengeCount;
|
||||
configElement.querySelector("#saltSize").value = key.config.saltSize;
|
||||
|
||||
configElement.querySelector("button").onclick = async () => {
|
||||
const name = configElement.querySelector("#keyName").value.trim();
|
||||
const difficulty = parseInt(
|
||||
configElement.querySelector("#difficulty").value,
|
||||
10
|
||||
);
|
||||
const challengeCount = parseInt(
|
||||
configElement.querySelector("#challengeCount").value,
|
||||
10
|
||||
);
|
||||
const saltSize = parseInt(
|
||||
configElement.querySelector("#saltSize").value,
|
||||
10
|
||||
);
|
||||
configElement.querySelector("button").onclick = async () => {
|
||||
const name = configElement.querySelector("#keyName").value.trim();
|
||||
const difficulty = parseInt(
|
||||
configElement.querySelector("#difficulty").value,
|
||||
10,
|
||||
);
|
||||
const challengeCount = parseInt(
|
||||
configElement.querySelector("#challengeCount").value,
|
||||
10,
|
||||
);
|
||||
const saltSize = parseInt(
|
||||
configElement.querySelector("#saltSize").value,
|
||||
10,
|
||||
);
|
||||
|
||||
if (
|
||||
!name ||
|
||||
isNaN(difficulty) ||
|
||||
isNaN(challengeCount) ||
|
||||
isNaN(saltSize)
|
||||
) {
|
||||
createModal(
|
||||
"Error",
|
||||
`<div class="content"><p>Please fill in all fields correctly.</p></div>`
|
||||
);
|
||||
if (
|
||||
!name ||
|
||||
isNaN(difficulty) ||
|
||||
isNaN(challengeCount) ||
|
||||
isNaN(saltSize)
|
||||
) {
|
||||
createModal(
|
||||
"Error",
|
||||
`<div class="content"><p>Please fill in all fields correctly.</p></div>`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
document.querySelector(".state-key-page h1").textContent =
|
||||
configElement.querySelector("#keyName").value;
|
||||
configElement.querySelector("button").disabled = true;
|
||||
document.querySelector(".state-key-page h1").textContent =
|
||||
configElement.querySelector("#keyName").value;
|
||||
configElement.querySelector("button").disabled = true;
|
||||
|
||||
const { success } = await makeApiRequest(
|
||||
"PUT",
|
||||
`/keys/${key.siteKey}/config`,
|
||||
{
|
||||
name,
|
||||
difficulty,
|
||||
challengeCount,
|
||||
saltSize,
|
||||
}
|
||||
);
|
||||
const { success } = await makeApiRequest(
|
||||
"PUT",
|
||||
`/keys/${key.siteKey}/config`,
|
||||
{
|
||||
name,
|
||||
difficulty,
|
||||
challengeCount,
|
||||
saltSize,
|
||||
},
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
createModal(
|
||||
"Error",
|
||||
`<div class="content"><p>Failed to save key.</p></div>`
|
||||
);
|
||||
}
|
||||
if (!success) {
|
||||
createModal(
|
||||
"Error",
|
||||
`<div class="content"><p>Failed to save key.</p></div>`,
|
||||
);
|
||||
}
|
||||
|
||||
configElement.querySelector("button").disabled = false;
|
||||
};
|
||||
configElement.querySelector("button").disabled = false;
|
||||
};
|
||||
|
||||
document
|
||||
.querySelector(".danger-zone-buttons .delete-key")
|
||||
.addEventListener("click", async () => {
|
||||
const modal = createModal(
|
||||
"Delete key?",
|
||||
`<div class="content"><p>Are you sure you want to delete this key? This action cannot be undone.</p></div>
|
||||
document
|
||||
.querySelector(".danger-zone-buttons .delete-key")
|
||||
.addEventListener("click", async () => {
|
||||
const modal = createModal(
|
||||
"Delete key?",
|
||||
`<div class="content"><p>Are you sure you want to delete this key? This action cannot be undone.</p></div>
|
||||
<button class="delete-confirm-button primary" style="margin-bottom:8px">Yes, delete key</button>
|
||||
<button class="delete-cancel-button primary secondary">Cancel</button>`
|
||||
);
|
||||
<button class="delete-cancel-button primary secondary">Cancel</button>`,
|
||||
);
|
||||
|
||||
document.body.appendChild(modal);
|
||||
document.body.appendChild(modal);
|
||||
|
||||
modal
|
||||
.querySelector(".delete-confirm-button")
|
||||
.addEventListener("click", async () => {
|
||||
modal.querySelector(".delete-confirm-button").disabled = true;
|
||||
const { success } = await makeApiRequest(
|
||||
"DELETE",
|
||||
`/keys/${key.siteKey}`
|
||||
);
|
||||
modal
|
||||
.querySelector(".delete-confirm-button")
|
||||
.addEventListener("click", async () => {
|
||||
modal.querySelector(".delete-confirm-button").disabled = true;
|
||||
const { success } = await makeApiRequest(
|
||||
"DELETE",
|
||||
`/keys/${key.siteKey}`,
|
||||
);
|
||||
|
||||
document.body.removeChild(modal);
|
||||
document.body.removeChild(modal);
|
||||
|
||||
if (success) {
|
||||
initHomepage();
|
||||
} else {
|
||||
createModal(
|
||||
"Error",
|
||||
`<div class="content"><p>Failed to delete key</p></div>`
|
||||
);
|
||||
}
|
||||
});
|
||||
if (success) {
|
||||
initHomepage();
|
||||
} else {
|
||||
createModal(
|
||||
"Error",
|
||||
`<div class="content"><p>Failed to delete key</p></div>`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
modal
|
||||
.querySelector(".delete-cancel-button")
|
||||
.addEventListener("click", () => {
|
||||
document.body.removeChild(modal);
|
||||
});
|
||||
});
|
||||
modal
|
||||
.querySelector(".delete-cancel-button")
|
||||
.addEventListener("click", () => {
|
||||
document.body.removeChild(modal);
|
||||
});
|
||||
});
|
||||
|
||||
document
|
||||
.querySelector(".danger-zone-buttons .rotate-secret")
|
||||
.addEventListener("click", async () => {
|
||||
const modal = createModal(
|
||||
"Rotate secret key?",
|
||||
`<div class="content"><p>Are you sure you want to rotate this key's secret? This won't delete the key itself.</p></div>
|
||||
document
|
||||
.querySelector(".danger-zone-buttons .rotate-secret")
|
||||
.addEventListener("click", async () => {
|
||||
const modal = createModal(
|
||||
"Rotate secret key?",
|
||||
`<div class="content"><p>Are you sure you want to rotate this key's secret? This won't delete the key itself.</p></div>
|
||||
<button class="rotate-confirm-button primary" style="margin-bottom:8px">Yes, rotate key</button>
|
||||
<button class="rotate-cancel-button primary secondary">Cancel</button>`
|
||||
);
|
||||
<button class="rotate-cancel-button primary secondary">Cancel</button>`,
|
||||
);
|
||||
|
||||
modal
|
||||
.querySelector(".rotate-confirm-button")
|
||||
.addEventListener("click", async () => {
|
||||
modal.querySelector(".rotate-confirm-button").disabled = true;
|
||||
const { secretKey } = await makeApiRequest(
|
||||
"POST",
|
||||
`/keys/${key.siteKey}/rotate-secret`
|
||||
);
|
||||
modal
|
||||
.querySelector(".rotate-confirm-button")
|
||||
.addEventListener("click", async () => {
|
||||
modal.querySelector(".rotate-confirm-button").disabled = true;
|
||||
const { secretKey } = await makeApiRequest(
|
||||
"POST",
|
||||
`/keys/${key.siteKey}/rotate-secret`,
|
||||
);
|
||||
|
||||
document.body.removeChild(modal);
|
||||
document.body.removeChild(modal);
|
||||
|
||||
if (!secretKey) {
|
||||
createModal(
|
||||
"Error",
|
||||
`<div class="content"><p>Failed to rotate key secret.</p></div>`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const successModal = createModal(
|
||||
"Secret key rotated",
|
||||
`<div class="content">
|
||||
if (!secretKey) {
|
||||
createModal(
|
||||
"Error",
|
||||
`<div class="content"><p>Failed to rotate key secret.</p></div>`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const successModal = createModal(
|
||||
"Secret key rotated",
|
||||
`<div class="content">
|
||||
<label for="secret-key">New secret key</label>
|
||||
<input type="text" name="secret-key" value="${secretKey}" readonly>
|
||||
|
||||
<p class="small-text">Make sure to copy your secret key as it will not be shown again later.</p>
|
||||
</div>
|
||||
|
||||
<button class="close-small-button primary secondary">Close</button>`
|
||||
);
|
||||
<button class="close-small-button primary secondary">Close</button>`,
|
||||
);
|
||||
|
||||
successModal
|
||||
.querySelector(".close-small-button")
|
||||
.addEventListener("click", () => {
|
||||
document.body.removeChild(successModal);
|
||||
});
|
||||
});
|
||||
successModal
|
||||
.querySelector(".close-small-button")
|
||||
.addEventListener("click", () => {
|
||||
document.body.removeChild(successModal);
|
||||
});
|
||||
});
|
||||
|
||||
modal
|
||||
.querySelector(".rotate-cancel-button")
|
||||
.addEventListener("click", () => {
|
||||
document.body.removeChild(modal);
|
||||
});
|
||||
});
|
||||
modal
|
||||
.querySelector(".rotate-cancel-button")
|
||||
.addEventListener("click", () => {
|
||||
document.body.removeChild(modal);
|
||||
});
|
||||
});
|
||||
|
||||
const copyButton = document.querySelector(".state-key-page .topbar button");
|
||||
copyButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-copy"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 7m0 2.667a2.667 2.667 0 0 1 2.667 -2.667h8.666a2.667 2.667 0 0 1 2.667 2.667v8.666a2.667 2.667 0 0 1 -2.667 2.667h-8.666a2.667 2.667 0 0 1 -2.667 -2.667z" /><path d="M4.012 16.737a2.005 2.005 0 0 1 -1.012 -1.737v-10c0 -1.1 .9 -2 2 -2h10c.75 0 1.158 .385 1.5 1" /></svg> Copy site key`;
|
||||
const copyButton = document.querySelector(".state-key-page .topbar button");
|
||||
copyButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-copy"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 7m0 2.667a2.667 2.667 0 0 1 2.667 -2.667h8.666a2.667 2.667 0 0 1 2.667 2.667v8.666a2.667 2.667 0 0 1 -2.667 2.667h-8.666a2.667 2.667 0 0 1 -2.667 -2.667z" /><path d="M4.012 16.737a2.005 2.005 0 0 1 -1.012 -1.737v-10c0 -1.1 .9 -2 2 -2h10c.75 0 1.158 .385 1.5 1" /></svg> Copy site key`;
|
||||
|
||||
copyButton.onclick = () => {
|
||||
navigator.clipboard.writeText(key.siteKey);
|
||||
const originalText = copyButton.innerHTML;
|
||||
copyButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-check"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12l5 5l10 -10" /></svg> Copied!`;
|
||||
setTimeout(() => {
|
||||
copyButton.innerHTML = originalText;
|
||||
}, 2000);
|
||||
};
|
||||
copyButton.onclick = () => {
|
||||
navigator.clipboard.writeText(key.siteKey);
|
||||
const originalText = copyButton.innerHTML;
|
||||
copyButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-check"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12l5 5l10 -10" /></svg> Copied!`;
|
||||
setTimeout(() => {
|
||||
copyButton.innerHTML = originalText;
|
||||
}, 2000);
|
||||
};
|
||||
}
|
||||
|
||||
async function loadChartData(duration = "today") {
|
||||
document.querySelector(".chart-container").style.opacity = "0.3";
|
||||
document.querySelector(".chart-container").style.pointerEvents = "none";
|
||||
document.querySelector(".chart-container").style.opacity = "0.3";
|
||||
document.querySelector(".chart-container").style.pointerEvents = "none";
|
||||
|
||||
const response = await makeApiRequest(
|
||||
"GET",
|
||||
`/keys/${currentSiteKey}?chartDuration=${duration}`
|
||||
);
|
||||
const response = await makeApiRequest(
|
||||
"GET",
|
||||
`/keys/${currentSiteKey}?chartDuration=${duration}`,
|
||||
);
|
||||
|
||||
document.querySelector(".chart-container").style.opacity = "1";
|
||||
document.querySelector(".chart-container").style.pointerEvents = "all";
|
||||
document.querySelector(".chart-container").style.opacity = "1";
|
||||
document.querySelector(".chart-container").style.pointerEvents = "all";
|
||||
|
||||
renderChart(response.chartData, duration);
|
||||
renderChart(response.chartData, duration);
|
||||
}
|
||||
|
||||
function formatDateLabel(timestamp, duration) {
|
||||
const date = new Date(timestamp * 1000);
|
||||
const date = new Date(timestamp * 1000);
|
||||
|
||||
switch (duration) {
|
||||
case "today":
|
||||
case "yesterday":
|
||||
return date.toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
hour12: true,
|
||||
});
|
||||
case "last7days":
|
||||
case "last28days":
|
||||
case "last91days":
|
||||
case "alltime":
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
default:
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
switch (duration) {
|
||||
case "today":
|
||||
case "yesterday":
|
||||
return date.toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
hour12: true,
|
||||
});
|
||||
case "last7days":
|
||||
case "last28days":
|
||||
case "last91days":
|
||||
case "alltime":
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
default:
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function renderChart(chartData, duration) {
|
||||
const canvas = document.getElementById("chart");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const canvas = document.getElementById("chart");
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
if (currentChart) {
|
||||
currentChart.destroy();
|
||||
}
|
||||
if (currentChart) {
|
||||
currentChart.destroy();
|
||||
}
|
||||
|
||||
const labels = [];
|
||||
const dataPoints = [];
|
||||
const labels = [];
|
||||
const dataPoints = [];
|
||||
|
||||
const { bucketSize, data } = chartData;
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const { bucketSize, data } = chartData;
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
let startTime, endTime, buckets;
|
||||
let startTime, endTime, buckets;
|
||||
|
||||
switch (duration) {
|
||||
case "today":
|
||||
startTime = Math.floor(Date.now() / 1000 / 86400) * 86400;
|
||||
endTime = startTime + 86400;
|
||||
buckets = Math.floor((endTime - startTime) / bucketSize);
|
||||
break;
|
||||
case "yesterday":
|
||||
startTime = Math.floor(Date.now() / 1000 / 86400) * 86400 - 86400;
|
||||
endTime = startTime + 86400;
|
||||
buckets = Math.floor((endTime - startTime) / bucketSize);
|
||||
break;
|
||||
case "last7days":
|
||||
startTime = now - 7 * 86400;
|
||||
endTime = now;
|
||||
buckets = 7;
|
||||
break;
|
||||
case "last28days":
|
||||
startTime = now - 28 * 86400;
|
||||
endTime = now;
|
||||
buckets = 28;
|
||||
break;
|
||||
case "last91days":
|
||||
startTime = now - 91 * 86400;
|
||||
endTime = now;
|
||||
buckets = 91;
|
||||
break;
|
||||
case "alltime":
|
||||
if (data.length > 0) {
|
||||
startTime = data[0].bucket;
|
||||
endTime = now;
|
||||
buckets = Math.ceil((endTime - startTime) / bucketSize);
|
||||
} else {
|
||||
startTime = now - 86400;
|
||||
endTime = now;
|
||||
buckets = 1;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
startTime = now - 86400;
|
||||
endTime = now;
|
||||
buckets = 24;
|
||||
}
|
||||
switch (duration) {
|
||||
case "today":
|
||||
startTime = Math.floor(Date.now() / 1000 / 86400) * 86400;
|
||||
endTime = startTime + 86400;
|
||||
buckets = Math.floor((endTime - startTime) / bucketSize);
|
||||
break;
|
||||
case "yesterday":
|
||||
startTime = Math.floor(Date.now() / 1000 / 86400) * 86400 - 86400;
|
||||
endTime = startTime + 86400;
|
||||
buckets = Math.floor((endTime - startTime) / bucketSize);
|
||||
break;
|
||||
case "last7days":
|
||||
startTime = now - 7 * 86400;
|
||||
endTime = now;
|
||||
buckets = 7;
|
||||
break;
|
||||
case "last28days":
|
||||
startTime = now - 28 * 86400;
|
||||
endTime = now;
|
||||
buckets = 28;
|
||||
break;
|
||||
case "last91days":
|
||||
startTime = now - 91 * 86400;
|
||||
endTime = now;
|
||||
buckets = 91;
|
||||
break;
|
||||
case "alltime":
|
||||
if (data.length > 0) {
|
||||
startTime = data[0].bucket;
|
||||
endTime = now;
|
||||
buckets = Math.ceil((endTime - startTime) / bucketSize);
|
||||
} else {
|
||||
startTime = now - 86400;
|
||||
endTime = now;
|
||||
buckets = 1;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
startTime = now - 86400;
|
||||
endTime = now;
|
||||
buckets = 24;
|
||||
}
|
||||
|
||||
const dataMap = new Map();
|
||||
data.forEach((item) => {
|
||||
dataMap.set(item.bucket, item.count);
|
||||
});
|
||||
const dataMap = new Map();
|
||||
data.forEach((item) => {
|
||||
dataMap.set(item.bucket, item.count);
|
||||
});
|
||||
|
||||
for (let i = 0; i < buckets; i++) {
|
||||
const bucketTime = startTime + i * bucketSize;
|
||||
labels.push(formatDateLabel(bucketTime, duration));
|
||||
for (let i = 0; i < buckets; i++) {
|
||||
const bucketTime = startTime + i * bucketSize;
|
||||
labels.push(formatDateLabel(bucketTime, duration));
|
||||
|
||||
const alignedBucket = Math.floor(bucketTime / bucketSize) * bucketSize;
|
||||
dataPoints.push(dataMap.get(alignedBucket) || 0);
|
||||
}
|
||||
const alignedBucket = Math.floor(bucketTime / bucketSize) * bucketSize;
|
||||
dataPoints.push(dataMap.get(alignedBucket) || 0);
|
||||
}
|
||||
|
||||
function createGradient(chart) {
|
||||
const { ctx, chartArea } = chart;
|
||||
if (!chartArea) {
|
||||
return null;
|
||||
}
|
||||
function createGradient(chart) {
|
||||
const { ctx, chartArea } = chart;
|
||||
if (!chartArea) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const gradient = ctx.createLinearGradient(
|
||||
0,
|
||||
chartArea.bottom,
|
||||
0,
|
||||
chartArea.top
|
||||
);
|
||||
const gradient = ctx.createLinearGradient(
|
||||
0,
|
||||
chartArea.bottom,
|
||||
0,
|
||||
chartArea.top,
|
||||
);
|
||||
|
||||
gradient.addColorStop(0, "rgba(0, 118, 216, 0.05)");
|
||||
gradient.addColorStop(0.5, "rgba(0, 118, 216, 0.3)");
|
||||
gradient.addColorStop(1, "rgba(0, 118, 216, 0.7)");
|
||||
gradient.addColorStop(0, "rgba(0, 118, 216, 0.05)");
|
||||
gradient.addColorStop(0.5, "rgba(0, 118, 216, 0.3)");
|
||||
gradient.addColorStop(1, "rgba(0, 118, 216, 0.7)");
|
||||
|
||||
return gradient;
|
||||
}
|
||||
return gradient;
|
||||
}
|
||||
|
||||
currentChart = new Chart(ctx, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: "Daily Count",
|
||||
data: dataPoints,
|
||||
fill: true,
|
||||
backgroundColor: function (context) {
|
||||
return createGradient(context.chart);
|
||||
},
|
||||
borderColor: "#0076D8",
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
pointBackgroundColor: "#0076D8",
|
||||
pointBorderColor: "#fff",
|
||||
pointBorderWidth: 1.5,
|
||||
pointHoverRadius: 0,
|
||||
pointHoverBackgroundColor: "#fff",
|
||||
pointHoverBorderColor: "#0076D8",
|
||||
pointHoverBorderWidth: 2,
|
||||
tension: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
mode: "index",
|
||||
intersect: false,
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: "rgba(255, 255, 255, 0.08)",
|
||||
borderColor: "rgba(255, 255, 255, 0.15)",
|
||||
drawTicks: false,
|
||||
drawOnChartArea: false,
|
||||
},
|
||||
ticks: {
|
||||
color: "#b0b0b0",
|
||||
maxRotation: 45,
|
||||
minRotation: 0,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
color: "rgba(255, 255, 255, 0.08)",
|
||||
borderColor: "rgba(255, 255, 255, 0.15)",
|
||||
},
|
||||
ticks: {
|
||||
color: "#b0b0b0",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
displayColors: false,
|
||||
callbacks: {
|
||||
title: function (tooltipItems) {
|
||||
return tooltipItems[0].label;
|
||||
},
|
||||
label: function (tooltipItem) {
|
||||
return `${tooltipItem.raw} solves`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
duration: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
currentChart = new Chart(ctx, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: "Daily Count",
|
||||
data: dataPoints,
|
||||
fill: true,
|
||||
backgroundColor: (context) => createGradient(context.chart),
|
||||
borderColor: "#007AFF",
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
pointBackgroundColor: "#007AFF",
|
||||
pointBorderColor: "#fff",
|
||||
pointBorderWidth: 1.5,
|
||||
pointHoverRadius: 0,
|
||||
pointHoverBackgroundColor: "#fff",
|
||||
pointHoverBorderColor: "#007AFF",
|
||||
pointHoverBorderWidth: 2,
|
||||
tension: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
mode: "index",
|
||||
intersect: false,
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: "rgba(255, 255, 255, 0.08)",
|
||||
borderColor: "rgba(255, 255, 255, 0.15)",
|
||||
drawTicks: false,
|
||||
drawOnChartArea: false,
|
||||
},
|
||||
ticks: {
|
||||
color: "#b0b0b0",
|
||||
maxRotation: 45,
|
||||
minRotation: 0,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
color: "rgba(255, 255, 255, 0.08)",
|
||||
borderColor: "rgba(255, 255, 255, 0.15)",
|
||||
},
|
||||
ticks: {
|
||||
color: "#b0b0b0",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
displayColors: false,
|
||||
callbacks: {
|
||||
title: (tooltipItems) => tooltipItems[0].label,
|
||||
label: (tooltipItem) => `${tooltipItem.raw} solves`,
|
||||
},
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
duration: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
if (currentChart) {
|
||||
currentChart.update("resize");
|
||||
}
|
||||
});
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
if (currentChart) {
|
||||
currentChart.update("resize");
|
||||
}
|
||||
});
|
||||
|
||||
resizeObserver.observe(canvas);
|
||||
resizeObserver.observe(canvas);
|
||||
}
|
||||
|
||||
function setupEventListeners() {
|
||||
const timeDurationSelect = document.querySelector(
|
||||
".state-key-page .time-duration"
|
||||
);
|
||||
const timeDurationSelect = document.querySelector(
|
||||
".state-key-page .time-duration",
|
||||
);
|
||||
|
||||
timeDurationSelect.onchange = (e) => {
|
||||
const duration = e.target.value;
|
||||
loadChartData(duration);
|
||||
};
|
||||
timeDurationSelect.onchange = (e) => {
|
||||
const duration = e.target.value;
|
||||
loadChartData(duration);
|
||||
};
|
||||
}
|
||||
|
||||
+141
-107
@@ -1,142 +1,176 @@
|
||||
import makeApiRequest from "./api.js";
|
||||
import setState from "./state.js";
|
||||
import { createModal } from "./utils.js";
|
||||
import makeApiRequest from "./api.js";
|
||||
|
||||
export const openSettings = () => {
|
||||
setState("settings");
|
||||
setState("settings");
|
||||
|
||||
const formatDate = (dateStr) => {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString(undefined, {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
};
|
||||
const formatDate = (dateStr) => {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString(undefined, {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
(async () => {
|
||||
const sessions = await makeApiRequest("GET", "/settings/sessions");
|
||||
(async () => {
|
||||
const sessions = await makeApiRequest("GET", "/settings/sessions");
|
||||
|
||||
const sessionsList = document.querySelector(
|
||||
".state-settings .sessions-list"
|
||||
);
|
||||
sessionsList.innerHTML = "";
|
||||
const sessionsList = document.querySelector(
|
||||
".state-settings .sessions-list",
|
||||
);
|
||||
sessionsList.innerHTML = "";
|
||||
function formatRelative(date) {
|
||||
const diff = new Date(date) - new Date();
|
||||
const past = diff < 0;
|
||||
const d = Math.abs(diff);
|
||||
|
||||
sessions.forEach((session) => {
|
||||
const sessionEl = document.createElement("div");
|
||||
sessionEl.classList.add("session");
|
||||
const ms = {
|
||||
year: 365 * 24 * 60 * 60 * 1000,
|
||||
month: 30 * 24 * 60 * 60 * 1000,
|
||||
week: 7 * 24 * 60 * 60 * 1000,
|
||||
day: 24 * 60 * 60 * 1000,
|
||||
hour: 60 * 60 * 1000,
|
||||
minute: 60 * 1000,
|
||||
};
|
||||
|
||||
sessionEl.innerHTML = `
|
||||
if (d >= ms.year) {
|
||||
const y = Math.floor(d / ms.year);
|
||||
const m = Math.floor((d - y * ms.year) / ms.month);
|
||||
return past
|
||||
? `${y} year${y > 1 ? "s" : ""}${m ? " " + m + " month" + (m > 1 ? "s" : "") : ""} ago`
|
||||
: `in ${y} year${y > 1 ? "s" : ""}${m ? " " + m + " month" + (m > 1 ? "s" : "") : ""}`;
|
||||
}
|
||||
|
||||
for (const [u, v] of Object.entries(ms)) {
|
||||
if (d >= v) {
|
||||
const val = Math.floor(d / v);
|
||||
return past
|
||||
? `${val} ${u}${val > 1 ? "s" : ""} ago`
|
||||
: `in ${val} ${u}${val > 1 ? "s" : ""}`;
|
||||
}
|
||||
}
|
||||
|
||||
return past ? "just now" : "in a few seconds";
|
||||
}
|
||||
|
||||
sessions.forEach((session) => {
|
||||
const sessionEl = document.createElement("div");
|
||||
sessionEl.classList.add("session");
|
||||
|
||||
sessionEl.innerHTML = `
|
||||
<div class="session-info">
|
||||
<span class="session-token">${session.token}...</span>
|
||||
<span class="session-expires">${
|
||||
JSON.parse(localStorage.getItem("cap_auth")).hash.endsWith(
|
||||
session.token
|
||||
)
|
||||
? "<b>current</b> • "
|
||||
: ""
|
||||
}expires ${formatDate(session.expires)}${
|
||||
JSON.parse(localStorage.getItem("cap_auth")).hash.endsWith(
|
||||
session.token
|
||||
)
|
||||
? ""
|
||||
: " • created " + formatDate(session.created)
|
||||
}</span>
|
||||
JSON.parse(localStorage.getItem("cap_auth")).hash.endsWith(
|
||||
session.token,
|
||||
)
|
||||
? "<b>current</b> • "
|
||||
: ""
|
||||
}expires ${formatRelative(session.expires)} • created ${formatRelative(session.created)}</span>
|
||||
</div>
|
||||
<button class="session-delete-button">Logout</button>
|
||||
`;
|
||||
|
||||
sessionsList.appendChild(sessionEl);
|
||||
sessionsList.appendChild(sessionEl);
|
||||
|
||||
sessionEl
|
||||
.querySelector(".session-delete-button")
|
||||
.addEventListener("click", async () => {
|
||||
const modal = createModal(
|
||||
"Logout session?",
|
||||
`<button class="logout-confirm-button primary" style="margin-bottom:8px;">Yes, log out</button>
|
||||
<button class="logout-cancel-button primary secondary">Cancel</button>`
|
||||
);
|
||||
sessionEl
|
||||
.querySelector(".session-delete-button")
|
||||
.addEventListener("click", async () => {
|
||||
const modal = createModal(
|
||||
"Logout session?",
|
||||
`<button class="logout-confirm-button primary" style="margin-bottom:8px;">Yes, log out</button>
|
||||
<button class="logout-cancel-button primary secondary">Cancel</button>`,
|
||||
);
|
||||
|
||||
modal
|
||||
.querySelector(".logout-confirm-button")
|
||||
.addEventListener("click", async () => {
|
||||
modal.querySelector(".logout-confirm-button").disabled = true;
|
||||
await makeApiRequest("POST", `/logout`, {
|
||||
session: session.token,
|
||||
});
|
||||
document.body.removeChild(modal);
|
||||
if (sessions.length === 1) {
|
||||
localStorage.removeItem("cap_auth");
|
||||
location.reload();
|
||||
}
|
||||
openSettings();
|
||||
});
|
||||
modal
|
||||
.querySelector(".logout-confirm-button")
|
||||
.addEventListener("click", async () => {
|
||||
modal.querySelector(".logout-confirm-button").disabled = true;
|
||||
await makeApiRequest("POST", `/logout`, {
|
||||
session: session.token,
|
||||
});
|
||||
document.body.removeChild(modal);
|
||||
if (sessions.length === 1) {
|
||||
localStorage.removeItem("cap_auth");
|
||||
location.reload();
|
||||
}
|
||||
openSettings();
|
||||
});
|
||||
|
||||
modal
|
||||
.querySelector(".logout-cancel-button")
|
||||
.addEventListener("click", () => {
|
||||
document.body.removeChild(modal);
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
modal
|
||||
.querySelector(".logout-cancel-button")
|
||||
.addEventListener("click", () => {
|
||||
document.body.removeChild(modal);
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
(async () => {
|
||||
const apikeys = await makeApiRequest("GET", "/settings/apikeys");
|
||||
(async () => {
|
||||
const apikeys = await makeApiRequest("GET", "/settings/apikeys");
|
||||
|
||||
const apikeysList = document.querySelector(".state-settings .apikeys-list");
|
||||
apikeysList.innerHTML = "";
|
||||
const apikeysList = document.querySelector(".state-settings .apikeys-list");
|
||||
apikeysList.innerHTML = "";
|
||||
|
||||
if (apikeys.length === 0) {
|
||||
apikeysList.innerHTML = `<p>You don't have any API keys</p>`;
|
||||
return;
|
||||
}
|
||||
if (apikeys.length === 0) {
|
||||
apikeysList.innerHTML = `<p>You don't have any API keys</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
apikeys.forEach((key) => {
|
||||
const keyEl = document.createElement("div");
|
||||
keyEl.classList.add("apikey");
|
||||
apikeys.forEach((key) => {
|
||||
const keyEl = document.createElement("div");
|
||||
keyEl.classList.add("apikey");
|
||||
|
||||
keyEl.innerHTML = `
|
||||
keyEl.innerHTML = `
|
||||
<div class="apikey-info">
|
||||
<span class="apikey-token">${key.name
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")}</span>
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")}</span>
|
||||
<span class="apikey-created" title="${key.id}...">${key.id.slice(
|
||||
0,
|
||||
12
|
||||
)}... • created ${formatDate(key.created)}</span>
|
||||
0,
|
||||
12,
|
||||
)}... • created ${formatDate(key.created)}</span>
|
||||
</div>
|
||||
<button class="apikey-delete-button">Delete</button>
|
||||
`;
|
||||
|
||||
apikeysList.appendChild(keyEl);
|
||||
apikeysList.appendChild(keyEl);
|
||||
|
||||
keyEl
|
||||
.querySelector(".apikey-delete-button")
|
||||
.addEventListener("click", async () => {
|
||||
const modal = createModal(
|
||||
"Delete API key?",
|
||||
`<button class="delete-confirm-button primary" style="margin-bottom:8px;">Yes, delete</button>
|
||||
<button class="delete-cancel-button primary secondary">Cancel</button>`
|
||||
);
|
||||
keyEl
|
||||
.querySelector(".apikey-delete-button")
|
||||
.addEventListener("click", async () => {
|
||||
const modal = createModal(
|
||||
"Delete API key?",
|
||||
`<button class="delete-confirm-button primary" style="margin-bottom:8px;">Yes, delete</button>
|
||||
<button class="delete-cancel-button primary secondary">Cancel</button>`,
|
||||
);
|
||||
|
||||
modal
|
||||
.querySelector(".delete-confirm-button")
|
||||
.addEventListener("click", async () => {
|
||||
modal.querySelector(".delete-confirm-button").disabled = true;
|
||||
await makeApiRequest("DELETE", `/settings/apikeys/${key.id}`);
|
||||
document.body.removeChild(modal);
|
||||
openSettings();
|
||||
});
|
||||
modal
|
||||
.querySelector(".delete-confirm-button")
|
||||
.addEventListener("click", async () => {
|
||||
modal.querySelector(".delete-confirm-button").disabled = true;
|
||||
await makeApiRequest("DELETE", `/settings/apikeys/${key.id}`);
|
||||
document.body.removeChild(modal);
|
||||
openSettings();
|
||||
});
|
||||
|
||||
modal
|
||||
.querySelector(".delete-cancel-button")
|
||||
.addEventListener("click", () => {
|
||||
document.body.removeChild(modal);
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
modal
|
||||
.querySelector(".delete-cancel-button")
|
||||
.addEventListener("click", () => {
|
||||
document.body.removeChild(modal);
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
(async () => {
|
||||
const info = await makeApiRequest("GET", "/about");
|
||||
|
||||
document.querySelector(".about-info").innerHTML =
|
||||
`You're using Cap Standalone <b>${info.ver}</b> on Bun <b>v${info.bun}</b>`;
|
||||
})();
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
margin-top: 14vh;
|
||||
background-color: #181818;
|
||||
background-color: #111113;
|
||||
color: #e8e8ea;
|
||||
}
|
||||
form {
|
||||
@@ -27,20 +27,29 @@
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
form h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
margin-bottom: 28px;
|
||||
margin-top: 0;
|
||||
}
|
||||
img {
|
||||
width: 60px;
|
||||
}
|
||||
label {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
margin-bottom: 3px;
|
||||
color: #d9dfe5eb;
|
||||
margin-bottom: 8px;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.password-wrapper {
|
||||
border: 1px solid #e3e4e810;
|
||||
background-color: #e3e4e807;
|
||||
box-shadow: inset 0 0 0 1px color(display-p3 0.9608 0.9608 1 / 0.242);
|
||||
color: #eee;
|
||||
border: none;
|
||||
display: flex;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
@@ -53,7 +62,7 @@
|
||||
color: white;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
margin-right: 8px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.password {
|
||||
@@ -80,7 +89,7 @@
|
||||
}
|
||||
|
||||
.password-wrapper:has(.password:focus) {
|
||||
box-shadow: 0px 0px 0px 3px rgba(255, 255, 255, 0.5);
|
||||
box-shadow: 0px 0px 0px 2px white;
|
||||
}
|
||||
|
||||
.submit {
|
||||
@@ -90,8 +99,8 @@
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
color: black;
|
||||
background: white;
|
||||
color: white;
|
||||
background: #007aff;
|
||||
background-origin: border-box;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
@@ -104,7 +113,7 @@
|
||||
.submit:focus {
|
||||
box-shadow: inset 0px 0.8px 0px -0.25px rgba(255, 255, 255, 0.2),
|
||||
0px 0.5px 1.5px rgba(54, 122, 246, 0.25),
|
||||
0px 0px 0px 3.5px rgba(255, 255, 255, 0.5);
|
||||
0px 0px 0px 3.5px rgba(0, 122, 255, 0.5);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
@@ -134,11 +143,13 @@
|
||||
color: rgb(255, 255, 255);
|
||||
opacity: 0.4;
|
||||
font-size: 14px;
|
||||
transition: opacity 0.2s;
|
||||
text-underline-offset: 2px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
opacity: 1;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.logo {
|
||||
@@ -150,7 +161,7 @@
|
||||
.logo img {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
margin-bottom: 1em;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -159,6 +170,7 @@
|
||||
<div class="logo">
|
||||
<img src="./public/logo-small.webp" alt="Cap logo" />
|
||||
</div>
|
||||
<h1>Welcome to Cap!</h1>
|
||||
<label for="password">Admin key</label>
|
||||
<div class="password-wrapper">
|
||||
<input
|
||||
@@ -192,15 +204,32 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="submit" disabled>Continue</button>
|
||||
<button type="submit" class="submit">Continue</button>
|
||||
<p class="err"></p>
|
||||
<p class="bottom">
|
||||
<!-- prettier-ignore -->
|
||||
<a href="https://github.com/tiagozip/cap" target="_blank" rel="follow noopener" style="height: 24px;width: 24px;">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 17 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M8.5 2.22168C5.23312 2.22168 2.58496 4.87398 2.58496 8.14677C2.58496 10.7642 4.27962 12.9853 6.63026 13.7684C6.92601 13.8228 7.03366 13.6401 7.03366 13.4827C7.03366 13.3425 7.02893 12.9693 7.02597 12.4754C5.38041 12.8333 5.0332 11.681 5.0332 11.681C4.76465 10.996 4.37663 10.8139 4.37663 10.8139C3.83954 10.4471 4.41744 10.4542 4.41744 10.4542C5.01072 10.4956 5.32303 11.0647 5.32303 11.0647C5.85065 11.9697 6.70774 11.7082 7.04431 11.5568C7.09873 11.1741 7.25134 10.9132 7.42051 10.7654C6.10737 10.6157 4.72621 10.107 4.72621 7.83683C4.72621 7.19031 4.95689 6.66092 5.33486 6.24686C5.27394 6.09721 5.07105 5.49447 5.39283 4.67938C5.39283 4.67938 5.88969 4.51967 7.01947 5.28626C7.502 5.15466 7.99985 5.08763 8.5 5.08692C9.00278 5.08929 9.50851 5.15495 9.98113 5.28626C11.1103 4.51967 11.606 4.67879 11.606 4.67879C11.9289 5.49447 11.7255 6.09721 11.6651 6.24686C12.0437 6.66092 12.2732 7.19031 12.2732 7.83683C12.2732 10.1129 10.8897 10.6139 9.5724 10.7606C9.78475 10.9434 9.97344 11.3048 9.97344 11.8579C9.97344 12.6493 9.96634 13.2887 9.96634 13.4827C9.96634 13.6413 10.0728 13.8258 10.3733 13.7678C11.5512 13.3728 12.5751 12.6175 13.3003 11.6089C14.0256 10.6002 14.4155 9.38912 14.415 8.14677C14.415 4.87398 11.7663 2.22168 8.5 2.22168Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg></a>
|
||||
<a
|
||||
href="https://github.com/tiagozip/cap/issues/new/choose"
|
||||
target="_blank"
|
||||
rel="nofollow noopener"
|
||||
>Need help?</a>
|
||||
rel="follow noopener"
|
||||
>
|
||||
Report issue
|
||||
</a>
|
||||
<a href="https://capjs.js.org/" target="_blank" class="poweredby">
|
||||
Powered by Cap
|
||||
</a>
|
||||
@@ -211,16 +240,6 @@
|
||||
let loading = false;
|
||||
document.querySelector("input").focus();
|
||||
|
||||
document.querySelector(".password").addEventListener("input", function (e) {
|
||||
if (document.querySelector(".password").value.trim().length < 30) {
|
||||
document.querySelector("button[type=submit]").disabled = true;
|
||||
return;
|
||||
}
|
||||
if (!loading) {
|
||||
document.querySelector("button[type=submit]").disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
document
|
||||
.querySelector("form")
|
||||
.addEventListener("submit", async function (e) {
|
||||
@@ -275,11 +294,11 @@
|
||||
|
||||
if (passwordInput.type === "password") {
|
||||
passwordInput.type = "text";
|
||||
passwordInput.placeholder = "";
|
||||
passwordInput.placeholder = "MyCapAdminKey1234567!";
|
||||
showhideButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye-off-icon lucide-eye-off"><path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/></svg>`;
|
||||
} else {
|
||||
passwordInput.type = "password";
|
||||
passwordInput.placeholder = "••••••••••••••";
|
||||
passwordInput.placeholder = "•••••••••••••••••••••";
|
||||
showhideButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye-icon lucide-eye"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg>`;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -505,6 +505,18 @@ export const server = new Elysia({
|
||||
},
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/about",
|
||||
async () => {
|
||||
const pkg = await import("../package.json", { assert: { type: "json" } });
|
||||
|
||||
return {
|
||||
bun: Bun.version,
|
||||
ver: pkg.default.version,
|
||||
};
|
||||
},
|
||||
{},
|
||||
)
|
||||
.post(
|
||||
"/logout",
|
||||
async ({ body, headers, set }) => {
|
||||
|
||||
Reference in New Issue
Block a user