From 5d9c2ba770fca34e7806915771a3f6a1a4d07656 Mon Sep 17 00:00:00 2001
From: Sebastian Mohr <sebastian@mohrenclan.de>
Date: Thu, 14 Sep 2023 20:00:50 +0200
Subject: [PATCH 01/15] Fixed strange scroll in edit page

---
 CHANGELOG.md                                  |  4 ++++
 app/components/books/BooksViewer.module.scss  |  6 -----
 .../books/edit/AccessEditor.module.scss       | 22 ++++++++++++++++++-
 3 files changed, 25 insertions(+), 7 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7a893842..4e53624e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+# 1.4.2
+
+-   (Fix) Strange scroll behavior in edit page
+
 # 1.4.1
 
 -   (Fix) Fixed some alignment problems with the frontpage on mobile devices
diff --git a/app/components/books/BooksViewer.module.scss b/app/components/books/BooksViewer.module.scss
index 87fce390..7366efe4 100644
--- a/app/components/books/BooksViewer.module.scss
+++ b/app/components/books/BooksViewer.module.scss
@@ -41,12 +41,6 @@ $padding: 1rem;
     }
 }
 
-.wrapper {
-    max-height: 100%;
-
-    overflow: scroll;
-}
-
 /** Sidebar tabs 
 * First we define the mixins used to style active and inactive tabs
 * also hover states
diff --git a/app/components/books/edit/AccessEditor.module.scss b/app/components/books/edit/AccessEditor.module.scss
index cebdcd24..9ccbaaeb 100644
--- a/app/components/books/edit/AccessEditor.module.scss
+++ b/app/components/books/edit/AccessEditor.module.scss
@@ -137,7 +137,7 @@
 .col_description {
     text-align: center;
     vertical-align: middle;
-    width: 50%;
+    width: 30%;
     word-break: break-all;
 }
 .col_remove {
@@ -145,3 +145,23 @@
     vertical-align: middle;
     width: 3rem;
 }
+
+.col_link {
+    word-break: break-all;
+}
+.link {
+    text-decoration: none;
+    word-break: break-all;
+    word-wrap: anywhere;
+    color: $primary;
+    background-color: red;
+
+    // Show copy mouse on hover
+    &:hover {
+        cursor: URL("/img/clipboard.svg"), auto;
+
+        &[data-copied="true"] {
+            cursor: URL("/img/clipboard-check.svg"), auto;
+        }
+    }
+}
-- 
GitLab


From 812a11876023cc189c47363748fa6894b84e57db Mon Sep 17 00:00:00 2001
From: Sebastian Mohr <sebastian@mohrenclan.de>
Date: Thu, 14 Sep 2023 20:05:32 +0200
Subject: [PATCH 02/15] Added support for read only mode via tokens

---
 CHANGELOG.md                                  |    4 +
 app/components/books/edit/AccessTokens.tsx    |  139 +-
 .../books/edit/BookInfo.module.scss           |    8 +-
 app/components/books/edit/BookInfo.tsx        |    8 +-
 app/components/books/edit/ShareLinks.tsx      |  162 ++
 app/components/books/edit/index.tsx           |    9 +-
 app/components/frontpage/features.tsx         |    2 +-
 app/components/utils/Resizer.tsx              |    1 -
 app/components/viewer/page.tsx                |    2 +-
 app/components/viewer/sidebar/index.tsx       |   44 +-
 .../viewer/sidebar/unplacedSnipPreviews.tsx   |    8 +-
 app/components/viewer/tools/doodle/drawing.ts |    6 +-
 app/components/viewer/tools/movePage.tool.tsx |   12 +-
 app/components/viewer/tools/text.tool.tsx     |   13 +-
 app/components/viewer/worker/pageWorker.tsx   |    8 +-
 .../viewer/worker/pageWorker/messaging.ts     |    1 +
 .../viewer/worker/pageWorker/worker.ts        |    2 +
 app/controller/book.ts                        |    6 +-
 app/controller/booksocket.ts                  |   30 +-
 app/controller/websocket.ts                   |    2 +-
 app/database/services/sql/config.service.ts   |    4 +
 app/database/sql.typings.ts                   |   34 +-
 app/lib/access_control/books.ts               |  121 +-
 app/lib/fetcher.ts                            |   15 +-
 app/lib/useUser.ts                            |   23 +
 app/middleware.ts                             |   12 +-
 app/pages/_app.tsx                            |    2 +-
 app/pages/api/_service.ts                     |    1 +
 app/pages/api/admin/rerenderPreviews.ts       |    2 +-
 app/pages/api/admin/set_acl.ts                |    1 +
 app/pages/api/admin/set_users_groups.ts       |    1 +
 app/pages/api/authentication/isAdmin.ts       |    5 +-
 app/pages/api/authentication/socket_token.ts  |   45 -
 app/pages/api/authentication/verify.ts        |    2 +-
 .../api/books/[id]/edit/get_acl_entity.ts     |    2 +-
 app/pages/api/books/[id]/edit/set_acl.ts      |   11 +-
 app/pages/api/books/[id]/edit/tokens.ts       |  102 +-
 app/pages/api/books/[id]/edit/update.ts       |   13 +-
 app/pages/api/books/[id]/socket_token.ts      |   81 +
 app/pages/api/books/[id]/upload.ts            |   13 +-
 app/pages/api/socket.ts                       |   45 +-
 app/pages/books/[id]/index.tsx                |   22 +-
 app/pages/books/index.tsx                     |    2 -
 app/public/img/clipboard-check.svg            |    5 +
 app/public/img/clipboard.svg                  |    4 +
 app/server.ts                                 |   14 +-
 app/snips/general/image.ts                    |    1 -
 app/socket/auth.ts                            |   60 +-
 app/socket/socket.ts                          |   39 +-
 database/migration/0016_token_types.sql       |    8 +
 package-lock.json                             | 1338 +++++++++--------
 package.json                                  |   84 +-
 52 files changed, 1628 insertions(+), 951 deletions(-)
 create mode 100644 app/components/books/edit/ShareLinks.tsx
 delete mode 100644 app/pages/api/authentication/socket_token.ts
 create mode 100644 app/pages/api/books/[id]/socket_token.ts
 create mode 100644 app/public/img/clipboard-check.svg
 create mode 100644 app/public/img/clipboard.svg
 create mode 100644 database/migration/0016_token_types.sql

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4e53624e..d85f5dc8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,10 @@
 # 1.4.2
 
 -   (Fix) Strange scroll behavior in edit page
+-   (Features) Read only mode
+    -   tools are dynamically picked by access level
+    -   see new field in /book/:id/edit/access
+-   (Fix) Fixed a bug where the snips to place where not updated when a user adds a new snip
 
 # 1.4.1
 
diff --git a/app/components/books/edit/AccessTokens.tsx b/app/components/books/edit/AccessTokens.tsx
index e20d943e..82e9535b 100644
--- a/app/components/books/edit/AccessTokens.tsx
+++ b/app/components/books/edit/AccessTokens.tsx
@@ -22,59 +22,29 @@ export default function AccessTokens() {
         fetcher
     );
 
-    const rows = data?.map((token) => (
-        <tr key={token.id}>
-            <td>{token.description}</td>
-            <td>{new Date(token.created_at).toDateString()}</td>
-            <td>{token.email}</td>
-            <td className={styles.remove}>
-                <button
-                    className="btn btn-outline-secondary"
-                    onClick={() => {
-                        fetch(`/api/books/${book.id}/edit/tokens`, {
-                            method: "DELETE",
-                            headers: {
-                                "Content-Type": "application/json",
-                            },
-                            body: JSON.stringify({
-                                token_id: token.id,
-                            }),
-                        }).then((r) => {
-                            if (r.ok) {
-                                mutate();
-                            }
-                        });
-                    }}
-                >
-                    {trash_svg}
-                </button>
-            </td>
-        </tr>
-    ));
-
-    const submitAdd = (e) => {
-        e.preventDefault();
-        fetch(`/api/books/${book.id}/edit/tokens`, {
-            method: "POST",
-            headers: {
-                "Content-Type": "application/json",
-            },
-            body: JSON.stringify({
-                description,
-            }),
-        })
-            .then((r) => {
-                if (r.ok) {
-                    return r.json();
-                } else {
-                    throw new Error("Failed to create token!");
-                }
-            })
-            .then((r) => {
-                setAddedToken(r);
-                setShowModal(true);
-            });
-    };
+    const rows = data
+        ?.filter((token) => token.type == "api")
+        .map((token) => (
+            <tr key={token.id}>
+                <td>{token.description}</td>
+                <td>{new Date(token.created_at).toDateString()}</td>
+                <td>{token.email}</td>
+                <td className={styles.remove}>
+                    <button
+                        className="btn btn-outline-secondary"
+                        onClick={() => {
+                            deleteToken(book.id, token.id).then((r) => {
+                                if (r.ok) {
+                                    mutate();
+                                }
+                            });
+                        }}
+                    >
+                        {trash_svg}
+                    </button>
+                </td>
+            </tr>
+        ));
 
     return (
         <div className={styles.container}>
@@ -85,14 +55,22 @@ export default function AccessTokens() {
                 every time. This feature is particularly useful for automated
                 insertion of snippets, as it allows you to upload code to your
                 book programmatically. Each token is associated with a specific
-                book, and can only be created with the{" "}
-                <span style={{ fontStyle: "italic" }}>pACL</span> permission tag
-                set. By using tokens, you can automate workflows and streamline
-                your development process. For more information on how to use
-                tokens effectively, see the TODO section in our documentation.
+                book, and allows for full read and write access to that book. By
+                using tokens, you can automate workflows and streamline your
+                development process. For more information on how to use tokens
+                effectively, see the TODO section in our documentation.
             </p>
 
-            <form onSubmit={submitAdd}>
+            <form
+                onSubmit={(e) => {
+                    e.preventDefault();
+                    createNewToken(book.id, description).then((r) => {
+                        setAddedToken(r);
+                        setShowModal(true);
+                        setDescription("");
+                    });
+                }}
+            >
                 <InputGroup className="p-3">
                     <Form.Control
                         type="text"
@@ -142,7 +120,7 @@ export default function AccessTokens() {
     );
 }
 
-const trash_svg = (
+export const trash_svg = (
     <svg
         xmlns="http://www.w3.org/2000/svg"
         className="icon"
@@ -207,3 +185,44 @@ function TokenModal({ show, onHide, addedToken }) {
         </Modal>
     );
 }
+
+export const createNewToken = async (
+    book_id,
+    description,
+    type = "api",
+    expireIn = undefined
+) => {
+    const payload = {
+        description,
+        type,
+    };
+    if (expireIn) {
+        payload["expireIn"] = expireIn;
+    }
+
+    return fetch(`/api/books/${book_id}/edit/tokens`, {
+        method: "POST",
+        headers: {
+            "Content-Type": "application/json",
+        },
+        body: JSON.stringify(payload),
+    }).then((r) => {
+        if (r.ok) {
+            return r.json();
+        } else {
+            throw new Error("Failed to create token!");
+        }
+    });
+};
+
+export const deleteToken = async (book_id, token_id) => {
+    return fetch(`/api/books/${book_id}/edit/tokens`, {
+        method: "DELETE",
+        headers: {
+            "Content-Type": "application/json",
+        },
+        body: JSON.stringify({
+            token_id: token_id,
+        }),
+    });
+};
diff --git a/app/components/books/edit/BookInfo.module.scss b/app/components/books/edit/BookInfo.module.scss
index 97d5a502..b851eb44 100644
--- a/app/components/books/edit/BookInfo.module.scss
+++ b/app/components/books/edit/BookInfo.module.scss
@@ -67,7 +67,7 @@ $inset_form: 1rem;
     display: grid;
     padding: 1rem;
     width: 100%;
-    align-items: baseline;
+    align-items: flex-start;
     grid-template-columns: 3fr 2fr;
     grid-template-rows: 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
     grid-gap: 1.5rem;
@@ -136,6 +136,10 @@ $inset_form: 1rem;
         align-items: center;
         height: 100%;
         width: 100%;
+
+        @media screen and (max-width: 768px) {
+            justify-content: center;
+        }
     }
 
     .last_updated {
@@ -168,7 +172,7 @@ $inset_form: 1rem;
         }
     }
 
-    @media only screen and (max-width: 667px) {
+    @media only screen and (max-width: 768px) {
         display: flex;
         flex-direction: column;
     }
diff --git a/app/components/books/edit/BookInfo.tsx b/app/components/books/edit/BookInfo.tsx
index 8d783067..f1ecd5eb 100644
--- a/app/components/books/edit/BookInfo.tsx
+++ b/app/components/books/edit/BookInfo.tsx
@@ -177,16 +177,16 @@ const Preview = () => {
     const { book } = useContext(EditBookContext);
 
     return (
-        <Link href={`/books/${book.id}`}>
-            <div className={styles.preview}>
+        <div className={styles.preview}>
+            <Link href={`/books/${book.id}`}>
                 <Image
                     src={"/api/books/" + book.id + "/cover_preview"}
                     alt="No cover page set!"
                     width={240}
                     height={350}
                 />
-            </div>
-        </Link>
+            </Link>
+        </div>
     );
 };
 
diff --git a/app/components/books/edit/ShareLinks.tsx b/app/components/books/edit/ShareLinks.tsx
new file mode 100644
index 00000000..38c88567
--- /dev/null
+++ b/app/components/books/edit/ShareLinks.tsx
@@ -0,0 +1,162 @@
+import { fetcher } from "lib/fetcher";
+import ms, { StringValue } from "ms";
+import { ApiGetToken } from "pages/api/books/[id]/edit/tokens";
+import { ChangeEvent, useContext, useMemo, useState } from "react";
+import { Button, Form, InputGroup, Table } from "react-bootstrap";
+import useSWR from "swr";
+
+import { EditBookContext } from ".";
+import { createNewToken, deleteToken, trash_svg } from "./AccessTokens";
+
+import styles from "./AccessEditor.module.scss";
+
+export default function ShareLinks() {
+    const { book } = useContext(EditBookContext);
+    const [description, setDescription] = useState("");
+    const [expiration, setExpiration] = useState("1d");
+
+    const valid_expiration = useMemo(() => isValidMs(expiration), [expiration]);
+
+    const { data, mutate } = useSWR<ApiGetToken[]>(
+        `/api/books/${book.id}/edit/tokens`,
+        fetcher
+    );
+
+    const rows = data
+        ?.filter((token) => token.type == "ui")
+        .map((token) => {
+            const url = `/books/${token.book_id}?token=${token.token}`;
+            const base_url = `${window.location.protocol}//${window.location.host}`;
+
+            return (
+                <tr key={token.id}>
+                    <td>{token.description}</td>
+                    <td
+                        className={styles.link}
+                        onClick={async (e) => {
+                            //Check if clipboard is already the link
+                            const elem = e.target as HTMLElement;
+                            copyToClipboard(e, base_url + url);
+                            elem.dataset.copied = "true";
+                            elem.style.color = "var(--bs-primary)";
+
+                            setTimeout(() => {
+                                elem.style.removeProperty("color");
+                            }, 1000);
+                        }}
+                    >
+                        {base_url + url}
+                    </td>
+                    <td>{new Date(token.created_at).toDateString()}</td>
+                    <td>{new Date(token.expires_at).toDateString()}</td>
+                    <td className={styles.remove}>
+                        <button
+                            className="btn btn-outline-secondary"
+                            onClick={() => {
+                                deleteToken(book.id, token.id).then((r) => {
+                                    if (r.ok) {
+                                        mutate();
+                                    }
+                                });
+                            }}
+                        >
+                            {trash_svg}
+                        </button>
+                    </td>
+                </tr>
+            );
+        });
+
+    return (
+        <div className={styles.container}>
+            <h2>Share Links</h2>
+            <p>
+                Share links allow you to share your book with others without the
+                need for them to create an account. They can be used to share
+                your book with anyone who has the link. You can also set an
+                expiration date for the link and revoke it at any time.
+            </p>
+            <p>This can only be used to grant read access!</p>
+            <form
+                onSubmit={(e) => {
+                    e.preventDefault();
+                    createNewToken(book.id, description, "ui", expiration).then(
+                        (r) => {
+                            setDescription("");
+                        }
+                    );
+                }}
+            >
+                <InputGroup className="p-3" hasValidation>
+                    <Form.Control
+                        type="text"
+                        placeholder="Description"
+                        style={{ width: "60%" }}
+                        onInput={(e: ChangeEvent<HTMLTextAreaElement>) => {
+                            setDescription(e.target.value);
+                        }}
+                        value={description}
+                    />
+                    <Form.Control
+                        type="text"
+                        placeholder="Expires In"
+                        style={{ width: "20%" }}
+                        onInput={(e: ChangeEvent<HTMLTextAreaElement>) => {
+                            // Check if valid using ms
+                            setExpiration(e.target.value);
+                        }}
+                        value={expiration}
+                        isValid={valid_expiration}
+                        isInvalid={!valid_expiration}
+                        required
+                    />
+                    <Button variant="outline-secondary" type="submit">
+                        Create link
+                    </Button>
+                    <Form.Control.Feedback type="invalid">
+                        Invalid expiration time use ms format i.e. 1d, 1h, 1m 1
+                        week etc.
+                    </Form.Control.Feedback>
+                </InputGroup>
+            </form>
+
+            {rows?.length > 0 && (
+                <Table striped bordered hover>
+                    <colgroup>
+                        <col className={styles.col_description} />
+                        <col className={styles.col_link} />
+                        <col />
+                        <col />
+                        <col className={styles.col_remove} />
+                    </colgroup>
+                    <thead>
+                        <tr>
+                            <th>Description</th>
+                            <th>Link</th>
+                            <th>Created at</th>
+                            <th>Expires at</th>
+                            <th></th>
+                        </tr>
+                    </thead>
+                    <tbody>{rows}</tbody>
+                </Table>
+            )}
+        </div>
+    );
+}
+
+const isValidMs = (value: string) => {
+    console.log(value);
+    try {
+        const r = ms(value as StringValue);
+        return r !== undefined;
+    } catch (e) {
+        console.log(e);
+        return false;
+    }
+};
+
+function copyToClipboard(e: React.MouseEvent, text: string) {
+    e.preventDefault();
+    navigator.clipboard.writeText(text);
+}
diff --git a/app/components/books/edit/index.tsx b/app/components/books/edit/index.tsx
index 3778eee5..7e60febf 100644
--- a/app/components/books/edit/index.tsx
+++ b/app/components/books/edit/index.tsx
@@ -24,6 +24,7 @@ import useSWR from "swr";
 
 import AccessTokens from "./AccessTokens";
 import { BackgroundTypeEditor } from "./BackgroundTypeEditor";
+import ShareLinks from "./ShareLinks";
 
 import styles from "./Edit.module.scss";
 
@@ -128,7 +129,13 @@ export default function EditBook({
                         {selectedTool === "background" && (
                             <BackgroundTypeEditor />
                         )}
-                        {selectedTool === "access" && <AccessEditor />}
+
+                        {selectedTool === "access" && (
+                            <>
+                                <AccessEditor />
+                                <ShareLinks />
+                            </>
+                        )}
                         {selectedTool === "access" && perms.pACL && (
                             <AccessTokens />
                         )}
diff --git a/app/components/frontpage/features.tsx b/app/components/frontpage/features.tsx
index 57d6952b..b3633184 100644
--- a/app/components/frontpage/features.tsx
+++ b/app/components/frontpage/features.tsx
@@ -72,7 +72,7 @@ interface CardProps {
     img?: string;
 }
 
-function Card({ children, title, img }: CardProps) {
+function Card({ children, title }: CardProps) {
     return (
         <div className={styles.card_wrapper}>
             <div className={styles.card_content}>
diff --git a/app/components/utils/Resizer.tsx b/app/components/utils/Resizer.tsx
index 4c689d8c..c08df26c 100644
--- a/app/components/utils/Resizer.tsx
+++ b/app/components/utils/Resizer.tsx
@@ -1,4 +1,3 @@
-import { hasOwnProperty } from "lib/utils";
 import { useEffect, useRef, useState } from "react";
 
 import styles from "./Resizer.module.scss";
diff --git a/app/components/viewer/page.tsx b/app/components/viewer/page.tsx
index d9dad772..737b9648 100644
--- a/app/components/viewer/page.tsx
+++ b/app/components/viewer/page.tsx
@@ -12,7 +12,7 @@ import {
 import { ToolBarContext } from "./toolbar";
 import { getContext } from "./tools";
 import { MovePageToolContext } from "./tools/movePage.tool";
-import { _centerElement, _fitElement } from "./tools/movePage/resizable";
+import { _centerElement } from "./tools/movePage/resizable";
 import { OverlayWorker, useOverlayWorker } from "./worker/overlayWorker";
 import { PageWorker, usePageWorker } from "./worker/pageWorker";
 
diff --git a/app/components/viewer/sidebar/index.tsx b/app/components/viewer/sidebar/index.tsx
index c75350a7..21feec11 100644
--- a/app/components/viewer/sidebar/index.tsx
+++ b/app/components/viewer/sidebar/index.tsx
@@ -102,7 +102,13 @@ export function SidebarContextProvider({
     );
 }
 
-export function Sidebar({ book }: { book: Book }) {
+export function Sidebar({
+    book,
+    showSnipsToPlace,
+}: {
+    book: Book;
+    showSnipsToPlace: boolean;
+}) {
     const {
         searchTermPages,
         setSearchTermPages,
@@ -184,17 +190,19 @@ export function Sidebar({ book }: { book: Book }) {
                         Pages
                     </button>
                 </li>
-                <li className="nav-item" role="presentation">
-                    <button
-                        className="nav-link"
-                        onClick={() => {
-                            setActiveTab("snips");
-                        }}
-                        data-active={activeTab == "snips"}
-                    >
-                        Snips to place
-                    </button>
-                </li>
+                {showSnipsToPlace ? (
+                    <li className="nav-item" role="presentation">
+                        <button
+                            className="nav-link"
+                            onClick={() => {
+                                setActiveTab("snips");
+                            }}
+                            data-active={activeTab == "snips"}
+                        >
+                            Snips to place
+                        </button>
+                    </li>
+                ) : null}
             </ul>
             {readOnly}
             <div className={styles.topNavigation}>
@@ -213,11 +221,13 @@ export function Sidebar({ book }: { book: Book }) {
                 book={book}
                 filter={filter}
             />
-            <TabSnips
-                active={activeTab === "snips"}
-                book={book}
-                filter={filter}
-            />
+            {showSnipsToPlace ? (
+                <TabSnips
+                    active={activeTab === "snips"}
+                    book={book}
+                    filter={filter}
+                />
+            ) : null}
             <div className={styles.extraFunctions}>
                 <DownloadBtn book={book} />
                 <FullScreenButton />
diff --git a/app/components/viewer/sidebar/unplacedSnipPreviews.tsx b/app/components/viewer/sidebar/unplacedSnipPreviews.tsx
index ead9d49d..20e39114 100644
--- a/app/components/viewer/sidebar/unplacedSnipPreviews.tsx
+++ b/app/components/viewer/sidebar/unplacedSnipPreviews.tsx
@@ -29,7 +29,7 @@ export function UnplacedSnipPreviews({
         });
 
         // new unplaced snip added (can be both from this client or another)
-        const insertUnplacedSnip = (snip) => {
+        const insertedIntoQueue = (snip) => {
             setSnips((snips) => {
                 return [...snips, get_snip_from_data(snip)];
             });
@@ -46,12 +46,12 @@ export function UnplacedSnipPreviews({
             });
         };
 
-        book.events.addListener("insertUnplacedSnip", insertUnplacedSnip);
+        book.events.addListener("book:insertedIntoQueue", insertedIntoQueue);
         book.events.addListener("snip:placed", removeUnplacedSnip);
         return () => {
             book.events.removeListener(
-                "insertUnplacedSnip",
-                insertUnplacedSnip
+                "book:insertedIntoQueue",
+                insertedIntoQueue
             );
             book.events.removeListener("snip:placed", removeUnplacedSnip);
         };
diff --git a/app/components/viewer/tools/doodle/drawing.ts b/app/components/viewer/tools/doodle/drawing.ts
index 5cbfb30d..c9ca66b1 100644
--- a/app/components/viewer/tools/doodle/drawing.ts
+++ b/app/components/viewer/tools/doodle/drawing.ts
@@ -209,7 +209,7 @@ export function useDrawing(
 
     // Prevent default touch events (zooming, panning )
     const touchStartHandler = useCallback(
-        (event: TouchEvent, pageIdx: number) => {
+        (event: TouchEvent) => {
             event.preventDefault();
         },
         []
@@ -224,8 +224,8 @@ export function useDrawing(
         const helpers_pointerdown = containers.map((container, idx) => {
             return (event: PointerEvent) => pointerDownHandler(event, idx);
         });
-        const helpers_touchstart = containers.map((container, idx) => {
-            return (event: TouchEvent) => touchStartHandler(event, idx);
+        const helpers_touchstart = containers.map(() => {
+            return (event: TouchEvent) => touchStartHandler(event);
         });
 
         containers.forEach((container, idx) => {
diff --git a/app/components/viewer/tools/movePage.tool.tsx b/app/components/viewer/tools/movePage.tool.tsx
index c811c780..886c1a83 100644
--- a/app/components/viewer/tools/movePage.tool.tsx
+++ b/app/components/viewer/tools/movePage.tool.tsx
@@ -1,18 +1,10 @@
-import useUser from "lib/useUser";
-import {
-    createContext,
-    useCallback,
-    useContext,
-    useEffect,
-    useRef,
-    useState,
-} from "react";
+import { createContext, useCallback, useContext, useEffect } from "react";
 
 import { PagesContext } from "../page";
 import { ToolBarContext } from "../toolbar";
 import { ToolContext } from ".";
 import { useDraggableHandlers } from "./movePage/draggable";
-import { resize, useResizable } from "./movePage/resizable";
+import { useResizable } from "./movePage/resizable";
 import { SubMenu } from "./movePage/submenu";
 
 import styles from "../toolbar/toolbar.module.scss";
diff --git a/app/components/viewer/tools/text.tool.tsx b/app/components/viewer/tools/text.tool.tsx
index 65d10196..02be4d4e 100644
--- a/app/components/viewer/tools/text.tool.tsx
+++ b/app/components/viewer/tools/text.tool.tsx
@@ -6,7 +6,6 @@ import {
     useCallback,
     useContext,
     useEffect,
-    useRef,
     useState,
 } from "react";
 
@@ -41,7 +40,7 @@ export function TextToolContextProvider({
 }: {
     children: React.ReactNode;
 }) {
-    const { config, isLoadingConfig, mutateConfig } = useUser();
+    const { config, isLoadingConfig, isErrorConfig, mutateConfig } = useUser();
 
     const { activeTool, setActiveTool, activeSubMenu, setActiveSubMenu } =
         useContext(ToolBarContext);
@@ -106,7 +105,13 @@ export function TextToolContextProvider({
     // Set values from config this is only revalidated on focus change
     // see useUser
     useEffect(() => {
-        if (!isLoadingConfig && config) {
+        if (
+            !isLoadingConfig &&
+            !isErrorConfig &&
+            config &&
+            config.user_id != -1
+        ) {
+            console.log("Setting text styles from config");
             _setTextStyles({
                 font: config.text_font as AllowedFonts,
                 fontSize: config.text_fontSize,
@@ -116,7 +121,7 @@ export function TextToolContextProvider({
             return;
         }
         // Update the config when the text styles change
-    }, [config, isLoadingConfig]);
+    }, [config, isErrorConfig, isLoadingConfig]);
 
     const ret: TextToolContext = {
         active,
diff --git a/app/components/viewer/worker/pageWorker.tsx b/app/components/viewer/worker/pageWorker.tsx
index 594ddc0a..99d8943d 100644
--- a/app/components/viewer/worker/pageWorker.tsx
+++ b/app/components/viewer/worker/pageWorker.tsx
@@ -109,10 +109,12 @@ export class PageWorker {
             new URL("./pageWorker/worker.ts", import.meta.url)
         );
         // Send init message
+        const url_params = new URLSearchParams(window.location.search);
         const offscreen = this.canvas.transferControlToOffscreen();
-        this.worker.postMessage({ type: "init", canvas: offscreen }, [
-            offscreen,
-        ]);
+        this.worker.postMessage(
+            { type: "init", canvas: offscreen, url_params },
+            [offscreen]
+        );
         return;
     }
 
diff --git a/app/components/viewer/worker/pageWorker/messaging.ts b/app/components/viewer/worker/pageWorker/messaging.ts
index cda66038..837234ce 100644
--- a/app/components/viewer/worker/pageWorker/messaging.ts
+++ b/app/components/viewer/worker/pageWorker/messaging.ts
@@ -7,6 +7,7 @@ export interface InitMsg {
      */
     type: "init";
     canvas: OffscreenCanvas;
