forked from akai/readest
Add basic user auth
This commit is contained in:
@@ -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",
|
||||
|
||||
Generated
+16
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
Generated
+181
@@ -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: {}
|
||||
|
||||
Reference in New Issue
Block a user