mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Add Sign Up functionality to desktop app (#5244)
# Description of Changes Adds Sign Up with email to desktop app. SSO sign up will come in a future PR.
This commit is contained in:
Generated
+93
@@ -589,6 +589,26 @@ dependencies = [
|
|||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-random"
|
||||||
|
version = "0.1.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
|
||||||
|
dependencies = [
|
||||||
|
"const-random-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-random-macro"
|
||||||
|
version = "0.1.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.16",
|
||||||
|
"once_cell",
|
||||||
|
"tiny-keccak",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "convert_case"
|
name = "convert_case"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@@ -716,6 +736,12 @@ version = "0.8.21"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crunchy"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
@@ -908,6 +934,15 @@ dependencies = [
|
|||||||
"syn 2.0.108",
|
"syn 2.0.108",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dlv-list"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
|
||||||
|
dependencies = [
|
||||||
|
"const-random",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "document-features"
|
name = "document-features"
|
||||||
version = "0.2.12"
|
version = "0.2.12"
|
||||||
@@ -1626,6 +1661,12 @@ dependencies = [
|
|||||||
"ahash",
|
"ahash",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.14.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
@@ -2820,6 +2861,16 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ordered-multimap"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
|
||||||
|
dependencies = [
|
||||||
|
"dlv-list",
|
||||||
|
"hashbrown 0.14.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ordered-stream"
|
name = "ordered-stream"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -3651,6 +3702,16 @@ dependencies = [
|
|||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-ini"
|
||||||
|
version = "0.21.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"ordered-multimap",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust_decimal"
|
name = "rust_decimal"
|
||||||
version = "1.39.0"
|
version = "1.39.0"
|
||||||
@@ -4256,6 +4317,7 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
|
"tauri-plugin-deep-link",
|
||||||
"tauri-plugin-fs",
|
"tauri-plugin-fs",
|
||||||
"tauri-plugin-http",
|
"tauri-plugin-http",
|
||||||
"tauri-plugin-log",
|
"tauri-plugin-log",
|
||||||
@@ -4615,6 +4677,27 @@ dependencies = [
|
|||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-deep-link"
|
||||||
|
version = "2.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e82759f7c7d51de3cbde51c04b3f2332de52436ed84541182cd8944b04e9e73"
|
||||||
|
dependencies = [
|
||||||
|
"dunce",
|
||||||
|
"plist",
|
||||||
|
"rust-ini",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"tauri-utils",
|
||||||
|
"thiserror 2.0.17",
|
||||||
|
"tracing",
|
||||||
|
"url",
|
||||||
|
"windows-registry",
|
||||||
|
"windows-result 0.3.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-fs"
|
name = "tauri-plugin-fs"
|
||||||
version = "2.4.4"
|
version = "2.4.4"
|
||||||
@@ -4735,6 +4818,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
|
"tauri-plugin-deep-link",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
@@ -4954,6 +5038,15 @@ dependencies = [
|
|||||||
"time-core",
|
"time-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tiny-keccak"
|
||||||
|
version = "2.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
|
||||||
|
dependencies = [
|
||||||
|
"crunchy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tiny_http"
|
name = "tiny_http"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
|
|||||||
@@ -29,9 +29,10 @@ tauri-plugin-log = "2.0.0-rc"
|
|||||||
tauri-plugin-shell = "2.1.0"
|
tauri-plugin-shell = "2.1.0"
|
||||||
tauri-plugin-fs = "2.4.4"
|
tauri-plugin-fs = "2.4.4"
|
||||||
tauri-plugin-http = "2.4.4"
|
tauri-plugin-http = "2.4.4"
|
||||||
tauri-plugin-single-instance = "2.0.1"
|
tauri-plugin-single-instance = { version = "2.3.6", features = ["deep-link"] }
|
||||||
tauri-plugin-store = "2.1.0"
|
tauri-plugin-store = "2.1.0"
|
||||||
tauri-plugin-opener = "2.0.0"
|
tauri-plugin-opener = "2.0.0"
|
||||||
|
tauri-plugin-deep-link = "2.4.5"
|
||||||
keyring = { version = "3.6.1", features = ["apple-native", "windows-native"] }
|
keyring = { version = "3.6.1", features = ["apple-native", "windows-native"] }
|
||||||
tokio = { version = "1.0", features = ["time", "sync"] }
|
tokio = { version = "1.0", features = ["time", "sync"] }
|
||||||
reqwest = { version = "0.11", features = ["json"] }
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
{
|
{
|
||||||
"identifier": "fs:allow-read-file",
|
"identifier": "fs:allow-read-file",
|
||||||
"allow": [{ "path": "**" }]
|
"allow": [{ "path": "**" }]
|
||||||
}
|
},
|
||||||
|
"opener:default",
|
||||||
|
"shell:allow-open"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use tauri::{Manager, RunEvent, WindowEvent, Emitter};
|
use tauri::{AppHandle, Emitter, Manager, RunEvent, WindowEvent};
|
||||||
|
|
||||||
mod utils;
|
mod utils;
|
||||||
mod commands;
|
mod commands;
|
||||||
@@ -28,6 +28,17 @@ use commands::{
|
|||||||
};
|
};
|
||||||
use state::connection_state::AppConnectionState;
|
use state::connection_state::AppConnectionState;
|
||||||
use utils::{add_log, get_tauri_logs};
|
use utils::{add_log, get_tauri_logs};
|
||||||
|
use tauri_plugin_deep_link::DeepLinkExt;
|
||||||
|
|
||||||
|
fn dispatch_deep_link(app: &AppHandle, url: &str) {
|
||||||
|
add_log(format!("🔗 Dispatching deep link: {}", url));
|
||||||
|
let _ = app.emit("deep-link", url.to_string());
|
||||||
|
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
let _ = window.set_focus();
|
||||||
|
let _ = window.unminimize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
@@ -42,6 +53,7 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_fs::init())
|
.plugin(tauri_plugin_fs::init())
|
||||||
.plugin(tauri_plugin_http::init())
|
.plugin(tauri_plugin_http::init())
|
||||||
.plugin(tauri_plugin_store::Builder::new().build())
|
.plugin(tauri_plugin_store::Builder::new().build())
|
||||||
|
.plugin(tauri_plugin_deep_link::init())
|
||||||
.manage(AppConnectionState::default())
|
.manage(AppConnectionState::default())
|
||||||
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
|
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
|
||||||
// This callback runs when a second instance tries to start
|
// This callback runs when a second instance tries to start
|
||||||
@@ -78,6 +90,29 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let app_handle = app.handle();
|
||||||
|
// On macOS the plugin registers schemes via bundle metadata, so runtime registration is required only on Windows/Linux
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||||
|
if let Err(err) = app_handle.deep_link().register_all() {
|
||||||
|
add_log(format!("⚠️ Failed to register deep link handler: {}", err));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(Some(urls)) = app_handle.deep_link().get_current() {
|
||||||
|
let initial_handle = app_handle.clone();
|
||||||
|
for url in urls {
|
||||||
|
dispatch_deep_link(&initial_handle, url.as_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let event_app_handle = app_handle.clone();
|
||||||
|
app_handle.deep_link().on_open_url(move |event| {
|
||||||
|
for url in event.urls() {
|
||||||
|
dispatch_deep_link(&event_app_handle, url.as_str());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Start backend immediately, non-blocking
|
// Start backend immediately, non-blocking
|
||||||
let app_handle = app.handle().clone();
|
let app_handle = app.handle().clone();
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,13 @@
|
|||||||
},
|
},
|
||||||
"fs": {
|
"fs": {
|
||||||
"requireLiteralLeadingDot": false
|
"requireLiteralLeadingDot": false
|
||||||
|
},
|
||||||
|
"deep-link": {
|
||||||
|
"desktop": {
|
||||||
|
"schemes": [
|
||||||
|
"stirlingpdf"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface SaaSLoginScreenProps {
|
|||||||
onLogin: (username: string, password: string) => Promise<void>;
|
onLogin: (username: string, password: string) => Promise<void>;
|
||||||
onOAuthSuccess: (userInfo: UserInfo) => Promise<void>;
|
onOAuthSuccess: (userInfo: UserInfo) => Promise<void>;
|
||||||
onSelfHostedClick: () => void;
|
onSelfHostedClick: () => void;
|
||||||
|
onSwitchToSignup: () => void;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
@@ -23,6 +24,7 @@ export const SaaSLoginScreen: React.FC<SaaSLoginScreenProps> = ({
|
|||||||
onLogin,
|
onLogin,
|
||||||
onOAuthSuccess,
|
onOAuthSuccess,
|
||||||
onSelfHostedClick,
|
onSelfHostedClick,
|
||||||
|
onSwitchToSignup,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -89,6 +91,20 @@ export const SaaSLoginScreen: React.FC<SaaSLoginScreenProps> = ({
|
|||||||
submitButtonText={t('setup.login.submit', 'Login')}
|
submitButtonText={t('setup.login.submit', 'Login')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="navigation-link-container" style={{ marginTop: '0.5rem', textAlign: 'right' }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setValidationError(null);
|
||||||
|
onSwitchToSignup();
|
||||||
|
}}
|
||||||
|
className="navigation-link-button"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{t('signup.signUp', 'Sign Up')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<SelfHostedLink onClick={onSelfHostedClick} disabled={loading} />
|
<SelfHostedLink onClick={onSelfHostedClick} disabled={loading} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import LoginHeader from '@app/routes/login/LoginHeader';
|
||||||
|
import ErrorMessage from '@app/routes/login/ErrorMessage';
|
||||||
|
import SignupForm from '@app/routes/signup/SignupForm';
|
||||||
|
import { useSignupFormValidation, SignupFieldErrors } from '@app/routes/signup/SignupFormValidation';
|
||||||
|
import { authService } from '@app/services/authService';
|
||||||
|
import '@app/routes/authShared/auth.css';
|
||||||
|
|
||||||
|
interface SaaSSignupScreenProps {
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
onLogin: (username: string, password: string) => Promise<void>;
|
||||||
|
onSwitchToLogin: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SaaSSignupScreen: React.FC<SaaSSignupScreenProps> = ({
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
onLogin: _onLogin,
|
||||||
|
onSwitchToLogin: _onSwitchToLogin,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
const [validationError, setValidationError] = useState<string | null>(null);
|
||||||
|
const [signupFieldErrors, setSignupFieldErrors] = useState<SignupFieldErrors>({});
|
||||||
|
const [signupSuccessMessage, setSignupSuccessMessage] = useState<string | null>(null);
|
||||||
|
const [isSignupSubmitting, setIsSignupSubmitting] = useState(false);
|
||||||
|
const { validateSignupForm } = useSignupFormValidation();
|
||||||
|
|
||||||
|
const displayError = error || validationError;
|
||||||
|
|
||||||
|
const handleSignupSubmit = async () => {
|
||||||
|
setValidationError(null);
|
||||||
|
setSignupSuccessMessage(null);
|
||||||
|
setSignupFieldErrors({});
|
||||||
|
|
||||||
|
const validation = validateSignupForm(email, password, confirmPassword);
|
||||||
|
if (!validation.isValid) {
|
||||||
|
setValidationError(validation.error);
|
||||||
|
setSignupFieldErrors(validation.fieldErrors || {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsSignupSubmitting(true);
|
||||||
|
await authService.signUpSaas(email.trim(), password);
|
||||||
|
setSignupSuccessMessage(t('signup.checkEmailConfirmation', 'Check your email for a confirmation link to complete your registration.'));
|
||||||
|
setSignupFieldErrors({});
|
||||||
|
setValidationError(null);
|
||||||
|
} catch (err) {
|
||||||
|
setSignupSuccessMessage(null);
|
||||||
|
const message = err instanceof Error ? err.message : t('signup.unexpectedError', { message: 'Unknown error' });
|
||||||
|
setValidationError(message);
|
||||||
|
} finally {
|
||||||
|
setIsSignupSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<LoginHeader
|
||||||
|
title={t('signup.title', 'Create an account')}
|
||||||
|
subtitle={t('signup.subtitle', 'Join Stirling PDF')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ErrorMessage error={displayError} />
|
||||||
|
{signupSuccessMessage && (
|
||||||
|
<div className="success-message">
|
||||||
|
<p className="success-message-text">{signupSuccessMessage}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SignupForm
|
||||||
|
email={email}
|
||||||
|
password={password}
|
||||||
|
confirmPassword={confirmPassword}
|
||||||
|
setEmail={(value) => {
|
||||||
|
setEmail(value);
|
||||||
|
setValidationError(null);
|
||||||
|
setSignupFieldErrors({});
|
||||||
|
}}
|
||||||
|
setPassword={(value) => {
|
||||||
|
setPassword(value);
|
||||||
|
setValidationError(null);
|
||||||
|
setSignupFieldErrors({});
|
||||||
|
}}
|
||||||
|
setConfirmPassword={(value) => {
|
||||||
|
setConfirmPassword(value);
|
||||||
|
setValidationError(null);
|
||||||
|
setSignupFieldErrors({});
|
||||||
|
}}
|
||||||
|
onSubmit={handleSignupSubmit}
|
||||||
|
isSubmitting={loading || isSignupSubmitting}
|
||||||
|
fieldErrors={signupFieldErrors}
|
||||||
|
showName={false}
|
||||||
|
showTerms={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,16 +2,20 @@ import React, { useState } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { DesktopAuthLayout } from '@app/components/SetupWizard/DesktopAuthLayout';
|
import { DesktopAuthLayout } from '@app/components/SetupWizard/DesktopAuthLayout';
|
||||||
import { SaaSLoginScreen } from '@app/components/SetupWizard/SaaSLoginScreen';
|
import { SaaSLoginScreen } from '@app/components/SetupWizard/SaaSLoginScreen';
|
||||||
|
import { SaaSSignupScreen } from '@app/components/SetupWizard/SaaSSignupScreen';
|
||||||
import { ServerSelectionScreen } from '@app/components/SetupWizard/ServerSelectionScreen';
|
import { ServerSelectionScreen } from '@app/components/SetupWizard/ServerSelectionScreen';
|
||||||
import { SelfHostedLoginScreen } from '@app/components/SetupWizard/SelfHostedLoginScreen';
|
import { SelfHostedLoginScreen } from '@app/components/SetupWizard/SelfHostedLoginScreen';
|
||||||
import { ServerConfig, connectionModeService } from '@app/services/connectionModeService';
|
import { ServerConfig, connectionModeService } from '@app/services/connectionModeService';
|
||||||
import { authService, UserInfo } from '@app/services/authService';
|
import { authService, UserInfo } from '@app/services/authService';
|
||||||
import { tauriBackendService } from '@app/services/tauriBackendService';
|
import { tauriBackendService } from '@app/services/tauriBackendService';
|
||||||
import { STIRLING_SAAS_URL } from '@desktop/constants/connection';
|
import { STIRLING_SAAS_URL } from '@desktop/constants/connection';
|
||||||
|
import { listen } from '@tauri-apps/api/event';
|
||||||
|
import { useEffect } from 'react';
|
||||||
import '@app/routes/authShared/auth.css';
|
import '@app/routes/authShared/auth.css';
|
||||||
|
|
||||||
enum SetupStep {
|
enum SetupStep {
|
||||||
SaaSLogin,
|
SaaSLogin,
|
||||||
|
SaaSSignup,
|
||||||
ServerSelection,
|
ServerSelection,
|
||||||
SelfHostedLogin,
|
SelfHostedLogin,
|
||||||
}
|
}
|
||||||
@@ -80,6 +84,16 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
|
|||||||
setActiveStep(SetupStep.ServerSelection);
|
setActiveStep(SetupStep.ServerSelection);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSwitchToSignup = () => {
|
||||||
|
setError(null);
|
||||||
|
setActiveStep(SetupStep.SaaSSignup);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSwitchToLogin = () => {
|
||||||
|
setError(null);
|
||||||
|
setActiveStep(SetupStep.SaaSLogin);
|
||||||
|
};
|
||||||
|
|
||||||
const handleServerSelection = (config: ServerConfig) => {
|
const handleServerSelection = (config: ServerConfig) => {
|
||||||
setServerConfig(config);
|
setServerConfig(config);
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -128,6 +142,48 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribePromise = listen<string>('deep-link', async (event) => {
|
||||||
|
const url = event.payload;
|
||||||
|
if (!url) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
|
||||||
|
// Supabase sends tokens in the URL hash
|
||||||
|
const hash = parsed.hash.replace(/^#/, '');
|
||||||
|
const params = new URLSearchParams(hash);
|
||||||
|
const accessToken = params.get('access_token');
|
||||||
|
const type = params.get('type') || parsed.searchParams.get('type');
|
||||||
|
|
||||||
|
if (!type || (type !== 'signup' && type !== 'recovery' && type !== 'magiclink')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
console.error('[SetupWizard] Deep link missing access_token');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
await authService.completeSupabaseSession(accessToken, serverConfig?.url || STIRLING_SAAS_URL);
|
||||||
|
await connectionModeService.switchToSaaS(serverConfig?.url || STIRLING_SAAS_URL);
|
||||||
|
tauriBackendService.startBackend().catch(console.error);
|
||||||
|
onComplete();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[SetupWizard] Failed to handle deep link', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to complete signup');
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
void unsubscribePromise.then((unsub) => unsub());
|
||||||
|
};
|
||||||
|
}, [onComplete, serverConfig?.url]);
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
setError(null);
|
setError(null);
|
||||||
if (activeStep === SetupStep.SelfHostedLogin) {
|
if (activeStep === SetupStep.SelfHostedLogin) {
|
||||||
@@ -135,6 +191,8 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
|
|||||||
} else if (activeStep === SetupStep.ServerSelection) {
|
} else if (activeStep === SetupStep.ServerSelection) {
|
||||||
setActiveStep(SetupStep.SaaSLogin);
|
setActiveStep(SetupStep.SaaSLogin);
|
||||||
setServerConfig({ url: STIRLING_SAAS_URL });
|
setServerConfig({ url: STIRLING_SAAS_URL });
|
||||||
|
} else if (activeStep === SetupStep.SaaSSignup) {
|
||||||
|
setActiveStep(SetupStep.SaaSLogin);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -147,11 +205,21 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
|
|||||||
onLogin={handleSaaSLogin}
|
onLogin={handleSaaSLogin}
|
||||||
onOAuthSuccess={handleSaaSLoginOAuth}
|
onOAuthSuccess={handleSaaSLoginOAuth}
|
||||||
onSelfHostedClick={handleSelfHostedClick}
|
onSelfHostedClick={handleSelfHostedClick}
|
||||||
|
onSwitchToSignup={handleSwitchToSignup}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
error={error}
|
error={error}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{activeStep === SetupStep.SaaSSignup && (
|
||||||
|
<SaaSSignupScreen
|
||||||
|
loading={loading}
|
||||||
|
error={error}
|
||||||
|
onLogin={handleSaaSLogin}
|
||||||
|
onSwitchToLogin={handleSwitchToLogin}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{activeStep === SetupStep.ServerSelection && (
|
{activeStep === SetupStep.ServerSelection && (
|
||||||
<ServerSelectionScreen
|
<ServerSelectionScreen
|
||||||
onSelect={handleServerSelection}
|
onSelect={handleServerSelection}
|
||||||
|
|||||||
@@ -6,6 +6,12 @@
|
|||||||
// The SaaS authentication server
|
// The SaaS authentication server
|
||||||
export const STIRLING_SAAS_URL: string = import.meta.env.VITE_SAAS_SERVER_URL || '';
|
export const STIRLING_SAAS_URL: string = import.meta.env.VITE_SAAS_SERVER_URL || '';
|
||||||
|
|
||||||
|
// SaaS signup URL for creating new cloud accounts
|
||||||
|
export const STIRLING_SAAS_SIGNUP_URL: string = import.meta.env.VITE_SAAS_SIGNUP_URL || '';
|
||||||
|
|
||||||
// Supabase publishable key from environment variable
|
// Supabase publishable key from environment variable
|
||||||
// Used for SaaS authentication
|
// Used for SaaS authentication
|
||||||
export const SUPABASE_KEY: string = import.meta.env.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY || 'sb_publishable_UHz2SVRF5mvdrPHWkRteyA_yNlZTkYb';
|
export const SUPABASE_KEY: string = import.meta.env.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY || 'sb_publishable_UHz2SVRF5mvdrPHWkRteyA_yNlZTkYb';
|
||||||
|
|
||||||
|
// Desktop deep link callback for Supabase email confirmations
|
||||||
|
export const DESKTOP_DEEP_LINK_CALLBACK = 'stirlingpdf://auth/callback';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { STIRLING_SAAS_URL, SUPABASE_KEY } from '@app/constants/connection';
|
import { DESKTOP_DEEP_LINK_CALLBACK, STIRLING_SAAS_URL, SUPABASE_KEY } from '@app/constants/connection';
|
||||||
|
|
||||||
export interface UserInfo {
|
export interface UserInfo {
|
||||||
username: string;
|
username: string;
|
||||||
@@ -131,6 +131,67 @@ export class AuthService {
|
|||||||
this.notifyListeners();
|
this.notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async completeSupabaseSession(accessToken: string, serverUrl: string): Promise<UserInfo> {
|
||||||
|
if (!accessToken || !accessToken.trim()) {
|
||||||
|
throw new Error('Invalid access token');
|
||||||
|
}
|
||||||
|
if (!SUPABASE_KEY) {
|
||||||
|
throw new Error('VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY is not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.saveTokenEverywhere(accessToken);
|
||||||
|
|
||||||
|
const userInfo = await this.fetchSupabaseUserInfo(serverUrl, accessToken);
|
||||||
|
|
||||||
|
await invoke('save_user_info', {
|
||||||
|
username: userInfo.username,
|
||||||
|
email: userInfo.email || null,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setAuthStatus('authenticated', userInfo);
|
||||||
|
return userInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
async signUpSaas(email: string, password: string): Promise<void> {
|
||||||
|
if (!STIRLING_SAAS_URL) {
|
||||||
|
throw new Error('VITE_SAAS_SERVER_URL is not configured');
|
||||||
|
}
|
||||||
|
if (!SUPABASE_KEY) {
|
||||||
|
throw new Error('VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY is not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectParam = encodeURIComponent(DESKTOP_DEEP_LINK_CALLBACK);
|
||||||
|
const signupUrl = `${STIRLING_SAAS_URL.replace(/\/$/, '')}/auth/v1/signup?redirect_to=${redirectParam}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
signupUrl,
|
||||||
|
{ email, password, email_redirect_to: DESKTOP_DEEP_LINK_CALLBACK },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json;charset=UTF-8',
|
||||||
|
apikey: SUPABASE_KEY,
|
||||||
|
Authorization: `Bearer ${SUPABASE_KEY}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status >= 400) {
|
||||||
|
throw new Error('Sign up failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const message =
|
||||||
|
error.response?.data?.error_description ||
|
||||||
|
error.response?.data?.msg ||
|
||||||
|
error.response?.data?.message ||
|
||||||
|
error.message;
|
||||||
|
throw new Error(message || 'Sign up failed');
|
||||||
|
}
|
||||||
|
throw error instanceof Error ? error : new Error('Sign up failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async login(serverUrl: string, username: string, password: string): Promise<UserInfo> {
|
async login(serverUrl: string, username: string, password: string): Promise<UserInfo> {
|
||||||
try {
|
try {
|
||||||
console.log('Logging in to:', serverUrl);
|
console.log('Logging in to:', serverUrl);
|
||||||
|
|||||||
Reference in New Issue
Block a user