(feat): add tauth
(fix): formatted
This commit is contained in:
parent
d02bc29be0
commit
fb742bd068
15 changed files with 141 additions and 74 deletions
|
|
@ -4,12 +4,7 @@ import { useLocation, useNavigate, useSearch } from "@tanstack/react-router";
|
|||
import { useCall } from "@tensamin/call/context";
|
||||
import Wrapper from "@tensamin/user/wrapper";
|
||||
import { Skeleton } from "@tensamin/ui";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from "@tensamin/ui";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "@tensamin/ui";
|
||||
import { displayCallId } from "@tensamin/call/utils";
|
||||
import { useState } from "react";
|
||||
import { SidebarTrigger, useSidebar } from "@tensamin/ui";
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@ import {
|
|||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
Input, Button,
|
||||
useIsMobile
|
||||
Input,
|
||||
Button,
|
||||
useIsMobile,
|
||||
} from "@tensamin/ui";
|
||||
import z from "zod";
|
||||
import { toast } from "@tensamin/shared/log";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Form from "@/components/screens/login/form";
|
||||
import {CreateScreen} from "@tensamin/ui";
|
||||
import { CreateScreen } from "@tensamin/ui";
|
||||
|
||||
/**
|
||||
* Executes Page.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
Button
|
||||
Button,
|
||||
} from "@tensamin/ui";
|
||||
|
||||
export default function Page() {
|
||||
|
|
|
|||
7
bun.lock
7
bun.lock
|
|
@ -192,6 +192,7 @@
|
|||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@tanstack/react-router": "^1.0.0",
|
||||
"@tauri-apps/api": "^2.10.1",
|
||||
"@tensamin/crypto": "workspace:*",
|
||||
"@tensamin/shared": "workspace:*",
|
||||
"@tensamin/storage": "workspace:*",
|
||||
|
|
@ -238,7 +239,7 @@
|
|||
},
|
||||
},
|
||||
"overrides": {
|
||||
"@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.4.tar.gz",
|
||||
"@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.9.tar.gz",
|
||||
"@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.25.tar.gz",
|
||||
},
|
||||
"packages": {
|
||||
|
|
@ -690,7 +691,7 @@
|
|||
|
||||
"@tensamin/ttp": ["@tensamin/ttp@workspace:packages/ttp"],
|
||||
|
||||
"@tensamin/ttp-core": ["@tensamin/ttp-core@https://git.methanium.net/tensamin/ttp/archive/0.0.4.tar.gz", { "dependencies": { "@eslint/js": "^10.0.1", "@typescript-eslint/parser": "^8.58.1", "globals": "^17.5.0", "typescript": "^6.0.2", "typescript-eslint": "^8.58.1", "zod": "^4.3.6" } }, "sha512-FVtxXH9DBwHz2GpuFZFO2KifWefBOoK4/dv6yH4xyvDIS0PIVRc7UrBAgf2HQ3+N9jWaDw9FwyJkZJCAdVCNJQ=="],
|
||||
"@tensamin/ttp-core": ["@tensamin/ttp-core@https://git.methanium.net/tensamin/ttp/archive/0.0.9.tar.gz", { "dependencies": { "@eslint/js": "^10.0.1", "@typescript-eslint/parser": "^8.58.1", "@webtransport-bun/webtransport": "^0.3.0", "globals": "^17.5.0", "typescript": "^6.0.2", "typescript-eslint": "^8.58.1", "zod": "^4.3.6" } }, "sha512-V9EK/t/tcBe1oNqzUMxtn41wc3UIS0mkLvGeR1mGfZaKn4d9bwwWQ2VmSmRSxodPYiQUJkziJV4fZvdzGjnzFw=="],
|
||||
|
||||
"@tensamin/ui": ["@tensamin/ui@https://git.methanium.net/tensamin/ui/archive/0.0.25.tar.gz", { "dependencies": { "@base-ui/react": "^1.3.0", "@fontsource-variable/inter": "^5.2.6", "@tauri-apps/api": "^2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", "lucide-react": "^1.8.0", "next-themes": "^0.4.6", "react-day-picker": "^9.14.0", "react-resizable-panels": "^4.10.0", "recharts": "3.8.0", "shadcn": "^3.5.0", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tw-animate-css": "^1.3.0", "vaul": "^1.1.2" }, "peerDependencies": { "react": "^19.2.0", "react-dom": "^19.2.0" } }, "sha512-pYkl4quhAhV/l/j1Yy5m4DDV5fh42H4G8v+FE6qEMCcJIOeA1LU7brIG7X9zURe7F4gyn1LQxMf2yABuV/NsMQ=="],
|
||||
|
||||
|
|
@ -770,6 +771,8 @@
|
|||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="],
|
||||
|
||||
"@webtransport-bun/webtransport": ["@webtransport-bun/webtransport@0.3.0", "", { "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ] }, "sha512-/OX/TCgBD64n/0BjMNytObq5NK2pAM0KOoXBEw49l3sTmbVXNTPP1ZRPIdIyV43agM+mKtZBaV3Er3k3YoB6sw=="],
|
||||
|
||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
|
||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
},
|
||||
"overrides": {
|
||||
"@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.25.tar.gz",
|
||||
"@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.4.tar.gz"
|
||||
"@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.9.tar.gz"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tensamin/ui": "*",
|
||||
|
|
|
|||
|
|
@ -6,11 +6,7 @@ import { log } from "@tensamin/shared/log";
|
|||
import Text from "@tensamin/markdown/text";
|
||||
import { AlertTriangle, Clock } from "lucide-react";
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@tensamin/ui";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@tensamin/ui";
|
||||
import { useIsMobile } from "@tensamin/ui";
|
||||
import {
|
||||
ContextMenu,
|
||||
|
|
@ -36,7 +32,11 @@ function getSafeMessageHeight(height: number | undefined) {
|
|||
* @param props Parameter props.
|
||||
* @returns unknown.
|
||||
*/
|
||||
function MessageComponent({ message, notEncrypted, measureRef }: {
|
||||
function MessageComponent({
|
||||
message,
|
||||
notEncrypted,
|
||||
measureRef,
|
||||
}: {
|
||||
message: RawMessage & {
|
||||
failed?: boolean;
|
||||
};
|
||||
|
|
@ -113,19 +113,17 @@ function MessageComponent({ message, notEncrypted, measureRef }: {
|
|||
: "bg-muted"
|
||||
} ${!isReady && "animate-pulse"} ${isMobile && "select-none"}`}
|
||||
>
|
||||
{message.failed &&
|
||||
message.message_state === "awaiting" && (
|
||||
<Tooltip>
|
||||
<TooltipContent>
|
||||
<p>Failed to send message</p>
|
||||
</TooltipContent>
|
||||
<TooltipTrigger render={<AlertTriangle size={17} />} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{!message.failed &&
|
||||
message.message_state === "awaiting" && (
|
||||
<Clock size={17} />
|
||||
)}
|
||||
{message.failed && message.message_state === "awaiting" && (
|
||||
<Tooltip>
|
||||
<TooltipContent>
|
||||
<p>Failed to send message</p>
|
||||
</TooltipContent>
|
||||
<TooltipTrigger render={<AlertTriangle size={17} />} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{!message.failed && message.message_state === "awaiting" && (
|
||||
<Clock size={17} />
|
||||
)}
|
||||
{isReady ? (
|
||||
<Text value={decodedContent} />
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -25,10 +25,7 @@ function bytesToB64u(value: Uint8Array): string {
|
|||
output += index + 2 < value.length ? alphabet[chunk & 63] : "=";
|
||||
}
|
||||
|
||||
return output
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/g, "");
|
||||
return output.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { legalDocsSchema } from "./features/legal/schema";
|
||||
import { settingsStorageDefaults, type SettingsStorageDefaults } from "./settings";
|
||||
import {
|
||||
settingsStorageDefaults,
|
||||
type SettingsStorageDefaults,
|
||||
} from "./settings";
|
||||
|
||||
const fileFromMessage = z.object({
|
||||
name: z.string(),
|
||||
|
|
@ -155,6 +158,14 @@ export const ttp = {
|
|||
}),
|
||||
},
|
||||
|
||||
load_txt_record: {
|
||||
request: z.object({
|
||||
path: z.string(),
|
||||
}),
|
||||
response: z.object({
|
||||
content: z.string(),
|
||||
}),
|
||||
},
|
||||
authenticate_app: {
|
||||
request: z.object({
|
||||
app_identifier: z.string(),
|
||||
|
|
@ -163,13 +174,12 @@ export const ttp = {
|
|||
challenge: z.base64(),
|
||||
}),
|
||||
},
|
||||
get_app: {
|
||||
create_app: {
|
||||
request: z.object({
|
||||
app_public_key: z.base64(),
|
||||
app_identifier: z.string(),
|
||||
}),
|
||||
response: z.object({
|
||||
app_public_key: z.base64(),
|
||||
}),
|
||||
response: z.object({}),
|
||||
},
|
||||
|
||||
// Calls
|
||||
|
|
|
|||
|
|
@ -49,10 +49,11 @@ type BooleanSettingNames<T extends SettingsSchema> = {
|
|||
export type SettingsStorageKey<T extends SettingsSchema = typeof settings> =
|
||||
`settings.${BooleanSettingNames<T>}`;
|
||||
|
||||
export type SettingsStorageDefaults<T extends SettingsSchema = typeof settings> =
|
||||
{
|
||||
[K in SettingsStorageKey<T>]: boolean;
|
||||
};
|
||||
export type SettingsStorageDefaults<
|
||||
T extends SettingsSchema = typeof settings,
|
||||
> = {
|
||||
[K in SettingsStorageKey<T>]: boolean;
|
||||
};
|
||||
|
||||
function buildSettingsStorageDefaults<T extends SettingsSchema>(
|
||||
schema: T,
|
||||
|
|
@ -62,7 +63,10 @@ function buildSettingsStorageDefaults<T extends SettingsSchema>(
|
|||
for (const category of Object.values(schema)) {
|
||||
for (const page of Object.values(category)) {
|
||||
for (const [settingName, setting] of Object.entries(page)) {
|
||||
if (setting.type === "boolean" && typeof setting.default === "boolean") {
|
||||
if (
|
||||
setting.type === "boolean" &&
|
||||
typeof setting.default === "boolean"
|
||||
) {
|
||||
const key = `settings.${settingName}` as SettingsStorageKey<T>;
|
||||
defaults[key] = setting.default;
|
||||
}
|
||||
|
|
@ -73,4 +77,4 @@ function buildSettingsStorageDefaults<T extends SettingsSchema>(
|
|||
return defaults as SettingsStorageDefaults<T>;
|
||||
}
|
||||
|
||||
export const settingsStorageDefaults = buildSettingsStorageDefaults(settings);
|
||||
export const settingsStorageDefaults = buildSettingsStorageDefaults(settings);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-router": "^1.0.0",
|
||||
"@tauri-apps/api": "^2.10.1",
|
||||
"@tensamin/crypto": "workspace:*",
|
||||
"@tensamin/shared": "workspace:*",
|
||||
"@tensamin/storage": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -16,77 +16,128 @@ import { useDeeplinks } from "@tensamin/tauri/deeplinkHandler";
|
|||
import { useTTP } from "@tensamin/ttp";
|
||||
import { log, toast } from "@tensamin/shared/log";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { isTauri } from "@tauri-apps/api/core";
|
||||
|
||||
export default function Wrapper({ children }: { children: ReactNode }) {
|
||||
const { getSharedSecret } = useCrypto();
|
||||
const { get } = useUser();
|
||||
const { load } = useStorage();
|
||||
const { send } = useTTP();
|
||||
const { decrypt } = useCrypto();
|
||||
const { decrypt, getSharedSecret } = useCrypto();
|
||||
const { searchStr } = useLocation();
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [identifier, setIdentifier] = useState<string | null>(null);
|
||||
const [redirect, setRedirect] = useState<string | null>(null);
|
||||
const [challenge, setChallenge] = useState<string | null>(null);
|
||||
const [allowChildern, setAllowChildern] = useState(false);
|
||||
|
||||
const { deeplinks } = useDeeplinks();
|
||||
|
||||
const authorizeApp = async () => {
|
||||
if (!identifier || !redirect) return;
|
||||
if (!identifier || !redirect || !challenge) return;
|
||||
|
||||
try {
|
||||
// Get Data
|
||||
const user = await get(await load("user_id"));
|
||||
const app = await send("get_app", {
|
||||
app_identifier: identifier,
|
||||
const {
|
||||
data: { content: appPublicKey },
|
||||
} = await send("load_txt_record", {
|
||||
path: "tauth." + identifier,
|
||||
});
|
||||
|
||||
// Get Shared Secret
|
||||
const sharedSecret = await getSharedSecret(
|
||||
await load("private_key"),
|
||||
user.public_key,
|
||||
app.data.app_public_key,
|
||||
);
|
||||
|
||||
// Get Challenge
|
||||
const res = await send("authenticate_app", {
|
||||
app_identifier: identifier,
|
||||
appPublicKey,
|
||||
).catch((err) => {
|
||||
log(1, "tauth", "red", "Failed to get shared secret", err, {
|
||||
appPublicKey,
|
||||
});
|
||||
throw new Error("Failed to get shared secret");
|
||||
});
|
||||
const challenge = res.data.challenge;
|
||||
|
||||
// Solve Challenge
|
||||
const solvedChallenge = await decrypt(sharedSecret, challenge);
|
||||
const solvedChallenge = await decrypt(sharedSecret, challenge).catch(
|
||||
(err) => {
|
||||
log(1, "tauth", "red", "Failed to solve challenge", err, {
|
||||
challenge,
|
||||
});
|
||||
throw new Error("Failed to solve challenge");
|
||||
},
|
||||
);
|
||||
|
||||
// Craft Redirect URL
|
||||
const finalUrl = new URL(redirect);
|
||||
finalUrl.searchParams.set("userId", String(await load("user_id")));
|
||||
finalUrl.searchParams.set("challenge", solvedChallenge);
|
||||
finalUrl.searchParams.set("originalChallenge", challenge);
|
||||
|
||||
const session = crypto.randomUUID();
|
||||
finalUrl.searchParams.set("session", session);
|
||||
const session = Date.now();
|
||||
finalUrl.searchParams.set("sessionId", String(session));
|
||||
|
||||
// Save Session
|
||||
await send("create_app", {
|
||||
app_public_key: appPublicKey,
|
||||
app_identifier: identifier,
|
||||
});
|
||||
|
||||
// Open Redirect URL
|
||||
window.open(finalUrl.toString(), "_blank");
|
||||
if (isTauri()) {
|
||||
window.open(finalUrl.toString(), "_blank");
|
||||
setLoading(false);
|
||||
setDialogOpen(false);
|
||||
setIdentifier(null);
|
||||
setRedirect(null);
|
||||
setChallenge(null);
|
||||
|
||||
toast("success", "App authorized successfully");
|
||||
return;
|
||||
} else {
|
||||
window.location.href = finalUrl.toString();
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
toast("error", "Failed to authorize app");
|
||||
log(1, "tauth", "red", "Failed to authorize app", err);
|
||||
} finally {
|
||||
setDialogOpen(false);
|
||||
|
||||
setLoading(false);
|
||||
setDialogOpen(false);
|
||||
setIdentifier(null);
|
||||
setRedirect(null);
|
||||
setChallenge(null);
|
||||
}
|
||||
};
|
||||
|
||||
function hexToBase64(hex: string): string {
|
||||
const bytes = hex.match(/.{2}/g)?.map((byte) => parseInt(byte, 16)) ?? [];
|
||||
const binaryString = String.fromCharCode(...bytes);
|
||||
return btoa(binaryString);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(searchStr);
|
||||
const identifier = params.get("identifier");
|
||||
const redirect = params.get("redirect");
|
||||
const challenge = params.get("challenge");
|
||||
if (!identifier || !redirect) return;
|
||||
|
||||
// @eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
setIdentifier(identifier);
|
||||
setRedirect(redirect);
|
||||
setDialogOpen(true);
|
||||
}, [searchStr]);
|
||||
load("user_id").then((userId) => {
|
||||
if (!challenge) {
|
||||
const newUrl = new URL(redirect);
|
||||
newUrl.searchParams.set("userId", String(userId));
|
||||
window.location.href = newUrl.toString();
|
||||
return;
|
||||
}
|
||||
|
||||
setAllowChildern(true);
|
||||
setIdentifier(identifier);
|
||||
setRedirect(redirect);
|
||||
setChallenge(hexToBase64(challenge || ""));
|
||||
setDialogOpen(true);
|
||||
});
|
||||
}, [searchStr, load]);
|
||||
|
||||
useEffect(() => {
|
||||
deeplinks.forEach((link) => {
|
||||
|
|
@ -98,6 +149,7 @@ export default function Wrapper({ children }: { children: ReactNode }) {
|
|||
if (identifier && redirect) {
|
||||
setIdentifier(identifier);
|
||||
setRedirect(redirect);
|
||||
setChallenge(hexToBase64(url.searchParams.get("challenge") || ""));
|
||||
setDialogOpen(true);
|
||||
}
|
||||
}
|
||||
|
|
@ -113,6 +165,7 @@ export default function Wrapper({ children }: { children: ReactNode }) {
|
|||
if (!value) {
|
||||
setIdentifier(null);
|
||||
setRedirect(null);
|
||||
setChallenge(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
@ -147,6 +200,7 @@ export default function Wrapper({ children }: { children: ReactNode }) {
|
|||
setDialogOpen(false);
|
||||
setIdentifier(null);
|
||||
setRedirect(null);
|
||||
setChallenge(null);
|
||||
}}
|
||||
>
|
||||
Deny
|
||||
|
|
@ -154,7 +208,7 @@ export default function Wrapper({ children }: { children: ReactNode }) {
|
|||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{children}
|
||||
{allowChildern && children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@ import {
|
|||
import { useCrypto } from "@tensamin/crypto/context";
|
||||
import { log } from "@tensamin/shared/log";
|
||||
import { useStorage } from "@tensamin/storage/context";
|
||||
import { createTransportClient, READY_STATE, type BoundSendFn } from "@tensamin/ttp-core";
|
||||
import {
|
||||
createTransportClient,
|
||||
READY_STATE,
|
||||
type BoundSendFn,
|
||||
} from "@tensamin/ttp-core";
|
||||
import type { PushHandler } from "@tensamin/ttp-core";
|
||||
import {
|
||||
PING_INTERVAL,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
export * from "@tensamin/ttp-core";
|
||||
export * from "./context";
|
||||
export * from "./values";
|
||||
export * from "./values";
|
||||
|
|
|
|||
|
|
@ -77,4 +77,4 @@ const moveBundleArtifact = (
|
|||
moveBundleArtifact(debDir, ".deb", `Tensamin-${version}.deb`);
|
||||
moveBundleArtifact(rpmDir, ".rpm", `Tensamin-${version}.rpm`);
|
||||
|
||||
console.log("Releases copied successfully.");
|
||||
console.log("Releases copied successfully.");
|
||||
|
|
|
|||
Loading…
Reference in a new issue