diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index 16e5af7fa..a55414819 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -20,6 +20,9 @@
"minutes": "m",
"seconds": "s"
},
+ "auth": {
+ "signout": "Sign out"
+ },
"widget": {
"missing_type": "Missing Widget Type: {{type}}",
"api_error": "API Error",
diff --git a/src/components/toggles/signout.jsx b/src/components/toggles/signout.jsx
new file mode 100644
index 000000000..2a0ac09fc
--- /dev/null
+++ b/src/components/toggles/signout.jsx
@@ -0,0 +1,21 @@
+import { signOut, useSession } from "next-auth/react";
+import { useTranslation } from "next-i18next/pages";
+import { MdLogout } from "react-icons/md";
+
+export default function SignOut() {
+ const { t } = useTranslation();
+ const { status } = useSession();
+
+ if (status !== "authenticated") {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/src/components/toggles/signout.test.jsx b/src/components/toggles/signout.test.jsx
new file mode 100644
index 000000000..2635cea83
--- /dev/null
+++ b/src/components/toggles/signout.test.jsx
@@ -0,0 +1,37 @@
+// @vitest-environment jsdom
+
+import { fireEvent, render } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+const { signOut, useSession } = vi.hoisted(() => ({
+ signOut: vi.fn(),
+ useSession: vi.fn(),
+}));
+
+vi.mock("next-auth/react", () => ({ signOut, useSession }));
+vi.mock("next-i18next/pages", () => ({ useTranslation: () => ({ t: (key) => key }) }));
+
+import SignOut from "./signout";
+
+describe("components/toggles/signout", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it.each(["unauthenticated", "loading"])("renders nothing when session status is %s", (status) => {
+ useSession.mockReturnValue({ status });
+
+ const { container } = render();
+
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it("renders and signs out when authenticated", () => {
+ useSession.mockReturnValue({ status: "authenticated" });
+
+ const { getByRole } = render();
+ fireEvent.click(getByRole("button"));
+
+ expect(signOut).toHaveBeenCalledWith({ callbackUrl: "/" });
+ });
+});
diff --git a/src/pages/index.jsx b/src/pages/index.jsx
index a51313e07..02adabe16 100644
--- a/src/pages/index.jsx
+++ b/src/pages/index.jsx
@@ -35,6 +35,10 @@ const ColorToggle = dynamic(() => import("components/toggles/color"), {
ssr: false,
});
+const SignOut = dynamic(() => import("components/toggles/signout"), {
+ ssr: false,
+});
+
const Version = dynamic(() => import("components/version"), {
ssr: false,
});
@@ -502,6 +506,7 @@ function Home({ initialSettings }) {
{!settings?.color && }
+
{!settings.theme && }