One-click backups for AT Protocol

fix: add auth complete

Turtlepaw a0511a16 7e5a48d2

Changed files
+77 -62
docs
app
auth
complete
public
src
src-tauri
capabilities
src
+4
.gitignore
··· 22 *.njsproj 23 *.sln 24 *.sw?
··· 22 *.njsproj 23 *.sln 24 *.sw? 25 + 26 + docs/node_modules 27 + docs/.next 28 + docs/public/_pagefind
+37
docs/app/auth/complete/page.tsx
···
··· 1 + "use client"; 2 + 3 + import { useEffect } from "react"; 4 + import { useSearchParams } from "next/navigation"; 5 + 6 + export default function AuthComplete() { 7 + const searchParams = useSearchParams(); 8 + 9 + useEffect(() => { 10 + // Get all URL parameters 11 + const params = new URLSearchParams(); 12 + searchParams.forEach((value, key) => { 13 + params.append(key, value); 14 + }); 15 + 16 + // Construct the redirect URL with all parameters 17 + const redirectUrl = `atprotobackups://auth?${params.toString()}`; 18 + 19 + // Open the URL in the system's default handler 20 + window.location.href = redirectUrl; 21 + }, [searchParams]); 22 + 23 + return ( 24 + <div className="min-h-screen flex items-center justify-center bg-gray-50"> 25 + <div className="max-w-md w-full space-y-8 p-8 bg-white rounded-lg shadow-md"> 26 + <div className="text-center"> 27 + <h2 className="mt-6 text-3xl font-bold text-gray-900"> 28 + Authentication Complete 29 + </h2> 30 + <p className="mt-2 text-sm text-gray-600"> 31 + Redirecting you back to the application... 32 + </p> 33 + </div> 34 + </div> 35 + </div> 36 + ); 37 + }
+1 -1
docs/public/client_metadata.json
··· 6 "tos_uri": "https://atproto-backup.pages.dev/tos", 7 "policy_uri": "https://atproto-backup.pages.dev/policy", 8 "redirect_uris": [ 9 - "atprotobackups://auth" 10 ], 11 "scope": "atproto", 12 "grant_types": [
··· 6 "tos_uri": "https://atproto-backup.pages.dev/tos", 7 "policy_uri": "https://atproto-backup.pages.dev/policy", 8 "redirect_uris": [ 9 + "https://atproto-backup.pages.dev/auth/complete" 10 ], 11 "scope": "atproto", 12 "grant_types": [
+1
src-tauri/capabilities/default.json
··· 10 "opener:default", 11 "core:window:default", 12 "core:window:allow-start-dragging", 13 "deep-link:default" 14 ] 15 }
··· 10 "opener:default", 11 "core:window:default", 12 "core:window:allow-start-dragging", 13 + "core:event:default", 14 "deep-link:default" 15 ] 16 }
-14
src-tauri/lib.rs
··· 1 - #[cfg_attr(mobile, tauri::mobile_entry_point)] 2 - pub fn run() { 3 - let mut builder = tauri::Builder::default(); 4 - 5 - #[cfg(desktop)] 6 - { 7 - builder = builder.plugin(tauri_plugin_single_instance::init(|_app, argv, _cwd| { 8 - println!("a new app instance was opened with {argv:?} and the deep link event was already triggered"); 9 - // when defining deep link schemes at runtime, you must also check `argv` here 10 - })); 11 - } 12 - 13 - builder = builder.plugin(tauri_plugin_deep_link::init()); 14 - }
···
+19 -1
src-tauri/src/lib.rs
··· 1 // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ 2 #[tauri::command] 3 fn greet(name: &str) -> String { 4 format!("Hello, {}! You've been greeted from Rust!", name) ··· 6 7 #[cfg_attr(mobile, tauri::mobile_entry_point)] 8 pub fn run() { 9 - tauri::Builder::default() 10 .plugin(tauri_plugin_deep_link::init()) 11 .plugin(tauri_plugin_opener::init()) 12 .invoke_handler(tauri::generate_handler![greet]) 13 .run(tauri::generate_context!()) 14 .expect("error while running tauri application"); 15 }
··· 1 // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ 2 + use tauri_plugin_deep_link::DeepLinkExt; 3 + 4 #[tauri::command] 5 fn greet(name: &str) -> String { 6 format!("Hello, {}! You've been greeted from Rust!", name) ··· 8 9 #[cfg_attr(mobile, tauri::mobile_entry_point)] 10 pub fn run() { 11 + let mut builder = tauri::Builder::default() 12 .plugin(tauri_plugin_deep_link::init()) 13 .plugin(tauri_plugin_opener::init()) 14 .invoke_handler(tauri::generate_handler![greet]) 15 + .setup(|app| { 16 + #[cfg(any(windows, target_os = "linux"))] 17 + { 18 + app.deep_link().register_all()?; 19 + } 20 + Ok(()) 21 + }); 22 + 23 + #[cfg(desktop)] 24 + { 25 + builder = builder.plugin(tauri_plugin_single_instance::init(|_app, argv, _cwd| { 26 + println!("A new app instance was opened with {argv:?} and the deep link event was already triggered."); 27 + })); 28 + } 29 + 30 + builder 31 .run(tauri::generate_context!()) 32 .expect("error while running tauri application"); 33 }
+5 -3
src/App.tsx
··· 3 import { invoke } from "@tauri-apps/api/core"; 4 import "./App.css"; 5 import { Button } from "./components/ui/button"; 6 7 function App() { 8 const [greetMsg, setGreetMsg] = useState(""); ··· 14 } 15 16 return ( 17 - <main className="container dark"> 18 - <div className="titlebar"> 19 - <div data-tauri-drag-region></div> 20 <div className="controls"> 21 <Button variant="ghost" id="titlebar-minimize" title="minimize"> 22 <svg ··· 53 </button> 54 </div> 55 </div> 56 </main> 57 ); 58 }
··· 3 import { invoke } from "@tauri-apps/api/core"; 4 import "./App.css"; 5 import { Button } from "./components/ui/button"; 6 + import LoginPage from "./routes/Login"; 7 8 function App() { 9 const [greetMsg, setGreetMsg] = useState(""); ··· 15 } 16 17 return ( 18 + <main className="dark"> 19 + <div className="titlebar" data-tauri-drag-region> 20 <div className="controls"> 21 <Button variant="ghost" id="titlebar-minimize" title="minimize"> 22 <svg ··· 53 </button> 54 </div> 55 </div> 56 + 57 + <LoginPage /> 58 </main> 59 ); 60 }
+10 -43
src/routes/Login.tsx
··· 7 BrowserOAuthClient, 8 BrowserOAuthClientOptions, 9 } from "@atproto/oauth-client-browser"; 10 11 type LoginMethod = "credential" | "oauth"; 12 ··· 24 useEffect(() => { 25 const initOAuthClient = async () => { 26 try { 27 - const client = new BrowserOAuthClient({ 28 - clientMetadata: { 29 - client_id: "http://localhost:3000", // Replace with your actual client ID 30 - redirect_uris: ["http://localhost:3000/callback"], // Replace with your redirect URI 31 - client_name: "ATProto Backup App", 32 - client_uri: "http://localhost:3000", 33 - scope: "atproto", 34 - }, 35 }); 36 setOauthClient(client); 37 } catch (err) { ··· 41 initOAuthClient(); 42 }, []); 43 44 - const handleCredentialLogin = async () => { 45 - setLoading(true); 46 - setError(""); 47 - const agent = new Agent(new CredentialSession()); 48 - 49 - try { 50 - const result = await agent.login({ identifier, password }); 51 - console.log("Credential login successful!", result); 52 - // Store session, redirect, etc. 53 - } catch (err: any) { 54 - console.error(err); 55 - setError(err.message || "Credential login failed"); 56 - } finally { 57 - setLoading(false); 58 - } 59 - }; 60 - 61 - const handleOAuthLogin = async () => { 62 if (!oauthClient) { 63 setError("OAuth client not initialized"); 64 return; ··· 135 136 handleCallback(); 137 }, [oauthClient]); 138 - 139 - const handleLogin = () => { 140 - if (loginMethod === "credential") { 141 - handleCredentialLogin(); 142 - } else { 143 - handleOAuthLogin(); 144 - } 145 - }; 146 - 147 return ( 148 <div className="min-h-screen flex items-center justify-center bg-background px-4"> 149 <Card className="w-full max-w-sm"> 150 <CardHeader> 151 - <CardTitle>Login to Bluesky</CardTitle> 152 </CardHeader> 153 <CardContent className="space-y-4"> 154 <Input ··· 156 value={identifier} 157 onChange={(e) => setIdentifier(e.target.value)} 158 /> 159 - <Input 160 - type="password" 161 - placeholder="Password" 162 - value={password} 163 - onChange={(e) => setPassword(e.target.value)} 164 - /> 165 {error && <p className="text-sm text-red-500">{error}</p>} 166 - <Button className="w-full" onClick={handleLogin} disabled={loading}> 167 {loading ? "Logging in..." : "Login"} 168 </Button> 169 </CardContent>
··· 7 BrowserOAuthClient, 8 BrowserOAuthClientOptions, 9 } from "@atproto/oauth-client-browser"; 10 + import { onOpenUrl } from "@tauri-apps/plugin-deep-link"; 11 12 type LoginMethod = "credential" | "oauth"; 13 ··· 25 useEffect(() => { 26 const initOAuthClient = async () => { 27 try { 28 + const client = await BrowserOAuthClient.load({ 29 + clientId: "https://atproto-backup.pages.dev/client_metadata.json", 30 }); 31 setOauthClient(client); 32 } catch (err) { ··· 36 initOAuthClient(); 37 }, []); 38 39 + const handleLogin = async () => { 40 if (!oauthClient) { 41 setError("OAuth client not initialized"); 42 return; ··· 113 114 handleCallback(); 115 }, [oauthClient]); 116 return ( 117 <div className="min-h-screen flex items-center justify-center bg-background px-4"> 118 <Card className="w-full max-w-sm"> 119 <CardHeader> 120 + <CardTitle>Login to your Bluesky account</CardTitle> 121 </CardHeader> 122 <CardContent className="space-y-4"> 123 <Input ··· 125 value={identifier} 126 onChange={(e) => setIdentifier(e.target.value)} 127 /> 128 {error && <p className="text-sm text-red-500">{error}</p>} 129 + <Button 130 + className="w-full" 131 + onClick={handleLogin} 132 + disabled={loading || identifier == null} 133 + > 134 {loading ? "Logging in..." : "Login"} 135 </Button> 136 </CardContent>