feat: add auth to tui

This commit is contained in:
Max Koon
2025-11-17 10:08:10 -05:00
parent 114eaf88eb
commit 9e11455db1
17 changed files with 388 additions and 16 deletions

View File

@@ -1,6 +1,6 @@
import { betterAuth } from "better-auth"; import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { genericOAuth } from "better-auth/plugins"; import { bearer, deviceAuthorization, genericOAuth } from "better-auth/plugins";
import { expo } from "@better-auth/expo"; import { expo } from "@better-auth/expo";
import { drizzleSchema } from "@money/shared/db"; import { drizzleSchema } from "@money/shared/db";
import { db } from "./db"; import { db } from "./db";
@@ -37,6 +37,8 @@ export const auth = betterAuth({
scopes: ["profile", "email"], scopes: ["profile", "email"],
} }
] ]
}) }),
deviceAuthorization(),
bearer(),
] ]
}); });

View File

@@ -99,6 +99,8 @@ const createMutators = (authData: AuthData | null) => {
id: randomUUID(), id: randomUUID(),
userId: authData.user.id, userId: authData.user.id,
token: data.access_token, token: data.access_token,
logoUrl: "",
name: ""
}); });
}, },

View File

@@ -45,6 +45,7 @@ export default function RootLayout() {
<Stack> <Stack>
<Stack.Protected guard={!isPending && !!session}> <Stack.Protected guard={!isPending && !!session}>
<Stack.Screen name="[...route]" options={{ headerShown: false }} /> <Stack.Screen name="[...route]" options={{ headerShown: false }} />
<Stack.Screen name="approve" />
</Stack.Protected> </Stack.Protected>
<Stack.Protected guard={!isPending && !session}> <Stack.Protected guard={!isPending && !session}>
<Stack.Screen name="auth" /> <Stack.Screen name="auth" />

23
apps/expo/app/approve.tsx Normal file
View File

@@ -0,0 +1,23 @@
import { authClient } from "@/lib/auth-client";
import { useLocalSearchParams } from "expo-router";
import { useEffect } from "react";
import { Text } from "react-native";
export default function Page() {
const { code } = useLocalSearchParams<{code: string }>();
const { isPending, data } = authClient.useSession();
if (isPending) return <Text>Loading...</Text>;
if (!isPending && !data) return <Text>Please log in</Text>;
useEffect(() => {
authClient.device.approve({
userCode: code,
});
}, []);
return <Text>
Approving: {code}
</Text>
}

View File

@@ -1,5 +1,5 @@
import { createAuthClient } from "better-auth/react"; import { createAuthClient } from "better-auth/react";
import { genericOAuthClient } from "better-auth/client/plugins"; import { deviceAuthorizationClient, genericOAuthClient } from "better-auth/client/plugins";
import { expoClient } from "@better-auth/expo/client"; import { expoClient } from "@better-auth/expo/client";
import * as SecureStore from "expo-secure-store"; import * as SecureStore from "expo-secure-store";
import { BASE_URL } from "@money/shared"; import { BASE_URL } from "@money/shared";
@@ -13,5 +13,6 @@ export const authClient = createAuthClient({
storage: SecureStore, storage: SecureStore,
}), }),
genericOAuthClient(), genericOAuthClient(),
deviceAuthorizationClient(),
] ]
}); });

View File

@@ -0,0 +1,11 @@
import { createAuthClient } from "better-auth/client";
import { deviceAuthorizationClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
baseURL: "http://laptop:3000",
plugins: [
deviceAuthorizationClient(),
]
});

View File

@@ -9,10 +9,12 @@
"react": "*" "react": "*"
}, },
"dependencies": { "dependencies": {
"@money/ui": "workspace:*",
"@money/shared": "workspace:*", "@money/shared": "workspace:*",
"@money/ui": "workspace:*",
"@opentui/core": "^0.1.39", "@opentui/core": "^0.1.39",
"@opentui/react": "^0.1.39", "@opentui/react": "^0.1.39",
"@types/qrcode": "^1.5.6",
"qrcode": "^1.5.4",
"react-native": "^0.82.1", "react-native": "^0.82.1",
"react-native-opentui": "workspace:*" "react-native-opentui": "workspace:*"
}, },

79
apps/tui/src/auth.tsx Normal file
View File

@@ -0,0 +1,79 @@
import { authClient } from "@/lib/auth-client";
import { use, useEffect, useState } from "react";
import { AuthContext } from "./auth/context";
import * as Code from "./auth/code";
const CLIENT_ID = "koon-family";
export function Auth() {
const { setAuth } = use(AuthContext);
const [code, setCode] = useState<string>();
const [error, setError] = useState<string>();
const pollForToken = async (code: string, interval = 5) => {
const { data, error } = await authClient.device.token({
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
device_code: code,
client_id: CLIENT_ID,
fetchOptions: { headers: { "user-agent": "My CLI" } },
});
if (data?.access_token) {
const { data: sessionData, error } = await authClient.getSession({
fetchOptions: {
auth: {
type: "Bearer",
token: data.access_token
}
}
});
if (error) return setError(error.message);
if (!sessionData) return setError("No data");
setAuth({
token: data.access_token,
auth: sessionData,
});
}
if (error) {
if (error.error === "authorization_pending") {
setTimeout(() => pollForToken(code, interval), interval * 1000);
} else if (error.error === "slow_down") {
setTimeout(() => pollForToken(code, interval + 5), (interval + 5) * 1000);
} else {
setError(`${error}`);
}
}
}
async function getCode() {
try {
const { data, error } = await authClient.device.code({
client_id: CLIENT_ID,
scope: "openid profile email",
});
if (error) return setError(error.error_description);
if (!data) return setError("No data returned");
setCode(data.user_code);
pollForToken(data?.device_code);
} catch (e) {
setError(`${e}`);
}
}
useEffect(() => {
getCode();
}, []);
if (error) return <Code.Error msg={error} />
return !code ? <Code.Loading /> : <Code.Display code={code} />;
}

View File

@@ -0,0 +1,48 @@
import { QR } from "@/util/qr";
import type { ReactNode } from "react";
function CodeDisplay({ children }: { children: ReactNode }) {
return (
<box alignItems="center" justifyContent="center" flexGrow={1}>
<box flexDirection="row" gap={2}>
{children}
</box>
</box>
);
}
export function Display({ code }: { code: string }) {
const URL = `https://money.koon.us/approve?code=${code}`;
return <CodeDisplay>
<text fg="black">{QR(URL)}</text>
<box justifyContent="center" gap={1}>
<text fg="black">Welcome to Koon Money</text>
<text fg="black">Go to: {URL}</text>
<text fg="black">Code: {code}</text>
</box>
</CodeDisplay>
}
export function Loading() {
return <CodeDisplay>
<box width={29} height={15} backgroundColor="gray" />
<box justifyContent="center" gap={1}>
<text fg="black">Welcome to Koon Money</text>
<text fg="black">You need to login first</text>
<text fg="black">Loading login information</text>
</box>
</CodeDisplay>
}
export function Error({ msg }: { msg: string }) {
return <CodeDisplay>
<box justifyContent="center" alignItems="center" gap={1}>
<text fg="red">Could not login</text>
<text fg="red">{msg}</text>
</box>
</CodeDisplay>
}

View File

@@ -0,0 +1,19 @@
import { type AuthData } from "@money/shared/auth";
import { createContext } from "react";
export type AuthType = {
auth: AuthData;
token: string;
};
export type AuthContextType = {
auth: AuthType | null;
setAuth: (auth: AuthContextType['auth']) => void;
};
export const AuthContext = createContext<AuthContextType>({
auth: null,
setAuth: () => {}
});

View File