+    url_params: URLSearchParams;
 }
 
 export interface ShutdownMsg {
diff --git a/app/components/viewer/worker/pageWorker/worker.ts b/app/components/viewer/worker/pageWorker/worker.ts
index 652d9a80..05d82c85 100644
--- a/app/components/viewer/worker/pageWorker/worker.ts
+++ b/app/components/viewer/worker/pageWorker/worker.ts
@@ -186,6 +186,8 @@ self.onmessage = async (event: MessageEvent<Message>) => {
     switch (event.data.type) {
         case "init":
             globalThis.worker = new PageWorker(event.data.canvas);
+
+            globalThis.url_params = event.data.url_params;
             break;
         case "shutdown":
             globalThis.worker.shutdown();
diff --git a/app/controller/book.ts b/app/controller/book.ts
index eeb2aa6b..ed4c809d 100644
--- a/app/controller/book.ts
+++ b/app/controller/book.ts
@@ -236,9 +236,9 @@ export class Book implements Required<BookData> {
         );
 
         // Update on insert of not yet placed snip
-        this.socket?.on("book:insertUnplacedSnip", (data) => {
-            console.log("book:insertUnplacedSnip", data);
-            this.events.emit("insertUnplacedSnip", data);
+        this.socket?.on("book:insertedIntoQueue", (data) => {
+            console.log("book:insertedIntoQueue", data);
+            this.events.emit("book:insertedIntoQueue", data);
         });
 
         this.socket.on("snip:placed", (page_id, snip_id) => {
diff --git a/app/controller/booksocket.ts b/app/controller/booksocket.ts
index 3c49679b..7a269fd1 100644
--- a/app/controller/booksocket.ts
+++ b/app/controller/booksocket.ts
@@ -60,8 +60,15 @@ export class BookSocket {
                 token: token,
             },
         });
-        socket.on("error", (error) => {
-            console.log("Socket error", error);
+        socket.on("error", (err) => {
+            console.log(err);
+        });
+        socket.on("connect_error", (err) => {
+            console.log(err);
+        });
+        socket.on("connect", () => {
+            console.log("Connected to book socket");
+            console.log(socket);
         });
         this.socket = socket;
     }
@@ -81,10 +88,27 @@ export async function getSocket(
 }
 
 async function fetchToken(book_id: string): Promise<string> {
-    const { token } = await fetch("/api/authentication/socket_token", {
+
+    const header_adds = {}
+    let token_url = null;
+    // In workers the url is parsed via globalThis
+    if (isWorker()) {
+        token_url = globalThis.url_params.get("token");
+    } else {
+        const this_url = new URL(window.location.href);
+        token_url = this_url.searchParams.get("token");
+    }
+
+    //Check if url has token
+    if (token_url) {
+        header_adds["Authorization"] = "Bearer " + token_url;
+    }
+
+    const { token } = await fetch(`/api/books/${book_id}/socket_token`, {
         method: "POST",
         headers: {
             "Content-Type": "application/json",
+            ...header_adds
         },
         body: JSON.stringify({ book_id }),
     }).then((res) => {
diff --git a/app/controller/websocket.ts b/app/controller/websocket.ts
index 88ae69c3..e2acf25f 100644
--- a/app/controller/websocket.ts
+++ b/app/controller/websocket.ts
@@ -11,7 +11,7 @@ if (!globalThis.ClientSockets) {
  * @param book_id
  * @returns {Socket}
  */
-export const createClientSocket = async function (
+const createClientSocket = async function (
     book_id: number | string
 ): Promise<Socket> {
     if (!isBrowser() && !isWorker()) {
diff --git a/app/database/services/sql/config.service.ts b/app/database/services/sql/config.service.ts
index 49ce9243..b152335f 100644
--- a/app/database/services/sql/config.service.ts
+++ b/app/database/services/sql/config.service.ts
@@ -23,6 +23,10 @@ const config_default: Required<UserConfigData> = {
 
 
 async function getByUserId(user_id: number): Promise<UserConfigData> {
+
+    if (!user_id || user_id === -1) {
+        return config_default;
+    }
     const sql = "SELECT * FROM user_config_resolved WHERE user_id = ?";
     const configData: UserConfigData = await pool.query<UserConfigData[]>(sql, [user_id])
         .then((res) => {
diff --git a/app/database/sql.typings.ts b/app/database/sql.typings.ts
index 1939362d..74d55067 100644
--- a/app/database/sql.typings.ts
+++ b/app/database/sql.typings.ts
@@ -42,8 +42,17 @@ export interface tokensData {
   'created_at'?: Date;
   'created_by': number;
   'description'?: string | null;
+  
+  /** The time the token expires (only for ui tokens) */
+  'expires_at'?: Date | null;
   'hashed_token': string;
   'id'?: number;
+  
+  /** The token (only for ui tokens) */
+  'token'?: string | null;
+  
+  /** The type of token (api or ui) */
+  'type'?: string;
 }
 
 /** The user configs for the app */
@@ -145,7 +154,7 @@ export interface booksData {
   'comment'?: string | null;
   'cover_page_id'?: number | null;
   'created'?: Date | null;
-  'default_background_type_id'?: number;
+  'default_background_type_id'?: number | null;
   'finished'?: Date | null;
   'id'?: number;
   'last_updated'?: Date;
@@ -161,7 +170,7 @@ export interface books_resolvedData {
   'comment'?: string | null;
   'created'?: Date | null;
   'default_background_type_description'?: string | null;
-  'default_background_type_id'?: number;
+  'default_background_type_id'?: number | null;
   'default_background_type_name': string | null;
   'finished'?: Date | null;
   'id'?: number;
@@ -190,19 +199,6 @@ export interface browserFolders_booksData {
   'browserFolder_id': number;
   'user_id': number;
 }
-export interface browserViewData {
-  'comment'?: string | null;
-  'created'?: Date | null;
-  'finished'?: Date | null;
-  'id'?: string | null;
-  'isDir'?: number;
-  'last_updated'?: Date | null;
-  'name'?: string;
-  'numPages'?: number | null;
-  'owner'?: string | null;
-  'parentId'?: string | null;
-  'user_id'?: number | null;
-}
 export interface instrumentsData {
   'created'?: Date;
   'description': string;
@@ -226,12 +222,6 @@ export interface pagesViewData {
   'last_updated'?: Date;
   'page_number'?: number | null;
 }
-export interface settingsData {
-  'book_folders'?: string | null;
-  'entity_id': number;
-  'entity_type_id'?: number;
-  'id'?: number;
-}
 export interface snipsData {
   'blob_id'?: number | null;
   'book_id': number;
@@ -247,7 +237,7 @@ export interface snipsData {
 export enum background_types {
   'blank' = 1,
   'checkered 4mm x 4mm' = 2,
-  'lined 7mm' = 3,
+  'lines 7mm' = 3,
 }
 export interface changelogData {
   'applied_at'?: Date;
diff --git a/app/lib/access_control/books.ts b/app/lib/access_control/books.ts
index 7b0586a8..092bb5cc 100644
--- a/app/lib/access_control/books.ts
+++ b/app/lib/access_control/books.ts
@@ -34,18 +34,28 @@ export function withAuthBookIdSsr<
         perms: PermissionACLData
     ) => GetServerSidePropsResult<P> | Promise<GetServerSidePropsResult<P>>
 ) {
-    return async (ctx) => {
+    return async (ctx: GetServerSidePropsContext<Q, D>) => {
         const { req, query } = ctx;
         const book_id = query.id as string;
-
-        // Get the users permission for this book
-        if (book_id) {
+        const user_id = req.session.user_id;
+        // First check for user
+        if (book_id && user_id) {
             const perms = await service.permission.getACLByUserBookReduced(
-                req.session.user_id,
+                user_id,
                 parseInt(book_id)
             );
             return await handler(ctx, perms);
         }
+
+        // Check if token is present
+        const url = new URL(req.url, "http://localhost");
+        const token = url.searchParams.get("token");
+        if (book_id && token) {
+            console.log("Ui token found", token);
+            const perms = await verify_with_token(token, book_id);
+            return await handler(ctx, perms);
+        }
+
         return {
             redirect: {
                 permanent: false,
@@ -76,16 +86,7 @@ export function withAuthBookId(handler: NextApiHandlerWithPerms) {
         // Get the users permission for this book
         if (book_id) {
             const token = req.headers.authorization?.split(" ")[1];
-
-            let perms;
-            if (token) {
-                perms = await verify_with_token(token, book_id);
-            } else {
-                perms = await service.permission.getACLByUserBookReduced(
-                    req.session.user_id,
-                    parseInt(book_id)
-                );
-            }
+            const perms = await bookAuth(req.session.user_id, book_id, token);
             return await handler(req, res, perms);
         } else {
             res.status(400).json({
@@ -97,48 +98,84 @@ export function withAuthBookId(handler: NextApiHandlerWithPerms) {
     };
 }
 
+
+/** This function can be used to secure any api route
+ * api/? route.
+ */
+export async function bookAuth(user_id: number, book_id: string | number, token?: string) {
+    let perms;
+    if (token) {
+        console.log("Api token found", token);
+        perms = await verify_with_token(token, book_id);
+    } else {
+        perms = await service.permission.getACLByUserBookReduced(
+            user_id,
+            parseInt(book_id as string)
+        );
+    }
+    return perms;
+}
+
+
+
 async function verify_with_token(
     token: string,
-    book_id: string
+    book_id: string | number,
 ): Promise<PermissionACLData> {
     const hashed_token = hashToken(token);
-    const found = await pool_permissions
+    const { found, type } = await pool_permissions
         .query("SELECT * FROM tokens WHERE book_id = ? AND hashed_token = ?", [
             book_id,
             hashed_token,
         ])
         .then((res) => {
+            console.log(res);
             if (res.length > 0) {
-                return true;
+                return {
+                    found: true,
+                    type: res[0].type,
+                }
             } else {
-                return false;
+                return {
+                    found: false,
+                    type: null,
+                }
             }
         })
         .catch((err) => {
             console.error(err);
-            return false;
+            return {
+                found: false,
+                type: null,
+            }
         });
-    if (found) {
-        return {
-            entity_id: -1,
-            entity_type: ENTITY_TOKEN,
-            pACL: false,
-            pDelete: true,
-            pRead: true,
-            pWrite: true,
-            resource_id: parseInt(book_id),
-            resource_type: RESOURCE_BOOK,
-        };
-    } else {
-        return {
-            entity_id: -1,
-            entity_type: ENTITY_TOKEN,
-            pACL: false,
-            pDelete: false,
-            pRead: false,
-            pWrite: false,
-            resource_id: parseInt(book_id),
-            resource_type: RESOURCE_BOOK,
-        };
+
+    const perms: PermissionACLData = {
+        entity_id: -1,
+        entity_type: ENTITY_TOKEN,
+        resource_id: parseInt(book_id as string),
+        resource_type: RESOURCE_BOOK,
+        pACL: false,
+        pDelete: false,
+        pRead: false,
+        pWrite: false,
+    }
+
+    if (!found) {
+        return perms;
     }
+
+
+    switch (type) {
+        case "api":
+            perms.pRead = true;
+            perms.pWrite = true;
+            perms.pDelete = true;
+            break;
+        case "ui":
+            perms.pRead = true;
+            break;
+    }
+
+    return perms;
 }
diff --git a/app/lib/fetcher.ts b/app/lib/fetcher.ts
index f236c325..e36549ed 100644
--- a/app/lib/fetcher.ts
+++ b/app/lib/fetcher.ts
@@ -8,7 +8,20 @@ interface ApiError_client extends Error {
 }
 
 export async function fetcher(url) {
-    const res = await fetch(url).then(res_parse_json);
+
+
+    //Check if token in winow
+
+    const headers = {};
+    const token = new URL(window.location.href).searchParams.get("token");
+    if (token) {
+        headers["Authorization"] = `Bearer ${token}`;
+    }
+    const res = await fetch(url,
+        {
+            headers: headers,
+        }
+    ).then(res_parse_json);
 
     return res;
 }
diff --git a/app/lib/useUser.ts b/app/lib/useUser.ts
index 3d765a6a..4ce17826 100644
--- a/app/lib/useUser.ts
+++ b/app/lib/useUser.ts
@@ -28,6 +28,13 @@ export default function useUser() {
         revalidateIfStale: false,
         revalidateOnFocus: false,
         revalidateOnReconnect: false,
+        fallbackData: {
+            created: new Date(),
+            email: "",
+            emailValidated: false,
+            id: -1,
+            last_updated: new Date(),
+        },
     });
 
     const {
@@ -37,6 +44,22 @@ export default function useUser() {
         revalidateIfStale: false,
         revalidateOnFocus: true,
         revalidateOnReconnect: true,
+        fallbackData: {
+            id: -1,
+            last_updated: new Date(),
+            pen_color: "#000000",
+            pen_size: 4,
+            pen_smoothing: 0.0,
+            text_font_id: 1,
+            text_font: "Arial",
+            text_fontColor: "#000000",
+            text_fontSize: 12,
+            text_lineHeight: 1.25,
+            text_lineWrap: 400,
+            user_id: -1,
+            zoom1: 1,
+            zoom2: 1,
+        }
     });
 
 
diff --git a/app/middleware.ts b/app/middleware.ts
index 436c8148..b070334e 100644
--- a/app/middleware.ts
+++ b/app/middleware.ts
@@ -72,9 +72,6 @@ async function middleware_ui(request: NextRequest): Promise<NextResponse> {
     const session = await getIronSession(request, response, sessionOptions);
     const { user_id, email_verified } = session;
 
-
-
-
     //Rewrite for index page
     if ((request.nextUrl.pathname === "/" && !user_id)) {
         return NextResponse.redirect(new URL(`/frontpage`, request.url));
@@ -82,6 +79,15 @@ async function middleware_ui(request: NextRequest): Promise<NextResponse> {
         return NextResponse.redirect(new URL(`/books`, request.url));
     }
 
+    // Special check for the books route if token is in header
+    if (request.nextUrl.pathname.startsWith("/books/")) {
+        const token = request.nextUrl.searchParams.get("token");
+        if (token) {
+            // Token is checked in the api route
+            return response;
+        }
+    }
+
     // No user_id means the user is not logged in
     if (!user_id) {
         const redirect = request.nextUrl.pathname;
diff --git a/app/pages/_app.tsx b/app/pages/_app.tsx
index 24053b85..3079bb4d 100644
--- a/app/pages/_app.tsx
+++ b/app/pages/_app.tsx
@@ -27,7 +27,7 @@ type AppPropsWithLayout = AppProps & {
 
 export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
     useEffect(() => {
-        typeof document !== undefined
+        typeof document !== "undefined"
             ? require("bootstrap/dist/js/bootstrap")
             : null;
     }, []);
diff --git a/app/pages/api/_service.ts b/app/pages/api/_service.ts
index d256bdc6..7d6ac641 100644
--- a/app/pages/api/_service.ts
+++ b/app/pages/api/_service.ts
@@ -2,6 +2,7 @@ import { SQLService } from "database/services";
 
 export interface ApiError {
     error: string;
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
     details?: any;
 }
 
diff --git a/app/pages/api/admin/rerenderPreviews.ts b/app/pages/api/admin/rerenderPreviews.ts
index ce7832ed..d2b10e2e 100644
--- a/app/pages/api/admin/rerenderPreviews.ts
+++ b/app/pages/api/admin/rerenderPreviews.ts
@@ -38,7 +38,7 @@ export default async function rerenderPreviews(
         promises.push(page_preview(page.id, Number(book_id), true));
     }
 
-    await Promise.all(promises).catch((e) => {
+    await Promise.all(promises).catch(() => {
         res.status(500).json({ error: "Internal Server Error" });
         return;
     });
diff --git a/app/pages/api/admin/set_acl.ts b/app/pages/api/admin/set_acl.ts
index a2f47108..8c9fd69a 100644
--- a/app/pages/api/admin/set_acl.ts
+++ b/app/pages/api/admin/set_acl.ts
@@ -50,6 +50,7 @@ export const parseBoolean = (value: number | string | boolean): boolean => {
     throw new Error("value must be a boolean, string or number!");
 };
 
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
 export function parseACL(acl: Array<any>): PermissionACLData[] {
     const acl_parsed = acl.map((acl) => {
         let { id } = acl;
diff --git a/app/pages/api/admin/set_users_groups.ts b/app/pages/api/admin/set_users_groups.ts
index bd527e7f..c933ad1c 100644
--- a/app/pages/api/admin/set_users_groups.ts
+++ b/app/pages/api/admin/set_users_groups.ts
@@ -7,6 +7,7 @@ import { pool_permissions } from "database/sql.connection";
 
 import { UsersGroups } from "components/admin_panel/utils";
 
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
 const parseUserGroups = (user_groups: Array<any>): UsersGroups[] => {
     const user_groups_parsed = user_groups.map((user_group) => {
         const { group_id, user_id } = user_group;
diff --git a/app/pages/api/authentication/isAdmin.ts b/app/pages/api/authentication/isAdmin.ts
index 077e502b..2b84684d 100644
--- a/app/pages/api/authentication/isAdmin.ts
+++ b/app/pages/api/authentication/isAdmin.ts
@@ -1,6 +1,7 @@
 import { withSessionRoute } from "lib/withSession";
 import { NextApiRequest, NextApiResponse } from "next/types";
 
+import getSocketClient from "../socket";
 export default withSessionRoute(isAdmin);
 
 /** THIS API SHOULD NOT BE USED FOR CRITICAL APPLICATION PARTS
@@ -10,9 +11,11 @@ export default withSessionRoute(isAdmin);
  *
  */
 async function isAdmin(req: NextApiRequest, res: NextApiResponse<boolean>) {
-    if (req.session.is_admin) {
+
+    if (req.session?.is_admin) {
         res.json(true);
     } else {
         res.json(false);
     }
+
 }
diff --git a/app/pages/api/authentication/socket_token.ts b/app/pages/api/authentication/socket_token.ts
deleted file mode 100644
index cdb74658..00000000
--- a/app/pages/api/authentication/socket_token.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { config } from "config";
-import { sign } from "jsonwebtoken";
-import { withSessionRoute } from "lib/withSession";
-import { NextApiRequest, NextApiResponse } from "next/types";
-
-import { ApiError } from "../_service";
-
-export default withSessionRoute(tokenRoute);
-
-async function tokenRoute(
-    req: NextApiRequest,
-    res: NextApiResponse<{ token: string } | ApiError>
-) {
-    if (req.method !== "POST") {
-        res.status(405).json({ error: "Method not allowed" });
-        return;
-    }
-
-    const { book_id } = await req.body;
-
-    // Check if bookid is unsigned int parseable
-    if (!book_id || !Number.isInteger(Number(book_id))) {
-        res.status(400).json({
-            error: "Bad Request",
-            details: "Book ID is required",
-        });
-        return;
-    }
-
-    // Check if user is logged in
-    if (!req.session.user_id) {
-        res.status(401).json({
-            error: "Unauthorized",
-        });
-        return;
-    }
-
-
-    // Create token with book_id and send back
-    const token = sign({ date: new Date(), book_id, user_id: req.session.user_id }, config.SOCKET_SECRET, {
-        expiresIn: "24h",
-    });
-
-    res.json({ token });
-}
diff --git a/app/pages/api/authentication/verify.ts b/app/pages/api/authentication/verify.ts
index 866cfacd..6a238c47 100644
--- a/app/pages/api/authentication/verify.ts
+++ b/app/pages/api/authentication/verify.ts
@@ -64,7 +64,7 @@ export async function verify_token(token: string): Promise<VerifyPayload> {
     });
 
     // check if email is in db
-    const payload = new Promise<VerifyPayload>((resolve, reject) => {
+    const payload = new Promise<VerifyPayload>((resolve,) => {
         email
             .then(async (email) => {
                 return {
diff --git a/app/pages/api/books/[id]/edit/get_acl_entity.ts b/app/pages/api/books/[id]/edit/get_acl_entity.ts
index 0796e13d..da989da0 100644
--- a/app/pages/api/books/[id]/edit/get_acl_entity.ts
+++ b/app/pages/api/books/[id]/edit/get_acl_entity.ts
@@ -158,7 +158,7 @@ async function get_entity(entity_name): Promise<Entity> {
             }
             return res[0];
         })
-        .catch((err) => {
+        .catch(() => {
             return { id: -1, entity_name: undefined, entity_type: undefined };
         });
     return entity;
diff --git a/app/pages/api/books/[id]/edit/set_acl.ts b/app/pages/api/books/[id]/edit/set_acl.ts
index f192de9b..6b155410 100644
--- a/app/pages/api/books/[id]/edit/set_acl.ts
+++ b/app/pages/api/books/[id]/edit/set_acl.ts
@@ -3,6 +3,7 @@ import { withSessionRoute } from "lib/withSession";
 import { NextApiRequest, NextApiResponse } from "next/types";
 import { ApiError, service } from "pages/api/_service";
 import { parseACL } from "pages/api/admin/set_acl";
+import getSocketClient from "pages/api/socket";
 import { update_perms } from "socket/auth";
 
 import {
@@ -50,9 +51,7 @@ async function set_acl(
         const successes = acl_parsed.map(async (acl) => {
             const {
                 entity_id,
-                resource_id,
                 entity_type,
-                resource_type,
                 pRead,
                 pWrite,
                 pDelete,
@@ -86,11 +85,11 @@ async function set_acl(
         if (successes.every((success) => success)) {
 
             // Reset all socket permissions
-            req["io"].of(`/book-${book_id}`).sockets.forEach((socket) => {
-                update_perms(socket);
-            });
-
+            const client = getSocketClient();
+            client.connect();
+            client.emit("resetSocketPerms", book_id);
             res.status(200).json(true);
+            client.close();
         } else {
             res.status(500).json({
                 error: "Internal Server Error",
diff --git a/app/pages/api/books/[id]/edit/tokens.ts b/app/pages/api/books/[id]/edit/tokens.ts
index efbd62be..92bf09e0 100644
--- a/app/pages/api/books/[id]/edit/tokens.ts
+++ b/app/pages/api/books/[id]/edit/tokens.ts
@@ -1,6 +1,7 @@
 import { withAuthBookId } from "lib/access_control/books";
 import { hashToken } from "lib/access_control/hash_token";
 import { withSessionRoute } from "lib/withSession";
+import ms from "ms";
 import { NextApiRequest, NextApiResponse } from "next/types";
 import { randomBytes } from "node:crypto";
 import { ApiError } from "pages/api/_service";
@@ -18,7 +19,11 @@ export type ApiGetToken = {
     created_at: Date;
     created_by: string;
     description: string;
+    book_id: string;
     email: string;
+    type: "api" | "ui";
+    expires_at: Date | null;
+    token: string | null;
 };
 
 /** Route to get all tokens, create a token or delete a token depending on the
@@ -86,7 +91,7 @@ async function tokens(
 
     // Add request method
     else if (req.method === "POST") {
-        const { description } = req.body;
+        const { description, type, expireIn } = req.body;
 
         if (perms.entity_type !== ENTITY_USER) {
             res.status(401).json({
@@ -95,11 +100,28 @@ async function tokens(
             });
             return;
         }
+        if (type != "api" && type != "ui") {
+            res.status(400).json({
+                error: "Bad Request",
+                details: "Type must be either 'api' or 'ui'!",
+            });
+            return;
+        }
+        //Check if expireIn is given
+        if (type === "ui" && !expireIn) {
+            res.status(400).json({
+                error: "Bad Request",
+                details: "expireIn is required for ui tokens!",
+            });
+            return;
+        }
 
         const token = await createToken(
             book_id,
             perms.entity_id,
-            description
+            description,
+            type,
+            expireIn
         ).catch((err) => {
             console.error(err);
             return;
@@ -117,10 +139,26 @@ async function tokens(
     }
 }
 
+
 async function createToken(
     book_id,
     user_id,
-    description
+    description,
+    type,
+    expireIn,
+): Promise<ApiGetToken & { token: string }> {
+    if (type === "api") {
+        return await _createTokenApi(book_id, user_id, description);
+    } else if (type === "ui") {
+        return await _createTokenUi(book_id, user_id, description, expireIn);
+    }
+    throw new Error("Invalid type!");
+}
+
+async function _createTokenApi(
+    book_id,
+    user_id,
+    description,
 ): Promise<ApiGetToken & { token: string }> {
     const token = randomBytes(24).toString("hex");
 
@@ -128,11 +166,11 @@ async function createToken(
     const hashed_token = hashToken(token);
 
     const sql = `
-        INSERT INTO tokens (hashed_token, book_id, created_by, description)
-        VALUES (?, ?, ?, ?);
+        INSERT INTO tokens (hashed_token, book_id, created_by, description, type)
+        VALUES (?, ?, ?, ?, ?);
     `;
     return await pool_permissions
-        .query(sql, [hashed_token, book_id, user_id, description])
+        .query(sql, [hashed_token, book_id, user_id, description, "api",])
         .then((res) => {
             if (res.warningStatus > 0) {
                 throw new Error("Insert failed!");
@@ -145,6 +183,50 @@ async function createToken(
         });
 }
 
+
+/** Creates a token used for the share links
+ * @param book_id
+ * @param user_id
+ * @param description
+ * @returns
+ */
+async function _createTokenUi(
+    book_id,
+    user_id,
+    description,
+    expireIn
+): Promise<ApiGetToken & { token: string }> {
+
+    // Validate expireIn
+    const eIn = ms(expireIn);
+    if (!eIn) {
+        throw new Error("Invalid expireIn!");
+    }
+    const expires_at = new Date(Date.now() + eIn);
+    const token = randomBytes(24).toString("hex");
+    const hashed_token = hashToken(token);
+
+    const sql = `
+        INSERT INTO tokens (token, hashed_token, book_id, created_by, description, type, expires_at)
+        VALUES (?, ?, ?, ?, ?, ?, ?);
+    `;
+    return await pool_permissions
+        .query(sql, [token, hashed_token, book_id, user_id, description, "ui", expires_at])
+        .then((res) => {
+            if (res.warningStatus > 0) {
+                throw new Error("Insert failed!");
+            }
+            return res.insertId;
+        })
+        .then(async (token_id) => {
+            const tokeninfo = await getTokenById(token_id);
+            return { ...tokeninfo, token };
+        });
+}
+
+
+
+
 /** Returns all tokens for the book
  */
 async function getTokens(book_id): Promise<ApiGetToken[]> {
@@ -154,6 +236,10 @@ async function getTokens(book_id): Promise<ApiGetToken[]> {
             tokens.created_at,
             tokens.created_by,
             tokens.description,
+            tokens.type,
+            tokens.expires_at,
+            tokens.token,
+            tokens.book_id,
             users.email
         FROM tokens
         LEFT JOIN users ON tokens.created_by = users.id
@@ -169,6 +255,10 @@ async function getTokenById(token_id): Promise<ApiGetToken> {
             tokens.created_at,
             tokens.created_by,
             tokens.description,
+            tokens.type,
+            tokens.expires_at,
+            tokens.token,
+            tokens.book_id,
             users.email
         FROM tokens
         LEFT JOIN users ON tokens.created_by = users.id
diff --git a/app/pages/api/books/[id]/edit/update.ts b/app/pages/api/books/[id]/edit/update.ts
index 65a008e8..8f62e5e1 100644
--- a/app/pages/api/books/[id]/edit/update.ts
+++ b/app/pages/api/books/[id]/edit/update.ts
@@ -2,6 +2,7 @@ import { withAuthBookId } from "lib/access_control/books";
 import { withSessionRoute } from "lib/withSession";
 import { NextApiRequest, NextApiResponse } from "next/types";
 import { ApiError } from "pages/api/_service";
+import getSocketClient from "pages/api/socket";
 import { update_perms } from "socket/auth";
 
 import { PermissionACLData } from "database/services/service.typings";
@@ -144,13 +145,11 @@ async function updateBook(
     if (success) {
 
         // Reset all socket permissions
-        req["io"].of(`/book-${book_id}`).sockets.forEach((socket) => {
-            update_perms(socket);
-        });
-
-        res.status(200).json(
-            true
-        );
+        const client = getSocketClient();
+        client.connect();
+        client.emit("resetSocketPerms", book_id);
+        res.status(200).json(true);
+        client.close();
     }
     return;
 }
diff --git a/app/pages/api/books/[id]/socket_token.ts b/app/pages/api/books/[id]/socket_token.ts
new file mode 100644
index 00000000..f6e2d7cd
--- /dev/null
+++ b/app/pages/api/books/[id]/socket_token.ts
@@ -0,0 +1,81 @@
+import { config } from "config";
+import { JwtPayload, sign, verify } from "jsonwebtoken";
+import { withAuthBookId } from "lib/access_control/books";
+import { withSessionRoute } from "lib/withSession";
+import { NextApiRequest, NextApiResponse } from "next/types";
+
+import { ApiError } from "../../_service";
+
+import { PermissionACLData } from "database/services/service.typings";
+
+export default withSessionRoute(withAuthBookId(tokenRoute));
+
+async function tokenRoute(
+    req: NextApiRequest,
+    res: NextApiResponse<{ token: string } | ApiError>,
+    perms: PermissionACLData
+) {
+    if (!perms.pRead) {
+        res.status(401).json({
+            error: "Unauthorized",
+            details:
+                "You do not have read access to this book! ",
+        });
+        return;
+    }
+
+    if (req.method !== "POST") {
+        res.status(405).json({
+            error: "Method not allowed",
+            details: "Only POST method is allowed",
+        });
+        return;
+    }
+    const { query } = req;
+    const book_id = query.id as string;
+
+    const user_id = req.session.user_id;
+    const token = req.headers.authorization?.split(" ")[1];
+
+    // Create token with book_id and send back
+    // see socket/auth.ts
+    const jwt_token = sign({
+        book_id,
+        user_id,
+        token,
+    }, config.SOCKET_SECRET, {
+        expiresIn: "24h",
+    });
+
+    res.json({ token: jwt_token });
+}
+
+
+export function verifyToken(token: string) {
+
+    const payload = verify(token, config.SOCKET_SECRET);
+
+    //Check if payload is valid
+    if (!payload || typeof payload !== "object") {
+        throw new Error("Invalid token" + JSON.stringify(payload));
+    }
+
+    // Check if book_id is present
+    if (!payload.book_id) {
+        throw new Error("Invalid token no book_id!");
+    }
+    // Either user_id or token must be present
+    if (!payload.user_id && !payload.token) {
+        throw new Error("Invalid token no user_id or token!");
+    }
+    // Add undefined to user_id and token
+    if (!payload.user_id) {
+        payload.user_id = undefined;
+    }
+    if (!payload.token) {
+        payload.token = undefined;
+    }
+
+
+    return payload as JwtPayload & { book_id: string, user_id: string | undefined, token: string | undefined };
+}
\ No newline at end of file
diff --git a/app/pages/api/books/[id]/upload.ts b/app/pages/api/books/[id]/upload.ts
index 0d6965fc..a08ba847 100644
--- a/app/pages/api/books/[id]/upload.ts
+++ b/app/pages/api/books/[id]/upload.ts
@@ -5,6 +5,7 @@ import { withAuthBookId } from "lib/access_control/books";
 import { withSessionRoute } from "lib/withSession";
 import { NextApiRequest, NextApiResponse } from "next/types";
 import { ApiError, service } from "pages/api/_service";
+import getSocketClient from "pages/api/socket";
 import { default as sharp } from "sharp";
 
 import { PermissionACLData } from "database/services/service.typings";
@@ -122,20 +123,20 @@ async function uploadFile(
     }
 
     // Save snips to db and emit event to update clients
+
+
+    const client = getSocketClient();
+    client.connect();
     const ids = [];
     for (const snip of snips) {
         let snip_data = snip.to_data();
         snip_data = await service.snip.insert(snip_data);
-        req["io"]
-            .of("book-" + book_id)
-            .emit("book:insertUnplacedSnip", snip_data);
+        client.emit("api:insertedIntoQueue", snip_data);
         ids.push(snip_data.id);
     }
-
-
     res.status(200).json({ ids: ids });
+    client.close();
 }
-
 /* Helper function to parse multiple files
  to snips
 */
diff --git a/app/pages/api/socket.ts b/app/pages/api/socket.ts
index c7cab197..d14d775c 100644
--- a/app/pages/api/socket.ts
+++ b/app/pages/api/socket.ts
@@ -1,14 +1,35 @@
-import { Server } from "socket.io";
+import { config } from "config";
+import { sign } from "jsonwebtoken";
+import io from "socket.io-client";
 
-/** Initialize websocket or load if already
- * running. See below for functionality.
+/** Connects to socket server in admin
+ * namespace and returns the socket.
+ * 
+ * This should only be used server side.
  */
-export default function SocketHandler(req, res) {
-    if (res.socket.server.io) {
-        return;
-    } else {
-        const io = new Server(res.socket.server);
-        res.socket.server.io = io;
-        console.log("[Socket] WebSocket Server initialized");
-    }
-}
+export default function getSocketClient() {
+    const token = sign(
+        {
+            api: true,
+            date: Date.now(),
+        }
+        , config.SOCKET_SECRET
+    );
+
+    const client = io(`ws://${config.HOSTNAME}:4000/api`, {
+        auth: {
+            token,
+        },
+        autoConnect: false,
+    });
+
+    client.on("error", (error) => {
+        console.log("Socket error", error);
+    });
+    client.on("connect_error", (error) => {
+        console.log("Socket connect error", error);
+        client.close();
+    });
+
+    return client;
+}
\ No newline at end of file
diff --git a/app/pages/books/[id]/index.tsx b/app/pages/books/[id]/index.tsx
index c80065e5..155c6379 100644
--- a/app/pages/books/[id]/index.tsx
+++ b/app/pages/books/[id]/index.tsx
@@ -15,12 +15,18 @@ import { type Tool } from "components/viewer/tools";
  * Also handles the active pages and the layout
  *
  */
-export default function BookById({ bookData }) {
+export default function BookById({ bookData, perms }) {
     // Create a book
     const [book, setBook] = useState<Book>();
 
     // Register desired tools (snipEditor is always last)
-    const desiredTools: Tool[] = ["movePage", "doodle", "text", "snipEditor"];
+    const desiredTools: Tool[] = ["movePage"];
+    if (perms && perms.pWrite) {
+        desiredTools.push("doodle");
+        desiredTools.push("text");
+        desiredTools.push("snipEditor");
+    }
+
     const { Viewer, ViewerContext, Toolbar } = viewer_factory(desiredTools);
 
     // Load book and pageData for all pages
@@ -35,11 +41,11 @@ export default function BookById({ bookData }) {
     }, [bookData]);
 
     // Return loading spinner
-    if (!book) return <Loading />;
+    if (!book || !perms) return <Loading />;
 
     return (
         <ViewerContext>
-            <Sidebar book={book} />
+            <Sidebar book={book} showSnipsToPlace={perms.pWrite} />
             <Resizer />
             <div
                 style={{
@@ -64,7 +70,13 @@ export const getServerSideProps = withSessionSsr(
         if (perms.pRead) {
             const book_id = context.query.id as string;
             const bookData = await SQLService.book.getById(Number(book_id));
-            return { props: { bookData, title: bookData.title } };
+            const p = {
+                pRead: perms.pRead,
+                pWrite: perms.pWrite,
+                pDelete: perms.pDelete,
+                pACL: perms.pACL,
+            };
+            return { props: { bookData, title: bookData.title, perms: p } };
         }
 
         return {
diff --git a/app/pages/books/index.tsx b/app/pages/books/index.tsx
index 3fc9f2a5..3a82e70a 100644
--- a/app/pages/books/index.tsx
+++ b/app/pages/books/index.tsx
@@ -1,6 +1,5 @@
 import { fetcher } from "lib/fetcher";
 import { withSessionSsr } from "lib/withSession";
-import { getBooksByUser } from "pages/api/books";
 import useSWR from "swr";
 
 import { FullBooksBrowser } from "components/books/browser/BooksBrowser";
@@ -8,7 +7,6 @@ import { DefaultLayoutWithSidebar } from "components/common/DefaultLayout";
 
 export default function Index() {
     //return <BooksViewer books={books} perms={perms} />;
-
     const {
         data: fileMap,
         isLoading,
diff --git a/app/public/img/clipboard-check.svg b/app/public/img/clipboard-check.svg
new file mode 100644
index 00000000..f7591aec
--- /dev/null
+++ b/app/public/img/clipboard-check.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard-check" viewBox="0 0 16 16">
+  <path fill-rule="evenodd" d="M10.854 7.146a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7.5 9.793l2.646-2.647a.5.5 0 0 1 .708 0z"/>
+  <path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/>
+  <path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>
+</svg>
\ No newline at end of file
diff --git a/app/public/img/clipboard.svg b/app/public/img/clipboard.svg
new file mode 100644
index 00000000..360e0894
--- /dev/null
+++ b/app/public/img/clipboard.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard" viewBox="0 0 16 16">
+  <path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/>
+  <path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>
+</svg>
\ No newline at end of file
diff --git a/app/server.ts b/app/server.ts
index 3922c2de..b4af5b01 100644
--- a/app/server.ts
+++ b/app/server.ts
@@ -6,6 +6,7 @@ import { createServer as createServerHttp } from "http";
 import { createServer as createServerHttps } from "https";
 import next from "next";
 import { getSocketIOServer } from "socket";
+import { type Server as IOServer } from "socket.io";
 import { parse } from "url";
 
 // Own function to setup server
@@ -20,6 +21,8 @@ const dir = "./app";
 const app = next({ dev, hostname, port, dir });
 const handle = app.getRequestHandler();
 
+
+
 // Define the callback function to add as http or https
 async function callback_server(
     req: IncomingMessage,
@@ -28,20 +31,13 @@ async function callback_server(
     // We attach the socket to the request object
     // to use it in the route handler
     // e.g. emitting event on post requests
-    req["io"] = snip_socket;
+
     try {
         // Be sure to pass `true` as the second argument to `url.parse`.
         // This tells it to parse the query portion of the URL.
         const parsedUrl = parse(req.url, true);
-        const { pathname, query } = parsedUrl;
+        await handle(req, res, parsedUrl);
 
-        if (pathname === "/a") {
-            await app.render(req, res, "/a", query);
-        } else if (pathname === "/b") {
-            await app.render(req, res, "/b", query);
-        } else {
-            await handle(req, res, parsedUrl);
-        }
     } catch (err) {
         console.error("Error occurred handling", req.url, err);
         res.statusCode = 500;
diff --git a/app/snips/general/image.ts b/app/snips/general/image.ts
index 491744ea..8cd92a76 100644
--- a/app/snips/general/image.ts
+++ b/app/snips/general/image.ts
@@ -1,6 +1,5 @@
 import EventEmitter from "events";
 import { sync as probe_sync } from "probe-image-size";
-import sharp from "sharp";
 
 import { BaseData, BaseSnip, BaseSnipArgs, BaseView, SnipData } from "./base";
 
diff --git a/app/socket/auth.ts b/app/socket/auth.ts
index 25278eab..502be798 100644
--- a/app/socket/auth.ts
+++ b/app/socket/auth.ts
@@ -1,23 +1,10 @@
 import { config } from "config";
 import { JwtPayload, verify } from "jsonwebtoken";
-import { service } from "pages/api/_service";
+import { bookAuth } from "lib/access_control/books";
+import { verifyToken } from "pages/api/books/[id]/socket_token";
 import type { Socket } from "socket.io";
 
-function _verify(token: string, book_id: string) {
-    const decoded = verify(token, config.SOCKET_SECRET);
-
-    if (
-        (decoded as JwtPayload).book_id == book_id &&
-        (decoded as JwtPayload).user_id) {
-        return decoded as JwtPayload;
-    } else {
-        throw new Error("Invalid token");
-    }
-}
-
 export async function authenticate(socket: Socket, next) {
-    // Print middleware message
-    console.log("[Socket][Middleware]", socket.nsp.name);
 
     // Verify token
     const token = socket.handshake.auth.token;
@@ -25,32 +12,51 @@ export async function authenticate(socket: Socket, next) {
 
     try {
         // Verify and decode payload
-        const decoded = _verify(token, book_id);
+        const decoded = verifyToken(token);
+        if (decoded.book_id != book_id) {
+            throw new Error("Invalid token, book_id does not match");
+        }
+
         // Set socket data
         socket.book_id = parseInt(decoded.book_id);
-        socket.user_id = decoded.user_id;
+        socket.user_id = parseInt(decoded.user_id) || undefined;
+        socket.auth_token = decoded.token;
+
     } catch (error) {
-        return next(new Error("Invalid token"));
+        return next(error);
     }
     return next();
 }
 
 
 export async function add_perms(socket: Socket, next) {
-    const perms = await service.permission.getACLByUserBookReduced(
-        socket.user_id,
-        socket.book_id
-    );
-
+    const perms = await bookAuth(socket.user_id, socket.book_id, socket.auth_token);
+    console.log(perms);
     socket.perms = perms;
     return next();
 }
 
 export async function update_perms(socket: Socket) {
-    const perms = await service.permission.getACLByUserBookReduced(
-        socket.user_id,
-        socket.book_id
-    );
+    const perms = await bookAuth(socket.user_id, socket.book_id, socket.auth_token);
+    console.log(perms);
     socket.perms = perms;
     return
+}
+
+
+/** For admin namespace only */
+export function authenticateApi(socket: Socket, next) {
+    // Verify token
+    const token = socket.handshake.auth.token;
+
+    try {
+        const decoded = verify(token, config.SOCKET_SECRET) as JwtPayload & { api: boolean, date: Date };
+
+        if (!decoded.api) {
+            throw new Error("Invalid token, not an api token");
+        }
+    } catch (error) {
+        return next(error);
+    }
+    return next();
 }
\ No newline at end of file
diff --git a/app/socket/socket.ts b/app/socket/socket.ts
index 43780789..1742c02e 100644
--- a/app/socket/socket.ts
+++ b/app/socket/socket.ts
@@ -1,21 +1,22 @@
 /* eslint-disable no-unused-vars */
 import { Namespace, Server, Socket } from "socket.io";
 
-import { add_perms, authenticate } from "./auth";
+import { add_perms, authenticate, authenticateApi, update_perms } from "./auth";
 import { setupBookPrefix } from "./book/socket";
 import { setupPagesPrefix } from "./page/socket";
 import { setupSnipPrefix } from "./snip/socket";
 
 import { SQLService } from "database/services";
 import { PermissionACLData } from "database/services/service.typings";
-import { BaseData, BaseView,SnipData } from "snips/general/base";
+import { BaseData, BaseView, SnipData } from "snips/general/base";
 
 // Extend socket to include book_id, user_id and perms
 // This is set in the the book_authenticate middleware
 declare module "socket.io" {
     interface Socket {
         book_id: number;
-        user_id: number;
+        user_id: number | undefined;
+        auth_token: string | undefined;
         perms: PermissionACLData;
     }
 }
@@ -103,4 +104,36 @@ export function setupNamespaces(server: Server) {
             socket.leave("page:" + page_id);
         });
     });
+
+    setupApiSpace(server);
 }
+
+
+function setupApiSpace(server: Server) {
+    const api_nsp = server.of("/api");
+
+    api_nsp.use(authenticateApi);
+
+    api_nsp.on("connection", async (socket: Socket) => {
+        log("[Api] Client (" + socket.id + ") connected");
+
+
+        socket.on("api:insertedIntoQueue", async (snip: SnipData<BaseData, BaseView>) => {
+            log("[Api] insertedIntoQueue");
+            server.of("/book-" + snip.book_id).emit("book:insertedIntoQueue", snip);
+        });
+
+        socket.on("resetSocketPerms", async (book_id: number) => {
+            log("[Api] resetSocketPerms");
+            server.of("/book-" + book_id).sockets.forEach((socket) => {
+                update_perms(socket);
+            });
+        });
+
+
+        socket.on("disconnect", () => {
+            log("[Api] Client (" + socket.id + ") disconnected");
+        });
+    });
+}
+
diff --git a/database/migration/0016_token_types.sql b/database/migration/0016_token_types.sql
new file mode 100644
index 00000000..e9c6cb24
--- /dev/null
+++ b/database/migration/0016_token_types.sql
@@ -0,0 +1,8 @@
+USE snip_perms;
+
+-- Add the user configs table (api or ui)
+ALTER TABLE tokens ADD COLUMN type VARCHAR(10) NOT NULL DEFAULT 'api' COMMENT 'The type of token (api or ui)';
+-- Add token column to allow for plain text tokens (only ui)
+ALTER TABLE tokens ADD COLUMN token TEXT NULL DEFAULT NULL COMMENT 'The token (only for ui tokens)';
+-- Add expires_at column to allow for token expiration (only ui( for now))
+ALTER TABLE tokens ADD COLUMN expires_at TIMESTAMP NULL DEFAULT NULL COMMENT 'The time the token expires (only for ui tokens)';
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index db077057..f97f260f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,75 +1,71 @@
 {
     "name": "snip",
-    "version": "1.4.0",
+    "version": "1.4.1",
     "lockfileVersion": 2,
     "requires": true,
     "packages": {
         "": {
             "name": "snip",
-            "version": "1.4.0",
+            "version": "1.4.1",
             "license": "GPL-3.0",
             "dependencies": {
-                "@next/bundle-analyzer": "13.4.7",
-                "@popperjs/core": "^2.11.5",
-                "bcrypt": "^5.0.1",
-                "bootstrap": "^5.2.0",
-                "bootstrap-icons": "^1.9.1",
+                "@next/bundle-analyzer": "^13.4.19",
+                "@popperjs/core": "^2.11.8",
+                "bcrypt": "^5.1.1",
+                "bootstrap": "^5.3.2",
+                "bootstrap-icons": "^1.11.0",
                 "canvas": "^2.11.2",
-                "cli-progress": "^3.11.2",
-                "dotenv": "^16.0.3",
-                "formidable": "^2.0.1",
+                "cli-progress": "^3.12.0",
+                "dotenv": "^16.3.1",
+                "formidable": "^3.5.1",
                 "gray-matter": "^4.0.3",
-                "highlight.js": "^11.7.0",
-                "iron-session": "^6.2.1",
-                "jsonwebtoken": "^9.0.0",
-                "mariadb": "^3.0.2",
+                "highlight.js": "^11.8.0",
+                "iron-session": "^6.3.1",
+                "jsonwebtoken": "^9.0.2",
+                "mariadb": "^3.2.1",
                 "markdown-it": "^13.0.1",
                 "markdown-it-highlightjs": "^4.0.1",
-                "mysql": "^2.18.1",
-                "next": "13.4.7",
-                "nodemailer": "^6.8.0",
+                "next": "^13.4.19",
+                "nodemailer": "^6.9.5",
                 "pdf-lib": "^1.17.1",
                 "probe-image-size": "^7.2.3",
                 "react": "^18.2.0",
-                "react-bootstrap": "^2.4.0",
+                "react-bootstrap": "^2.8.0",
                 "react-dom": "^18.2.0",
-                "react-dropzone": "^14.2.2",
+                "react-dropzone": "^14.2.3",
                 "react-popper": "^2.3.0",
-                "sass": "^1.54.0",
-                "sharp": "^0.32.1",
-                "socket.io": "^4.5.1",
-                "socket.io-client": "^4.5.1",
-                "sprintf-js": "^1.1.2",
-                "swr": "^2.1.5",
-                "tslib": "^2.4.0"
+                "sass": "^1.67.0",
+                "sharp": "^0.32.5",
+                "socket.io": "^4.7.2",
+                "socket.io-client": "^4.7.2",
+                "sprintf-js": "^1.1.3",
+                "swr": "^2.2.2",
+                "tslib": "^2.6.2"
             },
             "devDependencies": {
-                "@rmp135/sql-ts": "^1.15.1",
+                "@rmp135/sql-ts": "^1.18.0",
                 "@types/bcrypt": "^5.0.0",
-                "@types/bootstrap": "^5.2.0",
-                "@types/cookie-parser": "^1.4.3",
-                "@types/ejs": "^3.1.1",
-                "@types/express": "^4.17.13",
-                "@types/express-mysql-session": "^2.1.3",
-                "@types/express-session": "^1.17.5",
-                "@types/formidable": "^2.0.5",
+                "@types/bootstrap": "^5.2.6",
+                "@types/cookie-parser": "^1.4.4",
+                "@types/ejs": "^3.1.2",
+                "@types/formidable": "^3.4.3",
                 "@types/jsonwebtoken": "^9.0.2",
                 "@types/mysql": "^2.15.21",
-                "@types/node": "^18.6",
-                "@types/nodemailer": "^6.4.6",
-                "@types/react": "^18.0.15",
+                "@types/node": "^20.6.0",
+                "@types/nodemailer": "^6.4.10",
+                "@types/react": "^18.2.21",
                 "@types/sprintf-js": "^1.1.2",
-                "@types/webpack-env": "^1.18.0",
-                "@typescript-eslint/eslint-plugin": "^5.42.0",
-                "@typescript-eslint/parser": "^5.42.0",
-                "eslint": "^8.42.0",
-                "eslint-config-next": "13.4.7",
-                "eslint-config-prettier": "^8.5.0",
+                "@types/webpack-env": "^1.18.1",
+                "@typescript-eslint/eslint-plugin": "^6.7.0",
+                "@typescript-eslint/parser": "^6.7.0",
+                "eslint": "^8.49.0",
+                "eslint-config-next": "^13.4.19",
+                "eslint-config-prettier": "^9.0.0",
                 "eslint-plugin-simple-import-sort": "^10.0.0",
                 "ts-node": "^10.9.1",
                 "ts-node-dev": "^2.0.0",
-                "tsconfig-paths": "^4.0.0",
-                "typescript": "^5.0.4"
+                "tsconfig-paths": "^4.2.0",
+                "typescript": "^5.2.2"
             }
         },
         "node_modules/@aashutoshrathi/word-wrap": {
@@ -129,9 +125,9 @@
             }
         },
         "node_modules/@eslint/eslintrc": {
-            "version": "2.1.1",
-            "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.1.tgz",
-            "integrity": "sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA==",
+            "version": "2.1.2",
+            "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz",
+            "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==",
             "dev": true,
             "dependencies": {
                 "ajv": "^6.12.4",
@@ -152,18 +148,18 @@
             }
         },
         "node_modules/@eslint/js": {
-            "version": "8.46.0",
-            "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.46.0.tgz",
-            "integrity": "sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA==",
+            "version": "8.49.0",
+            "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz",
+            "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==",
             "dev": true,
             "engines": {
                 "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
             }
         },
         "node_modules/@humanwhocodes/config-array": {
-            "version": "0.11.10",
-            "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
-            "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==",
+            "version": "0.11.11",
+            "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz",
+            "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==",
             "dev": true,
             "dependencies": {
                 "@humanwhocodes/object-schema": "^1.2.1",
@@ -238,31 +234,91 @@
             }
         },
         "node_modules/@next/bundle-analyzer": {
-            "version": "13.4.7",
-            "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-13.4.7.tgz",
-            "integrity": "sha512-cIcU07u6WdDUF1nrFYOSPvx/pie8euxSnWB1Y/XMYj2g5ls+iyaDr/ChyZugD5hNW754x0NFy1CwzpObA0bffQ==",
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-13.4.19.tgz",
+            "integrity": "sha512-nXKHz63dM0Kn3XPFOKrv2wK+hP9rdBg2iR1PsxuXLHVBoZhMyS1/ldRcX80YFsm2VUws9zM4a0E/1HlLI+P92g==",
             "dependencies": {
                 "webpack-bundle-analyzer": "4.7.0"
             }
         },
         "node_modules/@next/env": {
-            "version": "13.4.7",
-            "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.7.tgz",
-            "integrity": "sha512-ZlbiFulnwiFsW9UV1ku1OvX/oyIPLtMk9p/nnvDSwI0s7vSoZdRtxXNsaO+ZXrLv/pMbXVGq4lL8TbY9iuGmVw=="
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.19.tgz",
+            "integrity": "sha512-FsAT5x0jF2kkhNkKkukhsyYOrRqtSxrEhfliniIq0bwWbuXLgyt3Gv0Ml+b91XwjwArmuP7NxCiGd++GGKdNMQ=="
         },
         "node_modules/@next/eslint-plugin-next": {
-            "version": "13.4.7",
-            "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-13.4.7.tgz",
-            "integrity": "sha512-ANEPltxzXbyyG7CvqxdY4PmeM5+RyWdAJGufTHnU+LA/i3J6IDV2r8Z4onKwskwKEhwqzz5lMaSYGGXLyHX+mg==",
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-13.4.19.tgz",
+            "integrity": "sha512-N/O+zGb6wZQdwu6atMZHbR7T9Np5SUFUjZqCbj0sXm+MwQO35M8TazVB4otm87GkXYs2l6OPwARd3/PUWhZBVQ==",
             "dev": true,
             "dependencies": {
                 "glob": "7.1.7"
             }
         },
+        "node_modules/@next/swc-darwin-arm64": {
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.19.tgz",
+            "integrity": "sha512-vv1qrjXeGbuF2mOkhkdxMDtv9np7W4mcBtaDnHU+yJG+bBwa6rYsYSCI/9Xm5+TuF5SbZbrWO6G1NfTh1TMjvQ==",
+            "cpu": [
+                "arm64"
+            ],
+            "optional": true,
+            "os": [
+                "darwin"
+            ],
+            "engines": {
+                "node": ">= 10"
+            }
+        },
+        "node_modules/@next/swc-darwin-x64": {
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.19.tgz",
+            "integrity": "sha512-jyzO6wwYhx6F+7gD8ddZfuqO4TtpJdw3wyOduR4fxTUCm3aLw7YmHGYNjS0xRSYGAkLpBkH1E0RcelyId6lNsw==",
+            "cpu": [
+                "x64"
+            ],
+            "optional": true,
+            "os": [
+                "darwin"
+            ],
+            "engines": {
+                "node": ">= 10"
+            }
+        },
+        "node_modules/@next/swc-linux-arm64-gnu": {
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.19.tgz",
+            "integrity": "sha512-vdlnIlaAEh6H+G6HrKZB9c2zJKnpPVKnA6LBwjwT2BTjxI7e0Hx30+FoWCgi50e+YO49p6oPOtesP9mXDRiiUg==",
+            "cpu": [
+                "arm64"
+            ],
+            "optional": true,
+            "os": [
+                "linux"
+            ],
+            "engines": {
+                "node": ">= 10"
+            }
+        },
+        "node_modules/@next/swc-linux-arm64-musl": {
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.19.tgz",
+            "integrity": "sha512-aU0HkH2XPgxqrbNRBFb3si9Ahu/CpaR5RPmN2s9GiM9qJCiBBlZtRTiEca+DC+xRPyCThTtWYgxjWHgU7ZkyvA==",
+            "cpu": [
+                "arm64"
+            ],
+            "optional": true,
+            "os": [
+                "linux"
+            ],
+            "engines": {
+                "node": ">= 10"
+            }
+        },
         "node_modules/@next/swc-linux-x64-gnu": {
-            "version": "13.4.7",
-            "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.7.tgz",
-            "integrity": "sha512-zaEC+iEiAHNdhl6fuwl0H0shnTzQoAoJiDYBUze8QTntE/GNPfTYpYboxF5LRYIjBwETUatvE0T64W6SKDipvg==",
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.19.tgz",
+            "integrity": "sha512-htwOEagMa/CXNykFFeAHHvMJeqZfNQEoQvHfsA4wgg5QqGNqD5soeCer4oGlCol6NGUxknrQO6VEustcv+Md+g==",
             "cpu": [
                 "x64"
             ],
@@ -275,9 +331,9 @@
             }
         },
         "node_modules/@next/swc-linux-x64-musl": {
-            "version": "13.4.7",
-            "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.7.tgz",
-            "integrity": "sha512-X6r12F8d8SKAtYJqLZBBMIwEqcTRvUdVm+xIq+l6pJqlgT2tNsLLf2i5Cl88xSsIytBICGsCNNHd+siD2fbWBA==",
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.19.tgz",
+            "integrity": "sha512-4Gj4vvtbK1JH8ApWTT214b3GwUh9EKKQjY41hH/t+u55Knxi/0wesMzwQRhppK6Ddalhu0TEttbiJ+wRcoEj5Q==",
             "cpu": [
                 "x64"
             ],
@@ -289,6 +345,51 @@
                 "node": ">= 10"
             }
         },
+        "node_modules/@next/swc-win32-arm64-msvc": {
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.19.tgz",
+            "integrity": "sha512-bUfDevQK4NsIAHXs3/JNgnvEY+LRyneDN788W2NYiRIIzmILjba7LaQTfihuFawZDhRtkYCv3JDC3B4TwnmRJw==",
+            "cpu": [
+                "arm64"
+            ],
+            "optional": true,
+            "os": [
+                "win32"
+            ],
+            "engines": {
+                "node": ">= 10"
+            }
+        },
+        "node_modules/@next/swc-win32-ia32-msvc": {
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.19.tgz",
+            "integrity": "sha512-Y5kikILFAr81LYIFaw6j/NrOtmiM4Sf3GtOc0pn50ez2GCkr+oejYuKGcwAwq3jiTKuzF6OF4iT2INPoxRycEA==",
+            "cpu": [
+                "ia32"
+            ],
+            "optional": true,
+            "os": [
+                "win32"
+            ],
+            "engines": {
+                "node": ">= 10"
+            }
+        },
+        "node_modules/@next/swc-win32-x64-msvc": {
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.19.tgz",
+            "integrity": "sha512-YzA78jBDXMYiINdPdJJwGgPNT3YqBNNGhsthsDoWHL9p24tEJn9ViQf/ZqTbwSpX/RrkPupLfuuTH2sf73JBAw==",
+            "cpu": [
+                "x64"
+            ],
+            "optional": true,
+            "os": [
+                "win32"
+            ],
+            "engines": {
+                "node": ">= 10"
+            }
+        },
         "node_modules/@nodelib/fs.scandir": {
             "version": "2.1.5",
             "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -600,9 +701,9 @@
             "integrity": "sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g=="
         },
         "node_modules/@types/cookie-parser": {
-            "version": "1.4.3",
-            "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.3.tgz",
-            "integrity": "sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==",
+            "version": "1.4.4",
+            "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.4.tgz",
+            "integrity": "sha512-Var+aj5I6ZgIqsQ05N2V8q5OBrFfZXtIGWWDSrEYLIbMw758obagSwdGcLCjwh1Ga7M7+wj0SDIAaAC/WT7aaA==",
             "dev": true,
             "dependencies": {
                 "@types/express": "*"
@@ -644,15 +745,6 @@
                 "@types/serve-static": "*"
             }
         },
-        "node_modules/@types/express-mysql-session": {
-            "version": "2.1.3",
-            "resolved": "https://registry.npmjs.org/@types/express-mysql-session/-/express-mysql-session-2.1.3.tgz",
-            "integrity": "sha512-Df6DqYTwvdH+L91Mn03Gv1qqFUUJueKb5fiFY+GbimwOKQuT7m9a5Ao8O7UZ86Cbri7S7yWFmw+fK7o4OP+voQ==",
-            "dev": true,
-            "dependencies": {
-                "@types/express-session": "*"
-            }
-        },
         "node_modules/@types/express-serve-static-core": {
             "version": "4.17.35",
             "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz",
@@ -664,19 +756,10 @@
                 "@types/send": "*"
             }
         },
-        "node_modules/@types/express-session": {
-            "version": "1.17.7",
-            "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.7.tgz",
-            "integrity": "sha512-L25080PBYoRLu472HY/HNCxaXY8AaGgqGC8/p/8+BYMhG0RDOLQ1wpXOpAzr4Gi5TGozTKyJv5BVODM5UNyVMw==",
-            "dev": true,
-            "dependencies": {
-                "@types/express": "*"
-            }
-        },
         "node_modules/@types/formidable": {
-            "version": "2.0.6",
-            "resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-2.0.6.tgz",
-            "integrity": "sha512-L4HcrA05IgQyNYJj6kItuIkXrInJvsXTPC5B1i64FggWKKqSL+4hgt7asiSNva75AoLQjq29oPxFfU4GAQ6Z2w==",
+            "version": "3.4.3",
+            "resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-3.4.3.tgz",
+            "integrity": "sha512-FRB5KNIYgCLDvw/ho7daBVqdQRG9Ptk1tMlZ2B5ibAjxDfXFkO6fJaP+ojmS8ljqmnC5If9u24WdWcVPaN3thg==",
             "dev": true,
             "dependencies": {
                 "@types/node": "*"
@@ -761,14 +844,14 @@
             }
         },
         "node_modules/@types/node": {
-            "version": "18.17.3",
-            "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.3.tgz",
-            "integrity": "sha512-2x8HWtFk0S99zqVQABU9wTpr8wPoaDHZUcAkoTKH+nL7kPv3WUI9cRi/Kk5Mz4xdqXSqTkKP7IWNoQQYCnDsTA=="
+            "version": "20.6.0",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.0.tgz",
+            "integrity": "sha512-najjVq5KN2vsH2U/xyh2opaSEz6cZMR2SetLIlxlj08nOcmPOemJmUK2o4kUzfLqfrWE0PIrNeE16XhYDd3nqg=="
         },
         "node_modules/@types/nodemailer": {
-            "version": "6.4.9",
-            "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.9.tgz",
-            "integrity": "sha512-XYG8Gv+sHjaOtUpiuytahMy2mM3rectgroNbs6R3djZEKmPNiIJwe9KqOJBGzKKnNZNKvnuvmugBgpq3w/S0ig==",
+            "version": "6.4.10",
+            "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.10.tgz",
+            "integrity": "sha512-oPW/IdhkU3FyZc1dzeqmS+MBjrjZNiiINnrEOrWALzccJlP5xTlbkNr2YnTnnyj9Eqm5ofjRoASEbrCYpA7BrA==",
             "dev": true,
             "dependencies": {
                 "@types/node": "*"
@@ -790,9 +873,9 @@
             "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw=="
         },
         "node_modules/@types/react": {
-            "version": "18.2.19",
-            "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.19.tgz",
-            "integrity": "sha512-e2S8wmY1ePfM517PqCG80CcE48Xs5k0pwJzuDZsfE8IZRRBfOMCF+XqnFxu6mWtyivum1MQm4aco+WIt6Coimw==",
+            "version": "18.2.21",
+            "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.21.tgz",
+            "integrity": "sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==",
             "dependencies": {
                 "@types/prop-types": "*",
                 "@types/scheduler": "*",
@@ -813,9 +896,9 @@
             "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ=="
         },
         "node_modules/@types/semver": {
-            "version": "7.5.0",
-            "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz",
-            "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==",
+            "version": "7.5.2",
+            "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.2.tgz",
+            "integrity": "sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw==",
             "dev": true
         },
         "node_modules/@types/send": {
@@ -867,32 +950,33 @@
             "dev": true
         },
         "node_modules/@typescript-eslint/eslint-plugin": {
-            "version": "5.62.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
-            "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
+            "version": "6.7.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.0.tgz",
+            "integrity": "sha512-gUqtknHm0TDs1LhY12K2NA3Rmlmp88jK9Tx8vGZMfHeNMLE3GH2e9TRub+y+SOjuYgtOmok+wt1AyDPZqxbNag==",
             "dev": true,
             "dependencies": {
-                "@eslint-community/regexpp": "^4.4.0",
-                "@typescript-eslint/scope-manager": "5.62.0",
-                "@typescript-eslint/type-utils": "5.62.0",
-                "@typescript-eslint/utils": "5.62.0",
+                "@eslint-community/regexpp": "^4.5.1",
+                "@typescript-eslint/scope-manager": "6.7.0",
+                "@typescript-eslint/type-utils": "6.7.0",
+                "@typescript-eslint/utils": "6.7.0",
+                "@typescript-eslint/visitor-keys": "6.7.0",
                 "debug": "^4.3.4",
                 "graphemer": "^1.4.0",
-                "ignore": "^5.2.0",
-                "natural-compare-lite": "^1.4.0",
-                "semver": "^7.3.7",
-                "tsutils": "^3.21.0"
+                "ignore": "^5.2.4",
+                "natural-compare": "^1.4.0",
+                "semver": "^7.5.4",
+                "ts-api-utils": "^1.0.1"
             },
             "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+                "node": "^16.0.0 || >=18.0.0"
             },
             "funding": {
                 "type": "opencollective",
                 "url": "https://opencollective.com/typescript-eslint"
             },
             "peerDependencies": {
-                "@typescript-eslint/parser": "^5.0.0",
-                "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+                "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha",
+                "eslint": "^7.0.0 || ^8.0.0"
             },
             "peerDependenciesMeta": {
                 "typescript": {
@@ -901,25 +985,26 @@
             }
         },
         "node_modules/@typescript-eslint/parser": {
-            "version": "5.62.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
-            "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
+            "version": "6.7.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.0.tgz",
+            "integrity": "sha512-jZKYwqNpNm5kzPVP5z1JXAuxjtl2uG+5NpaMocFPTNC2EdYIgbXIPImObOkhbONxtFTTdoZstLZefbaK+wXZng==",
             "dev": true,
             "dependencies": {
-                "@typescript-eslint/scope-manager": "5.62.0",
-                "@typescript-eslint/types": "5.62.0",
-                "@typescript-eslint/typescript-estree": "5.62.0",
+                "@typescript-eslint/scope-manager": "6.7.0",
+                "@typescript-eslint/types": "6.7.0",
+                "@typescript-eslint/typescript-estree": "6.7.0",
+                "@typescript-eslint/visitor-keys": "6.7.0",
                 "debug": "^4.3.4"
             },
             "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+                "node": "^16.0.0 || >=18.0.0"
             },
             "funding": {
                 "type": "opencollective",
                 "url": "https://opencollective.com/typescript-eslint"
             },
             "peerDependencies": {
-                "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+                "eslint": "^7.0.0 || ^8.0.0"
             },
             "peerDependenciesMeta": {
                 "typescript": {
@@ -928,16 +1013,16 @@
             }
         },
         "node_modules/@typescript-eslint/scope-manager": {
-            "version": "5.62.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
-            "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==",
+            "version": "6.7.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.7.0.tgz",
+            "integrity": "sha512-lAT1Uau20lQyjoLUQ5FUMSX/dS07qux9rYd5FGzKz/Kf8W8ccuvMyldb8hadHdK/qOI7aikvQWqulnEq2nCEYA==",
             "dev": true,
             "dependencies": {
-                "@typescript-eslint/types": "5.62.0",
-                "@typescript-eslint/visitor-keys": "5.62.0"
+                "@typescript-eslint/types": "6.7.0",
+                "@typescript-eslint/visitor-keys": "6.7.0"
             },
             "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+                "node": "^16.0.0 || >=18.0.0"
             },
             "funding": {
                 "type": "opencollective",
@@ -945,25 +1030,25 @@
             }
         },
         "node_modules/@typescript-eslint/type-utils": {
-            "version": "5.62.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz",
-            "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==",
+            "version": "6.7.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.7.0.tgz",
+            "integrity": "sha512-f/QabJgDAlpSz3qduCyQT0Fw7hHpmhOzY/Rv6zO3yO+HVIdPfIWhrQoAyG+uZVtWAIS85zAyzgAFfyEr+MgBpg==",
             "dev": true,
             "dependencies": {
-                "@typescript-eslint/typescript-estree": "5.62.0",
-                "@typescript-eslint/utils": "5.62.0",
+                "@typescript-eslint/typescript-estree": "6.7.0",
+                "@typescript-eslint/utils": "6.7.0",
                 "debug": "^4.3.4",
-                "tsutils": "^3.21.0"
+                "ts-api-utils": "^1.0.1"
             },
             "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+                "node": "^16.0.0 || >=18.0.0"
             },
             "funding": {
                 "type": "opencollective",
                 "url": "https://opencollective.com/typescript-eslint"
             },
             "peerDependencies": {
-                "eslint": "*"
+                "eslint": "^7.0.0 || ^8.0.0"
             },
             "peerDependenciesMeta": {
                 "typescript": {
@@ -972,12 +1057,12 @@
             }
         },
         "node_modules/@typescript-eslint/types": {
-            "version": "5.62.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
-            "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==",
+            "version": "6.7.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.0.tgz",
+            "integrity": "sha512-ihPfvOp7pOcN/ysoj0RpBPOx3HQTJTrIN8UZK+WFd3/iDeFHHqeyYxa4hQk4rMhsz9H9mXpR61IzwlBVGXtl9Q==",
             "dev": true,
             "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+                "node": "^16.0.0 || >=18.0.0"
             },
             "funding": {
                 "type": "opencollective",
@@ -985,21 +1070,21 @@
             }
         },
         "node_modules/@typescript-eslint/typescript-estree": {
-            "version": "5.62.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz",
-            "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==",
+            "version": "6.7.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.0.tgz",
+            "integrity": "sha512-dPvkXj3n6e9yd/0LfojNU8VMUGHWiLuBZvbM6V6QYD+2qxqInE7J+J/ieY2iGwR9ivf/R/haWGkIj04WVUeiSQ==",
             "dev": true,
             "dependencies": {
-                "@typescript-eslint/types": "5.62.0",
-                "@typescript-eslint/visitor-keys": "5.62.0",
+                "@typescript-eslint/types": "6.7.0",
+                "@typescript-eslint/visitor-keys": "6.7.0",
                 "debug": "^4.3.4",
                 "globby": "^11.1.0",
                 "is-glob": "^4.0.3",
-                "semver": "^7.3.7",
-                "tsutils": "^3.21.0"
+                "semver": "^7.5.4",
+                "ts-api-utils": "^1.0.1"
             },
             "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+                "node": "^16.0.0 || >=18.0.0"
             },
             "funding": {
                 "type": "opencollective",
@@ -1012,42 +1097,41 @@
             }
         },
         "node_modules/@typescript-eslint/utils": {
-            "version": "5.62.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz",
-            "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==",
+            "version": "6.7.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.7.0.tgz",
+            "integrity": "sha512-MfCq3cM0vh2slSikQYqK2Gq52gvOhe57vD2RM3V4gQRZYX4rDPnKLu5p6cm89+LJiGlwEXU8hkYxhqqEC/V3qA==",
             "dev": true,
             "dependencies": {
-                "@eslint-community/eslint-utils": "^4.2.0",
-                "@types/json-schema": "^7.0.9",
-                "@types/semver": "^7.3.12",
-                "@typescript-eslint/scope-manager": "5.62.0",
-                "@typescript-eslint/types": "5.62.0",
-                "@typescript-eslint/typescript-estree": "5.62.0",
-                "eslint-scope": "^5.1.1",
-                "semver": "^7.3.7"
+                "@eslint-community/eslint-utils": "^4.4.0",
+                "@types/json-schema": "^7.0.12",
+                "@types/semver": "^7.5.0",
+                "@typescript-eslint/scope-manager": "6.7.0",
+                "@typescript-eslint/types": "6.7.0",
+                "@typescript-eslint/typescript-estree": "6.7.0",
+                "semver": "^7.5.4"
             },
             "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+                "node": "^16.0.0 || >=18.0.0"
             },
             "funding": {
                 "type": "opencollective",
                 "url": "https://opencollective.com/typescript-eslint"
             },
             "peerDependencies": {
-                "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+                "eslint": "^7.0.0 || ^8.0.0"
             }
         },
         "node_modules/@typescript-eslint/visitor-keys": {
-            "version": "5.62.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz",
-            "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==",
+            "version": "6.7.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.0.tgz",
+            "integrity": "sha512-/C1RVgKFDmGMcVGeD8HjKv2bd72oI1KxQDeY8uc66gw9R0OK0eMq48cA+jv9/2Ag6cdrsUGySm1yzYmfz0hxwQ==",
             "dev": true,
             "dependencies": {
-                "@typescript-eslint/types": "5.62.0",
-                "eslint-visitor-keys": "^3.3.0"
+                "@typescript-eslint/types": "6.7.0",
+                "eslint-visitor-keys": "^3.4.1"
             },
             "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+                "node": "^16.0.0 || >=18.0.0"
             },
             "funding": {
                 "type": "opencollective",
@@ -1439,12 +1523,12 @@
             }
         },
         "node_modules/bcrypt": {
-            "version": "5.1.0",
-            "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz",
-            "integrity": "sha512-RHBS7HI5N5tEnGTmtR/pppX0mmDSBpQ4aCBsj7CEQfYXDcO74A8sIBYcJMuCsis2E81zDxeENYhv66oZwLiA+Q==",
+            "version": "5.1.1",
+            "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
+            "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==",
             "hasInstallScript": true,
             "dependencies": {
-                "@mapbox/node-pre-gyp": "^1.0.10",
+                "@mapbox/node-pre-gyp": "^1.0.11",
                 "node-addon-api": "^5.0.0"
             },
             "engines": {
@@ -1464,6 +1548,9 @@
             "version": "9.0.0",
             "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz",
             "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==",
+            "dev": true,
+            "optional": true,
+            "peer": true,
             "engines": {
                 "node": "*"
             }
@@ -1523,9 +1610,9 @@
             }
         },
         "node_modules/bootstrap": {
-            "version": "5.3.1",
-            "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.1.tgz",
-            "integrity": "sha512-jzwza3Yagduci2x0rr9MeFSORjcHpt0lRZukZPZQJT1Dth5qzV7XcgGqYzi39KGAVYR8QEDVoO0ubFKOxzMG+g==",
+            "version": "5.3.2",
+            "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.2.tgz",
+            "integrity": "sha512-D32nmNWiQHo94BKHLmOrdjlL05q1c8oxbtBphQFb9Z5to6eGRDCm0QgeaZ4zFBHzfg2++rqa2JkqCcxDy0sH0g==",
             "funding": [
                 {
                     "type": "github",
@@ -1541,9 +1628,9 @@
             }
         },
         "node_modules/bootstrap-icons": {
-            "version": "1.10.5",
-            "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.10.5.tgz",
-            "integrity": "sha512-oSX26F37V7QV7NCE53PPEL45d7EGXmBgHG3pDpZvcRaKVzWMqIRL9wcqJUyEha1esFtM3NJzvmxFXDxjJYD0jQ==",
+            "version": "1.11.0",
+            "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.11.0.tgz",
+            "integrity": "sha512-bLTbtACfUqwZf6f/xUYUb7bTRZC68QaQwwy9h1b96NPKfnwqzSatHqDypW6R2CBW7zUE7lP+O93GdZuPY3RIHA==",
             "funding": [
                 {
                     "type": "github",
@@ -1651,6 +1738,7 @@
             "version": "1.0.2",
             "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
             "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+            "dev": true,
             "dependencies": {
                 "function-bind": "^1.1.1",
                 "get-intrinsic": "^1.0.2"
@@ -1929,7 +2017,10 @@
         "node_modules/core-util-is": {
             "version": "1.0.3",
             "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
-            "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
+            "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+            "dev": true,
+            "optional": true,
+            "peer": true
         },
         "node_modules/cors": {
             "version": "2.8.5",
@@ -2406,16 +2497,16 @@
             }
         },
         "node_modules/eslint": {
-            "version": "8.46.0",
-            "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.46.0.tgz",
-            "integrity": "sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg==",
+            "version": "8.49.0",
+            "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz",
+            "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==",
             "dev": true,
             "dependencies": {
                 "@eslint-community/eslint-utils": "^4.2.0",
                 "@eslint-community/regexpp": "^4.6.1",
-                "@eslint/eslintrc": "^2.1.1",
-                "@eslint/js": "^8.46.0",
-                "@humanwhocodes/config-array": "^0.11.10",
+                "@eslint/eslintrc": "^2.1.2",
+                "@eslint/js": "8.49.0",
+                "@humanwhocodes/config-array": "^0.11.11",
                 "@humanwhocodes/module-importer": "^1.0.1",
                 "@nodelib/fs.walk": "^1.2.8",
                 "ajv": "^6.12.4",
@@ -2425,7 +2516,7 @@
                 "doctrine": "^3.0.0",
                 "escape-string-regexp": "^4.0.0",
                 "eslint-scope": "^7.2.2",
-                "eslint-visitor-keys": "^3.4.2",
+                "eslint-visitor-keys": "^3.4.3",
                 "espree": "^9.6.1",
                 "esquery": "^1.4.2",
                 "esutils": "^2.0.2",
@@ -2460,20 +2551,20 @@
             }
         },
         "node_modules/eslint-config-next": {
-            "version": "13.4.7",
-            "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-13.4.7.tgz",
-            "integrity": "sha512-+IRAyD0+J1MZaTi9RQMPUfr6Q+GCZ1wOkK6XM52Vokh7VI4R6YFGOFzdkEFHl4ZyIX4FKa5vcwUP2WscSFNjNQ==",
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-13.4.19.tgz",
+            "integrity": "sha512-WE8367sqMnjhWHvR5OivmfwENRQ1ixfNE9hZwQqNCsd+iM3KnuMc1V8Pt6ytgjxjf23D+xbesADv9x3xaKfT3g==",
             "dev": true,
             "dependencies": {
-                "@next/eslint-plugin-next": "13.4.7",
+                "@next/eslint-plugin-next": "13.4.19",
                 "@rushstack/eslint-patch": "^1.1.3",
-                "@typescript-eslint/parser": "^5.42.0",
+                "@typescript-eslint/parser": "^5.4.2 || ^6.0.0",
                 "eslint-import-resolver-node": "^0.3.6",
                 "eslint-import-resolver-typescript": "^3.5.2",
                 "eslint-plugin-import": "^2.26.0",
                 "eslint-plugin-jsx-a11y": "^6.5.1",
                 "eslint-plugin-react": "^7.31.7",
-                "eslint-plugin-react-hooks": "^4.5.0"
+                "eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705"
             },
             "peerDependencies": {
                 "eslint": "^7.23.0 || ^8.0.0",
@@ -2486,9 +2577,9 @@
             }
         },
         "node_modules/eslint-config-prettier": {
-            "version": "8.10.0",
-            "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz",
-            "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==",
+            "version": "9.0.0",
+            "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz",
+            "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==",
             "dev": true,
             "bin": {
                 "eslint-config-prettier": "bin/cli.js"
@@ -2813,32 +2904,10 @@
                 "eslint": ">=5.0.0"
             }
         },
-        "node_modules/eslint-scope": {
-            "version": "5.1.1",
-            "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
-            "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
-            "dev": true,
-            "dependencies": {
-                "esrecurse": "^4.3.0",
-                "estraverse": "^4.1.1"
-            },
-            "engines": {
-                "node": ">=8.0.0"
-            }
-        },
-        "node_modules/eslint-scope/node_modules/estraverse": {
-            "version": "4.3.0",
-            "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
-            "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
-            "dev": true,
-            "engines": {
-                "node": ">=4.0"
-            }
-        },
         "node_modules/eslint-visitor-keys": {
-            "version": "3.4.2",
-            "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz",
-            "integrity": "sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==",
+            "version": "3.4.3",
+            "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+            "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
             "dev": true,
             "engines": {
                 "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -3124,14 +3193,13 @@
             }
         },
         "node_modules/formidable": {
-            "version": "2.1.2",
-            "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz",
-            "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==",
+            "version": "3.5.1",
+            "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.1.tgz",
+            "integrity": "sha512-WJWKelbRHN41m5dumb0/k8TeAx7Id/y3a+Z7QfhxP/htI9Js5zYaEDtG8uMgG0vM0lOlqnmjE99/kfpOYi/0Og==",
             "dependencies": {
                 "dezalgo": "^1.0.4",
                 "hexoid": "^1.0.0",
-                "once": "^1.4.0",
-                "qs": "^6.11.0"
+                "once": "^1.4.0"
             },
             "funding": {
                 "url": "https://ko-fi.com/tunnckoCore/commissions"
@@ -3172,7 +3240,8 @@
         "node_modules/function-bind": {
             "version": "1.1.1",
             "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
-            "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+            "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+            "dev": true
         },
         "node_modules/function.prototype.name": {
             "version": "1.1.5",
@@ -3233,6 +3302,7 @@
             "version": "1.2.1",
             "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
             "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
+            "dev": true,
             "dependencies": {
                 "function-bind": "^1.1.1",
                 "has": "^1.0.3",
@@ -3340,9 +3410,9 @@
             "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="
         },
         "node_modules/globals": {
-            "version": "13.20.0",
-            "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
-            "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
+            "version": "13.21.0",
+            "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz",
+            "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==",
             "dev": true,
             "dependencies": {
                 "type-fest": "^0.20.2"
@@ -3490,6 +3560,7 @@
             "version": "1.0.3",
             "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
             "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+            "dev": true,
             "dependencies": {
                 "function-bind": "^1.1.1"
             },
@@ -3530,6 +3601,7 @@
             "version": "1.0.1",
             "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
             "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+            "dev": true,
             "engines": {
                 "node": ">= 0.4"
             },
@@ -3541,6 +3613,7 @@
             "version": "1.0.3",
             "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
             "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+            "dev": true,
             "engines": {
                 "node": ">= 0.4"
             },
@@ -4119,7 +4192,10 @@
         "node_modules/isarray": {
             "version": "1.0.0",
             "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
-            "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
+            "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+            "dev": true,
+            "optional": true,
+            "peer": true
         },
         "node_modules/isexe": {
             "version": "2.0.0",
@@ -4169,14 +4245,20 @@
             }
         },
         "node_modules/jsonwebtoken": {
-            "version": "9.0.1",
-            "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz",
-            "integrity": "sha512-K8wx7eJ5TPvEjuiVSkv167EVboBDv9PZdDoF7BgeQnBLVvZWW9clr2PsQHVJDTKaEIH5JBIwHujGcHp7GgI2eg==",
+            "version": "9.0.2",
+            "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
+            "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
             "dependencies": {
                 "jws": "^3.2.2",
-                "lodash": "^4.17.21",
+                "lodash.includes": "^4.3.0",
+                "lodash.isboolean": "^3.0.3",
+                "lodash.isinteger": "^4.0.4",
+                "lodash.isnumber": "^3.0.3",
+                "lodash.isplainobject": "^4.0.6",
+                "lodash.isstring": "^4.0.1",
+                "lodash.once": "^4.0.0",
                 "ms": "^2.1.1",
-                "semver": "^7.3.8"
+                "semver": "^7.5.4"
             },
             "engines": {
                 "node": ">=12",
@@ -4341,11 +4423,46 @@
             "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
             "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
         },
+        "node_modules/lodash.includes": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+            "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
+        },
+        "node_modules/lodash.isboolean": {
+            "version": "3.0.3",
+            "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+            "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
+        },
+        "node_modules/lodash.isinteger": {
+            "version": "4.0.4",
+            "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+            "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
+        },
+        "node_modules/lodash.isnumber": {
+            "version": "3.0.3",
+            "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+            "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
+        },
+        "node_modules/lodash.isplainobject": {
+            "version": "4.0.6",
+            "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+            "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
+        },
+        "node_modules/lodash.isstring": {
+            "version": "4.0.1",
+            "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+            "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
+        },
         "node_modules/lodash.merge": {
             "version": "4.6.2",
             "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
             "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
         },
+        "node_modules/lodash.once": {
+            "version": "4.1.1",
+            "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+            "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
+        },
         "node_modules/loose-envify": {
             "version": "1.4.0",
             "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -4367,11 +4484,11 @@
             }
         },
         "node_modules/lru-cache": {
-            "version": "7.18.3",
-            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
-            "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+            "version": "10.0.1",
+            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz",
+            "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==",
             "engines": {
-                "node": ">=12"
+                "node": "14 || >=16.14"
             }
         },
         "node_modules/make-dir": {
@@ -4403,15 +4520,15 @@
             "dev": true
         },
         "node_modules/mariadb": {
-            "version": "3.2.0",
-            "resolved": "https://registry.npmjs.org/mariadb/-/mariadb-3.2.0.tgz",
-            "integrity": "sha512-IH2nidQat1IBMxP5gjuNxG6dADtz1PESEC6rKrcATen5v3ngFyZITjehyYiwNfz3zUNQupfYmVntz93M+Pz8pQ==",
+            "version": "3.2.1",
+            "resolved": "https://registry.npmjs.org/mariadb/-/mariadb-3.2.1.tgz",
+            "integrity": "sha512-EWCsvu8IwK5WRYPJ9iRekFiBfHfKB/wBcUwAzbhQKhSc6ivbvLcO/cktkYTCz3HVoGNYVWhBN38485Cq3qMSbQ==",
             "dependencies": {
                 "@types/geojson": "^7946.0.10",
                 "@types/node": "^17.0.45",
                 "denque": "^2.1.0",
                 "iconv-lite": "^0.6.3",
-                "lru-cache": "^7.14.0"
+                "lru-cache": "^10.0.1"
             },
             "engines": {
                 "node": ">= 12"
@@ -4603,6 +4720,9 @@
             "version": "2.18.1",
             "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz",
             "integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==",
+            "dev": true,
+            "optional": true,
+            "peer": true,
             "dependencies": {
                 "bignumber.js": "9.0.0",
                 "readable-stream": "2.3.7",
@@ -4616,7 +4736,10 @@
         "node_modules/mysql/node_modules/safe-buffer": {
             "version": "5.1.2",
             "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
-            "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+            "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+            "dev": true,
+            "optional": true,
+            "peer": true
         },
         "node_modules/nan": {
             "version": "2.17.0",
@@ -4651,12 +4774,6 @@
             "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
             "dev": true
         },
-        "node_modules/natural-compare-lite": {
-            "version": "1.4.0",
-            "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
-            "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
-            "dev": true
-        },
         "node_modules/needle": {
             "version": "2.9.1",
             "resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz",
@@ -4707,11 +4824,11 @@
             "dev": true
         },
         "node_modules/next": {
-            "version": "13.4.7",
-            "resolved": "https://registry.npmjs.org/next/-/next-13.4.7.tgz",
-            "integrity": "sha512-M8z3k9VmG51SRT6v5uDKdJXcAqLzP3C+vaKfLIAM0Mhx1um1G7MDnO63+m52qPdZfrTFzMZNzfsgvm3ghuVHIQ==",
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/next/-/next-13.4.19.tgz",
+            "integrity": "sha512-HuPSzzAbJ1T4BD8e0bs6B9C1kWQ6gv8ykZoRWs5AQoiIuqbGHHdQO7Ljuvg05Q0Z24E2ABozHe6FxDvI6HfyAw==",
             "dependencies": {
-                "@next/env": "13.4.7",
+                "@next/env": "13.4.19",
                 "@swc/helpers": "0.5.1",
                 "busboy": "1.6.0",
                 "caniuse-lite": "^1.0.30001406",
@@ -4727,19 +4844,18 @@
                 "node": ">=16.8.0"
             },
             "optionalDependencies": {
-                "@next/swc-darwin-arm64": "13.4.7",
-                "@next/swc-darwin-x64": "13.4.7",
-                "@next/swc-linux-arm64-gnu": "13.4.7",
-                "@next/swc-linux-arm64-musl": "13.4.7",
-                "@next/swc-linux-x64-gnu": "13.4.7",
-                "@next/swc-linux-x64-musl": "13.4.7",
-                "@next/swc-win32-arm64-msvc": "13.4.7",
-                "@next/swc-win32-ia32-msvc": "13.4.7",
-                "@next/swc-win32-x64-msvc": "13.4.7"
+                "@next/swc-darwin-arm64": "13.4.19",
+                "@next/swc-darwin-x64": "13.4.19",
+                "@next/swc-linux-arm64-gnu": "13.4.19",
+                "@next/swc-linux-arm64-musl": "13.4.19",
+                "@next/swc-linux-x64-gnu": "13.4.19",
+                "@next/swc-linux-x64-musl": "13.4.19",
+                "@next/swc-win32-arm64-msvc": "13.4.19",
+                "@next/swc-win32-ia32-msvc": "13.4.19",
+                "@next/swc-win32-x64-msvc": "13.4.19"
             },
             "peerDependencies": {
                 "@opentelemetry/api": "^1.1.0",
-                "fibers": ">= 3.1.0",
                 "react": "^18.2.0",
                 "react-dom": "^18.2.0",
                 "sass": "^1.3.0"
@@ -4748,9 +4864,6 @@
                 "@opentelemetry/api": {
                     "optional": true
                 },
-                "fibers": {
-                    "optional": true
-                },
                 "sass": {
                     "optional": true
                 }
@@ -4802,9 +4915,9 @@
             }
         },
         "node_modules/nodemailer": {
-            "version": "6.9.4",
-            "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.4.tgz",
-            "integrity": "sha512-CXjQvrQZV4+6X5wP6ZIgdehJamI63MFoYFGGPtHudWym9qaEHDNdPzaj5bfMCvxG1vhAileSWW90q7nL0N36mA==",
+            "version": "6.9.5",
+            "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.5.tgz",
+            "integrity": "sha512-/dmdWo62XjumuLc5+AYQZeiRj+PRR8y8qKtFCOyuOl1k/hckZd8durUUHs/ucKx6/8kN+wFxqKJlQ/LK/qR5FA==",
             "engines": {
                 "node": ">=6.0.0"
             }
@@ -4881,6 +4994,7 @@
             "version": "1.12.3",
             "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
             "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
+            "dev": true,
             "funding": {
                 "url": "https://github.com/sponsors/ljharb"
             }
@@ -5370,7 +5484,10 @@
         "node_modules/process-nextick-args": {
             "version": "2.0.1",
             "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
-            "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
+            "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+            "dev": true,
+            "optional": true,
+            "peer": true
         },
         "node_modules/prop-types": {
             "version": "15.8.1",
@@ -5428,20 +5545,6 @@
                 "node": ">=6.0.0"
             }
         },
-        "node_modules/qs": {
-            "version": "6.11.2",
-            "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz",
-            "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==",
-            "dependencies": {
-                "side-channel": "^1.0.4"
-            },
-            "engines": {
-                "node": ">=0.6"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
         "node_modules/queue-microtask": {
             "version": "1.2.3",
             "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -5605,6 +5708,9 @@
             "version": "2.3.7",
             "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
             "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+            "dev": true,
+            "optional": true,
+            "peer": true,
             "dependencies": {
                 "core-util-is": "~1.0.0",
                 "inherits": "~2.0.3",
@@ -5618,7 +5724,10 @@
         "node_modules/readable-stream/node_modules/safe-buffer": {
             "version": "5.1.2",
             "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
-            "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+            "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+            "dev": true,
+            "optional": true,
+            "peer": true
         },
         "node_modules/readdirp": {
             "version": "3.6.0",
@@ -5923,9 +6032,9 @@
             "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
         },
         "node_modules/sass": {
-            "version": "1.64.2",
-            "resolved": "https://registry.npmjs.org/sass/-/sass-1.64.2.tgz",
-            "integrity": "sha512-TnDlfc+CRnUAgLO9D8cQLFu/GIjJIzJCGkE7o4ekIGQOH7T3GetiRR/PsTWJUHhkzcSPrARkPI+gNWn5alCzDg==",
+            "version": "1.67.0",
+            "resolved": "https://registry.npmjs.org/sass/-/sass-1.67.0.tgz",
+            "integrity": "sha512-SVrO9ZeX/QQyEGtuZYCVxoeAL5vGlYjJ9p4i4HFuekWl8y/LtJ7tJc10Z+ck1c8xOuoBm2MYzcLfTAffD0pl/A==",
             "dependencies": {
                 "chokidar": ">=3.0.0 <4.0.0",
                 "immutable": "^4.0.0",
@@ -6005,9 +6114,9 @@
             "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
         },
         "node_modules/sharp": {
-            "version": "0.32.4",
-            "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.4.tgz",
-            "integrity": "sha512-exUnZewqVZC6UXqXuQ8fyJJv0M968feBi04jb9GcUHrWtkRoAKnbJt8IfwT4NJs7FskArbJ14JAFGVuooszoGg==",
+            "version": "0.32.5",
+            "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.5.tgz",
+            "integrity": "sha512-0dap3iysgDkNaPOaOL4X/0akdu0ma62GcdC2NBQ+93eqpePdDdr2/LM0sFdDSMmN7yS+odyZtPsb7tx/cYBKnQ==",
             "hasInstallScript": true,
             "dependencies": {
                 "color": "^4.2.3",
@@ -6105,6 +6214,7 @@
             "version": "1.0.4",
             "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
             "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+            "dev": true,
             "dependencies": {
                 "call-bind": "^1.0.0",
                 "get-intrinsic": "^1.0.2",
@@ -6267,14 +6377,17 @@
             }
         },
         "node_modules/sprintf-js": {
-            "version": "1.1.2",
-            "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
-            "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug=="
+            "version": "1.1.3",
+            "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
+            "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="
         },
         "node_modules/sqlstring": {
             "version": "2.3.1",
             "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz",
             "integrity": "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ==",
+            "dev": true,
+            "optional": true,
+            "peer": true,
             "engines": {
                 "node": ">= 0.6"
             }
@@ -6510,10 +6623,11 @@
             }
         },
         "node_modules/swr": {
-            "version": "2.2.0",
-            "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.0.tgz",
-            "integrity": "sha512-AjqHOv2lAhkuUdIiBu9xbuettzAzWXmCEcLONNKJRba87WAefz8Ca9d6ds/SzrPc235n1IxWYdhJ2zF3MNUaoQ==",
+            "version": "2.2.2",
+            "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.2.tgz",
+            "integrity": "sha512-CbR41AoMD4TQBQw9ic3GTXspgfM9Y8Mdhb5Ob4uIKXhWqnRLItwA5fpGvB7SmSw3+zEjb0PdhiEumtUvYoQ+bQ==",
             "dependencies": {
+                "client-only": "^0.0.1",
                 "use-sync-external-store": "^1.2.0"
             },
             "peerDependencies": {
@@ -6650,6 +6764,18 @@
                 "tree-kill": "cli.js"
             }
         },
+        "node_modules/ts-api-utils": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz",
+            "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==",
+            "dev": true,
+            "engines": {
+                "node": ">=16.13.0"
+            },
+            "peerDependencies": {
+                "typescript": ">=4.2.0"
+            }
+        },
         "node_modules/ts-node": {
             "version": "10.9.1",
             "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
@@ -6775,30 +6901,9 @@
             }
         },
         "node_modules/tslib": {
-            "version": "2.6.1",
-            "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz",
-            "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig=="
-        },
-        "node_modules/tsutils": {
-            "version": "3.21.0",
-            "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
-            "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
-            "dev": true,
-            "dependencies": {
-                "tslib": "^1.8.1"
-            },
-            "engines": {
-                "node": ">= 6"
-            },
-            "peerDependencies": {
-                "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
-            }
-        },
-        "node_modules/tsutils/node_modules/tslib": {
-            "version": "1.14.1",
-            "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
-            "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
-            "dev": true
+            "version": "2.6.2",
+            "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+            "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
         },
         "node_modules/tunnel-agent": {
             "version": "0.6.0",
@@ -6901,9 +7006,9 @@
             }
         },
         "node_modules/typescript": {
-            "version": "5.1.6",
-            "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
-            "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
+            "version": "5.2.2",
+            "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
+            "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
             "dev": true,
             "bin": {
                 "tsc": "bin/tsc",
@@ -7353,9 +7458,9 @@
             "dev": true
         },
         "@eslint/eslintrc": {
-            "version": "2.1.1",
-            "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.1.tgz",
-            "integrity": "sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA==",
+            "version": "2.1.2",
+            "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz",
+            "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==",
             "dev": true,
             "requires": {
                 "ajv": "^6.12.4",
@@ -7370,15 +7475,15 @@
             }
         },
         "@eslint/js": {
-            "version": "8.46.0",
-            "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.46.0.tgz",
-            "integrity": "sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA==",
+            "version": "8.49.0",
+            "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz",
+            "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==",
             "dev": true
         },
         "@humanwhocodes/config-array": {
-            "version": "0.11.10",
-            "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
-            "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==",
+            "version": "0.11.11",
+            "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz",
+            "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==",
             "dev": true,
             "requires": {
                 "@humanwhocodes/object-schema": "^1.2.1",
@@ -7437,37 +7542,79 @@
             }
         },
         "@next/bundle-analyzer": {
-            "version": "13.4.7",
-            "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-13.4.7.tgz",
-            "integrity": "sha512-cIcU07u6WdDUF1nrFYOSPvx/pie8euxSnWB1Y/XMYj2g5ls+iyaDr/ChyZugD5hNW754x0NFy1CwzpObA0bffQ==",
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-13.4.19.tgz",
+            "integrity": "sha512-nXKHz63dM0Kn3XPFOKrv2wK+hP9rdBg2iR1PsxuXLHVBoZhMyS1/ldRcX80YFsm2VUws9zM4a0E/1HlLI+P92g==",
             "requires": {
                 "webpack-bundle-analyzer": "4.7.0"
             }
         },
         "@next/env": {
-            "version": "13.4.7",
-            "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.7.tgz",
-            "integrity": "sha512-ZlbiFulnwiFsW9UV1ku1OvX/oyIPLtMk9p/nnvDSwI0s7vSoZdRtxXNsaO+ZXrLv/pMbXVGq4lL8TbY9iuGmVw=="
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.19.tgz",
+            "integrity": "sha512-FsAT5x0jF2kkhNkKkukhsyYOrRqtSxrEhfliniIq0bwWbuXLgyt3Gv0Ml+b91XwjwArmuP7NxCiGd++GGKdNMQ=="
         },
         "@next/eslint-plugin-next": {
-            "version": "13.4.7",
-            "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-13.4.7.tgz",
-            "integrity": "sha512-ANEPltxzXbyyG7CvqxdY4PmeM5+RyWdAJGufTHnU+LA/i3J6IDV2r8Z4onKwskwKEhwqzz5lMaSYGGXLyHX+mg==",
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-13.4.19.tgz",
+            "integrity": "sha512-N/O+zGb6wZQdwu6atMZHbR7T9Np5SUFUjZqCbj0sXm+MwQO35M8TazVB4otm87GkXYs2l6OPwARd3/PUWhZBVQ==",
             "dev": true,
             "requires": {
                 "glob": "7.1.7"
             }
         },
+        "@next/swc-darwin-arm64": {
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.19.tgz",
+            "integrity": "sha512-vv1qrjXeGbuF2mOkhkdxMDtv9np7W4mcBtaDnHU+yJG+bBwa6rYsYSCI/9Xm5+TuF5SbZbrWO6G1NfTh1TMjvQ==",
+            "optional": true
+        },
+        "@next/swc-darwin-x64": {
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.19.tgz",
+            "integrity": "sha512-jyzO6wwYhx6F+7gD8ddZfuqO4TtpJdw3wyOduR4fxTUCm3aLw7YmHGYNjS0xRSYGAkLpBkH1E0RcelyId6lNsw==",
+            "optional": true
+        },
+        "@next/swc-linux-arm64-gnu": {
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.19.tgz",
+            "integrity": "sha512-vdlnIlaAEh6H+G6HrKZB9c2zJKnpPVKnA6LBwjwT2BTjxI7e0Hx30+FoWCgi50e+YO49p6oPOtesP9mXDRiiUg==",
+            "optional": true
+        },
+        "@next/swc-linux-arm64-musl": {
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.19.tgz",
+            "integrity": "sha512-aU0HkH2XPgxqrbNRBFb3si9Ahu/CpaR5RPmN2s9GiM9qJCiBBlZtRTiEca+DC+xRPyCThTtWYgxjWHgU7ZkyvA==",
+            "optional": true
+        },
         "@next/swc-linux-x64-gnu": {
-            "version": "13.4.7",
-            "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.7.tgz",
-            "integrity": "sha512-zaEC+iEiAHNdhl6fuwl0H0shnTzQoAoJiDYBUze8QTntE/GNPfTYpYboxF5LRYIjBwETUatvE0T64W6SKDipvg==",
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.19.tgz",
+            "integrity": "sha512-htwOEagMa/CXNykFFeAHHvMJeqZfNQEoQvHfsA4wgg5QqGNqD5soeCer4oGlCol6NGUxknrQO6VEustcv+Md+g==",
             "optional": true
         },
         "@next/swc-linux-x64-musl": {
-            "version": "13.4.7",
-            "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.7.tgz",
-            "integrity": "sha512-X6r12F8d8SKAtYJqLZBBMIwEqcTRvUdVm+xIq+l6pJqlgT2tNsLLf2i5Cl88xSsIytBICGsCNNHd+siD2fbWBA==",
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.19.tgz",
+            "integrity": "sha512-4Gj4vvtbK1JH8ApWTT214b3GwUh9EKKQjY41hH/t+u55Knxi/0wesMzwQRhppK6Ddalhu0TEttbiJ+wRcoEj5Q==",
+            "optional": true
+        },
+        "@next/swc-win32-arm64-msvc": {
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.19.tgz",
+            "integrity": "sha512-bUfDevQK4NsIAHXs3/JNgnvEY+LRyneDN788W2NYiRIIzmILjba7LaQTfihuFawZDhRtkYCv3JDC3B4TwnmRJw==",
+            "optional": true
+        },
+        "@next/swc-win32-ia32-msvc": {
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.19.tgz",
+            "integrity": "sha512-Y5kikILFAr81LYIFaw6j/NrOtmiM4Sf3GtOc0pn50ez2GCkr+oejYuKGcwAwq3jiTKuzF6OF4iT2INPoxRycEA==",
+            "optional": true
+        },
+        "@next/swc-win32-x64-msvc": {
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.19.tgz",
+            "integrity": "sha512-YzA78jBDXMYiINdPdJJwGgPNT3YqBNNGhsthsDoWHL9p24tEJn9ViQf/ZqTbwSpX/RrkPupLfuuTH2sf73JBAw==",
             "optional": true
         },
         "@nodelib/fs.scandir": {
@@ -7715,9 +7862,9 @@
             "integrity": "sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g=="
         },
         "@types/cookie-parser": {
-            "version": "1.4.3",
-            "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.3.tgz",
-            "integrity": "sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==",
+            "version": "1.4.4",
+            "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.4.tgz",
+            "integrity": "sha512-Var+aj5I6ZgIqsQ05N2V8q5OBrFfZXtIGWWDSrEYLIbMw758obagSwdGcLCjwh1Ga7M7+wj0SDIAaAC/WT7aaA==",
             "dev": true,
             "requires": {
                 "@types/express": "*"
@@ -7759,15 +7906,6 @@
                 "@types/serve-static": "*"
             }
         },
-        "@types/express-mysql-session": {
-            "version": "2.1.3",
-            "resolved": "https://registry.npmjs.org/@types/express-mysql-session/-/express-mysql-session-2.1.3.tgz",
-            "integrity": "sha512-Df6DqYTwvdH+L91Mn03Gv1qqFUUJueKb5fiFY+GbimwOKQuT7m9a5Ao8O7UZ86Cbri7S7yWFmw+fK7o4OP+voQ==",
-            "dev": true,
-            "requires": {
-                "@types/express-session": "*"
-            }
-        },
         "@types/express-serve-static-core": {
             "version": "4.17.35",
             "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz",
@@ -7779,19 +7917,10 @@
                 "@types/send": "*"
             }
         },
-        "@types/express-session": {
-            "version": "1.17.7",
-            "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.7.tgz",
-            "integrity": "sha512-L25080PBYoRLu472HY/HNCxaXY8AaGgqGC8/p/8+BYMhG0RDOLQ1wpXOpAzr4Gi5TGozTKyJv5BVODM5UNyVMw==",
-            "dev": true,
-            "requires": {
-                "@types/express": "*"
-            }
-        },
         "@types/formidable": {
-            "version": "2.0.6",
-            "resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-2.0.6.tgz",
-            "integrity": "sha512-L4HcrA05IgQyNYJj6kItuIkXrInJvsXTPC5B1i64FggWKKqSL+4hgt7asiSNva75AoLQjq29oPxFfU4GAQ6Z2w==",
+            "version": "3.4.3",
+            "resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-3.4.3.tgz",
+            "integrity": "sha512-FRB5KNIYgCLDvw/ho7daBVqdQRG9Ptk1tMlZ2B5ibAjxDfXFkO6fJaP+ojmS8ljqmnC5If9u24WdWcVPaN3thg==",
             "dev": true,
             "requires": {
                 "@types/node": "*"
@@ -7876,14 +8005,14 @@
             }
         },
         "@types/node": {
-            "version": "18.17.3",
-            "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.3.tgz",
-            "integrity": "sha512-2x8HWtFk0S99zqVQABU9wTpr8wPoaDHZUcAkoTKH+nL7kPv3WUI9cRi/Kk5Mz4xdqXSqTkKP7IWNoQQYCnDsTA=="
+            "version": "20.6.0",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.0.tgz",
+            "integrity": "sha512-najjVq5KN2vsH2U/xyh2opaSEz6cZMR2SetLIlxlj08nOcmPOemJmUK2o4kUzfLqfrWE0PIrNeE16XhYDd3nqg=="
         },
         "@types/nodemailer": {
-            "version": "6.4.9",
-            "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.9.tgz",
-            "integrity": "sha512-XYG8Gv+sHjaOtUpiuytahMy2mM3rectgroNbs6R3djZEKmPNiIJwe9KqOJBGzKKnNZNKvnuvmugBgpq3w/S0ig==",
+            "version": "6.4.10",
+            "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.10.tgz",
+            "integrity": "sha512-oPW/IdhkU3FyZc1dzeqmS+MBjrjZNiiINnrEOrWALzccJlP5xTlbkNr2YnTnnyj9Eqm5ofjRoASEbrCYpA7BrA==",
             "dev": true,
             "requires": {
                 "@types/node": "*"
@@ -7905,9 +8034,9 @@
             "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw=="
         },
         "@types/react": {
-            "version": "18.2.19",
-            "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.19.tgz",
-            "integrity": "sha512-e2S8wmY1ePfM517PqCG80CcE48Xs5k0pwJzuDZsfE8IZRRBfOMCF+XqnFxu6mWtyivum1MQm4aco+WIt6Coimw==",
+            "version": "18.2.21",
+            "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.21.tgz",
+            "integrity": "sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==",
             "requires": {
                 "@types/prop-types": "*",
                 "@types/scheduler": "*",
@@ -7928,9 +8057,9 @@
             "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ=="
         },
         "@types/semver": {
-            "version": "7.5.0",
-            "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz",
-            "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==",
+            "version": "7.5.2",
+            "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.2.tgz",
+            "integrity": "sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw==",
             "dev": true
         },
         "@types/send": {
@@ -7982,102 +8111,103 @@
             "dev": true
         },
         "@typescript-eslint/eslint-plugin": {
-            "version": "5.62.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
-            "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
+            "version": "6.7.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.0.tgz",
+            "integrity": "sha512-gUqtknHm0TDs1LhY12K2NA3Rmlmp88jK9Tx8vGZMfHeNMLE3GH2e9TRub+y+SOjuYgtOmok+wt1AyDPZqxbNag==",
             "dev": true,
             "requires": {
-                "@eslint-community/regexpp": "^4.4.0",
-                "@typescript-eslint/scope-manager": "5.62.0",
-                "@typescript-eslint/type-utils": "5.62.0",
-                "@typescript-eslint/utils": "5.62.0",
+                "@eslint-community/regexpp": "^4.5.1",
+                "@typescript-eslint/scope-manager": "6.7.0",
+                "@typescript-eslint/type-utils": "6.7.0",
+                "@typescript-eslint/utils": "6.7.0",
+                "@typescript-eslint/visitor-keys": "6.7.0",
                 "debug": "^4.3.4",
                 "graphemer": "^1.4.0",
-                "ignore": "^5.2.0",
-                "natural-compare-lite": "^1.4.0",
-                "semver": "^7.3.7",
-                "tsutils": "^3.21.0"
+                "ignore": "^5.2.4",
+                "natural-compare": "^1.4.0",
+                "semver": "^7.5.4",
+                "ts-api-utils": "^1.0.1"
             }
         },
         "@typescript-eslint/parser": {
-            "version": "5.62.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
-            "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
+            "version": "6.7.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.0.tgz",
+            "integrity": "sha512-jZKYwqNpNm5kzPVP5z1JXAuxjtl2uG+5NpaMocFPTNC2EdYIgbXIPImObOkhbONxtFTTdoZstLZefbaK+wXZng==",
             "dev": true,
             "requires": {
-                "@typescript-eslint/scope-manager": "5.62.0",
-                "@typescript-eslint/types": "5.62.0",
-                "@typescript-eslint/typescript-estree": "5.62.0",
+                "@typescript-eslint/scope-manager": "6.7.0",
+                "@typescript-eslint/types": "6.7.0",
+                "@typescript-eslint/typescript-estree": "6.7.0",
+                "@typescript-eslint/visitor-keys": "6.7.0",
                 "debug": "^4.3.4"
             }
         },
         "@typescript-eslint/scope-manager": {
-            "version": "5.62.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
-            "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==",
+            "version": "6.7.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.7.0.tgz",
+            "integrity": "sha512-lAT1Uau20lQyjoLUQ5FUMSX/dS07qux9rYd5FGzKz/Kf8W8ccuvMyldb8hadHdK/qOI7aikvQWqulnEq2nCEYA==",
             "dev": true,
             "requires": {
-                "@typescript-eslint/types": "5.62.0",
-                "@typescript-eslint/visitor-keys": "5.62.0"
+                "@typescript-eslint/types": "6.7.0",
+                "@typescript-eslint/visitor-keys": "6.7.0"
             }
         },
         "@typescript-eslint/type-utils": {
-            "version": "5.62.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz",
-            "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==",
+            "version": "6.7.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.7.0.tgz",
+            "integrity": "sha512-f/QabJgDAlpSz3qduCyQT0Fw7hHpmhOzY/Rv6zO3yO+HVIdPfIWhrQoAyG+uZVtWAIS85zAyzgAFfyEr+MgBpg==",
             "dev": true,
             "requires": {
-                "@typescript-eslint/typescript-estree": "5.62.0",
-                "@typescript-eslint/utils": "5.62.0",
+                "@typescript-eslint/typescript-estree": "6.7.0",
+                "@typescript-eslint/utils": "6.7.0",
                 "debug": "^4.3.4",
-                "tsutils": "^3.21.0"
+                "ts-api-utils": "^1.0.1"
             }
         },
         "@typescript-eslint/types": {
-            "version": "5.62.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
-            "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==",
+            "version": "6.7.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.0.tgz",
+            "integrity": "sha512-ihPfvOp7pOcN/ysoj0RpBPOx3HQTJTrIN8UZK+WFd3/iDeFHHqeyYxa4hQk4rMhsz9H9mXpR61IzwlBVGXtl9Q==",
             "dev": true
         },
         "@typescript-eslint/typescript-estree": {
-            "version": "5.62.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz",
-            "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==",
+            "version": "6.7.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.0.tgz",
+            "integrity": "sha512-dPvkXj3n6e9yd/0LfojNU8VMUGHWiLuBZvbM6V6QYD+2qxqInE7J+J/ieY2iGwR9ivf/R/haWGkIj04WVUeiSQ==",
             "dev": true,
             "requires": {
-                "@typescript-eslint/types": "5.62.0",
-                "@typescript-eslint/visitor-keys": "5.62.0",
+                "@typescript-eslint/types": "6.7.0",
+                "@typescript-eslint/visitor-keys": "6.7.0",
                 "debug": "^4.3.4",
                 "globby": "^11.1.0",
                 "is-glob": "^4.0.3",
-                "semver": "^7.3.7",
-                "tsutils": "^3.21.0"
+                "semver": "^7.5.4",
+                "ts-api-utils": "^1.0.1"
             }
         },
         "@typescript-eslint/utils": {
-            "version": "5.62.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz",
-            "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==",
+            "version": "6.7.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.7.0.tgz",
+            "integrity": "sha512-MfCq3cM0vh2slSikQYqK2Gq52gvOhe57vD2RM3V4gQRZYX4rDPnKLu5p6cm89+LJiGlwEXU8hkYxhqqEC/V3qA==",
             "dev": true,
             "requires": {
-                "@eslint-community/eslint-utils": "^4.2.0",
-                "@types/json-schema": "^7.0.9",
-                "@types/semver": "^7.3.12",
-                "@typescript-eslint/scope-manager": "5.62.0",
-                "@typescript-eslint/types": "5.62.0",
-                "@typescript-eslint/typescript-estree": "5.62.0",
-                "eslint-scope": "^5.1.1",
-                "semver": "^7.3.7"
+                "@eslint-community/eslint-utils": "^4.4.0",
+                "@types/json-schema": "^7.0.12",
+                "@types/semver": "^7.5.0",
+                "@typescript-eslint/scope-manager": "6.7.0",
+                "@typescript-eslint/types": "6.7.0",
+                "@typescript-eslint/typescript-estree": "6.7.0",
+                "semver": "^7.5.4"
             }
         },
         "@typescript-eslint/visitor-keys": {
-            "version": "5.62.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz",
-            "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==",
+            "version": "6.7.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.0.tgz",
+            "integrity": "sha512-/C1RVgKFDmGMcVGeD8HjKv2bd72oI1KxQDeY8uc66gw9R0OK0eMq48cA+jv9/2Ag6cdrsUGySm1yzYmfz0hxwQ==",
             "dev": true,
             "requires": {
-                "@typescript-eslint/types": "5.62.0",
-                "eslint-visitor-keys": "^3.3.0"
+                "@typescript-eslint/types": "6.7.0",
+                "eslint-visitor-keys": "^3.4.1"
             }
         },
         "abbrev": {
@@ -8360,11 +8490,11 @@
             "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="
         },
         "bcrypt": {
-            "version": "5.1.0",
-            "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz",
-            "integrity": "sha512-RHBS7HI5N5tEnGTmtR/pppX0mmDSBpQ4aCBsj7CEQfYXDcO74A8sIBYcJMuCsis2E81zDxeENYhv66oZwLiA+Q==",
+            "version": "5.1.1",
+            "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
+            "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==",
             "requires": {
-                "@mapbox/node-pre-gyp": "^1.0.10",
+                "@mapbox/node-pre-gyp": "^1.0.11",
                 "node-addon-api": "^5.0.0"
             }
         },
@@ -8377,7 +8507,10 @@
         "bignumber.js": {
             "version": "9.0.0",
             "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz",
-            "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A=="
+            "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==",
+            "dev": true,
+            "optional": true,
+            "peer": true
         },
         "binary-extensions": {
             "version": "2.2.0",
@@ -8416,15 +8549,15 @@
             }
         },
         "bootstrap": {
-            "version": "5.3.1",
-            "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.1.tgz",
-            "integrity": "sha512-jzwza3Yagduci2x0rr9MeFSORjcHpt0lRZukZPZQJT1Dth5qzV7XcgGqYzi39KGAVYR8QEDVoO0ubFKOxzMG+g==",
+            "version": "5.3.2",
+            "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.2.tgz",
+            "integrity": "sha512-D32nmNWiQHo94BKHLmOrdjlL05q1c8oxbtBphQFb9Z5to6eGRDCm0QgeaZ4zFBHzfg2++rqa2JkqCcxDy0sH0g==",
             "requires": {}
         },
         "bootstrap-icons": {
-            "version": "1.10.5",
-            "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.10.5.tgz",
-            "integrity": "sha512-oSX26F37V7QV7NCE53PPEL45d7EGXmBgHG3pDpZvcRaKVzWMqIRL9wcqJUyEha1esFtM3NJzvmxFXDxjJYD0jQ=="
+            "version": "1.11.0",
+            "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.11.0.tgz",
+            "integrity": "sha512-bLTbtACfUqwZf6f/xUYUb7bTRZC68QaQwwy9h1b96NPKfnwqzSatHqDypW6R2CBW7zUE7lP+O93GdZuPY3RIHA=="
         },
         "bplist-parser": {
             "version": "0.2.0",
@@ -8493,6 +8626,7 @@
             "version": "1.0.2",
             "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
             "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+            "dev": true,
             "requires": {
                 "function-bind": "^1.1.1",
                 "get-intrinsic": "^1.0.2"
@@ -8705,7 +8839,10 @@
         "core-util-is": {
             "version": "1.0.3",
             "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
-            "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
+            "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+            "dev": true,
+            "optional": true,
+            "peer": true
         },
         "cors": {
             "version": "2.8.5",
@@ -9074,16 +9211,16 @@
             "dev": true
         },
         "eslint": {
-            "version": "8.46.0",
-            "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.46.0.tgz",
-            "integrity": "sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg==",
+            "version": "8.49.0",
+            "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz",
+            "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==",
             "dev": true,
             "requires": {
                 "@eslint-community/eslint-utils": "^4.2.0",
                 "@eslint-community/regexpp": "^4.6.1",
-                "@eslint/eslintrc": "^2.1.1",
-                "@eslint/js": "^8.46.0",
-                "@humanwhocodes/config-array": "^0.11.10",
+                "@eslint/eslintrc": "^2.1.2",
+                "@eslint/js": "8.49.0",
+                "@humanwhocodes/config-array": "^0.11.11",
                 "@humanwhocodes/module-importer": "^1.0.1",
                 "@nodelib/fs.walk": "^1.2.8",
                 "ajv": "^6.12.4",
@@ -9093,7 +9230,7 @@
                 "doctrine": "^3.0.0",
                 "escape-string-regexp": "^4.0.0",
                 "eslint-scope": "^7.2.2",
-                "eslint-visitor-keys": "^3.4.2",
+                "eslint-visitor-keys": "^3.4.3",
                 "espree": "^9.6.1",
                 "esquery": "^1.4.2",
                 "esutils": "^2.0.2",
@@ -9131,26 +9268,26 @@
             }
         },
         "eslint-config-next": {
-            "version": "13.4.7",
-            "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-13.4.7.tgz",
-            "integrity": "sha512-+IRAyD0+J1MZaTi9RQMPUfr6Q+GCZ1wOkK6XM52Vokh7VI4R6YFGOFzdkEFHl4ZyIX4FKa5vcwUP2WscSFNjNQ==",
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-13.4.19.tgz",
+            "integrity": "sha512-WE8367sqMnjhWHvR5OivmfwENRQ1ixfNE9hZwQqNCsd+iM3KnuMc1V8Pt6ytgjxjf23D+xbesADv9x3xaKfT3g==",
             "dev": true,
             "requires": {
-                "@next/eslint-plugin-next": "13.4.7",
+                "@next/eslint-plugin-next": "13.4.19",
                 "@rushstack/eslint-patch": "^1.1.3",
-                "@typescript-eslint/parser": "^5.42.0",
+                "@typescript-eslint/parser": "^5.4.2 || ^6.0.0",
                 "eslint-import-resolver-node": "^0.3.6",
                 "eslint-import-resolver-typescript": "^3.5.2",
                 "eslint-plugin-import": "^2.26.0",
                 "eslint-plugin-jsx-a11y": "^6.5.1",
                 "eslint-plugin-react": "^7.31.7",
-                "eslint-plugin-react-hooks": "^4.5.0"
+                "eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705"
             }
         },
         "eslint-config-prettier": {
-            "version": "8.10.0",
-            "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz",
-            "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==",
+            "version": "9.0.0",
+            "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz",
+            "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==",
             "dev": true,
             "requires": {}
         },
@@ -9403,28 +9540,10 @@
             "dev": true,
             "requires": {}
         },
-        "eslint-scope": {
-            "version": "5.1.1",
-            "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
-            "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
-            "dev": true,
-            "requires": {
-                "esrecurse": "^4.3.0",
-                "estraverse": "^4.1.1"
-            },
-            "dependencies": {
-                "estraverse": {
-                    "version": "4.3.0",
-                    "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
-                    "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
-                    "dev": true
-                }
-            }
-        },
         "eslint-visitor-keys": {
-            "version": "3.4.2",
-            "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz",
-            "integrity": "sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==",
+            "version": "3.4.3",
+            "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+            "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
             "dev": true
         },
         "esm": {
@@ -9626,14 +9745,13 @@
             }
         },
         "formidable": {
-            "version": "2.1.2",
-            "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz",
-            "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==",
+            "version": "3.5.1",
+            "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.1.tgz",
+            "integrity": "sha512-WJWKelbRHN41m5dumb0/k8TeAx7Id/y3a+Z7QfhxP/htI9Js5zYaEDtG8uMgG0vM0lOlqnmjE99/kfpOYi/0Og==",
             "requires": {
                 "dezalgo": "^1.0.4",
                 "hexoid": "^1.0.0",
-                "once": "^1.4.0",
-                "qs": "^6.11.0"
+                "once": "^1.4.0"
             }
         },
         "fs-constants": {
@@ -9667,7 +9785,8 @@
         "function-bind": {
             "version": "1.1.1",
             "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
-            "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+            "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+            "dev": true
         },
         "function.prototype.name": {
             "version": "1.1.5",
@@ -9713,6 +9832,7 @@
             "version": "1.2.1",
             "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
             "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
+            "dev": true,
             "requires": {
                 "function-bind": "^1.1.1",
                 "has": "^1.0.3",
@@ -9790,9 +9910,9 @@
             "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="
         },
         "globals": {
-            "version": "13.20.0",
-            "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
-            "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
+            "version": "13.21.0",
+            "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz",
+            "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==",
             "dev": true,
             "requires": {
                 "type-fest": "^0.20.2"
@@ -9901,6 +10021,7 @@
             "version": "1.0.3",
             "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
             "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+            "dev": true,
             "requires": {
                 "function-bind": "^1.1.1"
             }
@@ -9928,12 +10049,14 @@
         "has-proto": {
             "version": "1.0.1",
             "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
-            "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg=="
+            "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+            "dev": true
         },
         "has-symbols": {
             "version": "1.0.3",
             "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
-            "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
+            "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+            "dev": true
         },
         "has-tostringtag": {
             "version": "1.0.0",
@@ -10309,7 +10432,10 @@
         "isarray": {
             "version": "1.0.0",
             "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
-            "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
+            "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+            "dev": true,
+            "optional": true,
+            "peer": true
         },
         "isexe": {
             "version": "2.0.0",
@@ -10350,14 +10476,20 @@
             "dev": true
         },
         "jsonwebtoken": {
-            "version": "9.0.1",
-            "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz",
-            "integrity": "sha512-K8wx7eJ5TPvEjuiVSkv167EVboBDv9PZdDoF7BgeQnBLVvZWW9clr2PsQHVJDTKaEIH5JBIwHujGcHp7GgI2eg==",
+            "version": "9.0.2",
+            "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
+            "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
             "requires": {
                 "jws": "^3.2.2",
-                "lodash": "^4.17.21",
+                "lodash.includes": "^4.3.0",
+                "lodash.isboolean": "^3.0.3",
+                "lodash.isinteger": "^4.0.4",
+                "lodash.isnumber": "^3.0.3",
+                "lodash.isplainobject": "^4.0.6",
+                "lodash.isstring": "^4.0.1",
+                "lodash.once": "^4.0.0",
                 "ms": "^2.1.1",
-                "semver": "^7.3.8"
+                "semver": "^7.5.4"
             }
         },
         "jsx-ast-utils": {
@@ -10473,11 +10605,46 @@
             "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
             "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
         },
+        "lodash.includes": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+            "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
+        },
+        "lodash.isboolean": {
+            "version": "3.0.3",
+            "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+            "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
+        },
+        "lodash.isinteger": {
+            "version": "4.0.4",
+            "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+            "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
+        },
+        "lodash.isnumber": {
+            "version": "3.0.3",
+            "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+            "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
+        },
+        "lodash.isplainobject": {
+            "version": "4.0.6",
+            "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+            "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
+        },
+        "lodash.isstring": {
+            "version": "4.0.1",
+            "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+            "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
+        },
         "lodash.merge": {
             "version": "4.6.2",
             "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
             "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
         },
+        "lodash.once": {
+            "version": "4.1.1",
+            "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+            "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
+        },
         "loose-envify": {
             "version": "1.4.0",
             "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -10496,9 +10663,9 @@
             }
         },
         "lru-cache": {
-            "version": "7.18.3",
-            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
-            "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="
+            "version": "10.0.1",
+            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz",
+            "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g=="
         },
         "make-dir": {
             "version": "3.1.0",
@@ -10522,15 +10689,15 @@
             "dev": true
         },
         "mariadb": {
-            "version": "3.2.0",
-            "resolved": "https://registry.npmjs.org/mariadb/-/mariadb-3.2.0.tgz",
-            "integrity": "sha512-IH2nidQat1IBMxP5gjuNxG6dADtz1PESEC6rKrcATen5v3ngFyZITjehyYiwNfz3zUNQupfYmVntz93M+Pz8pQ==",
+            "version": "3.2.1",
+            "resolved": "https://registry.npmjs.org/mariadb/-/mariadb-3.2.1.tgz",
+            "integrity": "sha512-EWCsvu8IwK5WRYPJ9iRekFiBfHfKB/wBcUwAzbhQKhSc6ivbvLcO/cktkYTCz3HVoGNYVWhBN38485Cq3qMSbQ==",
             "requires": {
                 "@types/geojson": "^7946.0.10",
                 "@types/node": "^17.0.45",
                 "denque": "^2.1.0",
                 "iconv-lite": "^0.6.3",
-                "lru-cache": "^7.14.0"
+                "lru-cache": "^10.0.1"
             },
             "dependencies": {
                 "@types/node": {
@@ -10672,6 +10839,9 @@
             "version": "2.18.1",
             "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz",
             "integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==",
+            "dev": true,
+            "optional": true,
+            "peer": true,
             "requires": {
                 "bignumber.js": "9.0.0",
                 "readable-stream": "2.3.7",
@@ -10682,7 +10852,10 @@
                 "safe-buffer": {
                     "version": "5.1.2",
                     "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
-                    "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+                    "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+                    "dev": true,
+                    "optional": true,
+                    "peer": true
                 }
             }
         },
@@ -10707,12 +10880,6 @@
             "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
             "dev": true
         },
-        "natural-compare-lite": {
-            "version": "1.4.0",
-            "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
-            "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
-            "dev": true
-        },
         "needle": {
             "version": "2.9.1",
             "resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz",
@@ -10753,20 +10920,20 @@
             "dev": true
         },
         "next": {
-            "version": "13.4.7",
-            "resolved": "https://registry.npmjs.org/next/-/next-13.4.7.tgz",
-            "integrity": "sha512-M8z3k9VmG51SRT6v5uDKdJXcAqLzP3C+vaKfLIAM0Mhx1um1G7MDnO63+m52qPdZfrTFzMZNzfsgvm3ghuVHIQ==",
-            "requires": {
-                "@next/env": "13.4.7",
-                "@next/swc-darwin-arm64": "13.4.7",
-                "@next/swc-darwin-x64": "13.4.7",
-                "@next/swc-linux-arm64-gnu": "13.4.7",
-                "@next/swc-linux-arm64-musl": "13.4.7",
-                "@next/swc-linux-x64-gnu": "13.4.7",
-                "@next/swc-linux-x64-musl": "13.4.7",
-                "@next/swc-win32-arm64-msvc": "13.4.7",
-                "@next/swc-win32-ia32-msvc": "13.4.7",
-                "@next/swc-win32-x64-msvc": "13.4.7",
+            "version": "13.4.19",
+            "resolved": "https://registry.npmjs.org/next/-/next-13.4.19.tgz",
+            "integrity": "sha512-HuPSzzAbJ1T4BD8e0bs6B9C1kWQ6gv8ykZoRWs5AQoiIuqbGHHdQO7Ljuvg05Q0Z24E2ABozHe6FxDvI6HfyAw==",
+            "requires": {
+                "@next/env": "13.4.19",
+                "@next/swc-darwin-arm64": "13.4.19",
+                "@next/swc-darwin-x64": "13.4.19",
+                "@next/swc-linux-arm64-gnu": "13.4.19",
+                "@next/swc-linux-arm64-musl": "13.4.19",
+                "@next/swc-linux-x64-gnu": "13.4.19",
+                "@next/swc-linux-x64-musl": "13.4.19",
+                "@next/swc-win32-arm64-msvc": "13.4.19",
+                "@next/swc-win32-ia32-msvc": "13.4.19",
+                "@next/swc-win32-x64-msvc": "13.4.19",
                 "@swc/helpers": "0.5.1",
                 "busboy": "1.6.0",
                 "caniuse-lite": "^1.0.30001406",
@@ -10808,9 +10975,9 @@
             }
         },
         "nodemailer": {
-            "version": "6.9.4",
-            "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.4.tgz",
-            "integrity": "sha512-CXjQvrQZV4+6X5wP6ZIgdehJamI63MFoYFGGPtHudWym9qaEHDNdPzaj5bfMCvxG1vhAileSWW90q7nL0N36mA=="
+            "version": "6.9.5",
+            "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.5.tgz",
+            "integrity": "sha512-/dmdWo62XjumuLc5+AYQZeiRj+PRR8y8qKtFCOyuOl1k/hckZd8durUUHs/ucKx6/8kN+wFxqKJlQ/LK/qR5FA=="
         },
         "nopt": {
             "version": "5.0.0",
@@ -10861,7 +11028,8 @@
         "object-inspect": {
             "version": "1.12.3",
             "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
-            "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g=="
+            "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
+            "dev": true
         },
         "object-keys": {
             "version": "1.1.1",
@@ -11220,7 +11388,10 @@
         "process-nextick-args": {
             "version": "2.0.1",
             "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
-            "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
+            "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+            "dev": true,
+            "optional": true,
+            "peer": true
         },
         "prop-types": {
             "version": "15.8.1",
@@ -11269,14 +11440,6 @@
             "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz",
             "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ=="
         },
-        "qs": {
-            "version": "6.11.2",
-            "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz",
-            "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==",
-            "requires": {
-                "side-channel": "^1.0.4"
-            }
-        },
         "queue-microtask": {
             "version": "1.2.3",
             "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -11391,6 +11554,9 @@
             "version": "2.3.7",
             "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
             "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+            "dev": true,
+            "optional": true,
+            "peer": true,
             "requires": {
                 "core-util-is": "~1.0.0",
                 "inherits": "~2.0.3",
@@ -11404,7 +11570,10 @@
                 "safe-buffer": {
                     "version": "5.1.2",
                     "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
-                    "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+                    "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+                    "dev": true,
+                    "optional": true,
+                    "peer": true
                 }
             }
         },
@@ -11605,9 +11774,9 @@
             "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
         },
         "sass": {
-            "version": "1.64.2",
-            "resolved": "https://registry.npmjs.org/sass/-/sass-1.64.2.tgz",
-            "integrity": "sha512-TnDlfc+CRnUAgLO9D8cQLFu/GIjJIzJCGkE7o4ekIGQOH7T3GetiRR/PsTWJUHhkzcSPrARkPI+gNWn5alCzDg==",
+            "version": "1.67.0",
+            "resolved": "https://registry.npmjs.org/sass/-/sass-1.67.0.tgz",
+            "integrity": "sha512-SVrO9ZeX/QQyEGtuZYCVxoeAL5vGlYjJ9p4i4HFuekWl8y/LtJ7tJc10Z+ck1c8xOuoBm2MYzcLfTAffD0pl/A==",
             "requires": {
                 "chokidar": ">=3.0.0 <4.0.0",
                 "immutable": "^4.0.0",
@@ -11671,9 +11840,9 @@
             "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
         },
         "sharp": {
-            "version": "0.32.4",
-            "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.4.tgz",
-            "integrity": "sha512-exUnZewqVZC6UXqXuQ8fyJJv0M968feBi04jb9GcUHrWtkRoAKnbJt8IfwT4NJs7FskArbJ14JAFGVuooszoGg==",
+            "version": "0.32.5",
+            "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.5.tgz",
+            "integrity": "sha512-0dap3iysgDkNaPOaOL4X/0akdu0ma62GcdC2NBQ+93eqpePdDdr2/LM0sFdDSMmN7yS+odyZtPsb7tx/cYBKnQ==",
             "requires": {
                 "color": "^4.2.3",
                 "detect-libc": "^2.0.2",
@@ -11734,6 +11903,7 @@
             "version": "1.0.4",
             "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
             "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+            "dev": true,
             "requires": {
                 "call-bind": "^1.0.0",
                 "get-intrinsic": "^1.0.2",
@@ -11858,14 +12028,17 @@
             }
         },
         "sprintf-js": {
-            "version": "1.1.2",
-            "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
-            "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug=="
+            "version": "1.1.3",
+            "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
+            "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="
         },
         "sqlstring": {
             "version": "2.3.1",
             "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz",
-            "integrity": "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ=="
+            "integrity": "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ==",
+            "dev": true,
+            "optional": true,
+            "peer": true
         },
         "stream-parser": {
             "version": "0.3.1",
@@ -12039,10 +12212,11 @@
             "dev": true
         },
         "swr": {
-            "version": "2.2.0",
-            "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.0.tgz",
-            "integrity": "sha512-AjqHOv2lAhkuUdIiBu9xbuettzAzWXmCEcLONNKJRba87WAefz8Ca9d6ds/SzrPc235n1IxWYdhJ2zF3MNUaoQ==",
+            "version": "2.2.2",
+            "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.2.tgz",
+            "integrity": "sha512-CbR41AoMD4TQBQw9ic3GTXspgfM9Y8Mdhb5Ob4uIKXhWqnRLItwA5fpGvB7SmSw3+zEjb0PdhiEumtUvYoQ+bQ==",
             "requires": {
+                "client-only": "^0.0.1",
                 "use-sync-external-store": "^1.2.0"
             }
         },
@@ -12143,6 +12317,13 @@
             "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
             "dev": true
         },
+        "ts-api-utils": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz",
+            "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==",
+            "dev": true,
+            "requires": {}
+        },
         "ts-node": {
             "version": "10.9.1",
             "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
@@ -12225,26 +12406,9 @@
             }
         },
         "tslib": {
-            "version": "2.6.1",
-            "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz",
-            "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig=="
-        },
-        "tsutils": {
-            "version": "3.21.0",
-            "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
-            "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
-            "dev": true,
-            "requires": {
-                "tslib": "^1.8.1"
-            },
-            "dependencies": {
-                "tslib": {
-                    "version": "1.14.1",
-                    "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
-                    "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
-                    "dev": true
-                }
-            }
+            "version": "2.6.2",
+            "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+            "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
         },
         "tunnel-agent": {
             "version": "0.6.0",
@@ -12317,9 +12481,9 @@
             }
         },
         "typescript": {
-            "version": "5.1.6",
-            "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
-            "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
+            "version": "5.2.2",
+            "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
+            "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
             "dev": true
         },
         "uc.micro": {
diff --git a/package.json b/package.json
index f32af84f..4d79db94 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
     "name": "snip",
-    "version": "1.4.0",
+    "version": "1.4.2",
     "description": "The digital labbook",
     "scripts": {
         "dev": "npm run prebuild && ts-node --project ./app/tsconfig.json ./app/server.ts",
@@ -25,66 +25,62 @@
     "author": "Sebastian B. Mohr, Markus Osterhoff",
     "license": "GPL-3.0",
     "devDependencies": {
-        "@rmp135/sql-ts": "^1.15.1",
+        "@rmp135/sql-ts": "^1.18.0",
         "@types/bcrypt": "^5.0.0",
-        "@types/bootstrap": "^5.2.0",
-        "@types/cookie-parser": "^1.4.3",
-        "@types/ejs": "^3.1.1",
-        "@types/express": "^4.17.13",
-        "@types/express-mysql-session": "^2.1.3",
-        "@types/express-session": "^1.17.5",
-        "@types/formidable": "^2.0.5",
+        "@types/bootstrap": "^5.2.6",
+        "@types/cookie-parser": "^1.4.4",
+        "@types/ejs": "^3.1.2",
+        "@types/formidable": "^3.4.3",
         "@types/jsonwebtoken": "^9.0.2",
         "@types/mysql": "^2.15.21",
-        "@types/node": "^18.6",
-        "@types/nodemailer": "^6.4.6",
-        "@types/react": "^18.0.15",
+        "@types/node": "^20.6.0",
+        "@types/nodemailer": "^6.4.10",
+        "@types/react": "^18.2.21",
         "@types/sprintf-js": "^1.1.2",
-        "@types/webpack-env": "^1.18.0",
-        "@typescript-eslint/eslint-plugin": "^5.42.0",
-        "@typescript-eslint/parser": "^5.42.0",
-        "eslint": "^8.42.0",
-        "eslint-config-next": "13.4.7",
-        "eslint-config-prettier": "^8.5.0",
+        "@types/webpack-env": "^1.18.1",
+        "@typescript-eslint/eslint-plugin": "^6.7.0",
+        "@typescript-eslint/parser": "^6.7.0",
+        "eslint": "^8.49.0",
+        "eslint-config-next": "^13.4.19",
+        "eslint-config-prettier": "^9.0.0",
         "eslint-plugin-simple-import-sort": "^10.0.0",
         "ts-node": "^10.9.1",
         "ts-node-dev": "^2.0.0",
-        "tsconfig-paths": "^4.0.0",
-        "typescript": "^5.0.4"
+        "tsconfig-paths": "^4.2.0",
+        "typescript": "^5.2.2"
     },
     "dependencies": {
-        "@next/bundle-analyzer": "13.4.7",
-        "@popperjs/core": "^2.11.5",
-        "bcrypt": "^5.0.1",
-        "bootstrap": "^5.2.0",
-        "bootstrap-icons": "^1.9.1",
+        "@next/bundle-analyzer": "^13.4.19",
+        "@popperjs/core": "^2.11.8",
+        "bcrypt": "^5.1.1",
+        "bootstrap": "^5.3.2",
+        "bootstrap-icons": "^1.11.0",
         "canvas": "^2.11.2",
-        "cli-progress": "^3.11.2",
-        "dotenv": "^16.0.3",
-        "formidable": "^2.0.1",
+        "cli-progress": "^3.12.0",
+        "dotenv": "^16.3.1",
+        "formidable": "^3.5.1",
         "gray-matter": "^4.0.3",
-        "highlight.js": "^11.7.0",
-        "iron-session": "^6.2.1",
-        "jsonwebtoken": "^9.0.0",
-        "mariadb": "^3.0.2",
+        "highlight.js": "^11.8.0",
+        "iron-session": "^6.3.1",
+        "jsonwebtoken": "^9.0.2",
+        "mariadb": "^3.2.1",
         "markdown-it": "^13.0.1",
         "markdown-it-highlightjs": "^4.0.1",
-        "mysql": "^2.18.1",
-        "next": "13.4.7",
-        "nodemailer": "^6.8.0",
+        "next": "^13.4.19",
+        "nodemailer": "^6.9.5",
         "pdf-lib": "^1.17.1",
         "probe-image-size": "^7.2.3",
         "react": "^18.2.0",
-        "react-bootstrap": "^2.4.0",
+        "react-bootstrap": "^2.8.0",
         "react-dom": "^18.2.0",
-        "react-dropzone": "^14.2.2",
+        "react-dropzone": "^14.2.3",
         "react-popper": "^2.3.0",
-        "sass": "^1.54.0",
-        "sharp": "^0.32.1",
-        "socket.io": "^4.5.1",
-        "socket.io-client": "^4.5.1",
-        "sprintf-js": "^1.1.2",
-        "swr": "^2.1.5",
-        "tslib": "^2.4.0"
+        "sass": "^1.67.0",
+        "sharp": "^0.32.5",
+        "socket.io": "^4.7.2",
+        "socket.io-client": "^4.7.2",
+        "sprintf-js": "^1.1.3",
+        "swr": "^2.2.2",
+        "tslib": "^2.6.2"
     }
 }
-- 
GitLab


From 4abf96473a9ceee56dc65deebe3406f4c7cf9c61 Mon Sep 17 00:00:00 2001
From: Sebastian Mohr <sebastian@mohrenclan.de>
Date: Thu, 14 Sep 2023 20:23:12 +0200
Subject: [PATCH 03/15] Same order token>user for all routes and added alert
 for token.

---
 app/components/viewer/sidebar/index.tsx | 20 +++++++++++++++-----
 app/lib/access_control/books.ts         | 21 ++++++++++-----------
 app/pages/books/[id]/index.tsx          | 16 +++++++++++++++-
 app/socket/auth.ts                      |  2 --
 4 files changed, 40 insertions(+), 19 deletions(-)

diff --git a/app/components/viewer/sidebar/index.tsx b/app/components/viewer/sidebar/index.tsx
index 21feec11..5b6233a1 100644
--- a/app/components/viewer/sidebar/index.tsx
+++ b/app/components/viewer/sidebar/index.tsx
@@ -1,4 +1,5 @@
 import { fetcher } from "lib/fetcher";
+import useUser from "lib/useUser";
 import { useDebounce } from "lib/utils";
 import {
     createContext,
@@ -105,9 +106,11 @@ export function SidebarContextProvider({
 export function Sidebar({
     book,
     showSnipsToPlace,
+    tokenAlert = false,
 }: {
     book: Book;
     showSnipsToPlace: boolean;
+    tokenAlert?: boolean;
 }) {
     const {
         searchTermPages,
@@ -135,16 +138,23 @@ export function Sidebar({
     }, fetcher);
 
     // Check if book is read only i.e. finished
-    let readOnly;
+    const alerts = [];
     if (book?.finished) {
-        readOnly = (
+        alerts.push(
             <div className={styles.readOnly + " alert alert-danger"}>
                 This book is finished! It can no longer be edited. All changes
                 are only local and will not be saved.
             </div>
         );
-    } else {
-        readOnly = null;
+    }
+
+    if (tokenAlert) {
+        alerts.push(
+            <div className={styles.readOnly + " alert alert-danger"}>
+                You are using a temporary access token you can only view the
+                book.
+            </div>
+        );
     }
 
     // Page previews for the pages tab
@@ -204,7 +214,7 @@ export function Sidebar({
                     </li>
                 ) : null}
             </ul>
-            {readOnly}
+            {alerts}
             <div className={styles.topNavigation}>
                 <SearchBar
                     isValidating={isValidating}
diff --git a/app/lib/access_control/books.ts b/app/lib/access_control/books.ts
index 092bb5cc..57b8f08f 100644
--- a/app/lib/access_control/books.ts
+++ b/app/lib/access_control/books.ts
@@ -38,24 +38,25 @@ export function withAuthBookIdSsr<
         const { req, query } = ctx;
         const book_id = query.id as string;
         const user_id = req.session.user_id;
-        // First check for user
-        if (book_id && user_id) {
-            const perms = await service.permission.getACLByUserBookReduced(
-                user_id,
-                parseInt(book_id)
-            );
-            return await handler(ctx, perms);
-        }
+
 
         // Check if token is present
         const url = new URL(req.url, "http://localhost");
         const token = url.searchParams.get("token");
         if (book_id && token) {
-            console.log("Ui token found", token);
             const perms = await verify_with_token(token, book_id);
             return await handler(ctx, perms);
         }
 
+        // First check for user
+        if (book_id && user_id) {
+            const perms = await service.permission.getACLByUserBookReduced(
+                user_id,
+                parseInt(book_id)
+            );
+            return await handler(ctx, perms);
+        }
+
         return {
             redirect: {
                 permanent: false,
@@ -105,7 +106,6 @@ export function withAuthBookId(handler: NextApiHandlerWithPerms) {
 export async function bookAuth(user_id: number, book_id: string | number, token?: string) {
     let perms;
     if (token) {
-        console.log("Api token found", token);
         perms = await verify_with_token(token, book_id);
     } else {
         perms = await service.permission.getACLByUserBookReduced(
@@ -129,7 +129,6 @@ async function verify_with_token(
             hashed_token,
         ])
         .then((res) => {
-            console.log(res);
             if (res.length > 0) {
                 return {
                     found: true,
diff --git a/app/pages/books/[id]/index.tsx b/app/pages/books/[id]/index.tsx
index 155c6379..9cc29b02 100644
--- a/app/pages/books/[id]/index.tsx
+++ b/app/pages/books/[id]/index.tsx
@@ -18,6 +18,7 @@ import { type Tool } from "components/viewer/tools";
 export default function BookById({ bookData, perms }) {
     // Create a book
     const [book, setBook] = useState<Book>();
+    const [tokenUsed, setTokenUsed] = useState(false);
 
     // Register desired tools (snipEditor is always last)
     const desiredTools: Tool[] = ["movePage"];
@@ -40,12 +41,25 @@ export default function BookById({ bookData, perms }) {
         }
     }, [bookData]);
 
+    useEffect(() => {
+        // Check params for token
+        const urlParams = new URLSearchParams(window.location.search);
+        const token = urlParams.get("token");
+        if (token) {
+            setTokenUsed(true);
+        }
+    }, []);
+
     // Return loading spinner
     if (!book || !perms) return <Loading />;
 
     return (
         <ViewerContext>
-            <Sidebar book={book} showSnipsToPlace={perms.pWrite} />
+            <Sidebar
+                book={book}
+                showSnipsToPlace={perms.pWrite}
+                tokenAlert={tokenUsed}
+            />
             <Resizer />
             <div
                 style={{
diff --git a/app/socket/auth.ts b/app/socket/auth.ts
index 502be798..aae984ed 100644
--- a/app/socket/auth.ts
+++ b/app/socket/auth.ts
@@ -31,14 +31,12 @@ export async function authenticate(socket: Socket, next) {
 
 export async function add_perms(socket: Socket, next) {
     const perms = await bookAuth(socket.user_id, socket.book_id, socket.auth_token);
-    console.log(perms);
     socket.perms = perms;
     return next();
 }
 
 export async function update_perms(socket: Socket) {
     const perms = await bookAuth(socket.user_id, socket.book_id, socket.auth_token);
-    console.log(perms);
     socket.perms = perms;
     return
 }
-- 
GitLab


From 5741328cbbf7bd7a464363101e63d9a37a8e8ce0 Mon Sep 17 00:00:00 2001
From: Sebastian Mohr <sebastian@mohrenclan.de>
Date: Thu, 14 Sep 2023 20:38:37 +0200
Subject: [PATCH 04/15] Removed some undefined variables

---
 app/components/books/edit/ShareLinks.tsx |   2 +-
 app/components/viewer/sidebar/index.tsx  |   3 +-
 app/controller/websocket.ts              | 111 -----------------------
 app/pages/api/authentication/isAdmin.ts  |   1 -
 app/pages/api/books/[id]/edit/set_acl.ts |   1 -
 app/pages/api/books/[id]/edit/update.ts  |   1 -
 app/server.ts                            |   1 -
 app/snips/general/image.ts               |   3 +-
 app/snips/general/text.ts                |   2 +-
 app/version.ts                           |   2 +-
 10 files changed, 5 insertions(+), 122 deletions(-)
 delete mode 100644 app/controller/websocket.ts

diff --git a/app/components/books/edit/ShareLinks.tsx b/app/components/books/edit/ShareLinks.tsx
index 38c88567..ee11d75c 100644
--- a/app/components/books/edit/ShareLinks.tsx
+++ b/app/components/books/edit/ShareLinks.tsx
@@ -81,7 +81,7 @@ export default function ShareLinks() {
                 onSubmit={(e) => {
                     e.preventDefault();
                     createNewToken(book.id, description, "ui", expiration).then(
-                        (r) => {
+                        () => {
                             setDescription("");
                         }
                     );
diff --git a/app/components/viewer/sidebar/index.tsx b/app/components/viewer/sidebar/index.tsx
index 5b6233a1..c1efa342 100644
--- a/app/components/viewer/sidebar/index.tsx
+++ b/app/components/viewer/sidebar/index.tsx
@@ -1,5 +1,4 @@
 import { fetcher } from "lib/fetcher";
-import useUser from "lib/useUser";
 import { useDebounce } from "lib/utils";
 import {
     createContext,
@@ -151,7 +150,7 @@ export function Sidebar({
     if (tokenAlert) {
         alerts.push(
             <div className={styles.readOnly + " alert alert-danger"}>
-                You are using a temporary access token you can only view the
+                You are using a temporary access token you shall only view this
                 book.
             </div>
         );
diff --git a/app/controller/websocket.ts b/app/controller/websocket.ts
deleted file mode 100644
index e2acf25f..00000000
--- a/app/controller/websocket.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import { io, Socket } from "socket.io-client";
-
-//Set global clientSockets
-if (!globalThis.ClientSockets) {
-    globalThis.ClientSockets = new Map<string, Socket>();
-}
-
-/** Creates Client side socket connection to the server.
- * At the moment only one socket connection is allowed per client.
- *
- * @param book_id
- * @returns {Socket}
- */
-const createClientSocket = async function (
-    book_id: number | string
-): Promise<Socket> {
-    if (!isBrowser() && !isWorker()) {
-        throw new Error("Not in browser or worker");
-    }
-    // Check if socket already exists
-    if (globalThis.ClientSockets.has(String(book_id))) {
-        return globalThis.ClientSockets.get(String(book_id));
-    }
-
-    // Fetch token for the user
-    const { token } = await fetch("/api/authentication/socket_token", {
-        method: "POST",
-        headers: {
-            "Content-Type": "application/json",
-        },
-        body: JSON.stringify({ book_id }),
-    }).then((res) => {
-        if (res.status === 200) {
-            return res.json();
-        } else {
-            alert("Could not establish secure connection");
-            throw new Error("Could not fetch token");
-        }
-    });
-
-    const book = String(book_id);
-
-    const socket = io("/book-" + book, {
-        auth: {
-            token: token,
-        },
-    });
-
-    socket.on("error", (error) => {
-        console.log("Socket error", error);
-    });
-
-    await globalThis.ClientSockets.set(book, socket);
-    return socket;
-};
-
-/** Create a client socket
- * @param book_id
- */
-export async function getClientSocket(book_id = undefined): Promise<Socket> {
-    if (!isBrowser() && !isWorker()) {
-        throw new Error("Not in browser or worker");
-    }
-    if (!routeSupported() && book_id == undefined) {
-        throw new Error("Route not supported");
-    }
-
-    let book = book_id || window.location.pathname.split("/")[2];
-    book = String(book);
-
-    let socket: Socket;
-    if (!globalThis.ClientSockets.has(book)) {
-        //Waiting for 0.5 second and then try again
-        await new Promise((resolve) => setTimeout(resolve, 500));
-        socket = await getClientSocket(book_id);
-    } else {
-        socket = globalThis.ClientSockets.get(book);
-    }
-
-    // Connect if not connected
-    if (!socket.connected) {
-        await socket.connect();
-    }
-    return socket;
-}
-
-export function isBrowser(): boolean {
-    return typeof window !== "undefined";
-}
-
-export function isWorker(): boolean {
-    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-    // @ts-ignore
-    return (
-        typeof WorkerGlobalScope !== "undefined" &&
-        self instanceof WorkerGlobalScope
-    );
-}
-
-export function routeSupported(): boolean {
-    if (isBrowser()) {
-        if (
-            window.location.pathname.split("/")[1] != "books" ||
-            window.location.pathname == "/books"
-        ) {
-            return false;
-        }
-        return true;
-    }
-    return false;
-}
diff --git a/app/pages/api/authentication/isAdmin.ts b/app/pages/api/authentication/isAdmin.ts
index 2b84684d..c5f8ee1d 100644
--- a/app/pages/api/authentication/isAdmin.ts
+++ b/app/pages/api/authentication/isAdmin.ts
@@ -1,7 +1,6 @@
 import { withSessionRoute } from "lib/withSession";
 import { NextApiRequest, NextApiResponse } from "next/types";
 
-import getSocketClient from "../socket";
 export default withSessionRoute(isAdmin);
 
 /** THIS API SHOULD NOT BE USED FOR CRITICAL APPLICATION PARTS
diff --git a/app/pages/api/books/[id]/edit/set_acl.ts b/app/pages/api/books/[id]/edit/set_acl.ts
index 6b155410..49b0a1cc 100644
--- a/app/pages/api/books/[id]/edit/set_acl.ts
+++ b/app/pages/api/books/[id]/edit/set_acl.ts
@@ -4,7 +4,6 @@ import { NextApiRequest, NextApiResponse } from "next/types";
 import { ApiError, service } from "pages/api/_service";
 import { parseACL } from "pages/api/admin/set_acl";
 import getSocketClient from "pages/api/socket";
-import { update_perms } from "socket/auth";
 
 import {
     ENTITY_USER,
diff --git a/app/pages/api/books/[id]/edit/update.ts b/app/pages/api/books/[id]/edit/update.ts
index 8f62e5e1..2b09d711 100644
--- a/app/pages/api/books/[id]/edit/update.ts
+++ b/app/pages/api/books/[id]/edit/update.ts
@@ -3,7 +3,6 @@ import { withSessionRoute } from "lib/withSession";
 import { NextApiRequest, NextApiResponse } from "next/types";
 import { ApiError } from "pages/api/_service";
 import getSocketClient from "pages/api/socket";
-import { update_perms } from "socket/auth";
 
 import { PermissionACLData } from "database/services/service.typings";
 import { pool_data } from "database/sql.connection";
diff --git a/app/server.ts b/app/server.ts
index b4af5b01..8b95f42a 100644
--- a/app/server.ts
+++ b/app/server.ts
@@ -6,7 +6,6 @@ import { createServer as createServerHttp } from "http";
 import { createServer as createServerHttps } from "https";
 import next from "next";
 import { getSocketIOServer } from "socket";
-import { type Server as IOServer } from "socket.io";
 import { parse } from "url";
 
 // Own function to setup server
diff --git a/app/snips/general/image.ts b/app/snips/general/image.ts
index 8cd92a76..46c08fd3 100644
--- a/app/snips/general/image.ts
+++ b/app/snips/general/image.ts
@@ -3,8 +3,7 @@ import { sync as probe_sync } from "probe-image-size";
 
 import { BaseData, BaseSnip, BaseSnipArgs, BaseView, SnipData } from "./base";
 
-import { isWorker } from "controller/booksocket";
-import { isBrowser } from "controller/websocket";
+import { isBrowser, isWorker } from "controller/booksocket";
 
 /** As we use the canvas package on the server side
  * we need to check if we are in the browser or not
diff --git a/app/snips/general/text.ts b/app/snips/general/text.ts
index 946463b7..91e75541 100644
--- a/app/snips/general/text.ts
+++ b/app/snips/general/text.ts
@@ -1,6 +1,6 @@
 import { BaseData, BaseSnip, BaseSnipArgs, BaseView, SnipData } from "./base";
 
-import { isBrowser } from "controller/websocket";
+import { isBrowser } from "controller/booksocket";
 
 export const allowedFontFamilies = [
     "PlexMono",
diff --git a/app/version.ts b/app/version.ts
index b37e1107..9fbbd7c3 100644
--- a/app/version.ts
+++ b/app/version.ts
@@ -1,4 +1,4 @@
 /* This file is generated by scripts/prebuild.ts */
 /* eslint-disable */
 
-export const LIB_VERSION = "1.4.0";
\ No newline at end of file
+export const LIB_VERSION = "1.4.2";
\ No newline at end of file
-- 
GitLab


From 24a09b09bc345e7f770957f3d9b0cf18638ddfb9 Mon Sep 17 00:00:00 2001
From: Sebastian Mohr <sebastian@mohrenclan.de>
Date: Fri, 15 Sep 2023 11:43:22 +0200
Subject: [PATCH 05/15] Removed some inline svgs.

---
 app/components/books/edit/BookInfo.tsx        |  5 ++-
 app/components/viewer/sidebar/downloadBtn.tsx | 21 +++--------
 .../viewer/sidebar/fullScreenBtn.tsx          | 37 +++++--------------
 .../viewer/sidebar/layoutSwitchBtn.tsx        | 36 +++++-------------
 .../viewer/sidebar/pageSwitchBtns.tsx         |  3 --
 .../viewer/sidebar/sidebar.module.scss        |  7 ++++
 app/next.config.js                            |  1 +
 app/public/img/viewer/download.svg            | 11 ++++++
 app/public/img/viewer/fullscreen_enter.svg    |  9 +++++
 app/public/img/viewer/fullscreen_exit.svg     |  9 +++++
 app/public/img/viewer/layout_single.svg       |  8 ++++
 app/public/img/viewer/layout_split.svg        |  9 +++++
 12 files changed, 83 insertions(+), 73 deletions(-)
 create mode 100644 app/public/img/viewer/download.svg
 create mode 100644 app/public/img/viewer/fullscreen_enter.svg
 create mode 100644 app/public/img/viewer/fullscreen_exit.svg
 create mode 100644 app/public/img/viewer/layout_single.svg
 create mode 100644 app/public/img/viewer/layout_split.svg

diff --git a/app/components/books/edit/BookInfo.tsx b/app/components/books/edit/BookInfo.tsx
index f1ecd5eb..e2554d0e 100644
--- a/app/components/books/edit/BookInfo.tsx
+++ b/app/components/books/edit/BookInfo.tsx
@@ -1,4 +1,5 @@
-import Image from "next/image";
+/* eslint-disable @next/next/no-img-element */
+//import Image from "next/image";
 import Link from "next/link";
 import React, { useContext, useState } from "react";
 import { Alert, Button, Form, InputGroup } from "react-bootstrap";
@@ -179,7 +180,7 @@ const Preview = () => {
     return (
         <div className={styles.preview}>
             <Link href={`/books/${book.id}`}>
-                <Image
+                <img
                     src={"/api/books/" + book.id + "/cover_preview"}
                     alt="No cover page set!"
                     width={240}
diff --git a/app/components/viewer/sidebar/downloadBtn.tsx b/app/components/viewer/sidebar/downloadBtn.tsx
index 391a8c1f..f0b80fa5 100644
--- a/app/components/viewer/sidebar/downloadBtn.tsx
+++ b/app/components/viewer/sidebar/downloadBtn.tsx
@@ -1,3 +1,4 @@
+import Image from "next/image";
 import { useState } from "react";
 
 import Modal from "components/common/Modal";
@@ -15,7 +16,11 @@ export default function DownloadBtn({ book }) {
                 data-active={false}
                 onClick={() => setShow(true)}
             >
-                {icon}
+                <Image
+                    src="/img/viewer/download.svg"
+                    alt="Download"
+                    layout="fill"
+                />
             </button>
 
             <Modal show={show} onHide={() => setShow(false)} centered>
@@ -28,17 +33,3 @@ export default function DownloadBtn({ book }) {
         </>
     );
 }
-
-const icon = (
-    <svg
-        xmlns="http://www.w3.org/2000/svg"
-        width="16"
-        height="16"
-        fill="currentColor"
-        className="bi bi-file-earmark-arrow-down"
-        viewBox="0 0 16 16"
-    >
-        <path d="M8.5 6.5a.5.5 0 0 0-1 0v3.793L6.354 9.146a.5.5 0 1 0-.708.708l2 2a.5.5 0 0 0 .708 0l2-2a.5.5 0 0 0-.708-.708L8.5 10.293V6.5z" />
-        <path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z" />
-    </svg>
-);
diff --git a/app/components/viewer/sidebar/fullScreenBtn.tsx b/app/components/viewer/sidebar/fullScreenBtn.tsx
index 9925e9d6..2bc62395 100644
--- a/app/components/viewer/sidebar/fullScreenBtn.tsx
+++ b/app/components/viewer/sidebar/fullScreenBtn.tsx
@@ -1,3 +1,4 @@
+import Image from "next/image";
 import { useState } from "react";
 
 import styles from "./sidebar.module.scss";
@@ -18,34 +19,16 @@ export default function FullScreenButton() {
     return (
         <div className={styles.fullscreenBtn} data-active={false}>
             <button className="btn" onClick={toggleFullScreen}>
-                {isFullScreen ? fullscreen_exit : fullscreen_enter}
+                <Image
+                    src={
+                        isFullScreen
+                            ? "/img/viewer/fullscreen_exit.svg"
+                            : "/img/viewer/fullscreen_enter.svg"
+                    }
+                    alt="Fullscreen"
+                    layout="fill"
+                />
             </button>
         </div>
     );
 }
-
-const fullscreen_enter = (
-    <svg
-        xmlns="http://www.w3.org/2000/svg"
-        width="16"
-        height="16"
-        fill="currentColor"
-        className="bi bi-fullscreen"
-        viewBox="0 0 16 16"
-    >
-        <path d="M1.5 1a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0v-4A1.5 1.5 0 0 1 1.5 0h4a.5.5 0 0 1 0 1h-4zM10 .5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 16 1.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5zM.5 10a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 0 14.5v-4a.5.5 0 0 1 .5-.5zm15 0a.5.5 0 0 1 .5.5v4a1.5 1.5 0 0 1-1.5 1.5h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5z" />
-    </svg>
-);
-
-const fullscreen_exit = (
-    <svg
-        xmlns="http://www.w3.org/2000/svg"
-        width="16"
-        height="16"
-        fill="currentColor"
-        className="bi bi-fullscreen-exit"
-        viewBox="0 0 16 16"
-    >
-        <path d="M5.5 0a.5.5 0 0 1 .5.5v4A1.5 1.5 0 0 1 4.5 6h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5zm5 0a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 10 4.5v-4a.5.5 0 0 1 .5-.5zM0 10.5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 6 11.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5zm10 1a1.5 1.5 0 0 1 1.5-1.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0v-4z" />
-    </svg>
-);
diff --git a/app/components/viewer/sidebar/layoutSwitchBtn.tsx b/app/components/viewer/sidebar/layoutSwitchBtn.tsx
index 50235e1b..97519c48 100644
--- a/app/components/viewer/sidebar/layoutSwitchBtn.tsx
+++ b/app/components/viewer/sidebar/layoutSwitchBtn.tsx
@@ -1,3 +1,4 @@
+import Image from "next/image";
 import { useContext } from "react";
 
 import { SidebarContext } from ".";
@@ -36,33 +37,16 @@ export default function LayoutSwitchBtn({ book }: { book: Book }) {
     return (
         <div className={styles.layoutSwitchBtn} data-active={isMultiPage}>
             <button className="btn" onClick={onClick}>
-                {isMultiPage ? svg_layout : svg_layout_split}
+                <Image
+                    src={
+                        isMultiPage
+                            ? "/img/viewer/layout_single.svg"
+                            : "/img/viewer/layout_split.svg"
+                    }
+                    alt="Fullscreen"
+                    layout="fill"
+                />
             </button>
         </div>
     );
 }
-
-const svg_layout_split = (
-    <svg
-        xmlns="http://www.w3.org/2000/svg"
-        width="16"
-        height="16"
-        fill="currentColor"
-        className="bi bi-layout-split"
-        viewBox="0 0 16 16"
-    >
-        <path d="M0 3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3zm8.5-1v12H14a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H8.5zm-1 0H2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h5.5V2z" />
-    </svg>
-);
-
-const svg_layout = (
-    <svg
-        className="bi bi-layout"
-        fill="currentColor"
-        version="1.1"
-        viewBox="0 0 16 16"
-        xmlns="http://www.w3.org/2000/svg"
-    >
-        <path d="m0 3c0-1.1046 0.89543-2 2-2h12c1.1046 0 2 0.89543 2 2v10c0 1.1046-0.89543 2-2 2h-12c-1.1046 0-2-0.89543-2-2zm2-1c-0.55228 0-1 0.44772-1 1v10c0 0.55228 0.44772 1 1 1h12c0.55228 0 1-0.44772 1-1v-10c0-0.55228-0.44772-1-1-1z" />
-    </svg>
-);
diff --git a/app/components/viewer/sidebar/pageSwitchBtns.tsx b/app/components/viewer/sidebar/pageSwitchBtns.tsx
index acca8d59..eeed9b0a 100644
--- a/app/components/viewer/sidebar/pageSwitchBtns.tsx
+++ b/app/components/viewer/sidebar/pageSwitchBtns.tsx
@@ -131,7 +131,6 @@ const File_left = (props) => {
             width="16"
             height="16"
             fill="currentColor"
-            className="bi bi-file-left"
             viewBox="0 0 16 16"
             {...props}
         >
@@ -153,7 +152,6 @@ const File_right = (props) => {
             width="16"
             height="16"
             fill="currentColor"
-            className="bi bi-file-right"
             viewBox="0 0 16 16"
             {...props}
         >
@@ -172,7 +170,6 @@ const File_plus = (props) => {
             width="16"
             height="16"
             fill="currentColor"
-            className="bi bi-file-plus"
             viewBox="0 0 16 16"
             {...props}
         >
diff --git a/app/components/viewer/sidebar/sidebar.module.scss b/app/components/viewer/sidebar/sidebar.module.scss
index 02a4aa23..f1e13419 100644
--- a/app/components/viewer/sidebar/sidebar.module.scss
+++ b/app/components/viewer/sidebar/sidebar.module.scss
@@ -81,6 +81,13 @@
         display: flex;
         height: 100%;
         flex: 1 0;
+        position: relative;
+        img {
+            display: flex;
+            justify-content: space-evenly;
+            align-items: center;
+            padding: 5px;
+        }
 
         a,
         button {
diff --git a/app/next.config.js b/app/next.config.js
index 7dfca2e8..8a29e2ec 100644
--- a/app/next.config.js
+++ b/app/next.config.js
@@ -19,6 +19,7 @@ module.exports = withBundleAnalyzer({
     // Do not show the X-Powered-By header in the responses
     poweredByHeader: false,
     compress: true,
+    swcMinify: true,
     eslint: {
         dirs: [
             "lib",
diff --git a/app/public/img/viewer/download.svg b/app/public/img/viewer/download.svg
new file mode 100644
index 00000000..792feef4
--- /dev/null
+++ b/app/public/img/viewer/download.svg
@@ -0,0 +1,11 @@
+<svg
+    xmlns="http://www.w3.org/2000/svg"
+    width="16"
+    height="16"
+    fill="currentColor"
+    className="bi bi-file-earmark-arrow-down"
+    viewBox="0 0 16 16"
+>
+    <path d="M8.5 6.5a.5.5 0 0 0-1 0v3.793L6.354 9.146a.5.5 0 1 0-.708.708l2 2a.5.5 0 0 0 .708 0l2-2a.5.5 0 0 0-.708-.708L8.5 10.293V6.5z" />
+    <path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z" />
+</svg>
\ No newline at end of file
diff --git a/app/public/img/viewer/fullscreen_enter.svg b/app/public/img/viewer/fullscreen_enter.svg
new file mode 100644
index 00000000..3af0d136
--- /dev/null
+++ b/app/public/img/viewer/fullscreen_enter.svg
@@ -0,0 +1,9 @@
+<svg
+    xmlns="http://www.w3.org/2000/svg"
+    width="16"
+    height="16"
+    fill="currentColor"
+    viewBox="0 0 16 16"
+>
+    <path d="M1.5 1a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0v-4A1.5 1.5 0 0 1 1.5 0h4a.5.5 0 0 1 0 1h-4zM10 .5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 16 1.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5zM.5 10a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 0 14.5v-4a.5.5 0 0 1 .5-.5zm15 0a.5.5 0 0 1 .5.5v4a1.5 1.5 0 0 1-1.5 1.5h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5z" />
+</svg>
\ No newline at end of file
diff --git a/app/public/img/viewer/fullscreen_exit.svg b/app/public/img/viewer/fullscreen_exit.svg
new file mode 100644
index 00000000..b68088a4
--- /dev/null
+++ b/app/public/img/viewer/fullscreen_exit.svg
@@ -0,0 +1,9 @@
+    <svg
+        xmlns="http://www.w3.org/2000/svg"
+        width="16"
+        height="16"
+        fill="currentColor"
+        viewBox="0 0 16 16"
+    >
+        <path d="M5.5 0a.5.5 0 0 1 .5.5v4A1.5 1.5 0 0 1 4.5 6h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5zm5 0a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 10 4.5v-4a.5.5 0 0 1 .5-.5zM0 10.5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 6 11.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5zm10 1a1.5 1.5 0 0 1 1.5-1.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0v-4z" />
+    </svg>
\ No newline at end of file
diff --git a/app/public/img/viewer/layout_single.svg b/app/public/img/viewer/layout_single.svg
new file mode 100644
index 00000000..74b2b639
--- /dev/null
+++ b/app/public/img/viewer/layout_single.svg
@@ -0,0 +1,8 @@
+<svg
+    fill="currentColor"
+    version="1.1"
+    viewBox="0 0 16 16"
+    xmlns="http://www.w3.org/2000/svg"
+>
+    <path d="m0 3c0-1.1046 0.89543-2 2-2h12c1.1046 0 2 0.89543 2 2v10c0 1.1046-0.89543 2-2 2h-12c-1.1046 0-2-0.89543-2-2zm2-1c-0.55228 0-1 0.44772-1 1v10c0 0.55228 0.44772 1 1 1h12c0.55228 0 1-0.44772 1-1v-10c0-0.55228-0.44772-1-1-1z" />
+</svg>
\ No newline at end of file
diff --git a/app/public/img/viewer/layout_split.svg b/app/public/img/viewer/layout_split.svg
new file mode 100644
index 00000000..99645890
--- /dev/null
+++ b/app/public/img/viewer/layout_split.svg
@@ -0,0 +1,9 @@
+<svg
+    xmlns="http://www.w3.org/2000/svg"
+    width="16"
+    height="16"
+    fill="currentColor"
+    viewBox="0 0 16 16"
+>
+    <path d="M0 3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3zm8.5-1v12H14a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H8.5zm-1 0H2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h5.5V2z" />
+</svg>
\ No newline at end of file
-- 
GitLab


From 619544b9a57d7703dc432dfd631a0bf0a2bb80e0 Mon Sep 17 00:00:00 2001
From: Sebastian Mohr <sebastian@mohrenclan.de>
Date: Mon, 18 Sep 2023 15:50:47 +0200
Subject: [PATCH 06/15] Fixed overlay sometimes persisted changes and reenabled
 live updates. Also fixed bug that background lines are not rendered.

---
 CHANGELOG.md                                  |  2 +
 app/.eslintrc.json                            |  3 +-
 app/components/viewer/page.tsx                |  6 +-
 app/components/viewer/sidebar/downloadBtn.tsx |  6 +-
 .../viewer/sidebar/fullScreenBtn.tsx          |  2 +-
 .../viewer/sidebar/layoutSwitchBtn.tsx        |  2 +-
 app/components/viewer/tools/doodle/drawing.ts |  2 +-
 .../viewer/worker/overlayWorker.tsx           | 71 +++++++++++++++++++
 .../viewer/worker/pageWorker/messaging.ts     |  4 +-
 .../backgrounds/render_background_by_id.ts    |  2 +-
 app/controller/booksocket.ts                  |  5 +-
 app/next.config.js                            |  5 ++
 12 files changed, 96 insertions(+), 14 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d85f5dc8..9e51d03a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,8 @@
     -   tools are dynamically picked by access level
     -   see new field in /book/:id/edit/access
 -   (Fix) Fixed a bug where the snips to place where not updated when a user adds a new snip
+-   (Fix) Overlay sometimes persisted on page change
+-   (Feature) Reenabled old live updates for drawing (might be disabled again depending on performance)
 
 # 1.4.1
 
diff --git a/app/.eslintrc.json b/app/.eslintrc.json
index ae8e0820..6e180808 100644
--- a/app/.eslintrc.json
+++ b/app/.eslintrc.json
@@ -37,6 +37,7 @@
                 ]
             }
         ],
-        "simple-import-sort/exports": "error"
+        "simple-import-sort/exports": "error",
+        "@typescript-eslint/no-unused-vars": "warn"
     }
 }
diff --git a/app/components/viewer/page.tsx b/app/components/viewer/page.tsx
index 737b9648..c0a0638e 100644
--- a/app/components/viewer/page.tsx
+++ b/app/components/viewer/page.tsx
@@ -96,12 +96,15 @@ export default function SinglePageViewer({
         /** Initial setup only run once
          *
          */
+        console.log("Initial setup all");
         const worker = workers[idx];
         const overlayWorker = overlayWorkers[idx];
         const draggable = draggableRef.current.get(idx);
         const pageData = pageDataRef.current.get(idx);
         worker.resize(draggable.clientWidth, draggable.clientHeight);
+        overlayWorker.resize(draggable.clientWidth, draggable.clientHeight);
         worker.setupPage(pageData);
+        overlayWorker.setupPage(pageData);
         centerPage(idx);
 
         /** Setup Rerender on resize
@@ -340,12 +343,13 @@ export function PagesContextProvider({
             for (let i = 0; i < s; i++) {
                 const pD = pageDataRef.current.get(i);
                 if (pD) {
+                    overlayWorkers[i].setupPage(pD);
                     workers[i].setupPage(pD);
                 }
             }
             setVisible(true);
         },
-        [updateHash, updateNumPages, workers]
+        [overlayWorkers, updateHash, updateNumPages, workers]
     );
 
     useEffect(() => {
diff --git a/app/components/viewer/sidebar/downloadBtn.tsx b/app/components/viewer/sidebar/downloadBtn.tsx
index f0b80fa5..ba060a08 100644
--- a/app/components/viewer/sidebar/downloadBtn.tsx
+++ b/app/components/viewer/sidebar/downloadBtn.tsx
@@ -16,11 +16,7 @@ export default function DownloadBtn({ book }) {
                 data-active={false}
                 onClick={() => setShow(true)}
             >
-                <Image
-                    src="/img/viewer/download.svg"
-                    alt="Download"
-                    layout="fill"
-                />
+                <Image src="/img/viewer/download.svg" alt="Download" fill />
             </button>
 
             <Modal show={show} onHide={() => setShow(false)} centered>
diff --git a/app/components/viewer/sidebar/fullScreenBtn.tsx b/app/components/viewer/sidebar/fullScreenBtn.tsx
index 2bc62395..cc8449dc 100644
--- a/app/components/viewer/sidebar/fullScreenBtn.tsx
+++ b/app/components/viewer/sidebar/fullScreenBtn.tsx
@@ -26,7 +26,7 @@ export default function FullScreenButton() {
                             : "/img/viewer/fullscreen_enter.svg"
                     }
                     alt="Fullscreen"
-                    layout="fill"
+                    fill
                 />
             </button>
         </div>
diff --git a/app/components/viewer/sidebar/layoutSwitchBtn.tsx b/app/components/viewer/sidebar/layoutSwitchBtn.tsx
index 97519c48..869c6f5d 100644
--- a/app/components/viewer/sidebar/layoutSwitchBtn.tsx
+++ b/app/components/viewer/sidebar/layoutSwitchBtn.tsx
@@ -44,7 +44,7 @@ export default function LayoutSwitchBtn({ book }: { book: Book }) {
                             : "/img/viewer/layout_split.svg"
                     }
                     alt="Fullscreen"
-                    layout="fill"
+                    fill
                 />
             </button>
         </div>
diff --git a/app/components/viewer/tools/doodle/drawing.ts b/app/components/viewer/tools/doodle/drawing.ts
index c9ca66b1..372f5067 100644
--- a/app/components/viewer/tools/doodle/drawing.ts
+++ b/app/components/viewer/tools/doodle/drawing.ts
@@ -114,7 +114,7 @@ export function useDrawing(
                     overlayWorkers[idx].render();
 
                     // Send preview event to other clients
-                    //active_snip.send_preview();
+                    overlayWorkers[idx].sendPreviewData(active_snip.to_data());
                 }
             }
             // Pointer move into canvas
diff --git a/app/components/viewer/worker/overlayWorker.tsx b/app/components/viewer/worker/overlayWorker.tsx
index f6f49e89..bdd1c659 100644
--- a/app/components/viewer/worker/overlayWorker.tsx
+++ b/app/components/viewer/worker/overlayWorker.tsx
@@ -14,6 +14,11 @@ import { MutableRefObject, useEffect, useState } from "react";
 
 import { type EleMap } from "../page";
 
+import { BookSocket, getSocket } from "controller/booksocket";
+import { PageData } from "controller/page";
+import { BaseData, BaseView, SnipData } from "snips/general/base";
+import { get_snip_from_data } from "snips/get_snip_from_data";
+
 const debug = (...args: unknown[]) => {
     console.debug("[OverlayWorker]", ...args);
 };
@@ -24,6 +29,9 @@ export class OverlayWorker {
 
     private stack = new Array<(ctx: CanvasRenderingContext2D) => void>();
 
+    private pageData: PageData | null = null;
+    private socket: BookSocket | null = null;
+
     constructor(canvas: HTMLCanvasElement) {
         this.canvas = canvas;
         this.ctx = canvas.getContext("2d", {
@@ -39,6 +47,69 @@ export class OverlayWorker {
         });
     }
 
+    public setupPage(_page: PageData) {
+        // Check if page is already setup
+        if (this.pageData && this.pageData.id === _page.id) {
+            return;
+        }
+        debug("Setup page:");
+        this.pageData = _page;
+
+        // Clear stack from previews
+        for (const renderfn of this.preview2render.values()) {
+            this.remove(renderfn);
+        }
+        this.preview2render.clear();
+        this.ctx.clearRect(0, 0, 1400, 2000);
+        this.render();
+
+        // Set socket in async function
+        (async () => {
+            this.socket = await getSocket(this.pageData.book_id);
+            //join room
+            await new Promise<void>((resolve, reject) => {
+                this.socket.emit(
+                    "page:join",
+                    this.pageData.id,
+                    (success: boolean) => {
+                        if (success) {
+                            resolve();
+                        } else {
+                            reject();
+                        }
+                    }
+                );
+            });
+            this.socket.on("snip:preview", this.receivePreviewData.bind(this));
+        })();
+    }
+
+    public sendPreviewData(snipData: SnipData<BaseData, BaseView>) {
+        if (!this.socket) return;
+        this.socket.volatile.emit("snip:preview", snipData);
+    }
+
+    private preview2render = new Map<
+        number,
+        (ctx: CanvasRenderingContext2D) => void
+    >();
+    public receivePreviewData(snipData: SnipData<BaseData, BaseView>) {
+        if (!this.pageData) return;
+
+        // Check if snip is already rendered and remove it
+        if (this.preview2render.has(snipData.id)) {
+            const renderfn = this.preview2render.get(snipData.id);
+            this.remove(renderfn);
+        }
+
+        // Add to stack
+        const snip = get_snip_from_data(snipData);
+        this.preview2render.set(snipData.id, (ctx) => {
+            snip.render(ctx);
+        });
+        this.add(this.preview2render.get(snipData.id));
+    }
+
     public resize(width: number, height: number) {
         // Normally the canvas should always be 1400x2000
         this.canvas.width = width;
diff --git a/app/components/viewer/worker/pageWorker/messaging.ts b/app/components/viewer/worker/pageWorker/messaging.ts
index 837234ce..baf2c8ab 100644
--- a/app/components/viewer/worker/pageWorker/messaging.ts
+++ b/app/components/viewer/worker/pageWorker/messaging.ts
@@ -49,6 +49,8 @@ export interface ResizeMsg {
     height: number;
 }
 
+
+
 export type Message =
     | InitMsg
     | SetupPageMsg
@@ -56,4 +58,4 @@ export type Message =
     | RenderMsg
     | ResizeMsg
     | InsertSnipMsg
-    | ShutdownMsg;
+    | ShutdownMsg;
\ No newline at end of file
diff --git a/app/controller/backgrounds/render_background_by_id.ts b/app/controller/backgrounds/render_background_by_id.ts
index 8053c05f..2029efa3 100644
--- a/app/controller/backgrounds/render_background_by_id.ts
+++ b/app/controller/backgrounds/render_background_by_id.ts
@@ -12,7 +12,7 @@ export function render_background_by_id(
         case background_types["checkered 4mm x 4mm"]:
             render_checkered(ctx, 4, 4);
             break;
-        case background_types["lined 7mm"]:
+        case background_types["lines 7mm"]:
             render_lined(ctx, 7, scaling);
             break;
         case background_types["blank"]:
diff --git a/app/controller/booksocket.ts b/app/controller/booksocket.ts
index 7a269fd1..8dff481d 100644
--- a/app/controller/booksocket.ts
+++ b/app/controller/booksocket.ts
@@ -67,8 +67,7 @@ export class BookSocket {
             console.log(err);
         });
         socket.on("connect", () => {
-            console.log("Connected to book socket");
-            console.log(socket);
+            console.log("Connected to server via websocket");
         });
         this.socket = socket;
     }
@@ -129,3 +128,5 @@ export function isBrowser(): boolean {
 export function isWorker(): boolean {
     return typeof importScripts === "function";
 }
+
+
diff --git a/app/next.config.js b/app/next.config.js
index 8a29e2ec..9035e30e 100644
--- a/app/next.config.js
+++ b/app/next.config.js
@@ -64,4 +64,9 @@ module.exports = withBundleAnalyzer({
             ];
         }
     },
+    compiler: {
+        removeConsole: {
+            exclude: ['error'],
+        },
+    },
 });
-- 
GitLab


From 11e387a5ac8a3bf9880cab902f5a4a21154abf9a Mon Sep 17 00:00:00 2001
From: Sebastian Mohr <sebastian@mohrenclan.de>
Date: Mon, 18 Sep 2023 16:24:29 +0200
Subject: [PATCH 07/15] Fixed chrome bug with urlsearchparams not
 serializable...

---
 app/components/viewer/sidebar/index.tsx              | 10 ++++++++--
 app/components/viewer/worker/pageWorker.tsx          |  8 ++++++--
 app/components/viewer/worker/pageWorker/messaging.ts |  2 +-
 app/components/viewer/worker/pageWorker/worker.ts    |  2 +-
 app/middleware.ts                                    |  3 ++-
 5 files changed, 18 insertions(+), 7 deletions(-)

diff --git a/app/components/viewer/sidebar/index.tsx b/app/components/viewer/sidebar/index.tsx
index c1efa342..024284f7 100644
--- a/app/components/viewer/sidebar/index.tsx
+++ b/app/components/viewer/sidebar/index.tsx
@@ -140,7 +140,10 @@ export function Sidebar({
     const alerts = [];
     if (book?.finished) {
         alerts.push(
-            <div className={styles.readOnly + " alert alert-danger"}>
+            <div
+                className={styles.readOnly + " alert alert-danger"}
+                key={"finished"}
+            >
                 This book is finished! It can no longer be edited. All changes
                 are only local and will not be saved.
             </div>
@@ -149,7 +152,10 @@ export function Sidebar({
 
     if (tokenAlert) {
         alerts.push(
-            <div className={styles.readOnly + " alert alert-danger"}>
+            <div
+                className={styles.readOnly + " alert alert-danger"}
+                key={"token"}
+            >
                 You are using a temporary access token you shall only view this
                 book.
             </div>
diff --git a/app/components/viewer/worker/pageWorker.tsx b/app/components/viewer/worker/pageWorker.tsx
index 99d8943d..407347e6 100644
--- a/app/components/viewer/worker/pageWorker.tsx
+++ b/app/components/viewer/worker/pageWorker.tsx
@@ -11,7 +11,7 @@
  * the main thread. And also via typical socket communication.
  *
  */
-import { MutableRefObject, useEffect, useState } from "react";
+import { MutableRefObject, useEffect, useRef, useState } from "react";
 
 import type { SetupPageMsg } from "./pageWorker/messaging";
 
@@ -112,7 +112,11 @@ export class PageWorker {
         const url_params = new URLSearchParams(window.location.search);
         const offscreen = this.canvas.transferControlToOffscreen();
         this.worker.postMessage(
-            { type: "init", canvas: offscreen, url_params },
+            {
+                type: "init",
+                canvas: offscreen,
+                url_params: url_params.toString(),
+            },
             [offscreen]
         );
         return;
diff --git a/app/components/viewer/worker/pageWorker/messaging.ts b/app/components/viewer/worker/pageWorker/messaging.ts
index baf2c8ab..1049430a 100644
--- a/app/components/viewer/worker/pageWorker/messaging.ts
+++ b/app/components/viewer/worker/pageWorker/messaging.ts
@@ -7,7 +7,7 @@ export interface InitMsg {
      */
     type: "init";
     canvas: OffscreenCanvas;
-    url_params: URLSearchParams;
+    url_params: string;
 }
 
 export interface ShutdownMsg {
diff --git a/app/components/viewer/worker/pageWorker/worker.ts b/app/components/viewer/worker/pageWorker/worker.ts
index 05d82c85..627d7975 100644
--- a/app/components/viewer/worker/pageWorker/worker.ts
+++ b/app/components/viewer/worker/pageWorker/worker.ts
@@ -187,7 +187,7 @@ self.onmessage = async (event: MessageEvent<Message>) => {
         case "init":
             globalThis.worker = new PageWorker(event.data.canvas);
 
-            globalThis.url_params = event.data.url_params;
+            globalThis.url_params = new URLSearchParams(event.data.url_params);
             break;
         case "shutdown":
             globalThis.worker.shutdown();
diff --git a/app/middleware.ts b/app/middleware.ts
index b070334e..f4a312fd 100644
--- a/app/middleware.ts
+++ b/app/middleware.ts
@@ -153,10 +153,11 @@ async function middleware_admin(request: NextRequest): Promise<NextResponse> {
  * "_next/image",
  * "favicon.ico",
  * "frontpage",
+ * "/img"
  */
 
 export const config = {
     matcher: [
-        "/((?!frontpage|account/login|account/signup|account/resetPassword|account/verify|account/forgotPassword|api/authentication/verify|api/authentication/resendVerify|api/account/login|api/account/signup|api/account/resetPassword|api/unauthorized|unauthorized|docs|privacy|imprint|_next/static|_next/image|favicon.ico).*)",
+        "/((?!frontpage|account/login|account/signup|account/resetPassword|account/verify|account/forgotPassword|api/authentication/verify|api/authentication/resendVerify|api/account/login|api/account/signup|api/account/resetPassword|api/unauthorized|unauthorized|docs|img|privacy|imprint|_next/static|_next/image|favicon.ico).*)",
     ],
 };
-- 
GitLab


From ecee7b069e1bc84bef79833158828cfe5681b651 Mon Sep 17 00:00:00 2001
From: Sebastian Mohr <sebastian@mohrenclan.de>
Date: Mon, 18 Sep 2023 16:56:47 +0200
Subject: [PATCH 08/15] Added a token expired page

---
 app/components/forms/LoginForm.tsx |  2 +-
 app/lib/access_control/books.ts    | 26 +++++++++++----
 app/middleware.ts                  |  3 +-
 app/pages/books/[id]/index.tsx     | 11 +++++++
 app/pages/token_expired.tsx        | 51 ++++++++++++++++++++++++++++++
 5 files changed, 84 insertions(+), 9 deletions(-)
 create mode 100644 app/pages/token_expired.tsx

diff --git a/app/components/forms/LoginForm.tsx b/app/components/forms/LoginForm.tsx
index b8260aa2..4832b09b 100644
--- a/app/components/forms/LoginForm.tsx
+++ b/app/components/forms/LoginForm.tsx
@@ -44,7 +44,7 @@ export function LoginForm(props) {
     const [error, setError] = useState(null);
 
     useEffect(() => {
-        if (user) {
+        if (user.id > 0) {
             if (props.prev_url) {
                 router.push(props.prev_url);
             }
diff --git a/app/lib/access_control/books.ts b/app/lib/access_control/books.ts
index 57b8f08f..b553bca2 100644
--- a/app/lib/access_control/books.ts
+++ b/app/lib/access_control/books.ts
@@ -31,7 +31,7 @@ export function withAuthBookIdSsr<
 >(
     handler: (
         context: GetServerSidePropsContext<Q, D>,
-        perms: PermissionACLData
+        perms: PermissionACLData & { token_used: boolean, expires_at?: Date }
     ) => GetServerSidePropsResult<P> | Promise<GetServerSidePropsResult<P>>
 ) {
     return async (ctx: GetServerSidePropsContext<Q, D>) => {
@@ -53,7 +53,8 @@ export function withAuthBookIdSsr<
             const perms = await service.permission.getACLByUserBookReduced(
                 user_id,
                 parseInt(book_id)
-            );
+            ) as PermissionACLData & { token_used: boolean, expires_at?: Date };
+            perms.token_used = false;
             return await handler(ctx, perms);
         }
 
@@ -117,13 +118,12 @@ export async function bookAuth(user_id: number, book_id: string | number, token?
 }
 
 
-
 async function verify_with_token(
     token: string,
     book_id: string | number,
-): Promise<PermissionACLData> {
+): Promise<PermissionACLData & { token_used: boolean, expires_at?: Date }> {
     const hashed_token = hashToken(token);
-    const { found, type } = await pool_permissions
+    const { found, type, expires_at } = await pool_permissions
         .query("SELECT * FROM tokens WHERE book_id = ? AND hashed_token = ?", [
             book_id,
             hashed_token,
@@ -133,11 +133,13 @@ async function verify_with_token(
                 return {
                     found: true,
                     type: res[0].type,
+                    expires_at: res[0].expires_at as Date
                 }
             } else {
                 return {
                     found: false,
                     type: null,
+                    expires_at: null
                 }
             }
         })
@@ -146,10 +148,12 @@ async function verify_with_token(
             return {
                 found: false,
                 type: null,
+                expires_at: null
             }
         });
 
-    const perms: PermissionACLData = {
+    const perms: PermissionACLData & { token_used: boolean, expires_at?: Date }
+        = {
         entity_id: -1,
         entity_type: ENTITY_TOKEN,
         resource_id: parseInt(book_id as string),
@@ -158,12 +162,20 @@ async function verify_with_token(
         pDelete: false,
         pRead: false,
         pWrite: false,
+        token_used: true,
+        expires_at: null
     }
-
     if (!found) {
         return perms;
     }
 
+    if (expires_at) {
+        // Should work as it is valid in the database
+        perms.expires_at = new Date(expires_at)
+        if (perms.expires_at < new Date()) {
+            return perms;
+        }
+    }
 
     switch (type) {
         case "api":
diff --git a/app/middleware.ts b/app/middleware.ts
index f4a312fd..017ec91e 100644
--- a/app/middleware.ts
+++ b/app/middleware.ts
@@ -154,10 +154,11 @@ async function middleware_admin(request: NextRequest): Promise<NextResponse> {
  * "favicon.ico",
  * "frontpage",
  * "/img"
+ * "token_expired",
  */
 
 export const config = {
     matcher: [
-        "/((?!frontpage|account/login|account/signup|account/resetPassword|account/verify|account/forgotPassword|api/authentication/verify|api/authentication/resendVerify|api/account/login|api/account/signup|api/account/resetPassword|api/unauthorized|unauthorized|docs|img|privacy|imprint|_next/static|_next/image|favicon.ico).*)",
+        "/((?!frontpage|account/login|account/signup|account/resetPassword|account/verify|account/forgotPassword|api/authentication/verify|api/authentication/resendVerify|api/account/login|api/account/signup|api/account/resetPassword|api/unauthorized|unauthorized|docs|img|token_expired|privacy|imprint|_next/static|_next/image|favicon.ico).*)",
     ],
 };
diff --git a/app/pages/books/[id]/index.tsx b/app/pages/books/[id]/index.tsx
index 9cc29b02..55118a49 100644
--- a/app/pages/books/[id]/index.tsx
+++ b/app/pages/books/[id]/index.tsx
@@ -93,6 +93,17 @@ export const getServerSideProps = withSessionSsr(
             return { props: { bookData, title: bookData.title, perms: p } };
         }
 
+        if (perms.token_used && !perms.pRead) {
+            const expired_date = new Date(perms.expires_at);
+            return {
+                redirect: {
+                    permanent: false,
+                    destination:
+                        "/token_expired/?expired=" + expired_date.toISOString(),
+                },
+            };
+        }
+
         return {
             redirect: {
                 permanent: false,
diff --git a/app/pages/token_expired.tsx b/app/pages/token_expired.tsx
new file mode 100644
index 00000000..6760167c
--- /dev/null
+++ b/app/pages/token_expired.tsx
@@ -0,0 +1,51 @@
+import { useRouter } from "next/router";
+import { useEffect, useState } from "react";
+
+import { getLayout } from "./account/login";
+
+/** This route is show if an user does an action for which
+ * he is not authorized.
+ */
+export default function TokenExpired({ expired }) {
+    const router = useRouter();
+    const [date, setDate] = useState();
+
+    useEffect(() => {
+        setDate(new Date(expired).toLocaleString());
+    }, [expired]);
+
+    return (
+        <>
+            <div
+                className="d-flex alert alert-warning flex-column"
+                role="alert"
+                style={{
+                    fontWeight: "bold",
+                }}
+            >
+                <p>The token your are trying to use has expired on {date}.</p>
+                <p>Please request a new token or login to access your book.</p>
+                <button
+                    className="btn btn-primary mt-2"
+                    onClick={() => {
+                        router.push("/");
+                    }}
+                >
+                    Home
+                </button>
+            </div>
+        </>
+    );
+}
+
+TokenExpired.getLayout = getLayout;
+
+export const getServerSideProps = async (context) => {
+    const { expired } = context.query;
+
+    return {
+        props: {
+            expired,
+        },
+    };
+};
-- 
GitLab


From a87564e6894ddba8e9dd71517059d2331fae8239 Mon Sep 17 00:00:00 2001
From: Sebastian Mohr <sebastian@mohrenclan.de>
Date: Mon, 18 Sep 2023 16:59:49 +0200
Subject: [PATCH 09/15] Reload on new link creation

---
 app/components/books/edit/ShareLinks.tsx | 1 +
 app/pages/token_expired.tsx              | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/app/components/books/edit/ShareLinks.tsx b/app/components/books/edit/ShareLinks.tsx
index ee11d75c..c9a9e811 100644
--- a/app/components/books/edit/ShareLinks.tsx
+++ b/app/components/books/edit/ShareLinks.tsx
@@ -83,6 +83,7 @@ export default function ShareLinks() {
                     createNewToken(book.id, description, "ui", expiration).then(
                         () => {
                             setDescription("");
+                            mutate();
                         }
                     );
                 }}
diff --git a/app/pages/token_expired.tsx b/app/pages/token_expired.tsx
index 6760167c..9c865575 100644
--- a/app/pages/token_expired.tsx
+++ b/app/pages/token_expired.tsx
@@ -8,7 +8,7 @@ import { getLayout } from "./account/login";
  */
 export default function TokenExpired({ expired }) {
     const router = useRouter();
-    const [date, setDate] = useState();
+    const [date, setDate] = useState<string>();
 
     useEffect(() => {
         setDate(new Date(expired).toLocaleString());
-- 
GitLab


From 5bee15c69b5aa43174cc1855b199fb10ffd0a350 Mon Sep 17 00:00:00 2001
From: Sebastian Mohr <sebastian@mohrenclan.de>
Date: Mon, 18 Sep 2023 17:12:01 +0200
Subject: [PATCH 10/15] Mutation of config now done optimistic.

---
 app/components/viewer/tools/movePage/resizable.ts |  2 +-
 app/lib/useUser.ts                                | 14 +++++++++++---
 2 files changed, 12 insertions(+), 4 deletions(-)

diff --git a/app/components/viewer/tools/movePage/resizable.ts b/app/components/viewer/tools/movePage/resizable.ts
index 1292770d..7111e22f 100644
--- a/app/components/viewer/tools/movePage/resizable.ts
+++ b/app/components/viewer/tools/movePage/resizable.ts
@@ -98,9 +98,9 @@ export function useResizable(
         if (!isLoadingConfig && config && firstRender.current) {
             firstRender.current = false;
             _setZoom(config.zoom1, 0);
-            if (numPages > 1) _setZoom(config.zoom2, 1);
             return;
         }
+        if (numPages > 1) _setZoom(config.zoom2, 1);
         // Update the config when the text styles change
     }, [config, containerRef, isLoadingConfig, numPages, _setZoom]);
 
diff --git a/app/lib/useUser.ts b/app/lib/useUser.ts
index 4ce17826..52ab7d4a 100644
--- a/app/lib/useUser.ts
+++ b/app/lib/useUser.ts
@@ -1,4 +1,4 @@
-import { Dispatch, SetStateAction, useEffect, useState } from "react";
+import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react";
 import useSWR, { KeyedMutator } from "swr";
 
 import { fetcher } from "./fetcher";
@@ -40,6 +40,7 @@ export default function useUser() {
     const {
         data: config,
         error: errorConfig,
+        mutate: mutateConfig,
     } = useSWR<UserConfigData>("/api/account/config", fetcher, {
         revalidateIfStale: false,
         revalidateOnFocus: true,
@@ -65,7 +66,14 @@ export default function useUser() {
 
     /** Debounce server update to prevent api spam 
     */
-    const [upd, setUpd] = useState<Partial<UserConfigData> | undefined>(undefined);
+    const [upd, _setUpd] = useState<Partial<UserConfigData> | undefined>(undefined);
+    const setUpd = useCallback((newUpd: Partial<UserConfigData>) => {
+        _setUpd(newUpd);
+        //Update local config
+        const newConfig = { ...config, ...newUpd };
+        mutateConfig(newConfig, false);
+    }, [config, mutateConfig]);
+
     const debounceUpdate = useDebounce(upd, 2000); // wait for 2 seconds before update database
     useEffect(() => {
         if (debounceUpdate) {
@@ -73,7 +81,7 @@ export default function useUser() {
                 setUpd(undefined);
             });
         }
-    }, [debounceUpdate, config])
+    }, [debounceUpdate, config, mutateConfig, setUpd])
 
 
     const ret: UserHook = {
-- 
GitLab


From 0eeac7663ae47e0e314647240365ff579414285e Mon Sep 17 00:00:00 2001
From: Sebastian Mohr <sebastian@mohrenclan.de>
Date: Mon, 18 Sep 2023 17:52:25 +0200
Subject: [PATCH 11/15] Hydration error in account json

---
 app/components/utils/PrettyPrintJson.tsx      | 10 +++++++++-
 app/components/viewer/tools/movePage.tool.tsx |  1 +
 app/lib/useUser.ts                            | 10 +++++-----
 3 files changed, 15 insertions(+), 6 deletions(-)

diff --git a/app/components/utils/PrettyPrintJson.tsx b/app/components/utils/PrettyPrintJson.tsx
index 07d8a877..13a5593d 100644
--- a/app/components/utils/PrettyPrintJson.tsx
+++ b/app/components/utils/PrettyPrintJson.tsx
@@ -1,7 +1,15 @@
+import { useEffect, useState } from "react";
+
 export default function PrettyPrintJson(props) {
+    const [data, setData] = useState<string>();
+
+    useEffect(() => {
+        setData(JSON.stringify(props, null, 2));
+    }, [props]);
+
     return (
         <div>
-            <pre>{JSON.stringify(props, null, 2)}</pre>
+            <pre>{data}</pre>
         </div>
     );
 }
diff --git a/app/components/viewer/tools/movePage.tool.tsx b/app/components/viewer/tools/movePage.tool.tsx
index 886c1a83..6cc59d70 100644
--- a/app/components/viewer/tools/movePage.tool.tsx
+++ b/app/components/viewer/tools/movePage.tool.tsx
@@ -181,6 +181,7 @@ export function MovePageToolContextProvider({
         pointerUpHandlerDrag,
         touchMoveHandlerResize,
         touchEndHandlerResize,
+        numPages,
     ]);
 
     const ret: MovePageToolContext = {
diff --git a/app/lib/useUser.ts b/app/lib/useUser.ts
index 52ab7d4a..34fe2800 100644
--- a/app/lib/useUser.ts
+++ b/app/lib/useUser.ts
@@ -25,7 +25,7 @@ export default function useUser() {
         mutate: mutateUser,
         error: errorUser,
     } = useSWR<UserData>("/api/account/user", fetcher, {
-        revalidateIfStale: false,
+        revalidateIfStale: true,
         revalidateOnFocus: false,
         revalidateOnReconnect: false,
         fallbackData: {
@@ -42,7 +42,7 @@ export default function useUser() {
         error: errorConfig,
         mutate: mutateConfig,
     } = useSWR<UserConfigData>("/api/account/config", fetcher, {
-        revalidateIfStale: false,
+        revalidateIfStale: true,
         revalidateOnFocus: true,
         revalidateOnReconnect: true,
         fallbackData: {
@@ -71,17 +71,17 @@ export default function useUser() {
         _setUpd(newUpd);
         //Update local config
         const newConfig = { ...config, ...newUpd };
-        mutateConfig(newConfig, false);
+        mutateConfig(newConfig, { revalidate: false });
     }, [config, mutateConfig]);
 
     const debounceUpdate = useDebounce(upd, 2000); // wait for 2 seconds before update database
     useEffect(() => {
-        if (debounceUpdate) {
+        if (debounceUpdate && user.id > 0) {
             updateServer(debounceUpdate, config).then(() => {
                 setUpd(undefined);
             });
         }
-    }, [debounceUpdate, config, mutateConfig, setUpd])
+    }, [debounceUpdate, config, mutateConfig, setUpd, user.id])
 
 
     const ret: UserHook = {
-- 
GitLab


From 206611c2afaa64278a921f02ac371ecd4d34dfe8 Mon Sep 17 00:00:00 2001
From: Sebastian Mohr <sebastian@mohrenclan.de>
Date: Mon, 18 Sep 2023 18:00:30 +0200
Subject: [PATCH 12/15] Duplicate draw on overlay with two pages fixed

---
 app/components/viewer/worker/overlayWorker.tsx | 7 ++++++-
 app/socket/snip/socket.ts                      | 3 ++-
 2 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/app/components/viewer/worker/overlayWorker.tsx b/app/components/viewer/worker/overlayWorker.tsx
index bdd1c659..ce6fea8b 100644
--- a/app/components/viewer/worker/overlayWorker.tsx
+++ b/app/components/viewer/worker/overlayWorker.tsx
@@ -53,6 +53,11 @@ export class OverlayWorker {
             return;
         }
         debug("Setup page:");
+        if (this.pageData && this.socket) {
+            // Leave room
+            this.socket.emit("page:leave", this.pageData.id);
+        }
+
         this.pageData = _page;
 
         // Clear stack from previews
@@ -94,7 +99,7 @@ export class OverlayWorker {
         (ctx: CanvasRenderingContext2D) => void
     >();
     public receivePreviewData(snipData: SnipData<BaseData, BaseView>) {
-        if (!this.pageData) return;
+        if (!this.pageData || snipData.page_id !== this.pageData.id) return;
 
         // Check if snip is already rendered and remove it
         if (this.preview2render.has(snipData.id)) {
diff --git a/app/socket/snip/socket.ts b/app/socket/snip/socket.ts
index a75a563c..fe4c16b9 100644
--- a/app/socket/snip/socket.ts
+++ b/app/socket/snip/socket.ts
@@ -76,8 +76,9 @@ export function setupSnipPrefix({ socket, book_nsp }: SocketData) {
                 );
             }
 
-            socket.volatile
+            socket
                 .to("page-" + String(SnipData.page_id))
+                .volatile
                 .emit("snip:preview", SnipData);
         }
     );
-- 
GitLab


From 75b667a6a0a9c5319ae049d6e3c71c2f6d184d79 Mon Sep 17 00:00:00 2001
From: Sebastian Mohr <sebastian@mohrenclan.de>
Date: Mon, 18 Sep 2023 18:05:58 +0200
Subject: [PATCH 13/15] Added gray border to multipage view to distinguish
 between pages easier

---
 app/components/viewer/page.module.scss | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/app/components/viewer/page.module.scss b/app/components/viewer/page.module.scss
index 70af3d24..a86b7810 100644
--- a/app/components/viewer/page.module.scss
+++ b/app/components/viewer/page.module.scss
@@ -11,6 +11,11 @@
     & * {
         touch-action: none !important;
     }
+    // Not last borders
+    &:not(:last-child) {
+        border-right: 1px solid $gray-200;
+        //drop-shadow(0px 0px 2px $gray-300);
+    }
 }
 
 $width_canvas: 1400px;
-- 
GitLab


From 4f70970b4cc939255fed52ee67a55dc079b24bde Mon Sep 17 00:00:00 2001
From: Sebastian Mohr <sebastian@mohrenclan.de>
Date: Mon, 18 Sep 2023 18:16:09 +0200
Subject: [PATCH 14/15] fixed small problem with user always available because
 of fallback

---
 app/components/common/Navbar.tsx    | 2 +-
 app/components/forms/SignupForm.tsx | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/components/common/Navbar.tsx b/app/components/common/Navbar.tsx
index 2387aa82..b156db10 100644
--- a/app/components/common/Navbar.tsx
+++ b/app/components/common/Navbar.tsx
@@ -75,7 +75,7 @@ function NavbarDropdown(props: NavbarDropdownProps) {
     // Render profile and logout button if user is logged in
     // else login form
     let elements = null;
-    if (user) {
+    if (user.id > 0) {
         elements = (
             <>
                 <li>
diff --git a/app/components/forms/SignupForm.tsx b/app/components/forms/SignupForm.tsx
index 955e8710..2136ab3c 100644
--- a/app/components/forms/SignupForm.tsx
+++ b/app/components/forms/SignupForm.tsx
@@ -43,7 +43,7 @@ export function SignupForm(props) {
     const router = useRouter();
     const { user, mutateUser } = useUser();
     useEffect(() => {
-        if (user) {
+        if (user.id > 0) {
             const redirect = props.prev_url || "";
             if (redirect[0] === "/") {
                 router.push(redirect);
-- 
GitLab


From 6c964cfc657a6f9486002cf250b3103ea542425e Mon Sep 17 00:00:00 2001
From: Sebastian Mohr <sebastian@mohrenclan.de>
Date: Mon, 18 Sep 2023 18:26:07 +0200
Subject: [PATCH 15/15] Increase version number

---
 package-lock.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index f97f260f..0998d51f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
     "name": "snip",
-    "version": "1.4.1",
+    "version": "1.4.2",
     "lockfileVersion": 2,
     "requires": true,
     "packages": {
         "": {
             "name": "snip",
-            "version": "1.4.1",
+            "version": "1.4.2",
             "license": "GPL-3.0",
             "dependencies": {
                 "@next/bundle-analyzer": "^13.4.19",
-- 
GitLab