Add basic user auth

This commit is contained in:
chrox
2024-12-16 19:16:19 +01:00
parent 3be4a26b14
commit ed177530ac
17 changed files with 693 additions and 17 deletions
+4
View File
@@ -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",
+16
View File
@@ -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"
+1
View File
@@ -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" }
@@ -58,6 +58,8 @@
"updater:default",
"process:default",
"process:allow-restart",
"cli:default"
"cli:default",
"oauth:allow-start",
"oauth:allow-cancel"
]
}
+14
View File
@@ -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<PathBuf>) {
}
}
#[command]
async fn start_server(window: Window) -> Result<u16, String> {
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())
+3 -3
View File
@@ -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": []
}
}
@@ -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;
}
@@ -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 (
<div className='bg-base-200/50 text-base-content hero h-screen items-center justify-center'>
<div className='hero-content text-neutral-content text-center'>
<div className='max-w-md'>
<h1 className='mb-5 text-5xl font-bold'>Authentication Error</h1>
<p className='mb-5'>
Something went wrong during the authentication process. Please try again.
</p>
<p className='mb-5'>You will be redirected to the login page shortly...</p>
<button className='btn btn-primary rounded-xl' onClick={() => router.push('/auth')}>
Go to Login
</button>
</div>
</div>
</div>
);
}
+187
View File
@@ -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<ProviderLoginProp> = ({ provider, handleSignIn, Icon, label }) => {
return (
<button
onClick={() => handleSignIn(provider)}
className={clsx(
'mb-2 flex w-64 items-center justify-center rounded border p-2.5',
'bg-base-100 border-gray-300 shadow-sm transition hover:bg-gray-50',
)}
>
<Icon size={20} />
<span className='text-neutral-content/70 px-2 text-sm'>{label}</span>
</button>
);
};
export default function AuthPage() {
const router = useRouter();
const { login } = useAuth();
const { isDarkMode } = useTheme();
const [port, setPort] = useState<number | null>(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() ? (
<div className='flex pt-11'>
<div style={{ maxWidth: '420px', margin: 'auto', padding: '2rem' }}>
<ProviderLogin
provider='google'
handleSignIn={signIn}
Icon={FcGoogle}
label='Sign in with Google'
/>
<ProviderLogin
provider='apple'
handleSignIn={signIn}
Icon={FaApple}
label='Sign in with Apple'
/>
<ProviderLogin
provider='azure'
handleSignIn={signIn}
Icon={VscAzure}
label='Sign in with Azure'
/>
<ProviderLogin
provider='github'
handleSignIn={signIn}
Icon={FaGithub}
label='Sign in with GitHub'
/>
<hr className='my-3 mt-6 w-64 border-t border-gray-200' />
<Auth
supabaseClient={supabase}
appearance={{ theme: ThemeSupa }}
theme={isDarkMode ? 'dark' : 'light'}
magicLink={true}
providers={[]}
redirectTo={`http://localhost:${port}`}
/>
</div>
</div>
) : (
<div style={{ maxWidth: '420px', margin: 'auto', padding: '2rem' }}>
<Auth
supabaseClient={supabase}
appearance={{ theme: ThemeSupa }}
theme={isDarkMode ? 'dark' : 'light'}
magicLink={true}
providers={['google', 'apple', 'azure', 'github']}
redirectTo='/auth/callback'
/>
</div>
);
}
@@ -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<LibraryHeaderProps> = ({
</button>
</div>
</div>
<WindowButtons
headerRef={headerRef}
showMinimize={!isTrafficLightVisible && appService?.appPlatform !== 'web'}
showMaximize={!isTrafficLightVisible && appService?.appPlatform !== 'web'}
showClose={!isTrafficLightVisible && appService?.appPlatform !== 'web'}
/>
<div className='flex h-full items-center'>
<Dropdown
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end mr-2'
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
toggleButton={<MdOutlineMenu size={16} />}
>
<SettingsMenu />
</Dropdown>
<WindowButtons
headerRef={headerRef}
showMinimize={!isTrafficLightVisible && appService?.appPlatform !== 'web'}
showMaximize={!isTrafficLightVisible && appService?.appPlatform !== 'web'}
showClose={!isTrafficLightVisible && appService?.appPlatform !== 'web'}
/>
</div>
</div>
</div>
);
@@ -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<BookMenuProps> = ({ 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 (
<div
tabIndex={0}
className='settings-menu dropdown-content no-triangle border-base-100 z-20 mt-3 w-60 shadow-2xl'
>
{user ? (
<MenuItem
label={userDisplayName ? `Logged in as ${userDisplayName}` : 'Logged in'}
labelClass='!max-w-40'
icon={
avatarUrl ? (
<Image
src={avatarUrl}
alt='User Avatar'
className='h-5 w-5 rounded-full'
width={20}
height={20}
/>
) : (
<PiUserCircleCheck size={20} />
)
}
>
<ul>
<MenuItem label='Sign Out' noIcon onClick={handleUserLogout} />
</ul>
</MenuItem>
) : (
<MenuItem
label='Sign In'
icon={<PiUserCircle size={20} />}
onClick={handleUserLogin}
></MenuItem>
)}
<hr className='border-base-200 my-1' />
{isWebApp && <MenuItem label='Download Readest' onClick={downloadReadest} />}
<MenuItem label='About Readest' onClick={showAboutReadest} />
</div>
);
};
export default SettingsMenu;
@@ -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 = () => {
</div>
</div>
))}
<AboutWindow />
</div>
);
};
+3 -1
View File
@@ -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<MenuItemProps> = ({
label,
labelClass,
shortcut,
disabled,
noIcon = false,
@@ -31,7 +33,7 @@ const MenuItem: React.FC<MenuItemProps> = ({
>
<div className='flex items-center'>
{!noIcon && <span style={{ minWidth: '20px' }}>{icon}</span>}
<span className='ml-2 max-w-32 truncate'>{label}</span>
<span className={clsx('ml-2 max-w-32 truncate', labelClass)}>{label}</span>
</div>
{shortcut && <span className='text-neutral-content text-sm'>{shortcut}</span>}
</button>
+60 -6
View File
@@ -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<AuthContextType | undefined>(undefined);
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [token, setToken] = useState<string | null>(null);
const [token, setToken] = useState<string | null>(() => {
if (typeof window !== 'undefined') {
return localStorage.getItem('token');
}
return null;
});
const [user, setUser] = useState<User | null>(() => {
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 <AuthContext.Provider value={{ token, login, logout }}>{children}</AuthContext.Provider>;
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 (
<AuthContext.Provider value={{ token, user, login, logout }}>{children}</AuthContext.Provider>
);
};
export const useAuth = (): AuthContextType => {
+50
View File
@@ -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();
}
+6
View File
@@ -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);
+181
View File
@@ -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: {}