@@ -1,28 +1,57 @@
import { RGBA, TextAttributes, createCliRenderer } from "@opentui/core"; import { createCliRenderer } from "@opentui/core";
import { createRoot } from "@opentui/react"; import { createRoot } from "@opentui/react";
import { App, type Route } from "@money/ui"; import { App, type Route } from "@money/ui";
import { ZeroProvider } from "@rocicorp/zero/react"; import { ZeroProvider } from "@rocicorp/zero/react";
import { schema } from '@money/shared'; import { schema } from '@money/shared';
import { useState } from "react"; import { use, useState } from "react";
import { writeFileSync, readFileSync, existsSync, rmSync, mkdirSync } from "fs";
import { join } from "path";
import { homedir } from "os";
import { Auth } from "./auth";
import { AuthContext, type AuthType } from "./auth/context";
const userID = "anon"; const userID = "anon";
const server = "http://laptop:4848"; const server = "http://laptop:4848";
const auth = undefined; // const auth = undefined;
const PATH = join(homedir(), ".local", "share", "money");
function Main({ auth: initalAuth }: { auth: AuthType | null }) {
const [auth, setAuth] = useState(initalAuth);
function Main() {
return ( return (
<ZeroProvider {...{ userID, auth, server, schema }}> <AuthContext.Provider value={{ auth, setAuth: (auth) => {
if (auth) {
mkdirSync(PATH, { recursive: true });
writeFileSync(PATH + "/auth.json", JSON.stringify(auth));
setAuth(auth);
} else {
rmSync(PATH + "token");
}
} }}>
{auth ? <Authed /> : <Auth />}
</AuthContext.Provider>
);
}
function Authed() {
const { auth } = use(AuthContext);
return (
<ZeroProvider {...{ userID, auth: auth?.token, server, schema }}>
<Router /> <Router />
</ZeroProvider> </ZeroProvider>
); );
} }
function Router() { function Router() {
const { auth } = use(AuthContext);
const [route, setRoute] = useState<Route>("/"); const [route, setRoute] = useState<Route>("/");
return ( return (
<App <App
auth={null} auth={auth?.auth || null}
route={route} route={route}
setRoute={setRoute} setRoute={setRoute}
/> />
@@ -30,4 +59,5 @@ function Router() {
} }
const renderer = await createCliRenderer(); const renderer = await createCliRenderer();
createRoot(renderer).render(<Main />); const auth = existsSync(PATH + "/auth.json") ? JSON.parse(readFileSync(PATH + "/auth.json", 'utf8')) as AuthType : null;
createRoot(renderer).render(<Main auth={auth} />);

View File

@@ -1,7 +1,8 @@
{ {
"compilerOptions": { "compilerOptions": {
"paths": { "paths": {
"react-native": ["../react-native-opentui"] "react-native": ["../react-native-opentui"],
"@/*": ["./*"]
}, },
// Environment setup & latest features // Environment setup & latest features
"lib": ["ESNext"], "lib": ["ESNext"],

26
apps/tui/util/qr.ts Normal file
View File

@@ -0,0 +1,26 @@
import QRCode from "qrcode";
export function QR(value: string): string {
const qr = QRCode.create(value, {
errorCorrectionLevel: "L",
version: 3,
});
const m = qr.modules.data;
const size = qr.modules.size;
// Use half-block characters to compress vertically
// Upper half = '▀', lower half = '▄', full = '█', empty = ' '
let out = "";
for (let y = 0; y < size; y += 2) {
for (let x = 0; x < size; x++) {
const top = m[y * size + x];
const bottom = m[(y + 1) * size + x];
out += top && bottom ? "█" : top ? "▀" : bottom ? "▄" : " ";
}
out += "\n";
}
return out;
}

View File

@@ -5,6 +5,7 @@ import {
text, text,
timestamp, timestamp,
uniqueIndex, uniqueIndex,
integer,
} from "drizzle-orm/pg-core"; } from "drizzle-orm/pg-core";
import { users } from "./public"; import { users } from "./public";
@@ -93,3 +94,19 @@ export const auditLogs = pgTable("audit_log", {
action: text("action").notNull(), action: text("action").notNull(),
}); });
export const deviceCodes = pgTable("deviceCode", {
id: text("id").primaryKey(),
deviceCode: text("device_code").notNull(),
userCode: text("user_code").notNull(),
userId: text("user_id").references(() => users.id, {
onDelete: "set null",
}),
clientId: text("client_id"),
scope: text("scope"),
status: text("status").notNull(),
expiresAt: timestamp("expires_at"),
lastPolledAt: timestamp("last_polled_at"),
pollingInterval: integer("polling_interval"),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(),
});

View File

@@ -3,6 +3,7 @@ import { Transactions } from "./transactions";
import { View, Text } from "react-native"; import { View, Text } from "react-native";
import { Settings } from "./settings"; import { Settings } from "./settings";
import { useKeyboard } from "./useKeyboard"; import { useKeyboard } from "./useKeyboard";
import type { AuthData } from "@money/shared/auth";
const PAGES = { const PAGES = {
@@ -41,10 +42,8 @@ type Routes<T> = {
export type Route = Routes<typeof PAGES>; export type Route = Routes<typeof PAGES>;
type Auth = any;
interface RouterContextType { interface RouterContextType {
auth: Auth; auth: AuthData | null;
route: Route; route: Route;
setRoute: (route: Route) => void; setRoute: (route: Route) => void;
} }
@@ -58,7 +57,7 @@ export const RouterContext = createContext<RouterContextType>({
type AppProps = { type AppProps = {
auth: Auth; auth: AuthData | null;
route: Route; route: Route;
setRoute: (page: Route) => void; setRoute: (page: Route) => void;
} }

107
pnpm-lock.yaml generated
View File

@@ -174,6 +174,12 @@ importers:
'@opentui/react': '@opentui/react':
specifier: ^0.1.39 specifier: ^0.1.39
version: 0.1.39(react@19.1.0)(stage-js@1.0.0-alpha.17)(typescript@5.9.3)(web-tree-sitter@0.25.10) version: 0.1.39(react@19.1.0)(stage-js@1.0.0-alpha.17)(typescript@5.9.3)(web-tree-sitter@0.25.10)
'@types/qrcode':
specifier: ^1.5.6
version: 1.5.6
qrcode:
specifier: ^1.5.4
version: 1.5.4
react: react:
specifier: '*' specifier: '*'
version: 19.1.0 version: 19.1.0
@@ -2854,6 +2860,9 @@ packages:
'@types/pg@8.15.6': '@types/pg@8.15.6':
resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==} resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==}
'@types/qrcode@1.5.6':
resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==}
'@types/react@19.1.17': '@types/react@19.1.17':
resolution: {integrity: sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==} resolution: {integrity: sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==}
@@ -3527,6 +3536,9 @@ packages:
client-only@0.0.1: client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
cliui@6.0.0:
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
cliui@8.0.1: cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -3684,6 +3696,10 @@ packages:
supports-color: supports-color:
optional: true optional: true
decamelize@1.2.0:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'}
decode-uri-component@0.2.2: decode-uri-component@0.2.2:
resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}
engines: {node: '>=0.10'} engines: {node: '>=0.10'}
@@ -3744,6 +3760,9 @@ packages:
detect-node-es@1.1.0: detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
dijkstrajs@1.0.3:
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
doctrine@2.1.0: doctrine@2.1.0:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -5705,6 +5724,10 @@ packages:
resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==} resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==}
engines: {node: '>=4.0.0'} engines: {node: '>=4.0.0'}
pngjs@5.0.0:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'}
pngjs@6.0.0: pngjs@6.0.0:
resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==} resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==}
engines: {node: '>=12.13.0'} engines: {node: '>=12.13.0'}
@@ -5826,6 +5849,11 @@ packages:
resolution: {integrity: sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==} resolution: {integrity: sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==}
hasBin: true hasBin: true
qrcode@1.5.4:
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
engines: {node: '>=10.13.0'}
hasBin: true
query-string@7.1.3: query-string@7.1.3:
resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -6052,6 +6080,9 @@ packages:
resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==}
engines: {node: '>=8.6.0'} engines: {node: '>=8.6.0'}
require-main-filename@2.0.0:
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
requireg@0.2.2: requireg@0.2.2:
resolution: {integrity: sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg==} resolution: {integrity: sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg==}
engines: {node: '>= 4.0.0'} engines: {node: '>= 4.0.0'}
@@ -6188,6 +6219,9 @@ packages:
server-only@0.0.1: server-only@0.0.1:
resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==}
set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
set-cookie-parser@2.7.2: set-cookie-parser@2.7.2:
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
@@ -6787,6 +6821,9 @@ packages:
resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
which-module@2.0.1:
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
which-typed-array@1.1.19: which-typed-array@1.1.19:
resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -6812,6 +6849,10 @@ packages:
resolution: {integrity: sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==} resolution: {integrity: sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==}
engines: {node: '>=12.17'} engines: {node: '>=12.17'}
wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
wrap-ansi@7.0.0: wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -6889,6 +6930,9 @@ packages:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'} engines: {node: '>=0.4'}
y18n@4.0.3:
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
y18n@5.0.8: y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -6905,10 +6949,18 @@ packages:
engines: {node: '>= 14.6'} engines: {node: '>= 14.6'}
hasBin: true hasBin: true
yargs-parser@18.1.3:
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
engines: {node: '>=6'}
yargs-parser@21.1.1: yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'} engines: {node: '>=12'}
yargs@15.4.1:
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
engines: {node: '>=8'}
yargs@17.7.2: yargs@17.7.2:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -10598,6 +10650,10 @@ snapshots:
pg-protocol: 1.10.3 pg-protocol: 1.10.3
pg-types: 2.2.0 pg-types: 2.2.0
'@types/qrcode@1.5.6':
dependencies:
'@types/node': 24.10.0
'@types/react@19.1.17': '@types/react@19.1.17':
dependencies: dependencies:
csstype: 3.1.3 csstype: 3.1.3
@@ -11345,6 +11401,12 @@ snapshots:
client-only@0.0.1: {} client-only@0.0.1: {}
cliui@6.0.0:
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 6.2.0
cliui@8.0.1: cliui@8.0.1:
dependencies: dependencies:
string-width: 4.2.3 string-width: 4.2.3
@@ -11503,6 +11565,8 @@ snapshots:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
decamelize@1.2.0: {}
decode-uri-component@0.2.2: {} decode-uri-component@0.2.2: {}
decompress-response@6.0.0: decompress-response@6.0.0:
@@ -11547,6 +11611,8 @@ snapshots:
detect-node-es@1.1.0: {} detect-node-es@1.1.0: {}
dijkstrajs@1.0.3: {}
doctrine@2.1.0: doctrine@2.1.0:
dependencies: dependencies:
esutils: 2.0.3 esutils: 2.0.3
@@ -14037,6 +14103,8 @@ snapshots:
pngjs@3.4.0: {} pngjs@3.4.0: {}
pngjs@5.0.0: {}
pngjs@6.0.0: {} pngjs@6.0.0: {}
pngjs@7.0.0: {} pngjs@7.0.0: {}
@@ -14153,6 +14221,12 @@ snapshots:
qrcode-terminal@0.11.0: {} qrcode-terminal@0.11.0: {}
qrcode@1.5.4:
dependencies:
dijkstrajs: 1.0.3
pngjs: 5.0.0
yargs: 15.4.1
query-string@7.1.3: query-string@7.1.3:
dependencies: dependencies:
decode-uri-component: 0.2.2 decode-uri-component: 0.2.2
@@ -14648,6 +14722,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
require-main-filename@2.0.0: {}
requireg@0.2.2: requireg@0.2.2:
dependencies: dependencies:
nested-error-stacks: 2.0.1 nested-error-stacks: 2.0.1
@@ -14797,6 +14873,8 @@ snapshots:
server-only@0.0.1: {} server-only@0.0.1: {}
set-blocking@2.0.0: {}
set-cookie-parser@2.7.2: {} set-cookie-parser@2.7.2: {}
set-function-length@1.2.2: set-function-length@1.2.2:
@@ -15479,6 +15557,8 @@ snapshots:
is-weakmap: 2.0.2 is-weakmap: 2.0.2
is-weakset: 2.0.4 is-weakset: 2.0.4
which-module@2.0.1: {}
which-typed-array@1.1.19: which-typed-array@1.1.19:
dependencies: dependencies:
available-typed-arrays: 1.0.7 available-typed-arrays: 1.0.7
@@ -15503,6 +15583,12 @@ snapshots:
wordwrapjs@5.1.1: {} wordwrapjs@5.1.1: {}
wrap-ansi@6.2.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi@7.0.0: wrap-ansi@7.0.0:
dependencies: dependencies:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
@@ -15553,6 +15639,8 @@ snapshots:
xtend@4.0.2: {} xtend@4.0.2: {}
y18n@4.0.3: {}
y18n@5.0.8: {} y18n@5.0.8: {}
yallist@3.1.1: {} yallist@3.1.1: {}
@@ -15561,8 +15649,27 @@ snapshots:
yaml@2.8.1: {} yaml@2.8.1: {}
yargs-parser@18.1.3:
dependencies:
camelcase: 5.3.1
decamelize: 1.2.0
yargs-parser@21.1.1: {} yargs-parser@21.1.1: {}
yargs@15.4.1:
dependencies:
cliui: 6.0.0
decamelize: 1.2.0
find-up: 4.1.0
get-caller-file: 2.0.5
require-directory: 2.1.1
require-main-filename: 2.0.0
set-blocking: 2.0.0
string-width: 4.2.3
which-module: 2.0.1
y18n: 4.0.3
yargs-parser: 18.1.3
yargs@17.7.2: yargs@17.7.2:
dependencies: dependencies:
cliui: 8.0.1 cliui: 8.0.1

4
tsconfig.json Normal file
View File

@@ -0,0 +1,4 @@
{
"compilerOptions": {},
"extends": "expo/tsconfig.base"
}