import { Button } from "@tensamin/ui"; import { Input } from "@tensamin/ui"; import { Label } from "@tensamin/ui"; import { useStorage } from "@tensamin/storage/context"; import { log, toast } from "@tensamin/shared/log"; import { File } from "lucide-react"; import * as React from "react"; import { z } from "zod"; import { isTauri } from "@tauri-apps/api/core"; import QrCodeScanner from "@tensamin/tauri/qrCodeScanner"; const fetchedUser = z.object({ id: z.uuidv4(), type: z.string(), data: z.object({ iota_id: z.number(), username: z.string(), sub_level: z.number(), public_key: z.base64(), user_id: z.number(), sub_end: z.number(), }), }); const formSchema = z.object({ username: z.string().min(1).max(255), private_key: z.string().min(1).max(92), }); /** * Parses a .tu file payload into credentials. * @param rawFileContent UTF-8 file content from an uploaded .tu file. * @returns Parsed user id and private key credentials. */ function parseTuFileContent(rawFileContent: string): { userId: number; privateKey: string; domain: string | null; } { if (rawFileContent.trim().length === 0) { throw new Error("File is empty"); } else if (!rawFileContent.includes("::")) { throw new Error("Invalid file"); } else if (rawFileContent.split("::").length !== 2) { throw new Error("Invalid file"); } const left = rawFileContent.split("::")[0]; const right = rawFileContent.split("::")[1]; if (left.length === 0) { throw new Error("Invalid file"); } else if (right.length === 0) { throw new Error("Invalid file"); } else if (isNaN(Number(left)) && !left.includes("@")) { throw new Error("Invalid file"); } const [userIdString, privateKey] = rawFileContent.split("::"); const userId = isNaN(Number(userIdString)) ? Number(userIdString.split("@")[0]) : Number(userIdString); const rawDomain = userIdString.includes("@") ? userIdString.split("@")[1] : null; const domain = rawDomain ? (rawDomain.includes(":") ? rawDomain : rawDomain + ":1984") : null; if (!userId || !privateKey) { throw new Error("Invalid file"); } console.log({ domain, rawDomain, userId, }); return { userId, privateKey, domain }; } /** * Renders the login form for file upload and manual credential login. * @returns Login form JSX. */ export default function Form() { const uploadRef = React.useRef(null); const { save } = useStorage(); /** * Handles uploaded .tu files and stores resolved credentials. * @param event Change event from the hidden file input. * @returns Promise that resolves when processing has finished. */ const handleFileInputChange = React.useCallback( async (event: React.ChangeEvent): Promise => { try { const file = event.currentTarget.files?.[0]; if (!file) { throw new Error("No file selected"); } const raw = await file.text(); const parsed = parseTuFileContent(raw); await save("session_id", Date.now()); await save("user_id", parsed.userId); await save("private_key", parsed.privateKey); if (parsed.domain) { await save("ttp_url", `https://${parsed.domain}/`); } location.href = "/"; } catch (error) { log(0, "login", "red", error); toast("error", "Failed to load file"); } }, [save], ); /** * Handles username and private key login submission. * @param event Form submit event. * @returns Promise that resolves after login processing. */ const handleCredentialsSubmit = React.useCallback( async (event: React.SubmitEvent): Promise => { event.preventDefault(); const formData = new FormData(event.currentTarget); const rawData = Object.fromEntries(formData); const inputParse = formSchema.safeParse(rawData); if (!inputParse.success) { toast("error", "Please enter valid data"); return; } const inputUsername = inputParse.data.username; const actualUsername = inputUsername.includes("@") ? inputUsername.split("@")[0] : inputUsername; const rawDomain = inputUsername.includes("@") ? inputUsername.split("@")[1] : null; const domain = rawDomain ? (rawDomain.includes(":") ? rawDomain : rawDomain + ":1984") : null; try { const response = await fetch( `https://omega.tensamin.net/api/get/id/${actualUsername}`, ); const rawData = await response.arrayBuffer(); const bytes = new Uint8Array(rawData); const jsonString = new TextDecoder().decode(bytes); const data = JSON.parse(jsonString); const parse = fetchedUser.shape.data.safeParse(data); if (!parse.success) { log(0, "login", "red", "Invalid response from server", parse.error); toast("error", "Invalid response from server"); return; } const user = parse.data; await save("session_id", Date.now()); await save("user_id", user.user_id); await save("private_key", inputParse.data.private_key); if (domain) { await save("ttp_url", `https://${domain}/`); } location.href = "/"; } catch (error) { log(0, "login", "red", error); toast("error", "Failed to fetch user data"); } }, [save], ); const isTauriEnv = isTauri(); return (
{isTauriEnv ? ( <> { if (!data.startsWith("tensamin://tu::")) { toast("error", "Invalid QR code"); return; } else { const decoded = data.replace("tensamin://tu::", ""); try { const { userId, privateKey, domain } = parseTuFileContent(decoded); await save("session_id", Date.now()); await save("user_id", userId); await save("private_key", privateKey); if (domain) { await save("ttp_url", `https://${domain}/`); } location.href = "/"; } catch (error) { log(0, "login", "red", error); toast("error", "Failed to parse QR code data"); } } }} /> ) : (
uploadRef.current?.click()} className="flex flex-col gap-3 cursor-pointer w-55 aspect-square bg-input/13 hover:bg-input/30 transition-all duration-300 ease-in-out border-3 items-center justify-center rounded-lg" >

Select .tu file

)}
); }