mirror of
https://github.com/infinilabs/coco-app.git
synced 2026-02-24 04:01:27 +01:00
refactor: refactoring login (#173)
* chore: add tips to login * refactor: refactoring login * chore: update release notes
This commit is contained in:
@@ -16,6 +16,8 @@ Information about release notes of Coco Server is provided here.
|
||||
|
||||
### Improvements
|
||||
- Improve app startup, init application search in background #172
|
||||
- Refactoring login #173
|
||||
|
||||
|
||||
## 0.1.0 (2015-02-16)
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ use crate::server::search::CocoSearchSource;
|
||||
use crate::server::servers::{get_server_by_id, persist_servers, persist_servers_token, save_access_token, save_server};
|
||||
use reqwest::{Client, StatusCode};
|
||||
use tauri::{AppHandle, Manager, Runtime};
|
||||
fn request_access_token_url(request_id: &str) -> String {
|
||||
fn request_access_token_url(request_id: &str, code: &str) -> String {
|
||||
// Remove the endpoint part and keep just the path for the request
|
||||
format!("/auth/request_access_token?request_id={}", request_id)
|
||||
format!("/auth/request_access_token?request_id={}&token={}", request_id, code)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -24,8 +24,8 @@ pub async fn handle_sso_callback<R: Runtime>(
|
||||
|
||||
if let Some(mut server) = server {
|
||||
// Prepare the URL for requesting the access token (endpoint is base URL, path is relative)
|
||||
save_access_token(server_id.clone(), ServerAccessToken::new(server_id.clone(), code.clone(), 60 * 15));
|
||||
let path = request_access_token_url(&request_id);
|
||||
// save_access_token(server_id.clone(), ServerAccessToken::new(server_id.clone(), code.clone(), 60 * 15));
|
||||
let path = request_access_token_url(&request_id, &code);
|
||||
|
||||
// Send the request for the access token using the util::http::HttpClient::get method
|
||||
let response = HttpClient::get(&server_id, &path)
|
||||
@@ -47,7 +47,7 @@ pub async fn handle_sso_callback<R: Runtime>(
|
||||
token.access_token.clone(),
|
||||
token.expire_at,
|
||||
);
|
||||
dbg!(&server_id, &request_id, &code, &token);
|
||||
// dbg!(&server_id, &request_id, &code, &token);
|
||||
save_access_token(server_id.clone(), access_token);
|
||||
persist_servers_token(&app_handle)?;
|
||||
|
||||
|
||||
@@ -29,8 +29,6 @@ pub fn get_connector_by_id(server_id: &str, connector_id: &str) -> Option<Connec
|
||||
}
|
||||
|
||||
pub async fn refresh_all_connectors<R: Runtime>(app_handle: &AppHandle<R>) -> Result<(), String> {
|
||||
// dbg!("Attempting to refresh all connectors");
|
||||
|
||||
let servers = get_all_servers();
|
||||
|
||||
// Collect all the tasks for fetching and refreshing connectors
|
||||
@@ -47,13 +45,11 @@ pub async fn refresh_all_connectors<R: Runtime>(app_handle: &AppHandle<R>) -> Re
|
||||
connectors_map
|
||||
}
|
||||
Err(_e) => {
|
||||
// dbg!("Failed to get connectors for server {}: {}", &server.id, e);
|
||||
HashMap::new() // Return empty map on failure
|
||||
}
|
||||
};
|
||||
|
||||
server_map.insert(server.id.clone(), connectors);
|
||||
// dbg!("end fetch connectors for server: {}", &server.id);
|
||||
}
|
||||
|
||||
// After all tasks have finished, perform a read operation on the cache
|
||||
@@ -66,8 +62,6 @@ pub async fn refresh_all_connectors<R: Runtime>(app_handle: &AppHandle<R>) -> Re
|
||||
cache.len()
|
||||
};
|
||||
|
||||
// dbg!("finished refresh connectors: {:?}", cache_size);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -101,8 +95,6 @@ pub async fn get_connectors_from_cache_or_remote(
|
||||
}
|
||||
|
||||
pub async fn fetch_connectors_by_server(id: &str) -> Result<Vec<Connector>, String> {
|
||||
// dbg!("start get_connectors_by_server: id =", &id);
|
||||
|
||||
// Use the generic GET method from HttpClient
|
||||
let resp = HttpClient::get(&id, "/connector/_search")
|
||||
.await
|
||||
@@ -111,34 +103,15 @@ pub async fn fetch_connectors_by_server(id: &str) -> Result<Vec<Connector>, Stri
|
||||
format!("Error fetching connector: {}", e)
|
||||
})?;
|
||||
|
||||
// // Log the raw response status and headers
|
||||
// dbg!("Response status: {:?}", resp.status());
|
||||
// dbg!("Response headers: {:?}", resp.headers());
|
||||
|
||||
// Ensure the response body is not empty or invalid
|
||||
if resp.status().is_success() {
|
||||
dbg!("Received successful response for id: {}", &id);
|
||||
} else {
|
||||
dbg!(
|
||||
"Failed to fetch connectors. Response status: {}",
|
||||
resp.status()
|
||||
);
|
||||
}
|
||||
|
||||
// Parse the search results directly from the response body
|
||||
let datasources: Vec<Connector> = parse_search_results(resp).await.map_err(|e| {
|
||||
// dbg!("Error parsing search results for id {}: {}", &id, &e);
|
||||
let datasource: Vec<Connector> = parse_search_results(resp).await.map_err(|e| {
|
||||
e.to_string()
|
||||
})?;
|
||||
|
||||
// Log the parsed results
|
||||
// dbg!("Parsed connectors: {:?}", &datasources);
|
||||
|
||||
// Save the connectors to the cache
|
||||
save_connectors_to_cache(&id, datasources.clone());
|
||||
save_connectors_to_cache(&id, datasource.clone());
|
||||
|
||||
// dbg!("end get_connectors_by_server: id =", &id);
|
||||
return Ok(datasources);
|
||||
Ok(datasource)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -146,8 +119,6 @@ pub async fn get_connectors_by_server<R: Runtime>(
|
||||
_app_handle: AppHandle<R>,
|
||||
id: String,
|
||||
) -> Result<Vec<Connector>, String> {
|
||||
//fetch_connectors_by_server
|
||||
|
||||
let connectors = fetch_connectors_by_server(&id).await?;
|
||||
Ok(connectors)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// use std::future::Future;
|
||||
use std::time::Duration;
|
||||
// use lazy_static::lazy_static;
|
||||
// use tauri::AppHandle;
|
||||
use crate::server::servers::{get_server_by_id, get_server_token};
|
||||
// use std::future::Future;
|
||||
use std::time::Duration;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use reqwest::{Client, Method, RequestBuilder};
|
||||
@@ -84,9 +84,6 @@ impl HttpClient {
|
||||
// Construct the URL
|
||||
let url = HttpClient::join_url(&s.endpoint, path);
|
||||
|
||||
// dbg!(&url);
|
||||
// dbg!(&server_id);
|
||||
|
||||
// Retrieve the token for the server (token is optional)
|
||||
let token = get_server_token(server_id).map(|t| t.access_token.clone());
|
||||
|
||||
@@ -99,6 +96,8 @@ impl HttpClient {
|
||||
);
|
||||
}
|
||||
|
||||
// dbg!(&server_id);
|
||||
// dbg!(&url);
|
||||
// dbg!(&headers);
|
||||
|
||||
Self::send_raw_request(method, &url, Some(headers), body).await
|
||||
|
||||
@@ -56,32 +56,22 @@ pub async fn connect_to_server(
|
||||
state: tauri::State<'_, WebSocketManager>,
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> Result<(), String> {
|
||||
dbg!("Connecting to server",id.as_str());
|
||||
|
||||
// Disconnect any existing connection first
|
||||
disconnect(state.clone()).await?;
|
||||
|
||||
dbg!("Disconnected from previous server",id.as_str());
|
||||
|
||||
// Retrieve server details
|
||||
let server = get_server_by_id(id.as_str())
|
||||
.ok_or_else(|| format!("Server with ID {} not found", id))?;
|
||||
let mut endpoint = convert_to_websocket(server.endpoint.as_str())?;
|
||||
|
||||
dbg!("Server endpoint",endpoint.as_str());
|
||||
let endpoint = convert_to_websocket(server.endpoint.as_str())?;
|
||||
|
||||
// Retrieve the token for the server (token is optional)
|
||||
let token = get_server_token(id.as_str()).map(|t| t.access_token.clone());
|
||||
|
||||
dbg!("Server token",token.as_ref().unwrap_or(&"".to_string()).as_str());
|
||||
|
||||
// Create the WebSocket request
|
||||
let mut request = tokio_tungstenite::tungstenite::client::IntoClientRequest::into_client_request(
|
||||
&endpoint
|
||||
).map_err(|e| format!("Failed to create WebSocket request: {}", e))?;
|
||||
|
||||
dbg!("WebSocket request");
|
||||
|
||||
// Add necessary headers
|
||||
request.headers_mut().insert("Connection", "Upgrade".parse().unwrap());
|
||||
request.headers_mut().insert("Upgrade", "websocket".parse().unwrap());
|
||||
@@ -91,20 +81,15 @@ pub async fn connect_to_server(
|
||||
generate_key().parse().unwrap(),
|
||||
);
|
||||
|
||||
dbg!("WebSocket headers",request.headers().iter().map(|(k, v)| format!("{}: {}", k.as_str(), v.to_str().unwrap())).collect::<Vec<String>>().join("\n"));
|
||||
|
||||
// If a token exists, add it to the headers
|
||||
if let Some(token) = token {
|
||||
request.headers_mut().insert("X-API-TOKEN", token.parse().unwrap());
|
||||
}
|
||||
|
||||
dbg!("WebSocket headers with token",request.headers().iter().map(|(k, v)| format!("{}: {}", k.as_str(), v.to_str().unwrap())).collect::<Vec<String>>().join("\n"));
|
||||
|
||||
// Establish the WebSocket connection
|
||||
dbg!(&request);
|
||||
// dbg!(&request);
|
||||
let (mut ws_remote, _) = connect_async(request).await
|
||||
.map_err(|e| {
|
||||
dbg!("WebSocket connection error",&e);
|
||||
match e {
|
||||
Error::ConnectionClosed => "WebSocket connection was closed".to_string(),
|
||||
Error::Protocol(protocol_error) => format!("Protocol error: {}", protocol_error),
|
||||
@@ -113,19 +98,12 @@ pub async fn connect_to_server(
|
||||
}
|
||||
})?;
|
||||
|
||||
dbg!("Connected to server1234",id.as_str());
|
||||
|
||||
// Create cancellation channel
|
||||
let (cancel_tx, mut cancel_rx) = mpsc::channel(1);
|
||||
|
||||
dbg!("try to lock Connected to server",id.as_str());
|
||||
|
||||
// Store connection and cancellation sender
|
||||
*state.ws_connection.lock().await = Some(ws_remote);
|
||||
*state.cancel_tx.lock().await = Some(cancel_tx);
|
||||
|
||||
dbg!("locked Connected to server",id.as_str());
|
||||
|
||||
// Spawn listener task with cancellation
|
||||
let app_handle_clone = app_handle.clone();
|
||||
let connection_clone = state.ws_connection.clone();
|
||||
@@ -133,8 +111,6 @@ pub async fn connect_to_server(
|
||||
let mut connection = connection_clone.lock().await;
|
||||
if let Some(ws) = connection.as_mut() {
|
||||
loop {
|
||||
dbg!("try to select Connected to server",id.as_str());
|
||||
|
||||
tokio::select! {
|
||||
msg = ws.next() => {
|
||||
match msg {
|
||||
@@ -153,8 +129,6 @@ pub async fn connect_to_server(
|
||||
}
|
||||
});
|
||||
|
||||
dbg!("END Connected to server");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ fn _register_shortcut_upon_start(app: &App, shortcut: Shortcut) {
|
||||
if window.is_visible().unwrap() {
|
||||
window.hide().unwrap();
|
||||
} else {
|
||||
dbg!("showing window");
|
||||
// dbg!("showing window");
|
||||
move_window_to_active_monitor(&window);
|
||||
window.set_visible_on_all_workspaces(true).unwrap();
|
||||
window.set_always_on_top(true).unwrap();
|
||||
|
||||
@@ -1,446 +1,439 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import {
|
||||
RefreshCcw,
|
||||
Globe,
|
||||
PackageOpen,
|
||||
GitFork,
|
||||
CalendarSync,
|
||||
Trash2,
|
||||
Copy,
|
||||
} from "lucide-react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import {
|
||||
onOpenUrl,
|
||||
getCurrent as getCurrentDeepLinkUrls,
|
||||
} from "@tauri-apps/plugin-deep-link";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {useCallback, useEffect, useRef, useState} from "react";
|
||||
import {CalendarSync, Copy, GitFork, Globe, PackageOpen, RefreshCcw, Trash2,} from "lucide-react";
|
||||
import {v4 as uuidv4} from "uuid";
|
||||
import {getCurrentWindow} from "@tauri-apps/api/window";
|
||||
import {getCurrent as getCurrentDeepLinkUrls, onOpenUrl,} from "@tauri-apps/plugin-deep-link";
|
||||
import {invoke} from "@tauri-apps/api/core";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
import { UserProfile } from "./UserProfile";
|
||||
import { DataSourcesList } from "./DataSourcesList";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
import { Connect } from "./Connect";
|
||||
import { OpenURLWithBrowser } from "@/utils";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import {UserProfile} from "./UserProfile";
|
||||
import {DataSourcesList} from "./DataSourcesList";
|
||||
import {Sidebar} from "./Sidebar";
|
||||
import {Connect} from "./Connect";
|
||||
import {OpenURLWithBrowser} from "@/utils";
|
||||
import {useAppStore} from "@/stores/appStore";
|
||||
import {useConnectStore} from "@/stores/connectStore";
|
||||
import bannerImg from "@/assets/images/coco-cloud-banner.jpeg";
|
||||
|
||||
export default function Cloud() {
|
||||
const { t } = useTranslation();
|
||||
const {t} = useTranslation();
|
||||
|
||||
const SidebarRef = useRef<{ refreshData: () => void }>(null);
|
||||
const SidebarRef = useRef<{ refreshData: () => void }>(null);
|
||||
|
||||
const error = useAppStore((state) => state.error);
|
||||
const setError = useAppStore((state) => state.setError);
|
||||
const error = useAppStore((state) => state.error);
|
||||
const setError = useAppStore((state) => state.setError);
|
||||
|
||||
const [isConnect, setIsConnect] = useState(true);
|
||||
const [isConnect, setIsConnect] = useState(true);
|
||||
|
||||
const ssoRequestID = useAppStore((state) => state.ssoRequestID);
|
||||
const setSSORequestID = useAppStore((state) => state.setSSORequestID);
|
||||
const ssoRequestID = useAppStore((state) => state.ssoRequestID);
|
||||
const setSSORequestID = useAppStore((state) => state.setSSORequestID);
|
||||
|
||||
const endpoint = useAppStore((state) => state.endpoint);
|
||||
const currentService = useConnectStore((state) => state.currentService);
|
||||
const setCurrentService = useConnectStore((state) => state.setCurrentService);
|
||||
|
||||
const currentService = useConnectStore((state) => state.currentService);
|
||||
const setCurrentService = useConnectStore((state) => state.setCurrentService);
|
||||
const serverList = useConnectStore((state) => state.serverList);
|
||||
const setServerList = useConnectStore((state) => state.setServerList);
|
||||
|
||||
const serverList = useConnectStore((state) => state.serverList);
|
||||
const setServerList = useConnectStore((state) => state.setServerList);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [refreshLoading, setRefreshLoading] = useState(false);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [refreshLoading, setRefreshLoading] = useState(false);
|
||||
// fetch the servers
|
||||
useEffect(() => {
|
||||
fetchServers(true);
|
||||
}, []);
|
||||
|
||||
// fetch the servers
|
||||
useEffect(() => {
|
||||
fetchServers(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("currentService", currentService);
|
||||
setLoading(false);
|
||||
setRefreshLoading(false);
|
||||
setError("");
|
||||
setIsConnect(true);
|
||||
}, [JSON.stringify(currentService)]);
|
||||
|
||||
const fetchServers = async (resetSelection: boolean) => {
|
||||
invoke("list_coco_servers")
|
||||
.then((res: any) => {
|
||||
console.log("list_coco_servers", res);
|
||||
setServerList(res);
|
||||
if (resetSelection && res.length > 0) {
|
||||
console.log("setCurrentService", res[res.length - 1]);
|
||||
setCurrentService(res[res.length - 1]);
|
||||
} else {
|
||||
console.warn("Service list is empty or last item has no id");
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
setError(err);
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
const add_coco_server = (endpointLink: string) => {
|
||||
if (!endpointLink) {
|
||||
throw new Error("Endpoint is required");
|
||||
}
|
||||
if (
|
||||
!endpointLink.startsWith("http://") &&
|
||||
!endpointLink.startsWith("https://")
|
||||
) {
|
||||
throw new Error("Invalid Endpoint");
|
||||
}
|
||||
|
||||
setRefreshLoading(true);
|
||||
|
||||
return invoke("add_coco_server", { endpoint: endpointLink })
|
||||
.then((res: any) => {
|
||||
console.log("add_coco_server", res);
|
||||
fetchServers(false)
|
||||
.then((r) => {
|
||||
console.log("fetchServers", r);
|
||||
setCurrentService(res);
|
||||
})
|
||||
.catch((err: any) => {
|
||||
console.error("fetchServers failed:", err);
|
||||
setError(err);
|
||||
throw err; // Propagate error back up to outer promise chain
|
||||
});
|
||||
})
|
||||
.catch((err: any) => {
|
||||
// Handle the invoke error
|
||||
console.error("add coco server failed:", err);
|
||||
setError(err);
|
||||
throw err; // Propagate error back up
|
||||
})
|
||||
.finally(() => {
|
||||
setRefreshLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleOAuthCallback = useCallback(
|
||||
async (code: string | null, serverId: string | null) => {
|
||||
if (!code) {
|
||||
setError("No authorization code received");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("Handling OAuth callback:", { code, serverId });
|
||||
await invoke("handle_sso_callback", {
|
||||
serverId: serverId, // Make sure 'server_id' is the correct argument
|
||||
requestId: ssoRequestID, // Make sure 'request_id' is the correct argument
|
||||
code: code,
|
||||
});
|
||||
|
||||
if (serverId != null) {
|
||||
refreshClick(serverId);
|
||||
}
|
||||
|
||||
getCurrentWindow()
|
||||
.setFocus()
|
||||
.catch((err) => {
|
||||
setError(err);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Sign in failed:", e);
|
||||
setError("SSO login failed: " + e);
|
||||
throw error;
|
||||
} finally {
|
||||
useEffect(() => {
|
||||
console.log("currentService", currentService);
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[ssoRequestID, endpoint]
|
||||
);
|
||||
setRefreshLoading(false);
|
||||
setError("");
|
||||
setIsConnect(true);
|
||||
}, [JSON.stringify(currentService)]);
|
||||
|
||||
const handleUrl = (url: string) => {
|
||||
try {
|
||||
const urlObject = new URL(url);
|
||||
console.log("handle urlObject:", urlObject);
|
||||
|
||||
// TODO, pass request_id and check with local, if the request_id are same, then continue
|
||||
const reqId = urlObject.searchParams.get("request_id");
|
||||
const code = urlObject.searchParams.get("code");
|
||||
|
||||
if (reqId != ssoRequestID) {
|
||||
console.log("Request ID not matched, skip");
|
||||
setError("Request ID not matched, skip");
|
||||
return;
|
||||
}
|
||||
|
||||
const serverId = currentService?.id;
|
||||
handleOAuthCallback(code, serverId);
|
||||
} catch (err) {
|
||||
console.error("Failed to parse URL:", err);
|
||||
setError("Invalid URL format: " + err);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch the initial deep link intent
|
||||
useEffect(() => {
|
||||
// Test the handleUrl function
|
||||
// handleUrl("coco://oauth_callback?code=cuq8asc61mdmvii032q0sx1e5akx10zo8bks45znpv3cx1gtyc6wsi0rvplizb34mwbsrbm3jar8jnefg3o5&request_id=3f1acedb-6a5b-4fe1-82fd-e66934e98a55&provider=coco-cloud/");
|
||||
// Function to handle pasted URL
|
||||
const handlePaste = (event: any) => {
|
||||
const pastedText = event.clipboardData.getData("text");
|
||||
console.log("handle paste text:", pastedText);
|
||||
if (isValidCallbackUrl(pastedText)) {
|
||||
// Handle the URL as if it's a deep link
|
||||
console.log("handle callback on paste:", pastedText);
|
||||
handleUrl(pastedText);
|
||||
}
|
||||
const fetchServers = async (resetSelection: boolean) => {
|
||||
invoke("list_coco_servers")
|
||||
.then((res: any) => {
|
||||
console.log("list_coco_servers", res);
|
||||
setServerList(res);
|
||||
if (resetSelection && res.length > 0) {
|
||||
console.log("setCurrentService", res[res.length - 1]);
|
||||
setCurrentService(res[res.length - 1]);
|
||||
} else {
|
||||
console.warn("Service list is empty or last item has no id");
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
setError(err);
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
// Function to check if the pasted URL is valid for our deep link scheme
|
||||
const isValidCallbackUrl = (url: string) => {
|
||||
return url && url.startsWith("coco://oauth_callback");
|
||||
};
|
||||
|
||||
// Adding event listener for paste events
|
||||
document.addEventListener("paste", handlePaste);
|
||||
|
||||
getCurrentDeepLinkUrls()
|
||||
.then((urls) => {
|
||||
console.log("URLs:", urls);
|
||||
if (urls && urls.length > 0) {
|
||||
if (isValidCallbackUrl(urls[0])) {
|
||||
handleUrl(urls[0]);
|
||||
}
|
||||
const add_coco_server = (endpointLink: string) => {
|
||||
if (!endpointLink) {
|
||||
throw new Error("Endpoint is required");
|
||||
}
|
||||
if (
|
||||
!endpointLink.startsWith("http://") &&
|
||||
!endpointLink.startsWith("https://")
|
||||
) {
|
||||
throw new Error("Invalid Endpoint");
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to get initial URLs:", err);
|
||||
setError("Failed to get initial URLs: " + err);
|
||||
});
|
||||
|
||||
const unlisten = onOpenUrl((urls) => handleUrl(urls[0]));
|
||||
setRefreshLoading(true);
|
||||
|
||||
return () => {
|
||||
unlisten.then((fn) => fn());
|
||||
document.removeEventListener("paste", handlePaste);
|
||||
return invoke("add_coco_server", {endpoint: endpointLink})
|
||||
.then((res: any) => {
|
||||
console.log("add_coco_server", res);
|
||||
fetchServers(false)
|
||||
.then((r) => {
|
||||
console.log("fetchServers", r);
|
||||
setCurrentService(res);
|
||||
})
|
||||
.catch((err: any) => {
|
||||
console.error("fetchServers failed:", err);
|
||||
setError(err);
|
||||
throw err; // Propagate error back up to outer promise chain
|
||||
});
|
||||
})
|
||||
.catch((err: any) => {
|
||||
// Handle the invoke error
|
||||
console.error("add coco server failed:", err);
|
||||
setError(err);
|
||||
throw err; // Propagate error back up
|
||||
})
|
||||
.finally(() => {
|
||||
setRefreshLoading(false);
|
||||
});
|
||||
};
|
||||
}, [ssoRequestID]);
|
||||
|
||||
const LoginClick = useCallback(() => {
|
||||
if (loading) return; // Prevent multiple clicks if already loading
|
||||
const handleOAuthCallback = useCallback(
|
||||
async (code: string | null, serverId: string | null) => {
|
||||
if (!code) {
|
||||
setError("No authorization code received");
|
||||
return;
|
||||
}
|
||||
|
||||
let requestID = uuidv4();
|
||||
setSSORequestID(requestID);
|
||||
try {
|
||||
console.log("Handling OAuth callback:", {code, serverId});
|
||||
await invoke("handle_sso_callback", {
|
||||
serverId: serverId, // Make sure 'server_id' is the correct argument
|
||||
requestId: ssoRequestID, // Make sure 'request_id' is the correct argument
|
||||
code: code,
|
||||
});
|
||||
|
||||
// Generate the login URL with the current appUid
|
||||
const url = `${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${requestID}`;
|
||||
if (serverId != null) {
|
||||
refreshClick(serverId);
|
||||
}
|
||||
|
||||
console.log("Open SSO link, requestID:", ssoRequestID, url);
|
||||
getCurrentWindow()
|
||||
.setFocus()
|
||||
.catch((err) => {
|
||||
setError(err);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Sign in failed:", e);
|
||||
setError("SSO login failed: " + e);
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[ssoRequestID,]
|
||||
);
|
||||
|
||||
// Open the URL in a browser
|
||||
OpenURLWithBrowser(url);
|
||||
const handleUrl = (url: string) => {
|
||||
try {
|
||||
const urlObject = new URL(url.trim());
|
||||
console.log("handle urlObject:", urlObject);
|
||||
|
||||
// Start loading state
|
||||
setLoading(true);
|
||||
}, [ssoRequestID, loading, currentService]);
|
||||
// pass request_id and check with local, if the request_id are same, then continue
|
||||
const reqId = urlObject.searchParams.get("request_id");
|
||||
const code = urlObject.searchParams.get("code");
|
||||
|
||||
const refreshClick = (id: string) => {
|
||||
setRefreshLoading(true);
|
||||
invoke("refresh_coco_server_info", { id })
|
||||
.then((res: any) => {
|
||||
console.log("refresh_coco_server_info", id, JSON.stringify(res));
|
||||
fetchServers(false).then((r) => {
|
||||
console.log("fetchServers", r);
|
||||
});
|
||||
// update currentService
|
||||
setCurrentService(res);
|
||||
})
|
||||
.catch((err: any) => {
|
||||
setError(err);
|
||||
console.error(err);
|
||||
})
|
||||
.finally(() => {
|
||||
setRefreshLoading(false);
|
||||
});
|
||||
};
|
||||
if (reqId != ssoRequestID) {
|
||||
console.log("Request ID not matched, skip");
|
||||
setError("Request ID not matched, skip");
|
||||
return;
|
||||
}
|
||||
|
||||
function onAddServer() {
|
||||
setIsConnect(false);
|
||||
}
|
||||
function onLogout(id: string) {
|
||||
console.log("onLogout", id);
|
||||
setRefreshLoading(true);
|
||||
invoke("logout_coco_server", { id })
|
||||
.then((res: any) => {
|
||||
console.log("logout_coco_server", id, JSON.stringify(res));
|
||||
refreshClick(id);
|
||||
})
|
||||
.catch((err: any) => {
|
||||
setError(err);
|
||||
console.error(err);
|
||||
})
|
||||
.finally(() => {
|
||||
setRefreshLoading(false);
|
||||
});
|
||||
}
|
||||
const serverId = currentService?.id;
|
||||
handleOAuthCallback(code, serverId);
|
||||
} catch (err) {
|
||||
console.error("Failed to parse URL:", err);
|
||||
setError("Invalid URL format: " + err);
|
||||
}
|
||||
};
|
||||
|
||||
const remove_coco_server = (id: string) => {
|
||||
invoke("remove_coco_server", { id })
|
||||
.then((res: any) => {
|
||||
console.log("remove_coco_server", id, JSON.stringify(res));
|
||||
fetchServers(true).then((r) => {
|
||||
console.log("fetchServers", r);
|
||||
});
|
||||
})
|
||||
.catch((err: any) => {
|
||||
// TODO display the error message
|
||||
setError(err);
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
// Fetch the initial deep link intent
|
||||
useEffect(() => {
|
||||
// Test the handleUrl function
|
||||
// handleUrl("coco://oauth_callback?code=cuq8asc61mdmvii032q0sx1e5akx10zo8bks45znpv3cx1gtyc6wsi0rvplizb34mwbsrbm3jar8jnefg3o5&request_id=3f1acedb-6a5b-4fe1-82fd-e66934e98a55&provider=coco-cloud/");
|
||||
// Function to handle pasted URL
|
||||
const handlePaste = (event: any) => {
|
||||
const pastedText = event.clipboardData.getData("text").trim();
|
||||
console.log("handle paste text:", pastedText);
|
||||
if (isValidCallbackUrl(pastedText)) {
|
||||
// Handle the URL as if it's a deep link
|
||||
console.log("handle callback on paste:", pastedText);
|
||||
handleUrl(pastedText);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex bg-gray-50 dark:bg-gray-900">
|
||||
<Sidebar
|
||||
ref={SidebarRef}
|
||||
onAddServer={onAddServer}
|
||||
serverList={serverList}
|
||||
/>
|
||||
// Function to check if the pasted URL is valid for our deep link scheme
|
||||
const isValidCallbackUrl = (url: string) => {
|
||||
return url && url.startsWith("coco://oauth_callback");
|
||||
};
|
||||
|
||||
<main className="flex-1 p-4 py-8">
|
||||
{isConnect ? (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="w-full rounded-[4px] bg-[rgba(229,229,229,1)] dark:bg-gray-800 mb-6">
|
||||
<img
|
||||
width="100%"
|
||||
src={currentService?.provider?.banner || bannerImg}
|
||||
alt="banner"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center text-gray-900 dark:text-white font-medium">
|
||||
{currentService?.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||
onClick={() =>
|
||||
OpenURLWithBrowser(currentService?.provider?.website)
|
||||
}
|
||||
>
|
||||
<Globe className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||
onClick={() => refreshClick(currentService?.id)}
|
||||
>
|
||||
<RefreshCcw
|
||||
className={`w-3.5 h-3.5 ${
|
||||
refreshLoading ? "animate-spin" : ""
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
// Adding event listener for paste events
|
||||
document.addEventListener("paste", handlePaste);
|
||||
|
||||
{!currentService?.builtin && (
|
||||
<button
|
||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||
onClick={() => remove_coco_server(currentService?.id)}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5 text-[#ff4747]" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
getCurrentDeepLinkUrls()
|
||||
.then((urls) => {
|
||||
console.log("URLs:", urls);
|
||||
if (urls && urls.length > 0) {
|
||||
if (isValidCallbackUrl(urls[0].trim())) {
|
||||
handleUrl(urls[0]);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to get initial URLs:", err);
|
||||
setError("Failed to get initial URLs: " + err);
|
||||
});
|
||||
|
||||
<div className="mb-8">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 mb-2 flex">
|
||||
const unlisten = onOpenUrl((urls) => handleUrl(urls[0]));
|
||||
|
||||
return () => {
|
||||
unlisten.then((fn) => fn());
|
||||
document.removeEventListener("paste", handlePaste);
|
||||
};
|
||||
}, [ssoRequestID]);
|
||||
|
||||
const LoginClick = useCallback(() => {
|
||||
if (loading) return; // Prevent multiple clicks if already loading
|
||||
|
||||
let requestID = uuidv4();
|
||||
setSSORequestID(requestID);
|
||||
|
||||
// Generate the login URL with the current appUid
|
||||
const url = `${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${requestID}`;
|
||||
|
||||
console.log("Open SSO link, requestID:", ssoRequestID, url);
|
||||
|
||||
// Open the URL in a browser
|
||||
OpenURLWithBrowser(url);
|
||||
|
||||
// Start loading state
|
||||
setLoading(true);
|
||||
}, [ssoRequestID, loading, currentService]);
|
||||
|
||||
const refreshClick = (id: string) => {
|
||||
setRefreshLoading(true);
|
||||
invoke("refresh_coco_server_info", {id})
|
||||
.then((res: any) => {
|
||||
console.log("refresh_coco_server_info", id, JSON.stringify(res));
|
||||
fetchServers(false).then((r) => {
|
||||
console.log("fetchServers", r);
|
||||
});
|
||||
// update currentService
|
||||
setCurrentService(res);
|
||||
})
|
||||
.catch((err: any) => {
|
||||
setError(err);
|
||||
console.error(err);
|
||||
})
|
||||
.finally(() => {
|
||||
setRefreshLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
function onAddServer() {
|
||||
setIsConnect(false);
|
||||
}
|
||||
|
||||
function onLogout(id: string) {
|
||||
console.log("onLogout", id);
|
||||
setRefreshLoading(true);
|
||||
invoke("logout_coco_server", {id})
|
||||
.then((res: any) => {
|
||||
console.log("logout_coco_server", id, JSON.stringify(res));
|
||||
refreshClick(id);
|
||||
})
|
||||
.catch((err: any) => {
|
||||
setError(err);
|
||||
console.error(err);
|
||||
})
|
||||
.finally(() => {
|
||||
setRefreshLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
const remove_coco_server = (id: string) => {
|
||||
invoke("remove_coco_server", {id})
|
||||
.then((res: any) => {
|
||||
console.log("remove_coco_server", id, JSON.stringify(res));
|
||||
fetchServers(true).then((r) => {
|
||||
console.log("fetchServers", r);
|
||||
});
|
||||
})
|
||||
.catch((err: any) => {
|
||||
// TODO display the error message
|
||||
setError(err);
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex bg-gray-50 dark:bg-gray-900">
|
||||
<Sidebar
|
||||
ref={SidebarRef}
|
||||
onAddServer={onAddServer}
|
||||
serverList={serverList}
|
||||
/>
|
||||
|
||||
<main className="flex-1 p-4 py-8">
|
||||
{isConnect ? (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="w-full rounded-[4px] bg-[rgba(229,229,229,1)] dark:bg-gray-800 mb-6">
|
||||
<img
|
||||
width="100%"
|
||||
src={currentService?.provider?.banner || bannerImg}
|
||||
alt="banner"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center text-gray-900 dark:text-white font-medium">
|
||||
{currentService?.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||
onClick={() =>
|
||||
OpenURLWithBrowser(currentService?.provider?.website)
|
||||
}
|
||||
>
|
||||
<Globe className="w-3.5 h-3.5"/>
|
||||
</button>
|
||||
<button
|
||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||
onClick={() => refreshClick(currentService?.id)}
|
||||
>
|
||||
<RefreshCcw
|
||||
className={`w-3.5 h-3.5 ${
|
||||
refreshLoading ? "animate-spin" : ""
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{!currentService?.builtin && (
|
||||
<button
|
||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||
onClick={() => remove_coco_server(currentService?.id)}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5 text-[#ff4747]"/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 mb-2 flex">
|
||||
<span className="flex items-center gap-1">
|
||||
<PackageOpen className="w-4 h-4" />{" "}
|
||||
{currentService?.provider?.name}
|
||||
<PackageOpen className="w-4 h-4"/>{" "}
|
||||
{currentService?.provider?.name}
|
||||
</span>
|
||||
<span className="mx-4">|</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<GitFork className="w-4 h-4" />{" "}
|
||||
{currentService?.version?.number}
|
||||
<span className="mx-4">|</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<GitFork className="w-4 h-4"/>{" "}
|
||||
{currentService?.version?.number}
|
||||
</span>
|
||||
<span className="mx-4">|</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<CalendarSync className="w-4 h-4" /> {currentService?.updated}
|
||||
<span className="mx-4">|</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<CalendarSync className="w-4 h-4"/> {currentService?.updated}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
|
||||
{currentService?.provider?.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
|
||||
{currentService?.provider?.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{currentService?.auth_provider?.sso?.url ? (
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||
{t('cloud.accountInfo')}
|
||||
</h2>
|
||||
{currentService?.profile ? (
|
||||
<UserProfile
|
||||
server={currentService?.id}
|
||||
userInfo={currentService?.profile}
|
||||
onLogout={onLogout}
|
||||
/>
|
||||
{currentService?.auth_provider?.sso?.url ? (
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||
{t('cloud.accountInfo')}
|
||||
</h2>
|
||||
{currentService?.profile ? (
|
||||
<UserProfile
|
||||
server={currentService?.id}
|
||||
userInfo={currentService?.profile}
|
||||
onLogout={onLogout}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
{/* Login Button (conditionally rendered when not loading) */}
|
||||
{!loading && (
|
||||
<button
|
||||
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors mb-3"
|
||||
onClick={LoginClick}
|
||||
>
|
||||
{t('cloud.login')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Cancel Button and Copy URL button while loading */}
|
||||
{loading && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
className="px-6 py-2 text-white bg-red-500 rounded-md hover:bg-red-600 transition-colors mb-3"
|
||||
onClick={() => setLoading(false)} // Reset loading state
|
||||
>
|
||||
{t('cloud.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(
|
||||
`${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${ssoRequestID}`
|
||||
);
|
||||
}}
|
||||
className="text-xl text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
<Copy className="inline mr-2"/>{" "}
|
||||
</button>
|
||||
<div className="text-justify italic text-xs">If the link did not
|
||||
open
|
||||
automatically, please
|
||||
copy and
|
||||
paste it into your browser manually.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Privacy Policy Link */}
|
||||
<button
|
||||
className="text-xs text-[#0096FB] dark:text-blue-400 block"
|
||||
onClick={() =>
|
||||
OpenURLWithBrowser(
|
||||
currentService?.provider?.privacy_policy
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('cloud.privacyPolicy')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{currentService?.profile ? (
|
||||
<DataSourcesList server={currentService?.id}/>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{/* Login Button (conditionally rendered when not loading) */}
|
||||
{!loading && (
|
||||
<button
|
||||
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors mb-3"
|
||||
onClick={LoginClick}
|
||||
>
|
||||
{t('cloud.login')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Cancel Button and Copy URL button while loading */}
|
||||
{loading && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
className="px-6 py-2 text-white bg-red-500 rounded-md hover:bg-red-600 transition-colors mb-3"
|
||||
onClick={() => setLoading(false)} // Reset loading state
|
||||
>
|
||||
{t('cloud.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(
|
||||
`${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${ssoRequestID}`
|
||||
);
|
||||
}}
|
||||
className="text-xl text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
<Copy className="inline mr-2" />{" "}
|
||||
{/* Lucide Copy Icon */}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Privacy Policy Link */}
|
||||
<button
|
||||
className="text-xs text-[#0096FB] dark:text-blue-400 block"
|
||||
onClick={() =>
|
||||
OpenURLWithBrowser(
|
||||
currentService?.provider?.privacy_policy
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('cloud.privacyPolicy')}
|
||||
</button>
|
||||
</div>
|
||||
<Connect setIsConnect={setIsConnect} onAddServer={add_coco_server}/>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{currentService?.profile ? (
|
||||
<DataSourcesList server={currentService?.id} />
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<Connect setIsConnect={setIsConnect} onAddServer={add_coco_server} />
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user