(feat): migrate ttp to mtp
(wip): crypto migration
This commit is contained in:
parent
4e69b8ef77
commit
930663d495
30 changed files with 559 additions and 749 deletions
|
|
@ -21,5 +21,5 @@ export function initTray() {
|
|||
|
||||
tray.on("click", (event) => {
|
||||
console.log(event);
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,13 +19,13 @@ export function Basic({
|
|||
extra?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Card className="animate-in fade-in duration-300 rounded-xl py-0 m-px">
|
||||
<Card className="animate-in fade-in duration-300 rounded-xl py-0 h-12.5!">
|
||||
<CardHeader className="flex flex-row gap-2.5 items-center justify-start p-2">
|
||||
<div className="relative shrink-0 overflow-visible">
|
||||
<Avatar>
|
||||
<AvatarImage src={user.avatar} />
|
||||
<AvatarImage src={user.Avatar} />
|
||||
<AvatarFallback>
|
||||
{user.display.slice(0, 2).toUpperCase()}
|
||||
{user.Display.slice(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<Tooltip>
|
||||
|
|
@ -49,7 +49,7 @@ export function Basic({
|
|||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 w-full items-start justify-center text-[15px]">
|
||||
<p>{user.display}</p>
|
||||
<p>{user.Display}</p>
|
||||
</div>
|
||||
<div className="pr-1">{extra}</div>
|
||||
</CardHeader>
|
||||
|
|
@ -58,5 +58,5 @@ export function Basic({
|
|||
}
|
||||
|
||||
export function Loading() {
|
||||
return <Skeleton className="h-12.5 rounded-2xl" />;
|
||||
return <Skeleton className="h-12.5! rounded-2xl" />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export default function Profile({ user }: { user: User }) {
|
|||
void (async () => {
|
||||
try {
|
||||
const ownId = await load("user_id");
|
||||
const privateKey = await load("private_key");
|
||||
const privateKey = await load("mtp_keyring");
|
||||
const ownData = await get(ownId);
|
||||
const secret = await getSharedSecret(
|
||||
privateKey,
|
||||
|
|
@ -61,17 +61,17 @@ export default function Profile({ user }: { user: User }) {
|
|||
<div className="flex flex-col gap-2">
|
||||
<div className="flex gap-2 items-center">
|
||||
<Avatar className="size-10">
|
||||
<AvatarImage src={user.avatar} />
|
||||
<AvatarImage src={user.Avatar} />
|
||||
<AvatarFallback className="text-lg">
|
||||
{user.display.slice(0, 2).toUpperCase()}
|
||||
{user.Display.slice(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-lg font-semibold">{user.display}</p>
|
||||
<p className="text-muted-foreground">{user.username}</p>
|
||||
<p className="text-lg font-semibold">{user.Display}</p>
|
||||
<p className="text-muted-foreground">{user.Username}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Text value={user.about || ""} />
|
||||
<Text value={user.About || ""} />
|
||||
<Button
|
||||
className="h-auto justify-start gap-1.5 px-0 py-1 text-base font-medium text-white no-underline hover:no-underline"
|
||||
variant="link"
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
|||
userId={id}
|
||||
component={(user) =>
|
||||
isMobile ? (
|
||||
<p className="font-medium text-[1.07rem]">{user?.display}</p>
|
||||
<p className="font-medium text-[1.07rem]">{user?.Display}</p>
|
||||
) : (
|
||||
<Popover open={userInfoOpen} onOpenChange={setUserInfoOpen}>
|
||||
<PopoverTrigger
|
||||
|
|
@ -85,7 +85,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
|||
}}
|
||||
>
|
||||
<p className="font-medium text-[1.07rem]">
|
||||
{user?.display}
|
||||
{user?.Display}
|
||||
</p>
|
||||
</Button>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const fetchedUser = z.object({
|
|||
|
||||
const formSchema = z.object({
|
||||
username: z.string().min(1).max(255),
|
||||
private_key: z.string().min(1).max(92),
|
||||
mtp_keyring: z.string().min(1).max(92),
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
@ -61,9 +61,7 @@ function parseTuFileContent(rawFileContent: string): {
|
|||
? Number(userIdString.split("@")[0])
|
||||
: Number(userIdString);
|
||||
|
||||
const domain = userIdString.includes("@")
|
||||
? userIdString.split("@")[1]
|
||||
: null;
|
||||
const domain = userIdString.includes("@") ? userIdString.split("@")[1] : null;
|
||||
|
||||
if (!userId || !privateKey) {
|
||||
throw new Error("Invalid file");
|
||||
|
|
@ -97,9 +95,9 @@ export default function Form() {
|
|||
|
||||
await save("session_id", Date.now());
|
||||
await save("user_id", parsed.userId);
|
||||
await save("private_key", parsed.privateKey);
|
||||
await save("mtp_keyring", parsed.privateKey);
|
||||
if (parsed.domain) {
|
||||
await save("mtp_url", `https://${parsed.domain}/`);
|
||||
await save("omega_url", `https://${parsed.domain}/`);
|
||||
}
|
||||
|
||||
location.href = "/";
|
||||
|
|
@ -240,9 +238,9 @@ export default function Form() {
|
|||
|
||||
await save("session_id", Date.now());
|
||||
await save("user_id", user.user_id);
|
||||
await save("private_key", inputParse.data.private_key);
|
||||
await save("mtp_keyring", inputParse.data.mtp_keyring);
|
||||
if (domain) {
|
||||
await save("mtp_url", `https://${domain}/`);
|
||||
await save("omega_url", `https://${domain}/`);
|
||||
}
|
||||
|
||||
location.href = "/";
|
||||
|
|
@ -273,10 +271,10 @@ export default function Form() {
|
|||
|
||||
await save("session_id", Date.now());
|
||||
await save("user_id", userId);
|
||||
await save("private_key", privateKey);
|
||||
await save("mtp_keyring", privateKey);
|
||||
|
||||
if (domain) {
|
||||
await save("mtp_url", `https://${domain}/`);
|
||||
await save("omega_url", `https://${domain}/`);
|
||||
}
|
||||
|
||||
location.href = "/";
|
||||
|
|
@ -335,8 +333,8 @@ export default function Form() {
|
|||
<Input required type="text" id="username" name="username" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="private_key">Private Key</Label>
|
||||
<Input required type="password" id="private_key" name="private_key" />
|
||||
<Label htmlFor="mtp_keyring">MTP Keyring</Label>
|
||||
<Input required type="password" id="mtp_keyring" name="mtp_keyring" />
|
||||
</div>
|
||||
<Button className="mt-auto" type="submit">
|
||||
Login
|
||||
|
|
|
|||
|
|
@ -40,9 +40,7 @@ import type z from "zod";
|
|||
import { mtp } from "@tensamin/shared/data";
|
||||
import { useMTP } from "@tensamin/mtp";
|
||||
|
||||
type OnlineStatus = z.infer<
|
||||
typeof mtp.get_user_data.response.shape.OnlineStatus
|
||||
>;
|
||||
type OnlineStatus = z.infer<typeof mtp.GetUserData.response.shape.OnlineStatus>;
|
||||
|
||||
const onlineStatusLabels: Record<OnlineStatus, string> = {
|
||||
user_online: "Online",
|
||||
|
|
@ -88,7 +86,7 @@ function StatusDialog({
|
|||
open={open}
|
||||
onOpenChange={(nextOpen) => {
|
||||
if (!nextOpen) {
|
||||
setDraftStatus(user.status ?? "");
|
||||
setDraftStatus(user.Status ?? "");
|
||||
setDraftOnlineStatus(user.OnlineStatus);
|
||||
setErrorMessage("");
|
||||
setSaveSucceeded(false);
|
||||
|
|
@ -147,8 +145,7 @@ function StatusDialog({
|
|||
OnlineStatus: draftOnlineStatus,
|
||||
};
|
||||
|
||||
const validation =
|
||||
mtp.change_user_data.request.safeParse(payload);
|
||||
const validation = mtp.ChangeUserData.request.safeParse(payload);
|
||||
|
||||
if (!validation.success) {
|
||||
setSaveSucceeded(false);
|
||||
|
|
@ -159,7 +156,7 @@ function StatusDialog({
|
|||
}
|
||||
|
||||
try {
|
||||
await send("change_user_data", validation.data);
|
||||
await send("ChangeUserData", validation.data);
|
||||
setSaveSucceeded(true);
|
||||
setErrorMessage("");
|
||||
} catch (err) {
|
||||
|
|
@ -229,7 +226,7 @@ export default function Sidebar() {
|
|||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setDraftStatus(user.status ?? "");
|
||||
setDraftStatus(user.Status ?? "");
|
||||
setDraftOnlineStatus(user.OnlineStatus);
|
||||
setStatusErrorMessage("");
|
||||
setStatusSaveSucceeded(false);
|
||||
|
|
@ -248,7 +245,7 @@ export default function Sidebar() {
|
|||
open={dialogOpen}
|
||||
onOpenChange={(nextOpen) => {
|
||||
if (!nextOpen) {
|
||||
setDraftStatus(user.status ?? "");
|
||||
setDraftStatus(user.Status ?? "");
|
||||
setDraftOnlineStatus(user.OnlineStatus);
|
||||
setStatusErrorMessage("");
|
||||
setStatusSaveSucceeded(false);
|
||||
|
|
|
|||
|
|
@ -55,8 +55,8 @@ function AddConversationButton() {
|
|||
}
|
||||
|
||||
// user existence check
|
||||
const user = await send("get_user_data", {
|
||||
username: result.data,
|
||||
const user = await send("GetUserData", {
|
||||
Username: result.data,
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.data.UserId === 0) {
|
||||
|
|
@ -80,7 +80,7 @@ function AddConversationButton() {
|
|||
// add the conv
|
||||
const timeout = setTimeout(() => setLoading(true), 500);
|
||||
|
||||
send("add_conversation", {
|
||||
send("AddConversation", {
|
||||
ChatPartnerName: result.data,
|
||||
})
|
||||
.then(() => {
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export default function Page() {
|
|||
const avatarUploadRef = useRef<HTMLInputElement>(null);
|
||||
const draftInitializedRef = useRef(false);
|
||||
const effectiveAvatar =
|
||||
draftUser.avatar === "none" ? undefined : draftUser.avatar;
|
||||
draftUser.Avatar === "none" ? undefined : draftUser.Avatar;
|
||||
|
||||
const updateDraftUser = (
|
||||
updater: (previous: Partial<User>) => Partial<User>,
|
||||
|
|
@ -102,8 +102,8 @@ export default function Page() {
|
|||
<Avatar className="size-14">
|
||||
<AvatarImage src={effectiveAvatar} />
|
||||
<AvatarFallback className="text-2xl">
|
||||
{draftUser.display?.slice(0, 2).toUpperCase() ||
|
||||
currentUser.display.slice(0, 2).toUpperCase()}
|
||||
{draftUser.Display?.slice(0, 2).toUpperCase() ||
|
||||
currentUser.Display.slice(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-col gap-1">
|
||||
|
|
@ -140,7 +140,7 @@ export default function Page() {
|
|||
}))
|
||||
}
|
||||
placeholder="Display Name"
|
||||
value={draftUser.display || ""}
|
||||
value={draftUser.Display || ""}
|
||||
/>
|
||||
<Input
|
||||
className="w-full"
|
||||
|
|
@ -151,7 +151,7 @@ export default function Page() {
|
|||
}))
|
||||
}
|
||||
placeholder="Username"
|
||||
value={draftUser.username || ""}
|
||||
value={draftUser.Username || ""}
|
||||
/>
|
||||
<MDInput
|
||||
styled
|
||||
|
|
@ -162,23 +162,23 @@ export default function Page() {
|
|||
setValue={(value) =>
|
||||
updateDraftUser((prev) => ({ ...prev, about: value }))
|
||||
}
|
||||
value={draftUser.about || ""}
|
||||
value={draftUser.About || ""}
|
||||
/>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const { avatar, ...draftUsersWithoutAvatar } = draftUser;
|
||||
const { Avatar, ...draftUsersWithoutAvatar } = draftUser;
|
||||
const payload = {
|
||||
...draftUsersWithoutAvatar,
|
||||
...(typeof avatar === "string"
|
||||
...(typeof Avatar === "string"
|
||||
? {
|
||||
avatar: avatar.startsWith("data:")
|
||||
? (avatar.split(",", 2)[1] ?? "")
|
||||
: avatar,
|
||||
avatar: Avatar.startsWith("data:")
|
||||
? (Avatar.split(",", 2)[1] ?? "")
|
||||
: Avatar,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
const validation = mtp.change_user_data.request.safeParse(payload);
|
||||
const validation = mtp.ChangeUserData.request.safeParse(payload);
|
||||
|
||||
if (!validation.success) {
|
||||
setSaveSucceeded(false);
|
||||
|
|
@ -189,7 +189,7 @@ export default function Page() {
|
|||
}
|
||||
|
||||
try {
|
||||
await send("change_user_data", validation.data);
|
||||
await send("ChangeUserData", validation.data);
|
||||
setSaveSucceeded(true);
|
||||
setErrorMessage("");
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -1,124 +1,83 @@
|
|||
import { useStorage } from "@tensamin/storage/context";
|
||||
import { Button, Input, Label } from "@tensamin/ui";
|
||||
import { useEffect, useState } from "react";
|
||||
import QRCode from "qrcode";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
Button,
|
||||
} from "@tensamin/ui";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
<QrCodeLogin />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const { save, load } = useStorage();
|
||||
const [draftMtpUrl, setDraftMtpUrl] = useState("");
|
||||
const [currentMtpUrl, setCurrentMtpUrl] = useState("");
|
||||
|
||||
// QR Code Login
|
||||
const generateQR = async (text: string): Promise<string> => {
|
||||
try {
|
||||
const url = await QRCode.toDataURL(text, {
|
||||
errorCorrectionLevel: "H",
|
||||
margin: 1,
|
||||
color: {
|
||||
dark: "#000000FF",
|
||||
light: "#FFFFFFFF",
|
||||
},
|
||||
});
|
||||
return url;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
function QrCodeLogin() {
|
||||
const { load } = useStorage();
|
||||
const [userId, setUserId] = useState<number>(0);
|
||||
const [privateKey, setPrivateKey] = useState<string>("");
|
||||
const [qrCodeBase64, setQrCodeBase64] = useState<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [connectionString, setConnectionString] = useState<string | null>(null);
|
||||
const [draftForcedOmikronUrl, setDraftForcedOmikronUrl] = useState("");
|
||||
const [currentForcedOmikronUrl, setForcedForcedOmikronUrl] = useState("");
|
||||
const [draftForcedOmikronPublicKey, setDraftForcedOmikronPublicKey] =
|
||||
useState("");
|
||||
const [currentForcedOmikronPublicKey, setForcedForcedOmikronPublicKey] =
|
||||
useState("");
|
||||
|
||||
useEffect(() => {
|
||||
load("private_key").then((value) => {
|
||||
if (value) {
|
||||
setPrivateKey(value);
|
||||
}
|
||||
load("omega_url").then((value) => {
|
||||
setDraftMtpUrl(value);
|
||||
setCurrentMtpUrl(value);
|
||||
});
|
||||
load("user_id").then((value) => {
|
||||
if (value) {
|
||||
setUserId(value);
|
||||
}
|
||||
load("forced_omikron_url").then((value) => {
|
||||
setDraftForcedOmikronUrl(value || "");
|
||||
setForcedForcedOmikronUrl(value || "");
|
||||
});
|
||||
load("forced_omikron_public_key").then((value) => {
|
||||
setDraftForcedOmikronPublicKey(value || "");
|
||||
setForcedForcedOmikronPublicKey(value || "");
|
||||
});
|
||||
load("mtp_url")
|
||||
.then((value) => {
|
||||
if (value) {
|
||||
const url = new URL(value);
|
||||
setConnectionString(`@${url.host}`);
|
||||
} else {
|
||||
setConnectionString("");
|
||||
}
|
||||
})
|
||||
.catch(() => setConnectionString(""));
|
||||
}, [load]);
|
||||
|
||||
useEffect(() => {
|
||||
if (userId && privateKey && connectionString !== null) {
|
||||
generateQR(
|
||||
`tensamin://tu::${userId}${connectionString}::${privateKey}`,
|
||||
).then(setQrCodeBase64);
|
||||
}
|
||||
}, [userId, privateKey, connectionString]);
|
||||
|
||||
const [qrCodeVisible, setQrCodeVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1 pl-2">
|
||||
<p>Login QR Code</p>
|
||||
<div className="relative rounded-lg w-50 border-3 aspect-square overflow-hidden">
|
||||
{qrCodeBase64 && (
|
||||
<img className="w-full h-full" src={qrCodeBase64} alt="QR Code" />
|
||||
)}
|
||||
{!qrCodeVisible && (
|
||||
<>
|
||||
<div className="absolute top-0 left-0 w-full h-full backdrop-blur-sm bg-background/50" />
|
||||
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center">
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger render={<Button>Show QR Code</Button>} />
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Exposing this QR code is the same as sharing your .tu
|
||||
file. Since changing your private key is quite tedious,
|
||||
only do this when you're confident the key won't be
|
||||
compromised.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
render={
|
||||
<Button onClick={() => setQrCodeVisible(true)}>
|
||||
Show QR Code
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex flex-col gap-8">
|
||||
<p className="text-destructive">
|
||||
It's best not to touch these! They can be exploited to gain access to
|
||||
your account!
|
||||
</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Omega Url</Label>
|
||||
<div className="flex gap-1">
|
||||
<Input
|
||||
value={draftMtpUrl}
|
||||
onChange={(e) => setDraftMtpUrl(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
disabled={currentMtpUrl === draftMtpUrl}
|
||||
onClick={() => {
|
||||
save("omega_url", draftMtpUrl);
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Forced Omikron</Label>
|
||||
<div className="flex gap-1">
|
||||
<Input
|
||||
placeholder="URL..."
|
||||
value={draftForcedOmikronUrl || ""}
|
||||
onChange={(e) => setDraftForcedOmikronUrl(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Public Key..."
|
||||
value={draftForcedOmikronPublicKey || ""}
|
||||
onChange={(e) => setDraftForcedOmikronPublicKey(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
disabled={
|
||||
currentForcedOmikronUrl === draftForcedOmikronUrl &&
|
||||
currentForcedOmikronPublicKey === draftForcedOmikronPublicKey
|
||||
}
|
||||
onClick={() => {
|
||||
save("forced_omikron_url", draftForcedOmikronUrl);
|
||||
save("forced_omikron_public_key", draftForcedOmikronPublicKey);
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue