This file is a merged representation of a subset of the codebase, containing specifically included files, combined into a single document by Repomix. This section contains a summary of this file. This file contains a packed representation of a subset of the repository's contents that is considered the most important context. It is designed to be easily consumable by AI systems for analysis, code review, or other automated processes. The content is organized as follows: 1. This summary section 2. Repository information 3. Directory structure 4. Repository files (if enabled) 5. Multiple file entries, each consisting of: - File path as an attribute - Full contents of the file - This file should be treated as read-only. Any changes should be made to the original repository files, not this packed version. - When processing this file, use the file path to distinguish between different files in the repository. - Be aware that this file may contain sensitive information. Handle it with the same level of security as you would the original repository. - Some files may have been excluded based on .gitignore rules and Repomix's configuration - Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files - Only files matching these patterns are included: **/*.ts, **/*.tsx - Files matching patterns in .gitignore are excluded - Files matching default ignore patterns are excluded - Files are sorted by Git change count (files with more changes are at the bottom) src/ api/ apiClient.ts components/ ui/ ComposeCanvas.tsx DateDisplay.tsx FormField.tsx Logo.tsx RouteGuards.tsx SplashScreen.tsx config/ endpoints.ts routes.ts hooks/ useAuth.ts pages/ Activate.tsx Drawer.tsx Editor.tsx Home.tsx Login.tsx Register.tsx VerifyEmail.tsx store/ useAuthStore.ts utils/ crypto.ts App.tsx main.tsx vite.config.ts This section contains the contents of the repository's files. import axios from "axios"; import { endpoints } from "../config/endpoints"; import { useAuthStore } from "../store/useAuthStore"; // publicApi for endpoints that don't need authentication (login, refresh, register) export const publicApi = axios.create({ baseURL: import.meta.env.VITE_API_URL, withCredentials: true, }); // api for all authenticated requests export const api = axios.create({ baseURL: import.meta.env.VITE_API_URL, withCredentials: true, }); // auto-attach access token to authenticated requests api.interceptors.request.use((config) => { const token = useAuthStore.getState().accessToken; if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); // Handle 401 errors by attempting a silent refresh api.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; // If 401 and we haven't tried refreshing yet if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true; try { // Attempt silent refresh const { data } = await publicApi.post(endpoints.REFRESH); const newAccessToken = data.access; // Update store const { user, setAuth } = useAuthStore.getState(); if (user) { setAuth(newAccessToken, user); } // Retry the original request with the new token originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; return api(originalRequest); } catch (refreshError) { // Refresh failed, perform logout to clear tokens console.error("Session expired, logging out..."); useAuthStore.getState().clearAuth(); return Promise.reject(refreshError); } } return Promise.reject(error); }, ); import * as fabric from "fabric"; import { forwardRef, useEffect, useImperativeHandle, useRef } from "react"; const PAD = 36; export type CanvasTools = { addImage: (url: string) => void; }; export const ComposeCanvas = forwardRef((_props, ref) => { const wrapperRef = useRef(null); const canvasRef = useRef(null); const fabricRef = useRef(null); const textboxRef = useRef(null); useEffect(() => { let isMounted = true; let canvas: fabric.Canvas | null = null; const init = async () => { // lazy populate await document.fonts.ready; const waitForLayout = (): Promise => { return new Promise((resolve) => { const check = () => { const wrapperWidth = wrapperRef.current?.clientWidth || 0; if (wrapperWidth > 0) resolve(wrapperWidth); else requestAnimationFrame(check); }; check(); }); }; const finalWidth = await waitForLayout(); if (!isMounted || !canvasRef.current || !wrapperRef.current) return; const initialHeight = Math.max( wrapperRef.current.clientHeight || 900, 600, ); // init canvas canvas = new fabric.Canvas(canvasRef.current, { width: finalWidth, height: initialHeight, selection: false, preserveObjectStacking: true, allowTouchScrolling: true, // for mobile }); fabricRef.current = canvas; // transparent background const wrapperEl = canvas.getElement().parentElement; if (wrapperEl) wrapperEl.style.background = "transparent"; // the core textbox const textbox = new fabric.Textbox("Take a deep breath...", { originX: "left", originY: "top", left: PAD, top: PAD, width: finalWidth - PAD * 2, fontSize: 16, fontWeight: 500, fontFamily: "Playfair Display Variable", fill: "#000", lineHeight: 1.5, editable: true, hasControls: false, hasBorders: false, objectCaching: false, // for font crispness splitByGrapheme: false, lockMovementX: true, lockMovementY: true, lockScalingX: true, lockScalingY: true, }); textboxRef.current = textbox; canvas.add(textbox); // automatically adjust height textbox.on("changed", () => { if (!canvas || !wrapperRef.current) return; const neededHeight = textbox.top + textbox.height + PAD; if (neededHeight > canvas.height) { const newH = neededHeight + PAD; canvas.setDimensions({ height: newH }); wrapperRef.current.style.height = `${newH}px`; } }); // auto focus setTimeout(() => { if (!isMounted) return; canvas?.setActiveObject(textbox); textbox.enterEditing(); canvas?.renderAll(); // Accessibility fix for Fabric.js hidden textarea // searching globally in case it is appended to body const hiddenTextareas = document.querySelectorAll( 'textarea[data-fabric="textarea"]', ); hiddenTextareas.forEach((ta) => { if (!ta.getAttribute("aria-label")) { ta.setAttribute("aria-label", "Canvas text input"); } }); }, 100); canvas.on("mouse:down", (opt) => { if (!opt.target || opt.target === textbox) { canvas?.setActiveObject(textbox); textbox.enterEditing(); canvas?.renderAll(); } }); }; init(); return () => { isMounted = false; canvas?.dispose(); fabricRef.current = null; textboxRef.current = null; }; }, []); useImperativeHandle(ref, () => ({ addImage: (url: string) => { if (!fabricRef.current) return; fabric.FabricImage.fromURL(url).then((img) => { img.scaleToWidth(300); img.set({ left: PAD, top: PAD, }); fabricRef.current?.add(img); fabricRef.current?.setActiveObject(img); fabricRef.current?.requestRenderAll(); URL.revokeObjectURL(url); // cleanup browser upload }); }, })); return (
); }); ComposeCanvas.displayName = "ComposeCanvas";
interface DateDisplayProps { date?: Date; className?: string; } export default function DateDisplay({ date = new Date(), className = "", }: DateDisplayProps) { const formattedDate = date.toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric", }); return (
Date {formattedDate}
); }
import type { UseFormRegisterReturn } from "react-hook-form"; interface FormFieldProps { label: string; type?: string; placeholder?: string; registration: UseFormRegisterReturn; error?: string; } export default function FormField({ label, type = "text", placeholder, registration, error, }: FormFieldProps) { return (
{error &&

{error}

}
); }
import { DotIcon } from "@phosphor-icons/react"; import "@fontsource/knewave/400.css"; export default function Logo() { return ( Pi Ku ); } import { Navigate, useLocation } from "react-router-dom"; import { ROUTES } from "../config/routes"; import { useAuth } from "../hooks/useAuth"; import SplashScreen from "./SplashScreen"; /** * Post-login routes. * Redirects to /login if not already authenticated. */ export function ProtectedRoute({ children }: { children: React.ReactNode }) { const { isAuthenticated, isInitializing } = useAuth(); const location = useLocation(); if (isInitializing) return ; if (!isAuthenticated) { // Save the intended location to redirect back after login return ; } return <>{children}; } /** * Pre-login flows. * Redirects to /drawer if already authenticated. */ export function PublicRoute({ children }: { children: React.ReactNode }) { const { isAuthenticated, isInitializing } = useAuth(); if (isInitializing) return ; if (isAuthenticated) { return ; } return <>{children}; } import Logo from "./Logo"; export default function SplashScreen() { return (

Initializing Identity

); }
export const endpoints = { LOGIN: "/api/auth/login/", REGISTER: "/api/auth/register/", VERIFY_EMAIL: "/api/auth/verify-email/", ACTIVATE: "/api/auth/activate/:uidb64/:token/", ME: "/api/auth/me/", REFRESH: "/api/auth/refresh/", LOGOUT: "/api/auth/logout/", }; // simple utility to handle path params export const replacePathParams = ( url: string, params: Record, ): string => { let result = url; Object.entries(params).forEach(([key, value]) => { result = result.replace(`:${key}`, value); }); return result; }; export const ROUTES = { HOME: "/", ONBOARD: "/onboard", VERIFY_EMAIL: "/verify-email", ACTIVATE: "/activate/:uidb64/:token", LOGIN: "/login", DRAWER: "/drawer", WRITE: "/quill", READ: "/read", }; import { useCallback } from "react"; import { api, publicApi } from "../api/apiClient"; import { endpoints } from "../config/endpoints"; import { type UserProfile, useAuthStore } from "../store/useAuthStore"; export const useAuth = () => { const { accessToken, user, isInitializing, setAuth, clearAuth } = useAuthStore(); const isAuthenticated = !!accessToken; const login = (access: string, profile: UserProfile) => { setAuth(access, profile); }; const logout = async () => { try { await api.post(endpoints.LOGOUT); } finally { clearAuth(); } }; const initialize = useCallback(async () => { const { accessToken, user, setAuth, clearAuth, setInitializing } = useAuthStore.getState(); // If session in memory, don't trigger refresh/me again if (accessToken && user) { setInitializing(false); return; } try { // try refresh const { data: refreshData } = await publicApi.post(endpoints.REFRESH); // fetch user profile with the new access token const { data: userData } = await api.get(endpoints.ME, { headers: { Authorization: `Bearer ${refreshData.access}` }, }); // update auth details in memory setAuth(refreshData.access, userData); } catch (err) { console.error("Initialization failed:", err); clearAuth(); } }, []); return { isAuthenticated, user, isInitializing, login, logout, initialize, }; }; import { CheckCircleIcon, XCircleIcon } from "@phosphor-icons/react"; import { useEffect, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { publicApi } from "../api/apiClient"; import Logo from "../components/Logo"; import { endpoints, replacePathParams } from "../config/endpoints"; import { ROUTES } from "../config/routes"; export default function Activate() { const { uidb64, token } = useParams(); const [status, setStatus] = useState<"loading" | "success" | "error">( "loading", ); const hasCalled = useRef(false); const navigate = useNavigate(); useEffect(() => { if (!uidb64 || !token || hasCalled.current) return; // prevent double api calls hasCalled.current = true; const activateAccount = async () => { try { const url = replacePathParams(endpoints.ACTIVATE, { uidb64, token, }); await publicApi.get(url); setStatus("success"); } catch (err) { console.error("Activation error:", err); setStatus("error"); } }; activateAccount(); }, [uidb64, token]); return (
{status === "loading" && (

Activating your account...

)} {status === "success" && (

Account Activated!

Welcome to
Your identity is now verified and ready for timeless letters.

)} {status === "error" && (

Activation Failed

The link might be expired or already used. Please try registering again.

)}
); }
import { useAuth } from "../hooks/useAuth"; export default function Drawer() { const { user, logout } = useAuth(); if (!user) return null; return (

Your Drawer

Welcome back, {user.full_name}

); } import { ImageIcon, LockIcon, TrayIcon } from "@phosphor-icons/react"; import { useRef, useState } from "react"; import { type CanvasTools, ComposeCanvas, } from "../components/ui/ComposeCanvas"; import DateDisplay from "../components/ui/DateDisplay"; export default function Editor() { const [recipient, setRecipient] = useState(""); const canvasRef = useRef(null); const fileInputRef = useRef(null); const handleImageUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; // pick one file at a time if (file) { const url = URL.createObjectURL(file); canvasRef.current?.addImage(url); } }; return (
setRecipient(e.target.value)} className="bg-transparent border-none outline-none text-4xl font-serif text-base-content placeholder:text-base-content/10 w-full" />
); }
import Logo from "../components/Logo"; export default function Home() { return (
); }
import { zodResolver } from "@hookform/resolvers/zod"; import axios from "axios"; import { useState } from "react"; import { useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; import { z } from "zod"; import { api, publicApi } from "../api/apiClient"; import Logo from "../components/Logo"; import FormField from "../components/ui/FormField"; import { endpoints } from "../config/endpoints"; import { ROUTES } from "../config/routes"; import { useAuth } from "../hooks/useAuth"; const loginSchema = z.object({ email: z.string().email("Please enter a valid email"), password: z.string().min(1, "Password is required"), }); type LoginInputs = z.infer; export default function Login() { const navigate = useNavigate(); const [isLoading, setIsLoading] = useState(false); const [apiError, setApiError] = useState(null); const { login } = useAuth(); const { register, handleSubmit, formState: { errors }, } = useForm({ resolver: zodResolver(loginSchema), }); const onSubmit = async (data: LoginInputs) => { setIsLoading(true); setApiError(null); try { // 1. Authenticate const { data: authData } = await publicApi.post(endpoints.LOGIN, data); // 2. Fetch User Profile with the fresh token // We pass the header explicitly to avoid any race conditions with interceptors const { data: userData } = await api.get(endpoints.ME, { headers: { Authorization: `Bearer ${authData.access}` }, }); // 3. Update store using the hook method await login(authData.access, userData); navigate(ROUTES.DRAWER); } catch (err) { console.error("Login error:", err); let message = "Invalid email or password"; if (axios.isAxiosError(err)) { message = err.response?.data?.detail || err.response?.data?.message || message; } setApiError(message); } finally { setIsLoading(false); } }; return (

Sign in to

{apiError && (
{apiError}
)}
Don't have an account?{" "}
); }
import { zodResolver } from "@hookform/resolvers/zod"; import { InfoIcon } from "@phosphor-icons/react"; import axios from "axios"; import { useState } from "react"; import { useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; import { z } from "zod"; import { publicApi } from "../api/apiClient"; import Logo from "../components/Logo"; import FormField from "../components/ui/FormField"; import { endpoints } from "../config/endpoints"; import { ROUTES } from "../config/routes"; // validation logic const registerSchema = z .object({ full_name: z.string().min(2, "Name must be at least 2 characters"), email: z.email("Please enter a valid email"), password: z.string().min(8, "Password must be at least 8 characters"), confirm_password: z.string(), }) .refine((data) => data.password === data.confirm_password, { message: "Passwords don't match", path: ["confirm_password"], }); type RegisterInputs = z.infer; export default function Register() { const navigate = useNavigate(); const [isLoading, setIsLoading] = useState(false); const [apiError, setApiError] = useState(null); const { register, handleSubmit, formState: { errors }, } = useForm({ resolver: zodResolver(registerSchema), }); const onSubmit = async (data: RegisterInputs) => { setIsLoading(true); setApiError(null); try { await publicApi.post(endpoints.REGISTER, { full_name: data.full_name, email: data.email, password: data.password, }); navigate(ROUTES.VERIFY_EMAIL); } catch (err) { console.error("Registration error:", err); let message = "Registration failed. Please try again."; if (axios.isAxiosError(err)) { message = err.response?.data?.message || message; } setApiError(message); } finally { setIsLoading(false); } }; return (

Create a Account

{apiError && (
{apiError}
)} {/* Warning */}

Choose a password you won't forget.
There is no reset.{" "} If you lose it, your letters cannot be recovered.

); }
import { EnvelopeSimpleOpenIcon } from "@phosphor-icons/react"; import Logo from "../components/Logo"; export default function VerifyEmail() { return (

Check Your Email

We've sent an activation link to your inbox.
Please click it to verify your account.

Didn't receive it? Check your spam folder or wait for a few minutes. The link will expire in 24 hours.

); }
import { create } from "zustand"; export interface UserProfile { public_id: string; email: string; full_name: string; } interface AuthState { accessToken: string | null; user: UserProfile | null; isInitializing: boolean; setAuth: (accessToken: string, user: UserProfile) => void; clearAuth: () => void; setInitializing: (v: boolean) => void; } export const useAuthStore = create((set) => ({ accessToken: null, user: null, isInitializing: true, setAuth: (accessToken, user) => set({ accessToken, user, isInitializing: false }), clearAuth: () => set({ accessToken: null, user: null, isInitializing: false }), setInitializing: (isInitializing) => set({ isInitializing }), })); /** * 0 knowledge cryptography. No Server involved in encryption/decryption */ const ITERATIONS = 100000; const KEY_ALGO = { name: "AES-GCM", length: 256 }; /** * Derives a Master Encryption Key from a password and email (salt). */ export async function deriveMasterKey( password: string, email: string, ): Promise { const encoder = new TextEncoder(); const passwordKey = await crypto.subtle.importKey( "raw", encoder.encode(password), "PBKDF2", false, ["deriveKey"], ); return crypto.subtle.deriveKey( { name: "PBKDF2", salt: encoder.encode(email.toLowerCase()), iterations: ITERATIONS, hash: "SHA-256", }, passwordKey, KEY_ALGO, false, ["encrypt", "decrypt", "wrapKey", "unwrapKey"], ); } /** * Encrypts a letter using Envelope Encryption. */ export async function encryptLetter(plaintext: string, masterKey: CryptoKey) { const encoder = new TextEncoder(); // Generate random Data Encryption Key (DEK) const dek = await crypto.subtle.generateKey(KEY_ALGO, true, [ "encrypt", "decrypt", ]); // Encrypt the content with the DEK const iv = crypto.getRandomValues(new Uint8Array(12)); const ciphertext = await crypto.subtle.encrypt( { name: "AES-GCM", iv }, dek, encoder.encode(plaintext), ); // encrpyt the DEK using the Master Key for the self access const keyIv = crypto.getRandomValues(new Uint8Array(12)); const wrappedKey = await crypto.subtle.wrapKey("raw", dek, masterKey, { name: "AES-GCM", iv: keyIv, }); // for recipients (link share), export DEK in raw format const rawKey = await crypto.subtle.exportKey("raw", dek); // conversion to base64 for transit const toBase64 = (buf: Uint8Array) => btoa(buf.reduce((acc, b) => acc + String.fromCharCode(b), "")); return { // This goes to the server encryptedPayload: { ciphertext: toBase64(new Uint8Array(ciphertext)), iv: toBase64(new Uint8Array(iv)), wrappedKey: toBase64(new Uint8Array(wrappedKey)), keyIv: toBase64(new Uint8Array(keyIv)), }, // This goes into the url for the recipient sharingKey: toBase64(new Uint8Array(rawKey)), }; } import { useEffect } from "react"; import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; import { ProtectedRoute, PublicRoute } from "./components/RouteGuards"; import SplashScreen from "./components/SplashScreen"; import { ROUTES } from "./config/routes"; import { useAuth } from "./hooks/useAuth"; import Activate from "./pages/Activate"; import Drawer from "./pages/Drawer"; import Editor from "./pages/Editor"; // Pages import Home from "./pages/Home"; import Login from "./pages/Login"; import Register from "./pages/Register"; import VerifyEmail from "./pages/VerifyEmail"; export default function App() { const { initialize, isInitializing } = useAuth(); useEffect(() => { initialize(); }, [initialize]); if (isInitializing) { return ; } return (
} /> } /> } /> } /> } /> } /> } /> } />
); }
import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import "./index.css"; import "@fontsource-variable/playwrite-hr-lijeva/wght.css"; import "@fontsource-variable/jost/wght.css"; import "@fontsource-variable/playfair-display/wght.css"; import App from "./App.tsx"; const root = document.getElementById("root"); if (root) { createRoot(root).render( , ); } import tailwindcss from "@tailwindcss/vite"; import react from "@vitejs/plugin-react"; import { defineConfig, loadEnv } from "vite"; // https://vite.dev/config/ export default defineConfig(({ mode }) => { const env = loadEnv(mode, "../", ""); return { envDir: "../", plugins: [react(), tailwindcss()], server: { port: Number(env.FRONTEND_PORT), host: env.FRONTEND_DOMAIN, }, } });