mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-09-13 04:08:22 +02:00
## Move editor under `frontend/editor/`
Pure restructure: `frontend/` becomes the workspace, `frontend/editor/`
holds
the PDF editor. 1775 file renames + 40 wiring edits. No logic changes.
### Why
`frontend/` is currently the editor — its `src/`, `public/`,
`src-tauri/`,
config files all sit at the root. Promoting `frontend/` to a
workspace and putting the editor in a sibling folder leaves room for
future
apps to drop in alongside it, sharing one `package.json` /
`node_modules` /
lint config / Storybook.
### What moves
frontend/
├── editor/ ← NEW: everything editor-specific
│ ├── src/ ← was frontend/src/
│ ├── public/ ← was frontend/public/
│ ├── src-tauri/ ← was frontend/src-tauri/
│ ├── index.html, vite.config.ts, vitest.config.ts, playwright.config.ts
│ ├── tsconfig*.json, tailwind.config.js, postcss.config.js
│ ├── scripts/
│ ├── .env, .env.desktop, .env.saas
│ └── DeveloperGuide.md
├── package.json, package-lock.json, node_modules/ ← workspace install
├── eslint.config.mjs, .prettierrc, .prettierignore ← shared tooling
├── .gitignore
└── README.md
### Wiring edits (40 files)
- `.taskfiles/frontend.yml`, `desktop.yml`, `e2e.yml`
- `build.gradle`, `app/core/build.gradle`
- `eslint.config.mjs`, `frontend/package.json`, `.gitignore`,
`.prettierignore`
- `docker/frontend/Dockerfile`
- 8 `.github/workflows/*.yml`, plus `.github/dependabot.yml`,
`.github/config/.files.yaml`, `.github/labeler-config-srvaroa.yml`
- `scripts/translations/**`
- Docs: `AGENTS.md`, `CLAUDE.md`, `ADDING_TOOLS.md`,
`DeveloperGuide.md`,
`WINDOWS_SIGNING.md`, `devGuide/HowToAddNewLanguage.md`,
`frontend/README.md`,
`frontend/editor/DeveloperGuide.md`
Plus 3 renamed + edited: `editor/vite.config.ts` (env path +
node_modules
walk-up), `editor/scripts/setup-env.mts` (renamed from `.ts` for
`import.meta.url`), `editor/scripts/build-provisioner.mjs` (resolve
src-tauri
relative to script).
### Verification
| Check | Result |
|---|---|
| `task frontend:typecheck:all` (6 variants) | exit 0 |
| `task frontend:lint` (eslint + dpdm) | exit 0 |
| `task frontend:format:check` | exit 0 |
| `task frontend:test` | 657 tests pass, 50 files |
| `task frontend:build:{core,proprietary,saas,desktop,prototypes}` | all
green |
| `task desktop:build` | full Tauri pipeline →
`Stirling-PDF_2.11.0_x64_en-US.msi` |
| `playwright test --list --project=stubbed` | 172 tests discovered |
`task desktop:build` exercises the heaviest path — Rust + WiX + MSI
bundle
against the moved `editor/src-tauri/`. If anything in the restructure
was
wrong it wouldn't have built.
### Test plan
- [ ] `frontend-validation.yml` green
- [ ] `e2e-stubbed.yml` green
- [ ] `tauri-build.yml` green on at least one platform
- [ ] `check_toml.yml` runs on a translation-touching PR
---------
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
190 lines
5.7 KiB
TypeScript
190 lines
5.7 KiB
TypeScript
import React, { useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { authService, UserInfo } from "@app/services/authService";
|
|
import { buildOAuthCallbackHtml } from "@app/utils/oauthCallbackHtml";
|
|
import { BASE_PATH } from "@app/constants/app";
|
|
import { STIRLING_SAAS_URL } from "@app/constants/connection";
|
|
import "@app/components/SetupWizard/desktopOAuth.css";
|
|
|
|
type KnownProviderId =
|
|
| "google"
|
|
| "github"
|
|
| "keycloak"
|
|
| "azure"
|
|
| "apple"
|
|
| "oidc";
|
|
export type OAuthProviderId = KnownProviderId | string;
|
|
|
|
export interface DesktopSSOProvider {
|
|
id: OAuthProviderId;
|
|
path?: string;
|
|
label?: string;
|
|
}
|
|
|
|
interface DesktopOAuthButtonsProps {
|
|
onOAuthSuccess: (userInfo: UserInfo) => Promise<void>;
|
|
onError: (error: string) => void;
|
|
isDisabled: boolean;
|
|
serverUrl: string;
|
|
providers: DesktopSSOProvider[];
|
|
mode?: "saas" | "selfHosted";
|
|
}
|
|
|
|
export const DesktopOAuthButtons: React.FC<DesktopOAuthButtonsProps> = ({
|
|
onOAuthSuccess,
|
|
onError,
|
|
isDisabled,
|
|
serverUrl,
|
|
providers,
|
|
mode = "saas",
|
|
}) => {
|
|
const { t } = useTranslation();
|
|
const [oauthLoading, setOauthLoading] = useState(false);
|
|
|
|
const handleOAuthLogin = async (provider: DesktopSSOProvider) => {
|
|
// Prevent concurrent OAuth attempts
|
|
if (oauthLoading || isDisabled) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setOauthLoading(true);
|
|
|
|
// Build callback page HTML with translations and dark mode support
|
|
const successHtml = buildOAuthCallbackHtml({
|
|
title: t("oauth.success.title", "Authentication Successful"),
|
|
message: t(
|
|
"oauth.success.message",
|
|
"You can close this window and return to Stirling PDF.",
|
|
),
|
|
isError: false,
|
|
});
|
|
|
|
const errorHtml = buildOAuthCallbackHtml({
|
|
title: t("oauth.error.title", "Authentication Failed"),
|
|
message: t(
|
|
"oauth.error.message",
|
|
"Authentication was not successful. You can close this window and try again.",
|
|
),
|
|
isError: true,
|
|
errorPlaceholder: true, // {error} will be replaced by Rust
|
|
});
|
|
|
|
const normalizedServer = serverUrl.replace(/\/+$/, "");
|
|
const usingSupabaseFlow =
|
|
mode === "saas" ||
|
|
normalizedServer === STIRLING_SAAS_URL.replace(/\/+$/, "");
|
|
const userInfo = usingSupabaseFlow
|
|
? await authService.loginWithOAuth(
|
|
provider.id,
|
|
serverUrl,
|
|
successHtml,
|
|
errorHtml,
|
|
)
|
|
: await authService.loginWithSelfHostedOAuth(
|
|
provider.path || provider.id,
|
|
serverUrl,
|
|
);
|
|
|
|
// Call the onOAuthSuccess callback to complete setup
|
|
await onOAuthSuccess(userInfo);
|
|
} catch (error) {
|
|
console.error("OAuth login failed:", error);
|
|
|
|
const errorMessage =
|
|
error instanceof Error
|
|
? error.message
|
|
: t(
|
|
"setup.login.error.oauthFailed",
|
|
"OAuth login failed. Please try again.",
|
|
);
|
|
|
|
onError(errorMessage);
|
|
setOauthLoading(false);
|
|
}
|
|
};
|
|
|
|
const providerConfig: Record<
|
|
KnownProviderId,
|
|
{ label: string; file: string }
|
|
> = {
|
|
google: { label: "Google", file: "google.svg" },
|
|
github: { label: "GitHub", file: "github.svg" },
|
|
keycloak: { label: "Keycloak", file: "keycloak.svg" },
|
|
azure: { label: "Microsoft", file: "microsoft.svg" },
|
|
apple: { label: "Apple", file: "apple.svg" },
|
|
oidc: { label: "OpenID", file: "oidc.svg" },
|
|
};
|
|
const isKnownProvider = (id: OAuthProviderId): id is KnownProviderId =>
|
|
(id as KnownProviderId) in providerConfig;
|
|
const GENERIC_PROVIDER_ICON = "oidc.svg";
|
|
|
|
console.log("[DesktopOAuthButtons] Received providers:", providers);
|
|
console.log("[DesktopOAuthButtons] Mode:", mode, "Server URL:", serverUrl);
|
|
|
|
if (providers.length === 0) {
|
|
console.warn(
|
|
"[DesktopOAuthButtons] No providers to display, returning null",
|
|
);
|
|
return null;
|
|
}
|
|
|
|
// Desktop always uses its own styling classes (independent of web)
|
|
return (
|
|
<div className="oauth-container-vertical-desktop">
|
|
{providers
|
|
.filter(
|
|
(providerConfigEntry) =>
|
|
providerConfigEntry && providerConfigEntry.id,
|
|
)
|
|
.map((providerEntry) => {
|
|
const iconConfig = isKnownProvider(providerEntry.id)
|
|
? providerConfig[providerEntry.id]
|
|
: undefined;
|
|
const label =
|
|
providerEntry.label ||
|
|
iconConfig?.label ||
|
|
(providerEntry.id
|
|
? providerEntry.id.charAt(0).toUpperCase() +
|
|
providerEntry.id.slice(1)
|
|
: t("setup.login.sso", "Single Sign-On"));
|
|
return (
|
|
<button
|
|
key={providerEntry.id}
|
|
onClick={() => handleOAuthLogin(providerEntry)}
|
|
disabled={isDisabled || oauthLoading}
|
|
className="oauth-button-vertical-desktop"
|
|
title={label}
|
|
>
|
|
<span className="oauth-button-left-desktop">
|
|
<span className="oauth-icon-wrapper-desktop">
|
|
<img
|
|
src={`${BASE_PATH}/Login/${iconConfig?.file || GENERIC_PROVIDER_ICON}`}
|
|
alt={label}
|
|
className="oauth-icon-tiny-desktop"
|
|
/>
|
|
</span>
|
|
<span className="oauth-button-text-desktop">{label}</span>
|
|
</span>
|
|
</button>
|
|
);
|
|
})}
|
|
{oauthLoading && (
|
|
<p
|
|
style={{
|
|
margin: "0.5rem 0",
|
|
fontSize: "0.875rem",
|
|
color: "#6b7280",
|
|
textAlign: "center",
|
|
}}
|
|
>
|
|
{t(
|
|
"setup.login.oauthPending",
|
|
"Opening browser for authentication...",
|
|
)}
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|