import { Button, cn, useIsMobile } from "@methanium/ui"; import { Input } from "@methanium/ui"; import { Label } from "@methanium/ui"; import { useStorage } from "@tensamin/storage/context"; import { log, toast } from "@tensamin/shared/log"; import { File } from "lucide-react"; import { type ChangeEvent, type FormEvent, useCallback, useEffect, useRef, useState, } from "react"; import { z } from "zod"; import { isTauri } from "@tauri-apps/api/core"; import QrCodeScanner from "@tensamin/tauri/qrCodeScanner"; import { useNavigate } from "@tanstack/react-router"; 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), mtp_keyring: 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 domain = userIdString.includes("@") ? userIdString.split("@")[1] : null; if (!userId || !privateKey) { throw new Error("Invalid file"); } console.log({ domain, userId, }); return { userId, privateKey, domain }; } export default function Form() { const isMobile = useIsMobile(); const uploadRef = useRef(null); const [isDragging, setIsDragging] = useState(false); const { save } = useStorage(); const navigate = useNavigate(); const loginPendingRef = useRef(false); const persistLogin = useCallback( async (userId: number, privateKey: string, domain?: string | null) => { if (loginPendingRef.current) return false; loginPendingRef.current = true; try { if (domain) await save("omega_url", `https://${domain}/`); await save("mtp_keyring", privateKey, { secure: true }); await save("session_id", Date.now()); await save("user_id", userId); await navigate({ to: "/", replace: true }); return true; } finally { loginPendingRef.current = false; } }, [navigate, save], ); // Process dropped files const processDroppedFile = useCallback( async (file: globalThis.File): Promise => { try { if (!file.name.endsWith(".tu")) { toast("error", "Please upload a .tu file"); return; } const raw = await file.text(); const parsed = parseTuFileContent(raw); await persistLogin(parsed.userId, parsed.privateKey, parsed.domain); } catch (error) { log(0, "login", "red", error); toast("error", "Failed to load file"); } }, [persistLogin], ); // Handle .tu files const handleFileInputChange = useCallback( async (event: ChangeEvent): Promise => { const file = event.currentTarget.files?.[0]; if (!file) { toast("error", "No file selected"); return; } await processDroppedFile(file); }, [processDroppedFile], ); // Drag and drop listener useEffect(() => { let dragCounter = 0; const handleDragEnter = (event: DragEvent) => { event.preventDefault(); event.stopPropagation(); dragCounter++; if (event.dataTransfer?.items?.length) { setIsDragging(true); } }; const handleDragLeave = (event: DragEvent) => { event.preventDefault(); event.stopPropagation(); dragCounter--; if (dragCounter <= 0) { setIsDragging(false); } }; const handleDragOver = (event: DragEvent) => { event.preventDefault(); event.stopPropagation(); if (event.dataTransfer) { event.dataTransfer.dropEffect = "copy"; } }; const handleDrop = async (event: DragEvent) => { event.preventDefault(); event.stopPropagation(); dragCounter = 0; setIsDragging(false); const file = event.dataTransfer?.files?.[0]; if (!file) { toast("error", "No file dropped"); return; } await processDroppedFile(file); }; window.addEventListener("dragenter", handleDragEnter); window.addEventListener("dragleave", handleDragLeave); window.addEventListener("dragover", handleDragOver); window.addEventListener("drop", handleDrop); return () => { window.removeEventListener("dragenter", handleDragEnter); window.removeEventListener("dragleave", handleDragLeave); window.removeEventListener("dragover", handleDragOver); window.removeEventListener("drop", handleDrop); }; }, [processDroppedFile]); /** * Handles username and private key login submission. * @param event Form submit event. * @returns Promise that resolves after login processing. */ const handleCredentialsSubmit = useCallback( async (event: FormEvent): 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 domain = inputUsername.includes("@") ? inputUsername.split("@")[1] : 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 persistLogin(user.user_id, inputParse.data.mtp_keyring, domain); } catch (error) { log(0, "login", "red", error); toast("error", "Failed to fetch user data"); } }, [persistLogin], ); return (
{isTauri() && isMobile ? ( <> { if (!data.startsWith("tensamin://tu::")) { toast("error", "Invalid QR code"); return; } const decoded = data.replace("tensamin://tu::", ""); try { const { userId, privateKey, domain } = parseTuFileContent(decoded); await persistLogin(userId, privateKey, domain); } catch (error) { log(0, "login", "red", error); toast("error", "Failed to parse QR code data"); } }} /> ) : (
uploadRef.current?.click()} className={cn( "flex flex-col gap-3 cursor-pointer w-55 aspect-square", "border-3 items-center justify-center rounded-lg", "transition-all duration-300 ease-in-out", "border-input", "bg-input/13 hover:bg-input/30", isDragging ? "animate-wiggle" : "", )} >

Select .tu file

)}
); }