diff --git a/bun.lockb b/bun.lockb index c49525a..4616867 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/encryption/models/secret-key.ts b/encryption/models/secret-key.ts index b731ec2..5aeebb3 100644 --- a/encryption/models/secret-key.ts +++ b/encryption/models/secret-key.ts @@ -4,9 +4,12 @@ import { decryptIV, encryptIV, pbkdf2, randomBytes } from "../crypto.ts"; export const SECRET_KEY_IV_LENGTH = 16; export const SECRET_KEY_PASSPORT_LENGTH = 32; export const SECRET_KEY_PASSPORT_ITERATIONS = 100000; -export const SECRET_KEY_BLOB_LENGTH = 80; +export const SECRET_KEY_BLOB_LENGTH = 32; +export const SECRET_KEY_BLOB_ENCRYPTED_LENGTH = 80; export const SECRET_KEY_LENGTH = - SECRET_KEY_IV_LENGTH + SECRET_KEY_PASSPORT_LENGTH + SECRET_KEY_BLOB_LENGTH; + SECRET_KEY_IV_LENGTH + + SECRET_KEY_PASSPORT_LENGTH + + SECRET_KEY_BLOB_ENCRYPTED_LENGTH; export default class SecretKey { iv: string; diff --git a/encryption/models/secret-lock.ts b/encryption/models/secret-lock.ts index 323e4aa..40e5287 100644 --- a/encryption/models/secret-lock.ts +++ b/encryption/models/secret-lock.ts @@ -5,27 +5,27 @@ export const SECRET_LOCK_DERIVED_LENGTH = 32; export const SECRET_LOCK_NC_ITERATIONS = 100000; export default class SecretLock { - nc: string; - password: string; + pk: string; + pw: string; - constructor(nc: string, password: string) { - this.nc = nc; - this.password = password; + constructor(nc: string, pw: string) { + this.pk = nc; + this.pw = pw; } toString(): string { - return "[nc=" + this.nc + ", password=" + this.password + "]"; + return "[nc=" + this.pk + ", pw=" + this.pw + "]"; } - static async withPassword(password: string): Promise { + static async withPassword(pw: string): Promise { const nc = await randomBytes(SECRET_LOCK_NC_LENGTH); - return new SecretLock(nc, password); + return new SecretLock(nc, pw); } async derive(): Promise { return await pbkdf2( - this.password, - this.nc, + this.pw, + this.pk, SECRET_LOCK_NC_ITERATIONS, SECRET_LOCK_DERIVED_LENGTH, ); diff --git a/web/app/pages.gen.ts b/web/app/pages.gen.ts index c2dc0b4..f9d47f0 100644 --- a/web/app/pages.gen.ts +++ b/web/app/pages.gen.ts @@ -13,6 +13,7 @@ import { createFileRoute } from "@tanstack/react-router"; // Import Routes import { Route as rootRoute } from "./../pages/__root"; +import { Route as HelloImport } from "./../pages/hello"; import { Route as AuthImport } from "./../pages/auth"; import { Route as AboutImport } from "./../pages/about"; import { Route as IndexImport } from "./../pages/index"; @@ -28,6 +29,11 @@ const FaqLazyRoute = FaqLazyImport.update({ getParentRoute: () => rootRoute, } as any).lazy(() => import("./../pages/faq.lazy").then((d) => d.Route)); +const HelloRoute = HelloImport.update({ + path: "/hello", + getParentRoute: () => rootRoute, +} as any); + const AuthRoute = AuthImport.update({ path: "/auth", getParentRoute: () => rootRoute, @@ -59,6 +65,10 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof AuthImport; parentRoute: typeof rootRoute; }; + "/hello": { + preLoaderRoute: typeof HelloImport; + parentRoute: typeof rootRoute; + }; "/faq": { preLoaderRoute: typeof FaqLazyImport; parentRoute: typeof rootRoute; @@ -72,6 +82,7 @@ export const routeTree = rootRoute.addChildren([ IndexRoute, AboutRoute, AuthRoute, + HelloRoute, FaqLazyRoute, ]); diff --git a/web/app/zustand.ts b/web/app/zustand.ts index e69de29..1cc0ace 100644 --- a/web/app/zustand.ts +++ b/web/app/zustand.ts @@ -0,0 +1,43 @@ +import SecretKey from "encryption/models/secret-key"; +import { create } from "zustand"; +import { createJSONStorage, persist } from "zustand/middleware"; +import type Note from "encryption/models/note"; + +type UserStore = { + username: string | undefined; + pk: string | undefined; + + authenticate: (username: string, nc: string) => void; +}; + +type SessionStore = { + secretKey: SecretKey | undefined; + notes: Note[]; + + initialize: (secretKey: string) => void; +}; + +export const useUserStore = create( + persist( + (set) => ({ + username: undefined, + pk: undefined, + + authenticate: (username: string, pk: string) => set({ username, pk }), + }), + { + name: "user", + storage: createJSONStorage(() => sessionStorage), + }, + ), +); + +export const useSessionStore = create((set) => ({ + secretKey: undefined, + notes: [], + + initialize: (secretKey: string) => + set({ secretKey: SecretKey.fromString(secretKey) }), +})); + +// 6#AjzB$c \ No newline at end of file diff --git a/web/components/ui/sonner.tsx b/web/components/ui/sonner.tsx new file mode 100644 index 0000000..452f4d9 --- /dev/null +++ b/web/components/ui/sonner.tsx @@ -0,0 +1,31 @@ +"use client" + +import { useTheme } from "next-themes" +import { Toaster as Sonner } from "sonner" + +type ToasterProps = React.ComponentProps + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme() + + return ( + + ) +} + +export { Toaster } diff --git a/web/components/ui/toaster.tsx b/web/components/ui/toaster.tsx deleted file mode 100644 index 5ff5709..0000000 --- a/web/components/ui/toaster.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { - Toast, - ToastClose, - ToastDescription, - ToastProvider, - ToastTitle, - ToastViewport, -} from "@/components/ui/toast"; -import { useToast } from "@/components/ui/use-toast"; - -export function Toaster() { - const { toasts } = useToast(); - - return ( - - {toasts.map(function ({ id, title, description, action, ...props }) { - return ( - -
- {title && {title}} - {description && ( - {description} - )} -
- {action} - -
- ); - })} - -
- ); -} diff --git a/web/components/ui/use-toast.ts b/web/components/ui/use-toast.ts deleted file mode 100644 index 5a03b78..0000000 --- a/web/components/ui/use-toast.ts +++ /dev/null @@ -1,189 +0,0 @@ -// Inspired by react-hot-toast library -import * as React from "react"; - -import type { ToastActionElement, ToastProps } from "@/components/ui/toast"; - -const TOAST_LIMIT = 1; -const TOAST_REMOVE_DELAY = 1000000; - -type ToasterToast = ToastProps & { - id: string; - title?: React.ReactNode; - description?: React.ReactNode; - action?: ToastActionElement; -}; - -const actionTypes = { - ADD_TOAST: "ADD_TOAST", - UPDATE_TOAST: "UPDATE_TOAST", - DISMISS_TOAST: "DISMISS_TOAST", - REMOVE_TOAST: "REMOVE_TOAST", -} as const; - -let count = 0; - -function genId() { - count = (count + 1) % Number.MAX_SAFE_INTEGER; - return count.toString(); -} - -type ActionType = typeof actionTypes; - -type Action = - | { - type: ActionType["ADD_TOAST"]; - toast: ToasterToast; - } - | { - type: ActionType["UPDATE_TOAST"]; - toast: Partial; - } - | { - type: ActionType["DISMISS_TOAST"]; - toastId?: ToasterToast["id"]; - } - | { - type: ActionType["REMOVE_TOAST"]; - toastId?: ToasterToast["id"]; - }; - -interface State { - toasts: ToasterToast[]; -} - -const toastTimeouts = new Map>(); - -const addToRemoveQueue = (toastId: string) => { - if (toastTimeouts.has(toastId)) { - return; - } - - const timeout = setTimeout(() => { - toastTimeouts.delete(toastId); - dispatch({ - type: "REMOVE_TOAST", - toastId: toastId, - }); - }, TOAST_REMOVE_DELAY); - - toastTimeouts.set(toastId, timeout); -}; - -export const reducer = (state: State, action: Action): State => { - switch (action.type) { - case "ADD_TOAST": - return { - ...state, - toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), - }; - - case "UPDATE_TOAST": - return { - ...state, - toasts: state.toasts.map((t) => - t.id === action.toast.id ? { ...t, ...action.toast } : t, - ), - }; - - case "DISMISS_TOAST": { - const { toastId } = action; - - // ! Side effects ! - This could be extracted into a dismissToast() action, - // but I'll keep it here for simplicity - if (toastId) { - addToRemoveQueue(toastId); - } else { - state.toasts.forEach((toast) => { - addToRemoveQueue(toast.id); - }); - } - - return { - ...state, - toasts: state.toasts.map((t) => - t.id === toastId || toastId === undefined - ? { - ...t, - open: false, - } - : t, - ), - }; - } - case "REMOVE_TOAST": - if (action.toastId === undefined) { - return { - ...state, - toasts: [], - }; - } - return { - ...state, - toasts: state.toasts.filter((t) => t.id !== action.toastId), - }; - } -}; - -const listeners: Array<(state: State) => void> = []; - -let memoryState: State = { toasts: [] }; - -function dispatch(action: Action) { - memoryState = reducer(memoryState, action); - listeners.forEach((listener) => { - listener(memoryState); - }); -} - -type Toast = Omit; - -function toast({ ...props }: Toast) { - const id = genId(); - - const update = (props: ToasterToast) => - dispatch({ - type: "UPDATE_TOAST", - toast: { ...props, id }, - }); - const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }); - - dispatch({ - type: "ADD_TOAST", - toast: { - ...props, - id, - open: true, - onOpenChange: (open) => { - if (!open) dismiss(); - }, - }, - }); - - return { - id: id, - dismiss, - update, - }; -} - -function useToast() { - const [state, setState] = React.useState(memoryState); - - React.useEffect(() => { - listeners.push(setState); - return () => { - const index = listeners.indexOf(setState); - if (index > -1) { - listeners.splice(index, 1); - } - }; - }, [state]); - - return { - ...state, - toast, - dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), - }; -} - -export { useToast, toast }; diff --git a/web/package.json b/web/package.json index dea14a0..02e7b76 100644 --- a/web/package.json +++ b/web/package.json @@ -17,9 +17,11 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "lucide-react": "^0.396.0", + "next-themes": "^0.3.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-i18next": "^14.1.2", + "sonner": "^1.5.0", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", "zustand": "^4.5.2" diff --git a/web/pages/__root.tsx b/web/pages/__root.tsx index 11e7921..439af9a 100644 --- a/web/pages/__root.tsx +++ b/web/pages/__root.tsx @@ -5,13 +5,13 @@ import { CircleUser } from "lucide-react"; import { Button } from "@/components/ui/button.tsx"; import { Separator } from "@/components/ui/separator.tsx"; import { twMerge } from "tailwind-merge"; -import { Toaster } from "@/components/ui/toaster.tsx"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip.tsx"; +import { useUserStore } from "@/app/zustand.ts"; const TanStackRouterDevtools = process.env.NODE_ENV === "production" @@ -25,6 +25,8 @@ const TanStackRouterDevtools = export const Route = createRootRoute({ component: Component }); function Component() { + const userStore = useUserStore(); + return (
@@ -67,7 +69,7 @@ function Component() {
@@ -100,7 +102,6 @@ function Component() { - ); } diff --git a/web/pages/auth.tsx b/web/pages/auth.tsx index 4e72d37..f48cf90 100644 --- a/web/pages/auth.tsx +++ b/web/pages/auth.tsx @@ -7,7 +7,7 @@ import { } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { createFileRoute } from "@tanstack/react-router"; +import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { useState } from "react"; import { CheckCircle, @@ -23,6 +23,8 @@ import { api, cn } from "@/app/utils"; import { useQuery } from "@tanstack/react-query"; import SecretKey from "encryption/models/secret-key"; import SecretLock from "encryption/models/secret-lock"; +import { toast } from "sonner"; +import { useSessionStore, useUserStore } from "@/app/zustand.ts"; export const Route = createFileRoute("/auth")({ component: Component }); @@ -152,6 +154,10 @@ function Action({ username: string; password: string; }) { + const navigate = useNavigate(); + const userStore = useUserStore(); + const sessionStore = useSessionStore(); + if (!username.match(VerifyUsername)) return (