aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/packages/ui/src/stores/auth.svelte.ts
diff options
context:
space:
mode:
author苏向夜 <46275354+fu050409@users.noreply.github.com>2026-01-19 14:33:07 +0800
committerGitHub <noreply@github.com>2026-01-19 14:33:07 +0800
commit49545e67ce1ab4ec86248ac6edb07ec89c282eec (patch)
tree50f5fc3ae156cc853660a1aa1556c0bced9054b4 /packages/ui/src/stores/auth.svelte.ts
parent887e415314014c3da7db3048fa0e724f3078c5cb (diff)
parent91d4590dff7ed3dbce5929926c718ac93aad056a (diff)
downloadDropOut-49545e67ce1ab4ec86248ac6edb07ec89c282eec.tar.gz
DropOut-49545e67ce1ab4ec86248ac6edb07ec89c282eec.zip
chore(ui): refactor workspace to monorepo (#70)
## Summary by Sourcery Refactor the UI project structure into a pnpm monorepo packages layout and align tooling and automation with the new paths. Enhancements: - Reorganize the UI app from the root ui directory into packages/ui within a pnpm workspace. - Update pnpm workspace configuration to include all packages under packages/*. - Adjust paths in changeset configuration so the @dropout/ui package resolves from packages/ui. Build: - Update pre-commit configuration paths and arguments to reflect the new UI location and normalize hook argument formatting. - Update Dependabot configuration so npm updates target /packages/ui instead of /ui. CI: - Update GitHub Actions workflows to watch packages/** instead of ui/** and to run frontend tasks from packages/ui. - Update pnpm cache dependency paths in workflows to use the root pnpm-lock.yaml. - Simplify frontend install steps in test workflows to run from the repository root. Chores: - Add a new index.html under packages/ui and remove the old ui/index.html to match the new project layout.
Diffstat (limited to 'packages/ui/src/stores/auth.svelte.ts')
-rw-r--r--packages/ui/src/stores/auth.svelte.ts192
1 files changed, 192 insertions, 0 deletions
diff --git a/packages/ui/src/stores/auth.svelte.ts b/packages/ui/src/stores/auth.svelte.ts
new file mode 100644
index 0000000..1b613a7
--- /dev/null
+++ b/packages/ui/src/stores/auth.svelte.ts
@@ -0,0 +1,192 @@
+import { invoke } from "@tauri-apps/api/core";
+import { open } from "@tauri-apps/plugin-shell";
+import { listen, type UnlistenFn } from "@tauri-apps/api/event";
+import type { Account, DeviceCodeResponse } from "../types";
+import { uiState } from "./ui.svelte";
+import { logsState } from "./logs.svelte";
+
+export class AuthState {
+ currentAccount = $state<Account | null>(null);
+ isLoginModalOpen = $state(false);
+ isLogoutConfirmOpen = $state(false);
+ loginMode = $state<"select" | "offline" | "microsoft">("select");
+ offlineUsername = $state("");
+ deviceCodeData = $state<DeviceCodeResponse | null>(null);
+ msLoginLoading = $state(false);
+ msLoginStatus = $state("Waiting for authorization...");
+
+ private pollInterval: ReturnType<typeof setInterval> | null = null;
+ private isPollingRequestActive = false;
+ private authProgressUnlisten: UnlistenFn | null = null;
+
+ async checkAccount() {
+ try {
+ const acc = await invoke("get_active_account");
+ this.currentAccount = acc as Account | null;
+ } catch (e) {
+ console.error("Failed to check account:", e);
+ }
+ }
+
+ openLoginModal() {
+ if (this.currentAccount) {
+ // Show custom logout confirmation dialog
+ this.isLogoutConfirmOpen = true;
+ return;
+ }
+ this.resetLoginState();
+ this.isLoginModalOpen = true;
+ }
+
+ cancelLogout() {
+ this.isLogoutConfirmOpen = false;
+ }
+
+ async confirmLogout() {
+ this.isLogoutConfirmOpen = false;
+ try {
+ await invoke("logout");
+ this.currentAccount = null;
+ uiState.setStatus("Logged out successfully");
+ } catch (e) {
+ console.error("Logout failed:", e);
+ }
+ }
+
+ closeLoginModal() {
+ this.stopPolling();
+ this.isLoginModalOpen = false;
+ }
+
+ resetLoginState() {
+ this.loginMode = "select";
+ this.offlineUsername = "";
+ this.deviceCodeData = null;
+ this.msLoginLoading = false;
+ }
+
+ async performOfflineLogin() {
+ if (!this.offlineUsername) return;
+ try {
+ this.currentAccount = (await invoke("login_offline", {
+ username: this.offlineUsername,
+ })) as Account;
+ this.isLoginModalOpen = false;
+ } catch (e) {
+ alert("Login failed: " + e);
+ }
+ }
+
+ async startMicrosoftLogin() {
+ this.loginMode = "microsoft";
+ this.msLoginLoading = true;
+ this.msLoginStatus = "Waiting for authorization...";
+ this.stopPolling();
+
+ // Setup auth progress listener
+ this.setupAuthProgressListener();
+
+ try {
+ this.deviceCodeData = (await invoke("start_microsoft_login")) as DeviceCodeResponse;
+
+ if (this.deviceCodeData) {
+ try {
+ await navigator.clipboard.writeText(this.deviceCodeData.user_code);
+ } catch (e) {
+ console.error("Clipboard failed", e);
+ }
+
+ open(this.deviceCodeData.verification_uri);
+ logsState.addLog(
+ "info",
+ "Auth",
+ "Microsoft login started, waiting for browser authorization...",
+ );
+
+ console.log("Starting polling for token...");
+ const intervalMs = (this.deviceCodeData.interval || 5) * 1000;
+ this.pollInterval = setInterval(
+ () => this.checkLoginStatus(this.deviceCodeData!.device_code),
+ intervalMs,
+ );
+ }
+ } catch (e) {
+ logsState.addLog("error", "Auth", `Failed to start Microsoft login: ${e}`);
+ alert("Failed to start Microsoft login: " + e);
+ this.loginMode = "select";
+ } finally {
+ this.msLoginLoading = false;
+ }
+ }
+
+ private async setupAuthProgressListener() {
+ // Clean up previous listener if exists
+ if (this.authProgressUnlisten) {
+ this.authProgressUnlisten();
+ this.authProgressUnlisten = null;
+ }
+
+ this.authProgressUnlisten = await listen<string>("auth-progress", (event) => {
+ const message = event.payload;
+ this.msLoginStatus = message;
+ logsState.addLog("info", "Auth", message);
+ });
+ }
+
+ private cleanupAuthListener() {
+ if (this.authProgressUnlisten) {
+ this.authProgressUnlisten();
+ this.authProgressUnlisten = null;
+ }
+ }
+
+ stopPolling() {
+ if (this.pollInterval) {
+ clearInterval(this.pollInterval);
+ this.pollInterval = null;
+ }
+ }
+
+ async checkLoginStatus(deviceCode: string) {
+ if (this.isPollingRequestActive) return;
+ this.isPollingRequestActive = true;
+
+ console.log("Polling Microsoft API...");
+ try {
+ this.currentAccount = (await invoke("complete_microsoft_login", {
+ deviceCode,
+ })) as Account;
+
+ console.log("Login Successful!", this.currentAccount);
+ this.stopPolling();
+ this.cleanupAuthListener();
+ this.isLoginModalOpen = false;
+ logsState.addLog(
+ "info",
+ "Auth",
+ `Login successful! Welcome, ${this.currentAccount.username}`,
+ );
+ uiState.setStatus("Welcome back, " + this.currentAccount.username);
+ } catch (e: any) {
+ const errStr = e.toString();
+ if (errStr.includes("authorization_pending")) {
+ console.log("Status: Waiting for user to authorize...");
+ } else {
+ console.error("Polling Error:", errStr);
+ this.msLoginStatus = "Error: " + errStr;
+ logsState.addLog("error", "Auth", `Login error: ${errStr}`);
+
+ if (errStr.includes("expired_token") || errStr.includes("access_denied")) {
+ this.stopPolling();
+ this.cleanupAuthListener();
+ alert("Login failed: " + errStr);
+ this.loginMode = "select";
+ }
+ }
+ } finally {
+ this.isPollingRequestActive = false;
+ }
+ }
+}
+
+export const authState = new AuthState();