diff --git a/apps/readest-app/package.json b/apps/readest-app/package.json index eb83560f..1ec39909 100644 --- a/apps/readest-app/package.json +++ b/apps/readest-app/package.json @@ -28,6 +28,10 @@ "release-macos-universial-appstore": "dotenv -e .env.tauri.local -e .env.apple-appstore.local -- bash scripts/release-mac-appstore.sh" }, "dependencies": { + "@fabianlars/tauri-plugin-oauth": "2", + "@supabase/auth-ui-react": "^0.4.7", + "@supabase/auth-ui-shared": "^0.1.8", + "@supabase/supabase-js": "^2.47.7", "@tauri-apps/api": "2.1.1", "@tauri-apps/plugin-cli": "^2.0.0", "@tauri-apps/plugin-dialog": "^2.0.1", diff --git a/apps/readest-app/src-tauri/Cargo.lock b/apps/readest-app/src-tauri/Cargo.lock index 184ff2d5..f7851559 100644 --- a/apps/readest-app/src-tauri/Cargo.lock +++ b/apps/readest-app/src-tauri/Cargo.lock @@ -20,6 +20,7 @@ dependencies = [ "tauri-plugin-fs", "tauri-plugin-http", "tauri-plugin-log", + "tauri-plugin-oauth", "tauri-plugin-os", "tauri-plugin-process", "tauri-plugin-shell", @@ -4900,6 +4901,21 @@ dependencies = [ "time", ] +[[package]] +name = "tauri-plugin-oauth" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda564acdb23185caf700f89dd6e5d4540225d6a991516b2cad0cbcf27e4dcd3" +dependencies = [ + "httparse", + "log", + "serde", + "tauri", + "tauri-plugin", + "thiserror 1.0.69", + "url", +] + [[package]] name = "tauri-plugin-os" version = "2.0.1" diff --git a/apps/readest-app/src-tauri/Cargo.toml b/apps/readest-app/src-tauri/Cargo.toml index ae9f0a92..284f42da 100644 --- a/apps/readest-app/src-tauri/Cargo.toml +++ b/apps/readest-app/src-tauri/Cargo.toml @@ -32,6 +32,7 @@ tauri-plugin-http = "2.0.3" tauri-plugin-devtools = "2.0.0" tauri-plugin-shell = "2" tauri-plugin-process = "2" +tauri-plugin-oauth = "2" [patch.crates-io] tauri = { path = "../../../packages/tauri/crates/tauri" } diff --git a/apps/readest-app/src-tauri/capabilities/default.json b/apps/readest-app/src-tauri/capabilities/default.json index 00898912..0b866036 100644 --- a/apps/readest-app/src-tauri/capabilities/default.json +++ b/apps/readest-app/src-tauri/capabilities/default.json @@ -58,6 +58,8 @@ "updater:default", "process:default", "process:allow-restart", - "cli:default" + "cli:default", + "oauth:allow-start", + "oauth:allow-cancel" ] } diff --git a/apps/readest-app/src-tauri/src/lib.rs b/apps/readest-app/src-tauri/src/lib.rs index 24a96be2..5a16192a 100644 --- a/apps/readest-app/src-tauri/src/lib.rs +++ b/apps/readest-app/src-tauri/src/lib.rs @@ -15,10 +15,12 @@ mod tauri_traffic_light_positioner_plugin; use tauri::TitleBarStyle; use std::path::PathBuf; +use tauri::{command, Window}; use tauri::{AppHandle, Emitter, Manager, Url}; use tauri::{WebviewUrl, WebviewWindowBuilder}; use tauri_plugin_dialog; use tauri_plugin_fs::FsExt; +use tauri_plugin_oauth::start; #[cfg(desktop)] use tauri::Listener; @@ -58,10 +60,22 @@ fn set_window_open_with_files(app: &AppHandle, files: Vec) { } } +#[command] +async fn start_server(window: Window) -> Result { + start(move |url| { + // Because of the unprotected localhost port, you must verify the URL here. + // Preferebly send back only the token, or nothing at all if you can handle everything else in Rust. + let _ = window.emit("redirect_uri", url); + }) + .map_err(|err| err.to_string()) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let builder = tauri::Builder::default() .plugin(tauri_plugin_updater::Builder::new().build()) + .plugin(tauri_plugin_oauth::init()) + .invoke_handler(tauri::generate_handler![start_server]) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_os::init()) diff --git a/apps/readest-app/src-tauri/tauri.conf.json b/apps/readest-app/src-tauri/tauri.conf.json index 9883d617..77f350bc 100644 --- a/apps/readest-app/src-tauri/tauri.conf.json +++ b/apps/readest-app/src-tauri/tauri.conf.json @@ -14,8 +14,8 @@ "security": { "csp": { "default-src": "'self' 'unsafe-inline' blob: customprotocol: asset: http://asset.localhost ipc: http://ipc.localhost https://fonts.gstatic.com https://db.onlinewebfonts.com", - "connect-src": "'self' blob: asset: http://asset.localhost ipc: http://ipc.localhost https://*.sentry.io https://*.posthog.com https://*.deepl.com https://*.wikipedia.org https://*.wiktionary.org", - "img-src": "'self' blob: data: asset: http://asset.localhost", + "connect-src": "'self' blob: asset: http://asset.localhost ipc: http://ipc.localhost https://*.sentry.io https://*.posthog.com https://*.deepl.com https://*.wikipedia.org https://*.wiktionary.org https://*.supabase.co", + "img-src": "'self' blob: data: asset: http://asset.localhost https://*", "style-src": "'self' 'unsafe-inline' blob: asset: http://asset.localhost", "frame-src": "'self' blob: asset: http://asset.localhost", "script-src": "'self' 'unsafe-inline' 'unsafe-eval' blob: asset: http://asset.localhost https://*.sentry.io https://*.posthog.com" @@ -23,7 +23,7 @@ "assetProtocol": { "enable": true, "scope": { - "allow": ["$RESOURCE/**", "$DOCUMENT/**/*", "$APPDATA/**/*", "$TEMP/**/*"], + "allow": ["$RESOURCE/**", "$APPDATA/**/*", "$TEMP/**/*"], "deny": [] } } diff --git a/apps/readest-app/src/app/auth/callback/page.tsx b/apps/readest-app/src/app/auth/callback/page.tsx new file mode 100644 index 00000000..390a0e5d --- /dev/null +++ b/apps/readest-app/src/app/auth/callback/page.tsx @@ -0,0 +1,24 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { handleAuthCallback } from '@/helpers/auth'; +import { useAuth } from '@/context/AuthContext'; + +export default function AuthCallback() { + const router = useRouter(); + const { login } = useAuth(); + + if (typeof window === 'undefined') { + return null; + } + const hash = window.location.hash || ''; + const params = new URLSearchParams(hash.slice(1)); + + const accessToken = params.get('access_token'); + const refreshToken = params.get('refresh_token'); + const next = params.get('next') ?? '/'; + + handleAuthCallback({ accessToken, refreshToken, next, login, navigate: router.push }); + + return null; +} diff --git a/apps/readest-app/src/app/auth/error/page.tsx b/apps/readest-app/src/app/auth/error/page.tsx new file mode 100644 index 00000000..a787fa4b --- /dev/null +++ b/apps/readest-app/src/app/auth/error/page.tsx @@ -0,0 +1,35 @@ +'use client'; + +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { useTheme } from '@/hooks/useTheme'; + +export default function AuthErrorPage() { + const router = useRouter(); + useTheme(); + + useEffect(() => { + const timer = setTimeout(() => { + router.push('/auth'); + }, 5000); + + return () => clearTimeout(timer); + }, [router]); + + return ( +
+
+
+

Authentication Error

+

+ Something went wrong during the authentication process. Please try again. +

+

You will be redirected to the login page shortly...

+ +
+
+
+ ); +} diff --git a/apps/readest-app/src/app/auth/page.tsx b/apps/readest-app/src/app/auth/page.tsx new file mode 100644 index 00000000..4743651f --- /dev/null +++ b/apps/readest-app/src/app/auth/page.tsx @@ -0,0 +1,187 @@ +'use client'; +import clsx from 'clsx'; +import { useEffect, useRef, useState } from 'react'; +import { useRouter } from 'next/navigation'; + +import { Auth } from '@supabase/auth-ui-react'; +import { ThemeSupa } from '@supabase/auth-ui-shared'; +import { FcGoogle } from 'react-icons/fc'; +import { FaApple } from 'react-icons/fa'; +import { VscAzure } from 'react-icons/vsc'; +import { FaGithub } from 'react-icons/fa'; + +import { useAuth } from '@/context/AuthContext'; +import { supabase } from '@/utils/supabase'; +import { useTheme } from '@/hooks/useTheme'; +import { isTauriAppPlatform } from '@/services/environment'; +import { open } from '@tauri-apps/plugin-shell'; +import { start, cancel, onUrl, onInvalidUrl } from '@fabianlars/tauri-plugin-oauth'; +import { handleAuthCallback } from '@/helpers/auth'; + +type OAuthProvider = 'google' | 'apple' | 'azure' | 'github'; + +interface ProviderLoginProp { + provider: OAuthProvider; + handleSignIn: (provider: OAuthProvider) => void; + Icon: React.ElementType; + label: string; +} + +const ProviderLogin: React.FC = ({ provider, handleSignIn, Icon, label }) => { + return ( + + ); +}; + +export default function AuthPage() { + const router = useRouter(); + const { login } = useAuth(); + const { isDarkMode } = useTheme(); + const [port, setPort] = useState(null); + const isOAuthServerRunning = useRef(false); + + const signIn = async (provider: OAuthProvider) => { + if (!supabase) { + throw new Error('No backend connected'); + } + supabase.auth.signOut(); + const { data, error } = await supabase.auth.signInWithOAuth({ + provider, + options: { + skipBrowserRedirect: true, + redirectTo: `http://localhost:${port}`, + }, + }); + + if (error) { + console.error('Authentication error:', error); + return; + } + open(data.url); + }; + + const startOAuthServer = async () => { + try { + const port = await start(); + setPort(port); + console.log(`OAuth server started on port ${port}`); + + await onUrl((url) => { + console.log('Received OAuth URL:', url); + const hashMatch = url.match(/#(.*)/); + if (hashMatch) { + const hash = hashMatch[1]; + const params = new URLSearchParams(hash); + const accessToken = params.get('access_token'); + const refreshToken = params.get('refresh_token'); + const next = params.get('next') ?? '/'; + handleAuthCallback({ accessToken, refreshToken, next, login, navigate: router.push }); + } + }); + + await onInvalidUrl((url) => { + console.log('Received invalid OAuth URL:', url); + }); + } catch (error) { + console.error('Error starting OAuth server:', error); + } + }; + + const stopOAuthServer = async () => { + try { + if (port) { + await cancel(port); + console.log('OAuth server stopped'); + } + } catch (error) { + console.error('Error stopping OAuth server:', error); + } + }; + + useEffect(() => { + if (!isTauriAppPlatform()) return; + if (isOAuthServerRunning.current) return; + isOAuthServerRunning.current = true; + + startOAuthServer(); + return () => { + isOAuthServerRunning.current = false; + stopOAuthServer(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + const { data: subscription } = supabase.auth.onAuthStateChange((_event, session) => { + if (session?.access_token && session.user) { + login(session.access_token, session.user); + router.push('/library'); + } + }); + + return () => { + subscription?.subscription.unsubscribe(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [router]); + + return isTauriAppPlatform() ? ( +
+
+ + + + +
+ +
+
+ ) : ( +
+ +
+ ); +} diff --git a/apps/readest-app/src/app/library/components/LibraryHeader.tsx b/apps/readest-app/src/app/library/components/LibraryHeader.tsx index 17ffc497..af30cbd1 100644 --- a/apps/readest-app/src/app/library/components/LibraryHeader.tsx +++ b/apps/readest-app/src/app/library/components/LibraryHeader.tsx @@ -7,6 +7,9 @@ import { PiSelectionAllDuotone } from 'react-icons/pi'; import { useEnv } from '@/context/EnvContext'; import useTrafficLight from '@/hooks/useTrafficLight'; import WindowButtons from '@/components/WindowButtons'; +import Dropdown from '@/components/Dropdown'; +import { MdOutlineMenu } from 'react-icons/md'; +import SettingsMenu from './SettingsMenu'; interface LibraryHeaderProps { isSelectMode: boolean; @@ -85,12 +88,21 @@ const LibraryHeader: React.FC = ({ - +
+ } + > + + + +
); diff --git a/apps/readest-app/src/app/library/components/SettingsMenu.tsx b/apps/readest-app/src/app/library/components/SettingsMenu.tsx new file mode 100644 index 00000000..7e57732d --- /dev/null +++ b/apps/readest-app/src/app/library/components/SettingsMenu.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import Image from 'next/image'; +import { useRouter } from 'next/navigation'; +import { PiUserCircle } from 'react-icons/pi'; +import { PiUserCircleCheck } from 'react-icons/pi'; + +import { setAboutDialogVisible } from '@/components/AboutWindow'; +import { isWebAppPlatform } from '@/services/environment'; +import { DOWNLOAD_READEST_URL } from '@/services/constants'; +import { useAuth } from '@/context/AuthContext'; +import MenuItem from '@/components/MenuItem'; + +interface BookMenuProps { + setIsDropdownOpen?: (isOpen: boolean) => void; +} + +const SettingsMenu: React.FC = ({ setIsDropdownOpen }) => { + const router = useRouter(); + const { user, logout } = useAuth(); + + const showAboutReadest = () => { + setAboutDialogVisible(true); + setIsDropdownOpen?.(false); + }; + const downloadReadest = () => { + window.open(DOWNLOAD_READEST_URL, '_blank'); + setIsDropdownOpen?.(false); + }; + + const handleUserLogin = () => { + router.push('/auth'); + setIsDropdownOpen?.(false); + }; + + const handleUserLogout = () => { + logout(); + setIsDropdownOpen?.(false); + }; + + const isWebApp = isWebAppPlatform(); + const avatarUrl = user?.user_metadata?.['picture'] || user?.user_metadata?.['avatar_url']; + const userFullName = user?.user_metadata?.['full_name']; + const userDisplayName = userFullName ? userFullName.split(' ')[0] : null; + + return ( +
+ {user ? ( + + ) : ( + + ) + } + > +
    + +
+
+ ) : ( + } + onClick={handleUserLogin} + > + )} +
+ {isWebApp && } + +
+ ); +}; + +export default SettingsMenu; diff --git a/apps/readest-app/src/app/library/page.tsx b/apps/readest-app/src/app/library/page.tsx index af972a78..ddce582a 100644 --- a/apps/readest-app/src/app/library/page.tsx +++ b/apps/readest-app/src/app/library/page.tsx @@ -21,6 +21,7 @@ import { useDemoBooks } from './hooks/useDemoBooks'; import Spinner from '@/components/Spinner'; import LibraryHeader from './components/LibraryHeader'; import Bookshelf from './components/Bookshelf'; +import { AboutWindow } from '@/components/AboutWindow'; const LibraryPage = () => { const router = useRouter(); @@ -230,6 +231,7 @@ const LibraryPage = () => { ))} + ); }; diff --git a/apps/readest-app/src/components/MenuItem.tsx b/apps/readest-app/src/components/MenuItem.tsx index 547ab522..3c712fc6 100644 --- a/apps/readest-app/src/components/MenuItem.tsx +++ b/apps/readest-app/src/components/MenuItem.tsx @@ -3,6 +3,7 @@ import React from 'react'; interface MenuItemProps { label: string; + labelClass?: string; shortcut?: string; disabled?: boolean; noIcon?: boolean; @@ -13,6 +14,7 @@ interface MenuItemProps { const MenuItem: React.FC = ({ label, + labelClass, shortcut, disabled, noIcon = false, @@ -31,7 +33,7 @@ const MenuItem: React.FC = ({ >
{!noIcon && {icon}} - {label} + {label}
{shortcut && {shortcut}} diff --git a/apps/readest-app/src/context/AuthContext.tsx b/apps/readest-app/src/context/AuthContext.tsx index f072f6e1..393f1fd2 100644 --- a/apps/readest-app/src/context/AuthContext.tsx +++ b/apps/readest-app/src/context/AuthContext.tsx @@ -1,22 +1,76 @@ 'use client'; -import React, { createContext, useState, useContext, ReactNode } from 'react'; +import React, { createContext, useState, useContext, ReactNode, useEffect } from 'react'; +import { User } from '@supabase/supabase-js'; +import { supabase } from '@/utils/supabase'; interface AuthContextType { token: string | null; - login: (token: string) => void; + user: User | null; + login: (token: string, user: User) => void; logout: () => void; } const AuthContext = createContext(undefined); export const AuthProvider = ({ children }: { children: ReactNode }) => { - const [token, setToken] = useState(null); + const [token, setToken] = useState(() => { + if (typeof window !== 'undefined') { + return localStorage.getItem('token'); + } + return null; + }); + const [user, setUser] = useState(() => { + if (typeof window !== 'undefined') { + const userJson = localStorage.getItem('user'); + return userJson ? JSON.parse(userJson) : null; + } + return null; + }); - const login = (newToken: string) => setToken(newToken); - const logout = () => setToken(null); + useEffect(() => { + const fetchSession = async () => { + const { + data: { session }, + error, + } = await supabase.auth.getSession(); + if (session) { + const { access_token, user } = session; + setToken(access_token); + setUser(user); + localStorage.setItem('token', access_token); + localStorage.setItem('user', JSON.stringify(user)); + } else if (error) { + console.error('Error fetching session:', error); + localStorage.removeItem('token'); + localStorage.removeItem('user'); + setToken(null); + setUser(null); + } + }; - return {children}; + console.log('Fetching session'); + fetchSession(); + }, [token]); + + const login = (newToken: string, newUser: User) => { + setToken(newToken); + setUser(newUser); + localStorage.setItem('token', newToken); + localStorage.setItem('user', JSON.stringify(newUser)); + }; + + const logout = () => { + setToken(null); + setUser(null); + localStorage.removeItem('token'); + localStorage.removeItem('user'); + supabase.auth.signOut(); + }; + + return ( + {children} + ); }; export const useAuth = (): AuthContextType => { diff --git a/apps/readest-app/src/helpers/auth.ts b/apps/readest-app/src/helpers/auth.ts new file mode 100644 index 00000000..fa9f7d79 --- /dev/null +++ b/apps/readest-app/src/helpers/auth.ts @@ -0,0 +1,50 @@ +import { User } from '@supabase/supabase-js'; +import { supabase } from '@/utils/supabase'; + +interface UseAuthCallbackOptions { + accessToken?: string | null; + refreshToken?: string | null; + login: (accessToken: string, user: User) => void; + navigate: (path: string) => void; + next?: string; +} + +export function handleAuthCallback({ + accessToken, + refreshToken, + login, + navigate, + next = '/', +}: UseAuthCallbackOptions) { + async function finalizeSession() { + if (!accessToken || !refreshToken) { + console.error('No access token or refresh token'); + navigate('/auth/error'); + return; + } + + const { error } = await supabase.auth.setSession({ + access_token: accessToken, + refresh_token: refreshToken, + }); + + if (error) { + console.error('Error setting session:', error); + navigate('/auth/error'); + return; + } + + const { + data: { user }, + } = await supabase.auth.getUser(); + if (user) { + login(accessToken, user); + navigate(next); + } else { + console.error('Error fetching user data'); + navigate('/auth/error'); + } + } + + finalizeSession(); +} diff --git a/apps/readest-app/src/utils/supabase.ts b/apps/readest-app/src/utils/supabase.ts new file mode 100644 index 00000000..9618c279 --- /dev/null +++ b/apps/readest-app/src/utils/supabase.ts @@ -0,0 +1,6 @@ +import { createClient } from '@supabase/supabase-js'; + +const supabaseUrl = process.env['NEXT_PUBLIC_SUPABASE_URL']!; +const supabaseAnonKey = process.env['NEXT_PUBLIC_SUPABASE_ANON_KEY']!; + +export const supabase = createClient(supabaseUrl, supabaseAnonKey); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a037b51..211c3760 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,18 @@ importers: apps/readest-app: dependencies: + '@fabianlars/tauri-plugin-oauth': + specifier: '2' + version: 2.0.0 + '@supabase/auth-ui-react': + specifier: ^0.4.7 + version: 0.4.7(@supabase/supabase-js@2.47.7) + '@supabase/auth-ui-shared': + specifier: ^0.1.8 + version: 0.1.8(@supabase/supabase-js@2.47.7) + '@supabase/supabase-js': + specifier: ^2.47.7 + version: 2.47.7 '@tauri-apps/api': specifier: 2.1.1 version: 2.1.1 @@ -247,6 +259,9 @@ packages: resolution: {integrity: sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fabianlars/tauri-plugin-oauth@2.0.0': + resolution: {integrity: sha512-I1s08ZXrsFuYfNWusAcpLyiCfr5TCvaBrRuKfTG+XQrcaqnAcwjdWH0U5J9QWuMDLwCUMnVxdobtMJzPR8raxQ==} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -630,6 +645,41 @@ packages: resolution: {integrity: sha512-+fUdfuDd/7O2OZ9/UvJy76IEWn2Tpvm2l+rwUoS2Yz4jCUTSNOQQv2PLWrwekt8cPLwHmpHaBpay34bkBmVl2Q==} engines: {node: '>=18'} + '@stitches/core@1.2.8': + resolution: {integrity: sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg==} + + '@supabase/auth-js@2.67.1': + resolution: {integrity: sha512-1SRZG9VkLFz4rtiyEc1l49tMq9jTYu4wJt3pMQEWi7yshZFIBdVH1o5sshk1plQd5LY6GcrPIpCydM2gGDxchA==} + + '@supabase/auth-ui-react@0.4.7': + resolution: {integrity: sha512-Lp4FQGFh7BMX1Y/BFaUKidbryL7eskj1fl6Lby7BeHrTctbdvDbCMjVKS8wZ2rxuI8FtPS2iU900fSb70FHknQ==} + peerDependencies: + '@supabase/supabase-js': ^2.21.0 + + '@supabase/auth-ui-shared@0.1.8': + resolution: {integrity: sha512-ouQ0DjKcEFg+0gZigFIEgu01V3e6riGZPzgVD0MJsCBNsMsiDT74+GgCEIElMUpTGkwSja3xLwdFRFgMNFKcjg==} + peerDependencies: + '@supabase/supabase-js': ^2.21.0 + + '@supabase/functions-js@2.4.3': + resolution: {integrity: sha512-sOLXy+mWRyu4LLv1onYydq+10mNRQ4rzqQxNhbrKLTLTcdcmS9hbWif0bGz/NavmiQfPs4ZcmQJp4WqOXlR4AQ==} + + '@supabase/node-fetch@2.6.15': + resolution: {integrity: sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==} + engines: {node: 4.x || >=6.0.0} + + '@supabase/postgrest-js@1.16.3': + resolution: {integrity: sha512-HI6dsbW68AKlOPofUjDTaosiDBCtW4XAm0D18pPwxoW3zKOE2Ru13Z69Wuys9fd6iTpfDViNco5sgrtnP0666A==} + + '@supabase/realtime-js@2.11.2': + resolution: {integrity: sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==} + + '@supabase/storage-js@2.7.1': + resolution: {integrity: sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==} + + '@supabase/supabase-js@2.47.7': + resolution: {integrity: sha512-ZhAiboKRo4rclM98PWu+6DjrHyYnF5Dzy10dNSdOalxYvcYjYS4Gf1JbVZeofgyXweepWEJd3diwoW1v1OroDw==} + '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} @@ -772,6 +822,9 @@ packages: '@types/node@22.10.1': resolution: {integrity: sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==} + '@types/phoenix@1.6.6': + resolution: {integrity: sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==} + '@types/prop-types@15.7.13': resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==} @@ -787,6 +840,9 @@ packages: '@types/tinycolor2@1.4.6': resolution: {integrity: sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==} + '@types/ws@8.5.13': + resolution: {integrity: sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==} + '@typescript-eslint/eslint-plugin@8.16.0': resolution: {integrity: sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3078,6 +3134,11 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + react-dom@19.0.0: resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==} peerDependencies: @@ -3091,6 +3152,10 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + react@19.0.0: resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} engines: {node: '>=0.10.0'} @@ -3219,6 +3284,9 @@ packages: safe-regex@1.1.0: resolution: {integrity: sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==} + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + scheduler@0.25.0: resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} @@ -3525,6 +3593,9 @@ packages: resolution: {integrity: sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==} engines: {node: '>=0.10.0'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + trim-right@1.0.1: resolution: {integrity: sha512-WZGXGstmCWgeevgTL54hrCuw1dyMQIzWy7ZfqRJfSmJZBwklI15egmQytFP6bPidmw3M8d5yEowl1niq4vmqZw==} engines: {node: '>=0.10.0'} @@ -3641,6 +3712,9 @@ packages: web-vitals@4.2.4: resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webpack-sources@3.2.3: resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} engines: {node: '>=10.13.0'} @@ -3655,6 +3729,9 @@ packages: webpack-cli: optional: true + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} @@ -3694,6 +3771,18 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -3793,6 +3882,10 @@ snapshots: dependencies: levn: 0.4.1 + '@fabianlars/tauri-plugin-oauth@2.0.0': + dependencies: + '@tauri-apps/api': 2.1.1 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -4053,6 +4146,63 @@ snapshots: '@sindresorhus/tsconfig@6.0.0': {} + '@stitches/core@1.2.8': {} + + '@supabase/auth-js@2.67.1': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/auth-ui-react@0.4.7(@supabase/supabase-js@2.47.7)': + dependencies: + '@stitches/core': 1.2.8 + '@supabase/auth-ui-shared': 0.1.8(@supabase/supabase-js@2.47.7) + '@supabase/supabase-js': 2.47.7 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@supabase/auth-ui-shared@0.1.8(@supabase/supabase-js@2.47.7)': + dependencies: + '@supabase/supabase-js': 2.47.7 + + '@supabase/functions-js@2.4.3': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/node-fetch@2.6.15': + dependencies: + whatwg-url: 5.0.0 + + '@supabase/postgrest-js@1.16.3': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/realtime-js@2.11.2': + dependencies: + '@supabase/node-fetch': 2.6.15 + '@types/phoenix': 1.6.6 + '@types/ws': 8.5.13 + ws: 8.18.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@supabase/storage-js@2.7.1': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/supabase-js@2.47.7': + dependencies: + '@supabase/auth-js': 2.67.1 + '@supabase/functions-js': 2.4.3 + '@supabase/node-fetch': 2.6.15 + '@supabase/postgrest-js': 1.16.3 + '@supabase/realtime-js': 2.11.2 + '@supabase/storage-js': 2.7.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@swc/counter@0.1.3': {} '@swc/helpers@0.5.15': @@ -4178,6 +4328,8 @@ snapshots: dependencies: undici-types: 6.20.0 + '@types/phoenix@1.6.6': {} + '@types/prop-types@15.7.13': {} '@types/react-dom@18.3.1': @@ -4193,6 +4345,10 @@ snapshots: '@types/tinycolor2@1.4.6': {} + '@types/ws@8.5.13': + dependencies: + '@types/node': 22.10.1 + '@typescript-eslint/eslint-plugin@8.16.0(@typescript-eslint/parser@8.16.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2))(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -7002,6 +7158,12 @@ snapshots: strip-json-comments: 2.0.1 optional: true + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + react-dom@19.0.0(react@19.0.0): dependencies: react: 19.0.0 @@ -7013,6 +7175,10 @@ snapshots: react-is@16.13.1: {} + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + react@19.0.0: {} read-cache@1.0.0: @@ -7179,6 +7345,10 @@ snapshots: dependencies: ret: 0.1.15 + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + scheduler@0.25.0: {} schema-utils@3.3.0: @@ -7569,6 +7739,8 @@ snapshots: regex-not: 1.0.2 safe-regex: 1.1.0 + tr46@0.0.3: {} + trim-right@1.0.1: {} ts-api-utils@1.4.3(typescript@5.7.2): @@ -7702,6 +7874,8 @@ snapshots: web-vitals@4.2.4: {} + webidl-conversions@3.0.1: {} + webpack-sources@3.2.3: {} webpack@5.96.1: @@ -7734,6 +7908,11 @@ snapshots: - esbuild - uglify-js + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + which-boxed-primitive@1.0.2: dependencies: is-bigint: 1.0.4 @@ -7797,6 +7976,8 @@ snapshots: wrappy@1.0.2: {} + ws@8.18.0: {} + y18n@5.0.8: {} yallist@2.1.2: {}