mirror of
https://github.com/immich-app/yucca.git
synced 2026-09-30 13:33:00 +08:00
chore: general clean up (#134)
This commit is contained in:
+19
-65
@@ -1,68 +1,22 @@
|
||||
#
|
||||
# Restic API
|
||||
#
|
||||
# 1password
|
||||
OP_ACCOUNT="team-futo.1password.com"
|
||||
|
||||
# RESTIC_API_PORT=
|
||||
# OIDC: Zitadel staging
|
||||
# WEB_PORT="5173"
|
||||
# OIDC_ISSUER="https://external-dev-gkhk8b.us1.zitadel.cloud"
|
||||
# OIDC_CLIENT_ID="op://yucca_tf_dev/CUSTOMER_ZITADEL_OAUTH_CLIENT_ID_DEV_TEST/password"
|
||||
# OIDC_CLIENT_SECRET="op://yucca_tf_dev/CUSTOMER_ZITADEL_OAUTH_CLIENT_SECRET_DEV_TEST/password"
|
||||
# OIDC_REDIRECT_URI="http://localhost:5173/api/auth/oidc/callback"
|
||||
# OIDC_LOGOUT_REDIRECT_URI="http://localhost:5173"
|
||||
|
||||
### JWT Verify
|
||||
# OIDC: Zitadel testing
|
||||
# OIDC_DEVICE_ISSUER="https://zitadel.raccoon.sh"
|
||||
# OIDC_DEVICE_CLIENT_ID="370711051198529553"
|
||||
# OIDC_CLIENT_ID="368822166944284767"
|
||||
# OIDC_CLIENT_SECRET=""
|
||||
|
||||
# JWT_PUBLIC_KEY=
|
||||
|
||||
### S3
|
||||
|
||||
# S3_ACCESS_KEY_ID=
|
||||
# S3_SECRET_ACCESS_KEY=
|
||||
# S3_REGION=
|
||||
# S3_ENDPOINT=
|
||||
# S3_FORCE_PATH_STYLE=
|
||||
|
||||
|
||||
|
||||
#
|
||||
# Yucca API
|
||||
#
|
||||
|
||||
# YUCCA_API_PORT=
|
||||
|
||||
### JWT Signing
|
||||
|
||||
# Generate with:
|
||||
# openssl ecparam -name prime256v1 -genkey -noout -out priv.pem
|
||||
# openssl pkcs8 -topk8 -nocrypt -in priv.pem -out priv-pkcs8.pem
|
||||
# openssl ec -in priv.pem -pubout -out pub.pem
|
||||
|
||||
# JWT_PRIVATE_KEY=
|
||||
# JWT_EXPIRES_IN=
|
||||
|
||||
### Postgres DB
|
||||
|
||||
# POSTGRES_HOST=
|
||||
# POSTGRES_PORT=
|
||||
# POSTGRES_USERNAME=
|
||||
# POSTGRES_PASSWORD=
|
||||
# POSTGRES_DATABASE=
|
||||
# POSTGRES_SSL=
|
||||
|
||||
### Customer OIDC configuration
|
||||
OIDC_ISSUER="https://external-dev-gkhk8b.us1.zitadel.cloud"
|
||||
OIDC_CLIENT_ID="op://yucca_tf_dev/CUSTOMER_ZITADEL_OAUTH_CLIENT_ID_DEV_TEST/password"
|
||||
OIDC_CLIENT_SECRET="op://yucca_tf_dev/CUSTOMER_ZITADEL_OAUTH_CLIENT_SECRET_DEV_TEST/password"
|
||||
# OIDC_ALLOW_INSECURE=
|
||||
# OIDC_REQUIRE_PKCE=
|
||||
# OIDC_REDIRECT_URI=
|
||||
# OIDC_LOGOUT_REDIRECT_URI=
|
||||
# OIDC_SCOPE=
|
||||
|
||||
### ... temp
|
||||
# RESTIC_API_HOST=
|
||||
# RESTIC_API_PORT=
|
||||
# RESTIC_ENDPOINT=
|
||||
|
||||
|
||||
|
||||
#
|
||||
# Development only
|
||||
#
|
||||
|
||||
# WEB_PORT=
|
||||
# ORCHESTRATOR_WEB_PORT=
|
||||
# Polar
|
||||
# POLAR_ACCESS_TOKEN=
|
||||
# POLAR_WEBHOOK_SECRET=
|
||||
# POLAR_PRODUCT_ID=d9bc9054-eb91-46c9-846e-dd5bbb8f0041
|
||||
# POLAR_APPLY_DISCOUNT_ID=391d9f04-889a-44ff-95f1-98cee8a5b0db
|
||||
|
||||
+8
-2
@@ -35,8 +35,14 @@ REPO_ROOT="$PWD"
|
||||
|
||||
# ─── Configuration ──────────────────────────────────────────────────────────────
|
||||
|
||||
API_PORT="${RESTIC_API_PORT:-3010}"
|
||||
API_HOST="${RESTIC_API_HOST:-localhost}"
|
||||
RESTIC_ENDPOINT="${RESTIC_ENDPOINT:-http://localhost:3010}"
|
||||
# bench spawns + port-manages the backend, so split the endpoint back into host/port
|
||||
_endpoint_hostport="${RESTIC_ENDPOINT#*://}"
|
||||
API_HOST="${_endpoint_hostport%%:*}"
|
||||
API_PORT="${_endpoint_hostport##*:}"
|
||||
API_PORT="${API_PORT%%/*}"
|
||||
# the restic-api / michael server bench launches binds RESTIC_API_PORT
|
||||
export RESTIC_API_PORT="$API_PORT"
|
||||
export JWT_SECRET="${JWT_SECRET:-cca13c34b450a77c1d4b9ecd25dff6aebc6d7417afdb31864f5943c59abd03a1}"
|
||||
|
||||
BACKEND=""
|
||||
|
||||
@@ -26,5 +26,4 @@ export OIDC_DEVICE_ISSUER=${OIDC_DEVICE_ISSUER:-http://localhost:8092}
|
||||
export OIDC_DEVICE_CLIENT_ID=${OIDC_DEVICE_CLIENT_ID:-device client ID}
|
||||
export OIDC_DEVICE_CLIENT_SECRET=${OIDC_DEVICE_CLIENT_SECRET:-client secret}
|
||||
|
||||
# ... temp
|
||||
export RESTIC_API_PORT=${RESTIC_API_PORT:-3010}
|
||||
export RESTIC_ENDPOINT=${RESTIC_ENDPOINT:-http://localhost:3010}
|
||||
|
||||
@@ -65,10 +65,8 @@ env:
|
||||
value: "device client ID"
|
||||
- name: OIDC_DEVICE_ALLOW_INSECURE
|
||||
value: "true"
|
||||
- name: RESTIC_API_HOST
|
||||
value: yucca-michael
|
||||
- name: RESTIC_API_PORT
|
||||
value: "3010"
|
||||
- name: RESTIC_ENDPOINT
|
||||
value: http://yucca-michael:3010
|
||||
- name: OTEL_METRICS
|
||||
value: http://victoria-metrics:8428/opentelemetry/v1/metrics
|
||||
- name: OTEL_LOGGING
|
||||
|
||||
@@ -11,7 +11,10 @@
|
||||
const { children } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
|
||||
<svelte:head>
|
||||
<title>FUTO Backups</title>
|
||||
<link rel="icon" href={favicon} />
|
||||
</svelte:head>
|
||||
<YuccaContext>
|
||||
{@render children()}
|
||||
</YuccaContext>
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "@immich/ui";
|
||||
import { t } from "svelte-i18n-lingui";
|
||||
import { defaults } from "@futo-org/backups-api-client";
|
||||
|
||||
import { locale } from "svelte-i18n-lingui";
|
||||
|
||||
async function setLocale(lang: "en" | "ja") {
|
||||
const { messages } = await import(`../locales/${lang}.ts`);
|
||||
locale.set(lang, messages);
|
||||
}
|
||||
import { Button, Card, CardBody, Heading, VStack } from "@immich/ui";
|
||||
import { t } from "svelte-i18n-lingui";
|
||||
</script>
|
||||
|
||||
<main class="p-4">
|
||||
<Button
|
||||
onclick={() => (location.href = defaults.baseUrl + "api/auth/oidc/login")}
|
||||
>{$t`Login`}</Button
|
||||
>
|
||||
<svelte:head><title>{$t`Login`} · FUTO Backups</title></svelte:head>
|
||||
|
||||
<Button onclick={() => setLocale("en")}>Switch to English</Button>
|
||||
<Button onclick={() => setLocale("ja")}>Switch to Test</Button>
|
||||
<main class="flex min-h-screen items-center justify-center p-4">
|
||||
<VStack gap={6} class="w-full max-w-sm items-center">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<VStack>
|
||||
<Heading>FUTO Backups</Heading>
|
||||
<Button
|
||||
onclick={() =>
|
||||
(location.href = defaults.baseUrl + "api/auth/oidc/login")}
|
||||
>{$t`Login`}</Button
|
||||
>
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</VStack>
|
||||
</main>
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
const { data } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Dashboard · FUTO Backups</title></svelte:head>
|
||||
|
||||
<Dashboard
|
||||
initialData={data.initialData.repositories}
|
||||
onNavigate={(route) => goto(`/dashboard/${route}`)}
|
||||
|
||||
@@ -4,4 +4,6 @@
|
||||
const { data } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Backups · FUTO Backups</title></svelte:head>
|
||||
|
||||
<BackupsList initialData={data.initialData} />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@import "tailwindcss";
|
||||
@import "@immich/ui/theme/default.css";
|
||||
@source "../../node_modules/@immich/ui";
|
||||
@source "../../node_modules/@futo-org/backups-orchestrator-ui";
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
} from "@immich/ui";
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Grant access · FUTO Backups</title></svelte:head>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Grant access to app?</CardTitle>
|
||||
|
||||
@@ -1,48 +1,40 @@
|
||||
import type { StringValue } from 'ms';
|
||||
import { z } from 'zod';
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
NODE_ENV: z.enum(['development', 'production', 'test', 'provision']).default('development'),
|
||||
const schema = z.object({
|
||||
NODE_ENV: z.enum(['development', 'production', 'test', 'provision']).default('development'),
|
||||
|
||||
YUCCA_API_PORT: z.coerce.number().min(1000),
|
||||
YUCCA_API_PORT: z.coerce.number().min(1000),
|
||||
|
||||
JWT_PRIVATE_KEY: z.string(),
|
||||
JWT_EXPIRES_IN: z
|
||||
.string()
|
||||
.regex(/^\d+\s*(ms|s|m|h|d|w|y)$/i, 'Expected a duration like "1d", "30m", "3600s"')
|
||||
.default('1d')
|
||||
.transform((value): StringValue => value as StringValue),
|
||||
JWT_PRIVATE_KEY: z.string(),
|
||||
JWT_EXPIRES_IN: z
|
||||
.string()
|
||||
.regex(/^\d+\s*(ms|s|m|h|d|w|y)$/i, 'Expected a duration like "1d", "30m", "3600s"')
|
||||
.default('1d')
|
||||
.transform((value): StringValue => value as StringValue),
|
||||
|
||||
POSTGRES_HOST: z.string(),
|
||||
POSTGRES_PORT: z.coerce.number().default(5432),
|
||||
POSTGRES_USERNAME: z.string(),
|
||||
POSTGRES_PASSWORD: z.string(),
|
||||
POSTGRES_DATABASE: z.string(),
|
||||
POSTGRES_SSL: z.union([z.enum(['require', 'allow', 'prefer', 'verify-full']), z.boolean()]).default(false),
|
||||
POSTGRES_HOST: z.string(),
|
||||
POSTGRES_PORT: z.coerce.number().default(5432),
|
||||
POSTGRES_USERNAME: z.string(),
|
||||
POSTGRES_PASSWORD: z.string(),
|
||||
POSTGRES_DATABASE: z.string(),
|
||||
POSTGRES_SSL: z.union([z.enum(['require', 'allow', 'prefer', 'verify-full']), z.boolean()]).default(false),
|
||||
|
||||
OIDC_ISSUER: z.url().transform((url) => new URL(url)),
|
||||
OIDC_CLIENT_ID: z.string(),
|
||||
OIDC_CLIENT_SECRET: z.string(),
|
||||
OIDC_ALLOW_INSECURE: z.coerce.boolean().default(false),
|
||||
OIDC_REQUIRE_PKCE: z.coerce.boolean().default(true),
|
||||
OIDC_REDIRECT_URI: z.string(),
|
||||
OIDC_LOGOUT_REDIRECT_URI: z.string(),
|
||||
OIDC_SCOPE: z.string().default('openid profile email'),
|
||||
OIDC_ISSUER: z.url().transform((url) => new URL(url)),
|
||||
OIDC_CLIENT_ID: z.string(),
|
||||
OIDC_CLIENT_SECRET: z.string(),
|
||||
OIDC_ALLOW_INSECURE: z.coerce.boolean().default(false),
|
||||
OIDC_REQUIRE_PKCE: z.coerce.boolean().default(true),
|
||||
OIDC_REDIRECT_URI: z.string(),
|
||||
OIDC_LOGOUT_REDIRECT_URI: z.string(),
|
||||
OIDC_SCOPE: z.string().default('openid profile email'),
|
||||
|
||||
OIDC_DEVICE_ISSUER: z.url().transform((url) => new URL(url)),
|
||||
OIDC_DEVICE_CLIENT_ID: z.string(),
|
||||
OIDC_DEVICE_ALLOW_INSECURE: z.coerce.boolean().default(false),
|
||||
OIDC_DEVICE_SCOPE: z.string().default('openid profile email'),
|
||||
OIDC_DEVICE_ISSUER: z.url().transform((url) => new URL(url)),
|
||||
OIDC_DEVICE_CLIENT_ID: z.string(),
|
||||
OIDC_DEVICE_ALLOW_INSECURE: z.coerce.boolean().default(false),
|
||||
OIDC_DEVICE_SCOPE: z.string().default('openid profile email'),
|
||||
|
||||
// temp.
|
||||
RESTIC_API_HOST: z.string().default('localhost'),
|
||||
RESTIC_API_PORT: z.coerce.number().min(1000),
|
||||
RESTIC_ENDPOINT: z.string().optional(),
|
||||
})
|
||||
.transform(({ RESTIC_ENDPOINT, ...options }) => ({
|
||||
...options,
|
||||
RESTIC_ENDPOINT: RESTIC_ENDPOINT ?? `http://${options.RESTIC_API_HOST}:${options.RESTIC_API_PORT}`,
|
||||
}));
|
||||
RESTIC_ENDPOINT: z.string().default('http://localhost:3010'),
|
||||
});
|
||||
|
||||
export const env = schema.parse(process.env);
|
||||
|
||||
@@ -27,7 +27,10 @@ class YuccaWellKnown {
|
||||
private data?: WellKnown;
|
||||
|
||||
async get() {
|
||||
this.data ??= await fetch(YUCCA_WELL_KNOWN).then((response) => response.json());
|
||||
this.data ??= await fetch(YUCCA_WELL_KNOWN, { signal: AbortSignal.timeout(8000) }).then((response) =>
|
||||
response.json(),
|
||||
);
|
||||
|
||||
return this.data! as WellKnown;
|
||||
}
|
||||
|
||||
|
||||
@@ -71,10 +71,16 @@ export class AuthService {
|
||||
onDisconnect: () => events.close(),
|
||||
});
|
||||
|
||||
const connectTimeout = setTimeout(() => events.close(), 10_000);
|
||||
|
||||
for await (const { data } of events) {
|
||||
clearTimeout(connectTimeout);
|
||||
const { userCode, verificationUri } = JSON.parse(data);
|
||||
|
||||
void this.waitForDeviceFlow(events, overrideEndpoint).catch(() => {});
|
||||
void this.waitForDeviceFlow(events, overrideEndpoint).catch((error) => {
|
||||
this.telemetry.submitStructuredLog('Device flow authentication errored', { error });
|
||||
this.events.publish({ type: 'DeviceFlowFailure' });
|
||||
});
|
||||
|
||||
return {
|
||||
userCode,
|
||||
|
||||
@@ -426,8 +426,11 @@ export class RepositoryService {
|
||||
|
||||
// ... in the future, this should push to all mirrors too
|
||||
}
|
||||
} catch {
|
||||
// no-op
|
||||
} catch (error) {
|
||||
this.telemetry.submitStructuredLog('Failed to update repository metrics', {
|
||||
repositoryId: id,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -671,8 +674,12 @@ export class RepositoryService {
|
||||
const snapshots = await this.restic.snapshots(endpoint, key);
|
||||
snapshots.sort((a, b) => +b.time - +a.time);
|
||||
paths = snapshots[0].paths;
|
||||
} catch {
|
||||
// no-op
|
||||
} catch (error) {
|
||||
this.telemetry.submitStructuredLog('Failed to infer backup paths during import', {
|
||||
repositoryId: id,
|
||||
backendId,
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
await this.repository.create({
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
LocalRepositoryDto,
|
||||
RepositoryBackendDto,
|
||||
} from "$lib/fetch-client";
|
||||
import { getBackendActions } from "$lib/services/backend.service";
|
||||
import {
|
||||
getBackendActions,
|
||||
useYuccaLogin,
|
||||
} from "$lib/services/backend.service";
|
||||
import { Badge, Icon, Text } from "@immich/ui";
|
||||
import { mdiCloudOutline, mdiHarddisk, mdiShieldCheckOutline } from "@mdi/js";
|
||||
import StackListItem from "../ui/StackListItem.svelte";
|
||||
@@ -30,8 +33,12 @@
|
||||
s3: "S3 Server",
|
||||
};
|
||||
|
||||
const yuccaLogin = useYuccaLogin();
|
||||
|
||||
const { LoginAgain, Reconfigure } = $derived(
|
||||
getBackendActions(repository, backend, repositoryBackend),
|
||||
getBackendActions(repository, backend, repositoryBackend, () =>
|
||||
yuccaLogin.mutate(undefined),
|
||||
),
|
||||
);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import { options } from "$lib/options";
|
||||
import {
|
||||
handleSetupLocalStorage,
|
||||
handleYuccaLogin,
|
||||
useBackendEventHandler,
|
||||
useBackends,
|
||||
useYuccaLogin,
|
||||
} from "$lib/services/backend.service";
|
||||
import { Button, HStack } from "@immich/ui";
|
||||
import StackList from "../ui/StackList.svelte";
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
const { advanced } = options;
|
||||
const query = useBackends();
|
||||
const yuccaLogin = useYuccaLogin();
|
||||
const { onBackendCreate } = useBackendEventHandler();
|
||||
|
||||
const repositoryBackends = $derived(
|
||||
@@ -61,8 +62,11 @@
|
||||
|
||||
{#if $advanced}
|
||||
<HStack>
|
||||
<Button size="small" variant="outline" onclick={() => handleYuccaLogin()}
|
||||
>Login with FUTO Backups</Button
|
||||
<Button
|
||||
size="small"
|
||||
variant="outline"
|
||||
loading={yuccaLogin.isPending}
|
||||
onclick={() => yuccaLogin.mutate(undefined)}>Login with FUTO Backups</Button
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
|
||||
+7
-1
@@ -13,6 +13,7 @@
|
||||
ModalFooter,
|
||||
Stack,
|
||||
Text,
|
||||
toastManager,
|
||||
VStack,
|
||||
} from "@immich/ui";
|
||||
import { mdiContentCopy } from "@mdi/js";
|
||||
@@ -35,6 +36,11 @@
|
||||
onClose();
|
||||
}
|
||||
|
||||
function onDeviceFlowFailure() {
|
||||
toastManager.danger("Failed to log into FUTO Backups");
|
||||
onClose();
|
||||
}
|
||||
|
||||
function onOpen() {
|
||||
window.open(verificationUri, "_blank");
|
||||
}
|
||||
@@ -44,7 +50,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<OnEvents {onBackendCreate} />
|
||||
<OnEvents {onBackendCreate} {onDeviceFlowFailure} />
|
||||
|
||||
<Modal title="Logging into FUTO Backups" icon={false} {onClose}>
|
||||
<ModalBody>
|
||||
|
||||
+12
-6
@@ -4,13 +4,14 @@
|
||||
import Suspense from "$lib/components/util/Suspense.svelte";
|
||||
import {
|
||||
handleSetupLocalStorage,
|
||||
handleYuccaLogin,
|
||||
useBackends,
|
||||
useYuccaLogin,
|
||||
} from "$lib/services/backend.service";
|
||||
import {
|
||||
Button,
|
||||
HStack,
|
||||
Icon,
|
||||
LoadingSpinner,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
@@ -37,6 +38,7 @@
|
||||
}: Props = $props();
|
||||
|
||||
const backends = useBackends();
|
||||
const yuccaLogin = useYuccaLogin();
|
||||
|
||||
const onFutoBackups = () => {
|
||||
const futoBackend = backends.data!.find(
|
||||
@@ -46,7 +48,7 @@
|
||||
if (futoBackend) {
|
||||
onSelect(futoBackend.id);
|
||||
} else {
|
||||
handleYuccaLogin(onSelect);
|
||||
yuccaLogin.mutate(onSelect);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -63,7 +65,7 @@
|
||||
|
||||
<Suspense query={backends}>
|
||||
<StackList>
|
||||
<StackListItem {disabled} onclick={onFutoBackups}>
|
||||
<StackListItem disabled={disabled || yuccaLogin.isPending} onclick={onFutoBackups}>
|
||||
{#snippet icon()}
|
||||
<Icon icon={mdiShieldCheck} size="36px" />
|
||||
{/snippet}
|
||||
@@ -74,10 +76,14 @@
|
||||
</Stack>
|
||||
|
||||
{#snippet trailing()}
|
||||
<Icon icon={mdiChevronRight} />
|
||||
{#if yuccaLogin.isPending}
|
||||
<LoadingSpinner />
|
||||
{:else}
|
||||
<Icon icon={mdiChevronRight} />
|
||||
{/if}
|
||||
{/snippet}
|
||||
</StackListItem>
|
||||
<StackListItem {disabled} onclick={onLocalBackups}>
|
||||
<StackListItem disabled={disabled || yuccaLogin.isPending} onclick={onLocalBackups}>
|
||||
{#snippet icon()}
|
||||
<Icon icon={mdiHarddisk} size="36px" />
|
||||
{/snippet}
|
||||
@@ -95,7 +101,7 @@
|
||||
{#each backends.data as backend}
|
||||
{#if backend.type === "local"}
|
||||
<StackListItem
|
||||
{disabled}
|
||||
disabled={disabled || yuccaLogin.isPending}
|
||||
onclick={() => {
|
||||
onSelect(backend.id);
|
||||
}}
|
||||
|
||||
+2
-2
@@ -53,9 +53,9 @@
|
||||
<Heading size="tiny">Your library</Heading>
|
||||
<Text>
|
||||
{#if repository.meter}
|
||||
Estimated <FormatBytes bytes={repository.meter.sizeBytes} />
|
||||
<FormatBytes bytes={repository.meter.sizeBytes} />
|
||||
{:else}
|
||||
<FormatBytes bytes={repository.metrics.sizeBytes} />
|
||||
Estimated <FormatBytes bytes={repository.metrics.sizeBytes} />
|
||||
{/if} ·
|
||||
<span class="lowercase"
|
||||
>{cronstrue.toString(schedule.cron, {
|
||||
|
||||
+12
-5
@@ -3,12 +3,13 @@
|
||||
import StackListItem from "$lib/components/ui/StackListItem.svelte";
|
||||
import {
|
||||
handleSetupLocalStorage,
|
||||
handleYuccaLogin,
|
||||
useYuccaLogin,
|
||||
} from "$lib/services/backend.service";
|
||||
import {
|
||||
Button,
|
||||
HStack,
|
||||
Icon,
|
||||
LoadingSpinner,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
@@ -25,8 +26,10 @@
|
||||
|
||||
const { restore = false, onNext, onCancel }: Props = $props();
|
||||
|
||||
const yuccaLogin = useYuccaLogin();
|
||||
|
||||
function onFutoBackups() {
|
||||
handleYuccaLogin(onNext);
|
||||
yuccaLogin.mutate(onNext);
|
||||
}
|
||||
|
||||
function onLocalBackups() {
|
||||
@@ -44,7 +47,7 @@
|
||||
>
|
||||
<ModalBody>
|
||||
<StackList>
|
||||
<StackListItem onclick={onFutoBackups}>
|
||||
<StackListItem disabled={yuccaLogin.isPending} onclick={onFutoBackups}>
|
||||
{#snippet icon()}
|
||||
<Icon icon={mdiShieldCheck} size="36px" />
|
||||
{/snippet}
|
||||
@@ -55,10 +58,14 @@
|
||||
</Stack>
|
||||
|
||||
{#snippet trailing()}
|
||||
<Icon icon={mdiChevronRight} />
|
||||
{#if yuccaLogin.isPending}
|
||||
<LoadingSpinner />
|
||||
{:else}
|
||||
<Icon icon={mdiChevronRight} />
|
||||
{/if}
|
||||
{/snippet}
|
||||
</StackListItem>
|
||||
<StackListItem onclick={onLocalBackups}>
|
||||
<StackListItem disabled={yuccaLogin.isPending} onclick={onLocalBackups}>
|
||||
{#snippet icon()}
|
||||
<Icon icon={mdiHarddisk} size="36px" />
|
||||
{/snippet}
|
||||
|
||||
@@ -48,22 +48,25 @@ export const useBackendEventHandler = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const handleYuccaLogin = async (
|
||||
onCreate?: (backendId: string) => void,
|
||||
) => {
|
||||
try {
|
||||
const response = await oidcDeviceFlow();
|
||||
void modalManager.show(OAuthDeviceFlow, {
|
||||
...response,
|
||||
onCreate,
|
||||
});
|
||||
window.open(response.verificationUri, '_blank');
|
||||
} catch (error) {
|
||||
handleError(error, 'Failed to start login');
|
||||
throw error;
|
||||
}
|
||||
const startYuccaLogin = async (onCreate?: (backendId: string) => void) => {
|
||||
const response = await oidcDeviceFlow();
|
||||
void modalManager.show(OAuthDeviceFlow, {
|
||||
...response,
|
||||
onCreate,
|
||||
});
|
||||
window.open(response.verificationUri, '_blank');
|
||||
};
|
||||
|
||||
export const useYuccaLogin = () =>
|
||||
createMutation(
|
||||
() => ({
|
||||
mutationFn: (onCreate?: (backendId: string) => void) =>
|
||||
startYuccaLogin(onCreate),
|
||||
onError: (error) => handleError(error, 'Failed to start login'),
|
||||
}),
|
||||
() => queryClient,
|
||||
);
|
||||
|
||||
export const handleSetupLocalStorage = (
|
||||
onCreate?: (backendId: string) => void,
|
||||
) => {
|
||||
@@ -99,11 +102,12 @@ export const getBackendActions = (
|
||||
repository: LocalRepositoryDto | undefined,
|
||||
backend: BackendDto,
|
||||
repositoryBackend?: RepositoryBackendDto & { primary?: boolean },
|
||||
onLogin?: () => void,
|
||||
) => {
|
||||
const LoginAgain: ActionItem = {
|
||||
icon: mdiLogin,
|
||||
title: 'Login again',
|
||||
onAction: () => void handleYuccaLogin(),
|
||||
onAction: () => onLogin?.(),
|
||||
$if: () => backend.type === 'yucca' && !backend.isOnline,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user