feat: enhance Stripstream configuration handling
- Introduced a new resolver function to streamline fetching Stripstream configuration from the database or environment variables. - Updated various components and API routes to utilize the new configuration resolver, improving code maintainability and reducing direct database calls. - Added optional environment variables for Stripstream URL and token in the .env.example file. - Refactored image loading logic in the reader components to improve performance and error handling.
This commit is contained in:
@@ -5,4 +5,8 @@ MONGODB_URI=mongodb://admin:password@host.docker.internal:27017/stripstream?auth
|
||||
|
||||
NEXTAUTH_SECRET=SECRET
|
||||
#openssl rand -base64 32
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
|
||||
# Stripstream Librarian (optionnel : fallback si l'utilisateur n'a pas sauvegardé d'URL/token en base)
|
||||
# STRIPSTREAM_URL=https://librarian.example.com
|
||||
# STRIPSTREAM_TOKEN=stl_xxxx_xxxxxxxx
|
||||
@@ -4,6 +4,7 @@ import { revalidatePath } from "next/cache";
|
||||
import prisma from "@/lib/prisma";
|
||||
import { getCurrentUser } from "@/lib/auth-utils";
|
||||
import { StripstreamProvider } from "@/lib/providers/stripstream/stripstream.provider";
|
||||
import { getResolvedStripstreamConfig } from "@/lib/providers/stripstream/stripstream-config-resolver";
|
||||
import { AppError } from "@/utils/errors";
|
||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||
import type { ProviderType } from "@/lib/providers/types";
|
||||
@@ -81,8 +82,8 @@ export async function setActiveProvider(
|
||||
if (!config) {
|
||||
return { success: false, message: "Komga n'est pas encore configuré" };
|
||||
}
|
||||
} else if (provider === "stripstream") {
|
||||
const config = await prisma.stripstreamConfig.findUnique({ where: { userId } });
|
||||
} else if (provider === "stripstream") {
|
||||
const config = await getResolvedStripstreamConfig(userId);
|
||||
if (!config) {
|
||||
return { success: false, message: "Stripstream n'est pas encore configuré" };
|
||||
}
|
||||
@@ -108,7 +109,8 @@ export async function setActiveProvider(
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère la configuration Stripstream de l'utilisateur
|
||||
* Récupère la configuration Stripstream de l'utilisateur (affichage settings).
|
||||
* Priorité : config en base, sinon env STRIPSTREAM_URL / STRIPSTREAM_TOKEN.
|
||||
*/
|
||||
export async function getStripstreamConfig(): Promise<{
|
||||
url?: string;
|
||||
@@ -119,13 +121,9 @@ export async function getStripstreamConfig(): Promise<{
|
||||
if (!user) return null;
|
||||
const userId = parseInt(user.id, 10);
|
||||
|
||||
const config = await prisma.stripstreamConfig.findUnique({
|
||||
where: { userId },
|
||||
select: { url: true },
|
||||
});
|
||||
|
||||
if (!config) return null;
|
||||
return { url: config.url, hasToken: true };
|
||||
const resolved = await getResolvedStripstreamConfig(userId);
|
||||
if (!resolved) return null;
|
||||
return { url: resolved.url, hasToken: true };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -166,15 +164,15 @@ export async function getProvidersStatus(): Promise<{
|
||||
}
|
||||
const userId = parseInt(user.id, 10);
|
||||
|
||||
const [dbUser, komgaConfig, stripstreamConfig] = await Promise.all([
|
||||
const [dbUser, komgaConfig, stripstreamResolved] = await Promise.all([
|
||||
prisma.user.findUnique({ where: { id: userId }, select: { activeProvider: true } }),
|
||||
prisma.komgaConfig.findUnique({ where: { userId }, select: { id: true } }),
|
||||
prisma.stripstreamConfig.findUnique({ where: { userId }, select: { id: true } }),
|
||||
getResolvedStripstreamConfig(userId),
|
||||
]);
|
||||
|
||||
return {
|
||||
komgaConfigured: !!komgaConfig,
|
||||
stripstreamConfigured: !!stripstreamConfig,
|
||||
stripstreamConfigured: !!stripstreamResolved,
|
||||
activeProvider: (dbUser?.activeProvider as ProviderType) ?? "komga",
|
||||
};
|
||||
} catch {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { getCurrentUser } from "@/lib/auth-utils";
|
||||
import prisma from "@/lib/prisma";
|
||||
import { getResolvedStripstreamConfig } from "@/lib/providers/stripstream/stripstream-config-resolver";
|
||||
import { StripstreamClient } from "@/lib/providers/stripstream/stripstream.client";
|
||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||
import { AppError } from "@/utils/errors";
|
||||
@@ -23,7 +23,7 @@ export async function GET(
|
||||
}
|
||||
|
||||
const userId = parseInt(user.id, 10);
|
||||
const config = await prisma.stripstreamConfig.findUnique({ where: { userId } });
|
||||
const config = await getResolvedStripstreamConfig(userId);
|
||||
if (!config) {
|
||||
throw new AppError(ERROR_CODES.STRIPSTREAM.MISSING_CONFIG);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { getCurrentUser } from "@/lib/auth-utils";
|
||||
import prisma from "@/lib/prisma";
|
||||
import { getResolvedStripstreamConfig } from "@/lib/providers/stripstream/stripstream-config-resolver";
|
||||
import { StripstreamClient } from "@/lib/providers/stripstream/stripstream.client";
|
||||
import { AppError } from "@/utils/errors";
|
||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||
@@ -20,7 +20,7 @@ export async function GET(
|
||||
}
|
||||
|
||||
const userId = parseInt(user.id, 10);
|
||||
const config = await prisma.stripstreamConfig.findUnique({ where: { userId } });
|
||||
const config = await getResolvedStripstreamConfig(userId);
|
||||
if (!config) {
|
||||
throw new AppError(ERROR_CODES.STRIPSTREAM.MISSING_CONFIG);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { ErrorMessage } from "@/components/ui/ErrorMessage";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
@@ -16,7 +15,6 @@ interface LoginFormProps {
|
||||
}
|
||||
|
||||
export function LoginForm({ from }: LoginFormProps) {
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<AppErrorType | null>(null);
|
||||
const { t } = useTranslate();
|
||||
@@ -57,8 +55,7 @@ export function LoginForm({ from }: LoginFormProps) {
|
||||
}
|
||||
|
||||
const redirectPath = getSafeRedirectPath(from);
|
||||
window.location.assign(redirectPath);
|
||||
router.refresh();
|
||||
window.location.href = redirectPath;
|
||||
} catch {
|
||||
setError({
|
||||
code: "AUTH_FETCH_ERROR",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { ErrorMessage } from "@/components/ui/ErrorMessage";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
@@ -16,7 +15,6 @@ interface RegisterFormProps {
|
||||
}
|
||||
|
||||
export function RegisterForm({ from }: RegisterFormProps) {
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<AppErrorType | null>(null);
|
||||
const { t } = useTranslate();
|
||||
@@ -77,8 +75,7 @@ export function RegisterForm({ from }: RegisterFormProps) {
|
||||
});
|
||||
} else {
|
||||
const redirectPath = getSafeRedirectPath(from);
|
||||
window.location.assign(redirectPath);
|
||||
router.refresh();
|
||||
window.location.href = redirectPath;
|
||||
}
|
||||
} catch {
|
||||
setError({
|
||||
|
||||
@@ -42,12 +42,14 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP
|
||||
const {
|
||||
loadedImages,
|
||||
imageBlobUrls,
|
||||
prefetchImage,
|
||||
prefetchPages,
|
||||
prefetchNextBook,
|
||||
cancelAllPrefetches,
|
||||
handleForceReload,
|
||||
getPageUrl,
|
||||
prefetchCount,
|
||||
isPageLoading,
|
||||
} = useImageLoader({
|
||||
pageUrlBuilder: bookPageUrlBuilder,
|
||||
pages,
|
||||
@@ -87,8 +89,26 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP
|
||||
|
||||
// Prefetch current and next pages
|
||||
useEffect(() => {
|
||||
// Prefetch pages starting from current page
|
||||
prefetchPages(currentPage, prefetchCount);
|
||||
// Determine visible pages that need to be loaded immediately
|
||||
const visiblePages: number[] = [];
|
||||
if (isDoublePage && shouldShowDoublePage(currentPage, pages.length)) {
|
||||
visiblePages.push(currentPage, currentPage + 1);
|
||||
} else {
|
||||
visiblePages.push(currentPage);
|
||||
}
|
||||
|
||||
// Load visible pages first (priority) to avoid duplicate requests from <img> tags
|
||||
// These will populate imageBlobUrls so <img> tags use blob URLs instead of making HTTP requests
|
||||
const loadVisiblePages = async () => {
|
||||
await Promise.all(visiblePages.map((page) => prefetchImage(page)));
|
||||
};
|
||||
loadVisiblePages().catch(() => {
|
||||
// Silently fail - will fallback to direct HTTP requests
|
||||
});
|
||||
|
||||
// Then prefetch other pages, excluding visible ones to avoid duplicates
|
||||
const concurrency = isDoublePage && shouldShowDoublePage(currentPage, pages.length) ? 2 : 4;
|
||||
prefetchPages(currentPage, prefetchCount, visiblePages, concurrency);
|
||||
|
||||
// If double page mode, also prefetch additional pages for smooth double page navigation
|
||||
if (
|
||||
@@ -96,7 +116,7 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP
|
||||
shouldShowDoublePage(currentPage, pages.length) &&
|
||||
currentPage + prefetchCount < pages.length
|
||||
) {
|
||||
prefetchPages(currentPage + prefetchCount, 1);
|
||||
prefetchPages(currentPage + prefetchCount, 1, visiblePages, concurrency);
|
||||
}
|
||||
|
||||
// If we're near the end of the book, prefetch the next book
|
||||
@@ -108,6 +128,7 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP
|
||||
currentPage,
|
||||
isDoublePage,
|
||||
shouldShowDoublePage,
|
||||
prefetchImage,
|
||||
prefetchPages,
|
||||
prefetchNextBook,
|
||||
prefetchCount,
|
||||
@@ -229,6 +250,7 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP
|
||||
imageBlobUrls={imageBlobUrls}
|
||||
getPageUrl={getPageUrl}
|
||||
isRTL={isRTL}
|
||||
isPageLoading={isPageLoading}
|
||||
/>
|
||||
|
||||
<NavigationBar
|
||||
|
||||
@@ -9,6 +9,7 @@ interface PageDisplayProps {
|
||||
imageBlobUrls: Record<number, string>;
|
||||
getPageUrl: (pageNum: number) => string;
|
||||
isRTL: boolean;
|
||||
isPageLoading?: (pageNum: number) => boolean;
|
||||
}
|
||||
|
||||
export function PageDisplay({
|
||||
@@ -19,6 +20,7 @@ export function PageDisplay({
|
||||
imageBlobUrls,
|
||||
getPageUrl,
|
||||
isRTL,
|
||||
isPageLoading,
|
||||
}: PageDisplayProps) {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
@@ -102,7 +104,10 @@ export function PageDisplay({
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
key={`page-${currentPage}-${imageBlobUrls[currentPage] || ""}`}
|
||||
src={imageBlobUrls[currentPage] || getPageUrl(currentPage)}
|
||||
src={
|
||||
imageBlobUrls[currentPage] ||
|
||||
(isPageLoading && isPageLoading(currentPage) ? undefined : getPageUrl(currentPage))
|
||||
}
|
||||
alt={`Page ${currentPage}`}
|
||||
className={cn(
|
||||
"max-h-full max-w-full cursor-pointer object-contain transition-opacity",
|
||||
@@ -166,7 +171,12 @@ export function PageDisplay({
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
key={`page-${currentPage + 1}-${imageBlobUrls[currentPage + 1] || ""}`}
|
||||
src={imageBlobUrls[currentPage + 1] || getPageUrl(currentPage + 1)}
|
||||
src={
|
||||
imageBlobUrls[currentPage + 1] ||
|
||||
(isPageLoading && isPageLoading(currentPage + 1)
|
||||
? undefined
|
||||
: getPageUrl(currentPage + 1))
|
||||
}
|
||||
alt={`Page ${currentPage + 1}`}
|
||||
className={cn(
|
||||
"max-h-full max-w-full cursor-pointer object-contain transition-opacity",
|
||||
|
||||
@@ -30,6 +30,8 @@ export function useImageLoader({
|
||||
// Track ongoing fetch requests to prevent duplicates
|
||||
const pendingFetchesRef = useRef<Set<ImageKey>>(new Set());
|
||||
const abortControllersRef = useRef<Map<ImageKey, AbortController>>(new Map());
|
||||
// Track promises for pages being loaded so we can await them
|
||||
const loadingPromisesRef = useRef<Map<ImageKey, Promise<void>>>(new Map());
|
||||
|
||||
// Keep refs in sync with state
|
||||
useEffect(() => {
|
||||
@@ -44,12 +46,14 @@ export function useImageLoader({
|
||||
isMountedRef.current = true;
|
||||
const abortControllers = abortControllersRef.current;
|
||||
const pendingFetches = pendingFetchesRef.current;
|
||||
const loadingPromises = loadingPromisesRef.current;
|
||||
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
abortControllers.forEach((controller) => controller.abort());
|
||||
abortControllers.clear();
|
||||
pendingFetches.clear();
|
||||
loadingPromises.clear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -57,6 +61,7 @@ export function useImageLoader({
|
||||
abortControllersRef.current.forEach((controller) => controller.abort());
|
||||
abortControllersRef.current.clear();
|
||||
pendingFetchesRef.current.clear();
|
||||
loadingPromisesRef.current.clear();
|
||||
}, []);
|
||||
|
||||
const runWithConcurrency = useCallback(
|
||||
@@ -92,73 +97,96 @@ export function useImageLoader({
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this page is already being fetched
|
||||
if (pendingFetchesRef.current.has(pageNum)) {
|
||||
return;
|
||||
// Check if this page is already being fetched - if so, wait for it
|
||||
const existingPromise = loadingPromisesRef.current.get(pageNum);
|
||||
if (existingPromise) {
|
||||
return existingPromise;
|
||||
}
|
||||
|
||||
// Mark as pending
|
||||
// Mark as pending and create promise
|
||||
pendingFetchesRef.current.add(pageNum);
|
||||
const controller = new AbortController();
|
||||
abortControllersRef.current.set(pageNum, controller);
|
||||
|
||||
try {
|
||||
// Use browser cache if available - the server sets Cache-Control headers
|
||||
const response = await fetch(getPageUrl(pageNum), {
|
||||
cache: "default", // Respect Cache-Control headers from server
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
// Create image to get dimensions
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
if (!isMountedRef.current || controller.signal.aborted) {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
const promise = (async () => {
|
||||
try {
|
||||
// Use browser cache if available - the server sets Cache-Control headers
|
||||
const response = await fetch(getPageUrl(pageNum), {
|
||||
cache: "default", // Respect Cache-Control headers from server
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadedImages((prev) => ({
|
||||
...prev,
|
||||
[pageNum]: { width: img.naturalWidth, height: img.naturalHeight },
|
||||
}));
|
||||
const blob = await response.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
// Store the blob URL for immediate use
|
||||
setImageBlobUrls((prev) => ({
|
||||
...prev,
|
||||
[pageNum]: blobUrl,
|
||||
}));
|
||||
};
|
||||
// Create image to get dimensions
|
||||
const img = new Image();
|
||||
|
||||
// Wait for image to load before resolving promise
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
img.onload = () => {
|
||||
if (!isMountedRef.current || controller.signal.aborted) {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
reject(new Error("Aborted"));
|
||||
return;
|
||||
}
|
||||
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
};
|
||||
setLoadedImages((prev) => ({
|
||||
...prev,
|
||||
[pageNum]: { width: img.naturalWidth, height: img.naturalHeight },
|
||||
}));
|
||||
|
||||
img.src = blobUrl;
|
||||
} catch {
|
||||
// Silently fail prefetch
|
||||
} finally {
|
||||
// Remove from pending set
|
||||
pendingFetchesRef.current.delete(pageNum);
|
||||
abortControllersRef.current.delete(pageNum);
|
||||
}
|
||||
// Store the blob URL for immediate use
|
||||
setImageBlobUrls((prev) => ({
|
||||
...prev,
|
||||
[pageNum]: blobUrl,
|
||||
}));
|
||||
|
||||
resolve();
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
reject(new Error("Image load error"));
|
||||
};
|
||||
|
||||
img.src = blobUrl;
|
||||
});
|
||||
} catch {
|
||||
// Silently fail prefetch
|
||||
} finally {
|
||||
// Remove from pending set and promise map
|
||||
pendingFetchesRef.current.delete(pageNum);
|
||||
abortControllersRef.current.delete(pageNum);
|
||||
loadingPromisesRef.current.delete(pageNum);
|
||||
}
|
||||
})();
|
||||
|
||||
// Store promise so other calls can await it
|
||||
loadingPromisesRef.current.set(pageNum, promise);
|
||||
|
||||
return promise;
|
||||
},
|
||||
[getPageUrl]
|
||||
);
|
||||
|
||||
// Prefetch multiple pages starting from a given page
|
||||
const prefetchPages = useCallback(
|
||||
async (startPage: number, count: number = prefetchCount) => {
|
||||
async (
|
||||
startPage: number,
|
||||
count: number = prefetchCount,
|
||||
excludePages: number[] = [],
|
||||
concurrency?: number
|
||||
) => {
|
||||
const pagesToPrefetch = [];
|
||||
const excludeSet = new Set(excludePages);
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const pageNum = startPage + i;
|
||||
if (pageNum <= _pages.length) {
|
||||
if (pageNum <= _pages.length && !excludeSet.has(pageNum)) {
|
||||
const hasDimensions = loadedImagesRef.current[pageNum];
|
||||
const hasBlobUrl = imageBlobUrlsRef.current[pageNum];
|
||||
const isPending = pendingFetchesRef.current.has(pageNum);
|
||||
@@ -170,10 +198,13 @@ export function useImageLoader({
|
||||
}
|
||||
}
|
||||
|
||||
// Use provided concurrency or default
|
||||
const effectiveConcurrency = concurrency ?? PREFETCH_CONCURRENCY;
|
||||
|
||||
// Let all prefetch requests run - the server queue will manage concurrency
|
||||
// The browser cache and our deduplication prevent redundant requests
|
||||
if (pagesToPrefetch.length > 0) {
|
||||
runWithConcurrency(pagesToPrefetch, prefetchImage).catch(() => {
|
||||
runWithConcurrency(pagesToPrefetch, prefetchImage, effectiveConcurrency).catch(() => {
|
||||
// Silently fail - prefetch is non-critical
|
||||
});
|
||||
}
|
||||
@@ -340,6 +371,14 @@ export function useImageLoader({
|
||||
};
|
||||
}, []); // Empty dependency array - only cleanup on unmount
|
||||
|
||||
// Check if a page is currently being loaded
|
||||
const isPageLoading = useCallback(
|
||||
(pageNum: number) => {
|
||||
return pendingFetchesRef.current.has(pageNum);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
loadedImages,
|
||||
imageBlobUrls,
|
||||
@@ -350,5 +389,6 @@ export function useImageLoader({
|
||||
handleForceReload,
|
||||
getPageUrl,
|
||||
prefetchCount,
|
||||
isPageLoading,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { getCurrentUser } from "@/lib/auth-utils";
|
||||
import { getResolvedStripstreamConfig } from "./stripstream/stripstream-config-resolver";
|
||||
import type { IMediaProvider } from "./provider.interface";
|
||||
|
||||
export async function getProvider(): Promise<IMediaProvider | null> {
|
||||
@@ -13,7 +14,7 @@ export async function getProvider(): Promise<IMediaProvider | null> {
|
||||
select: {
|
||||
activeProvider: true,
|
||||
config: { select: { url: true, authHeader: true } },
|
||||
stripstreamConfig: { select: { url: true, token: true } },
|
||||
stripstreamConfig: { select: { id: true } },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -21,12 +22,12 @@ export async function getProvider(): Promise<IMediaProvider | null> {
|
||||
|
||||
const activeProvider = dbUser.activeProvider ?? "komga";
|
||||
|
||||
if (activeProvider === "stripstream" && dbUser.stripstreamConfig) {
|
||||
const { StripstreamProvider } = await import("./stripstream/stripstream.provider");
|
||||
return new StripstreamProvider(
|
||||
dbUser.stripstreamConfig.url,
|
||||
dbUser.stripstreamConfig.token
|
||||
);
|
||||
if (activeProvider === "stripstream") {
|
||||
const resolved = await getResolvedStripstreamConfig(userId);
|
||||
if (resolved) {
|
||||
const { StripstreamProvider } = await import("./stripstream/stripstream.provider");
|
||||
return new StripstreamProvider(resolved.url, resolved.token);
|
||||
}
|
||||
}
|
||||
|
||||
if (activeProvider === "komga" || !dbUser.activeProvider) {
|
||||
|
||||
26
src/lib/providers/stripstream/stripstream-config-resolver.ts
Normal file
26
src/lib/providers/stripstream/stripstream-config-resolver.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export interface ResolvedStripstreamConfig {
|
||||
url: string;
|
||||
token: string;
|
||||
source: "db" | "env";
|
||||
}
|
||||
|
||||
/**
|
||||
* Résout la config Stripstream : d'abord en base (par utilisateur), sinon depuis les env STRIPSTREAM_URL et STRIPSTREAM_TOKEN.
|
||||
*/
|
||||
export async function getResolvedStripstreamConfig(
|
||||
userId: number
|
||||
): Promise<ResolvedStripstreamConfig | null> {
|
||||
const fromDb = await prisma.stripstreamConfig.findUnique({
|
||||
where: { userId },
|
||||
select: { url: true, token: true },
|
||||
});
|
||||
if (fromDb) return { ...fromDb, source: "db" };
|
||||
|
||||
const url = process.env.STRIPSTREAM_URL?.trim();
|
||||
const token = process.env.STRIPSTREAM_TOKEN?.trim();
|
||||
if (url && token) return { url, token, source: "env" };
|
||||
|
||||
return null;
|
||||
}
|
||||
4
src/types/env.d.ts
vendored
4
src/types/env.d.ts
vendored
@@ -3,5 +3,9 @@ declare namespace NodeJS {
|
||||
NEXT_PUBLIC_APP_URL: string;
|
||||
NEXT_PUBLIC_DEFAULT_KOMGA_URL?: string;
|
||||
NEXT_PUBLIC_APP_VERSION: string;
|
||||
/** URL Stripstream Librarian (fallback si pas de config en base) */
|
||||
STRIPSTREAM_URL?: string;
|
||||
/** Token API Stripstream (fallback si pas de config en base) */
|
||||
STRIPSTREAM_TOKEN?: string;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user