diff --git a/src/app/api/admin/ai/route.ts b/src/app/api/admin/ai/route.ts index 8afeb3b..f0435fe 100644 --- a/src/app/api/admin/ai/route.ts +++ b/src/app/api/admin/ai/route.ts @@ -5,6 +5,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { getAuthInfoFromCookie } from '@/lib/auth'; import { getConfig } from '@/lib/config'; import { db } from '@/lib/db'; +import { normalizeApiBaseUrl } from '@/lib/url'; export const runtime = 'nodejs'; @@ -156,22 +157,22 @@ export async function POST(request: NextRequest) { Enabled, Provider, OpenAIApiKey, - OpenAIBaseURL, + OpenAIBaseURL: normalizeApiBaseUrl(OpenAIBaseURL), OpenAIModel, ClaudeApiKey, ClaudeModel, CustomApiKey, - CustomBaseURL, + CustomBaseURL: normalizeApiBaseUrl(CustomBaseURL), CustomModel, EnableDecisionModel, DecisionProvider, DecisionOpenAIApiKey, - DecisionOpenAIBaseURL, + DecisionOpenAIBaseURL: normalizeApiBaseUrl(DecisionOpenAIBaseURL), DecisionOpenAIModel, DecisionClaudeApiKey, DecisionClaudeModel, DecisionCustomApiKey, - DecisionCustomBaseURL, + DecisionCustomBaseURL: normalizeApiBaseUrl(DecisionCustomBaseURL), DecisionCustomModel, EnableWebSearch, WebSearchProvider, diff --git a/src/app/api/admin/music/route.ts b/src/app/api/admin/music/route.ts index 8a62fe5..6993bd6 100644 --- a/src/app/api/admin/music/route.ts +++ b/src/app/api/admin/music/route.ts @@ -5,6 +5,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { getAuthInfoFromCookie } from '@/lib/auth'; import { getConfig, setCachedConfig } from '@/lib/config'; import { db } from '@/lib/db'; +import { normalizeApiBaseUrl } from '@/lib/url'; export const runtime = 'nodejs'; @@ -63,7 +64,7 @@ export async function POST(request: NextRequest) { // 更新缓存中的音乐配置 adminConfig.MusicConfig = { Enabled, - BaseUrl, + BaseUrl: normalizeApiBaseUrl(BaseUrl), Token, ProxyEnabled: ProxyEnabled ?? true, }; diff --git a/src/app/api/admin/openlist/route.ts b/src/app/api/admin/openlist/route.ts index 3f9cef0..5347020 100644 --- a/src/app/api/admin/openlist/route.ts +++ b/src/app/api/admin/openlist/route.ts @@ -11,6 +11,7 @@ import { normalizePathMetaMap, type OpenListPathMetaMap, } from '@/lib/openlist-path-meta'; +import { normalizeApiBaseUrl } from '@/lib/url'; export const runtime = 'nodejs'; @@ -82,18 +83,20 @@ export async function POST(request: NextRequest) { if (action === 'save') { const cleanedPathMeta = parsePathMeta(PathMeta); + const normalizedURL = normalizeApiBaseUrl(URL); + const normalizedOfflineDownloadURL = normalizeApiBaseUrl(OfflineDownloadURL); // 如果功能未启用,允许保存空配置 if (!Enabled) { adminConfig.OpenListConfig = { Enabled: false, - URL: URL || '', + URL: normalizedURL, Username: Username || '', Password: Password || '', RootPaths: RootPaths || ['/'], OfflineDownloadPath: OfflineDownloadPath || '/', OfflineDownloadUseCustomSource: OfflineDownloadUseCustomSource || false, - OfflineDownloadURL: OfflineDownloadURL || '', + OfflineDownloadURL: normalizedOfflineDownloadURL, OfflineDownloadUsername: OfflineDownloadUsername || '', OfflineDownloadPassword: OfflineDownloadPassword || '', LastRefreshTime: adminConfig.OpenListConfig?.LastRefreshTime, @@ -113,7 +116,7 @@ export async function POST(request: NextRequest) { } // 功能启用时,验证必填字段 - if (!URL || !Username || !Password) { + if (!normalizedURL || !Username || !Password) { return NextResponse.json( { error: '请提供 URL、账号和密码' }, { status: 400 } @@ -122,7 +125,9 @@ export async function POST(request: NextRequest) { if ( OfflineDownloadUseCustomSource && - (!OfflineDownloadURL || !OfflineDownloadUsername || !OfflineDownloadPassword) + (!normalizedOfflineDownloadURL || + !OfflineDownloadUsername || + !OfflineDownloadPassword) ) { return NextResponse.json( { error: '请提供离线下载 OpenList URL、账号和密码' }, @@ -153,7 +158,7 @@ export async function POST(request: NextRequest) { // 验证账号密码是否正确 try { console.log('[OpenList Config] 验证账号密码'); - await OpenListClient.login(URL, Username, Password); + await OpenListClient.login(normalizedURL, Username, Password); console.log('[OpenList Config] 账号密码验证成功'); } catch (error) { console.error('[OpenList Config] 账号密码验证失败:', error); @@ -167,7 +172,7 @@ export async function POST(request: NextRequest) { try { console.log('[OpenList Config] 验证离线下载 OpenList 账号密码'); await OpenListClient.login( - OfflineDownloadURL, + normalizedOfflineDownloadURL, OfflineDownloadUsername, OfflineDownloadPassword ); @@ -183,13 +188,13 @@ export async function POST(request: NextRequest) { adminConfig.OpenListConfig = { Enabled: true, - URL, + URL: normalizedURL, Username, Password, RootPaths: cleanedRootPaths, OfflineDownloadPath: OfflineDownloadPath || '/', OfflineDownloadUseCustomSource: OfflineDownloadUseCustomSource || false, - OfflineDownloadURL: OfflineDownloadURL || '', + OfflineDownloadURL: normalizedOfflineDownloadURL, OfflineDownloadUsername: OfflineDownloadUsername || '', OfflineDownloadPassword: OfflineDownloadPassword || '', LastRefreshTime: adminConfig.OpenListConfig?.LastRefreshTime, diff --git a/src/app/api/admin/site/route.ts b/src/app/api/admin/site/route.ts index e5dac12..64cd9f1 100644 --- a/src/app/api/admin/site/route.ts +++ b/src/app/api/admin/site/route.ts @@ -5,6 +5,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { getAuthInfoFromCookie } from '@/lib/auth'; import { getConfig } from '@/lib/config'; import { db } from '@/lib/db'; +import { normalizeApiBaseUrl } from '@/lib/url'; export const runtime = 'nodejs'; @@ -264,6 +265,7 @@ export async function POST(request: NextRequest) { } // 更新缓存中的站点设置 + // API Base URL 统一去尾斜杠,避免运行时拼接路径出现 // adminConfig.SiteConfig = { SiteName, Announcement, @@ -271,33 +273,33 @@ export async function POST(request: NextRequest) { SearchDownstreamMaxPage, SiteInterfaceCacheTime, DoubanProxyType, - DoubanProxy, + DoubanProxy: normalizeApiBaseUrl(DoubanProxy), DoubanImageProxyType, - DoubanImageProxy, + DoubanImageProxy: normalizeApiBaseUrl(DoubanImageProxy), DisableYellowFilter, FluidSearch, DanmakuSourceType, - DanmakuApiBase, + DanmakuApiBase: normalizeApiBaseUrl(DanmakuApiBase), DanmakuApiToken, DanmakuAutoLoadDefault, TMDBApiKey, - TMDBProxy, - TMDBReverseProxy, + TMDBProxy: normalizeApiBaseUrl(TMDBProxy), + TMDBReverseProxy: normalizeApiBaseUrl(TMDBReverseProxy), BangumiDataSource, - BangumiApiBaseUrl, - BangumiImageBaseUrl, - BangumiProxy, + BangumiApiBaseUrl: normalizeApiBaseUrl(BangumiApiBaseUrl), + BangumiImageBaseUrl: normalizeApiBaseUrl(BangumiImageBaseUrl), + BangumiProxy: normalizeApiBaseUrl(BangumiProxy), BannerDataSource, RecommendationDataSource, - PansouApiUrl, + PansouApiUrl: normalizeApiBaseUrl(PansouApiUrl), PansouUsername, PansouPassword, PansouKeywordBlocklist, - MagnetProxy, - MagnetMikanReverseProxy, - MagnetDmhyReverseProxy, - MagnetAcgripReverseProxy, - MagnetNyaaReverseProxy, + MagnetProxy: normalizeApiBaseUrl(MagnetProxy), + MagnetMikanReverseProxy: normalizeApiBaseUrl(MagnetMikanReverseProxy), + MagnetDmhyReverseProxy: normalizeApiBaseUrl(MagnetDmhyReverseProxy), + MagnetAcgripReverseProxy: normalizeApiBaseUrl(MagnetAcgripReverseProxy), + MagnetNyaaReverseProxy: normalizeApiBaseUrl(MagnetNyaaReverseProxy), EnableComments, CustomAdFilterCode, CustomAdFilterVersion, @@ -311,7 +313,7 @@ export async function POST(request: NextRequest) { DefaultUserTags, EnableOIDCLogin, EnableOIDCRegistration, - OIDCIssuer, + OIDCIssuer: normalizeApiBaseUrl(OIDCIssuer), OIDCAuthorizationEndpoint, OIDCTokenEndpoint, OIDCUserInfoEndpoint, diff --git a/src/app/api/admin/xiaoya/route.ts b/src/app/api/admin/xiaoya/route.ts index 86fca9f..9bf60ef 100644 --- a/src/app/api/admin/xiaoya/route.ts +++ b/src/app/api/admin/xiaoya/route.ts @@ -5,6 +5,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { getAuthInfoFromCookie } from '@/lib/auth'; import { getConfig } from '@/lib/config'; import { db } from '@/lib/db'; +import { normalizeApiBaseUrl } from '@/lib/url'; import { XiaoyaClient } from '@/lib/xiaoya.client'; export const runtime = 'nodejs'; @@ -27,7 +28,7 @@ export async function POST(request: NextRequest) { // 测试连接 try { const client = new XiaoyaClient( - configData.ServerURL, + normalizeApiBaseUrl(configData.ServerURL), configData.Username, configData.Password, configData.Token @@ -51,7 +52,7 @@ export async function POST(request: NextRequest) { config.XiaoyaConfig = { Enabled: configData.Enabled || false, - ServerURL: configData.ServerURL || '', + ServerURL: normalizeApiBaseUrl(configData.ServerURL), Token: configData.Token, Username: configData.Username, Password: configData.Password, diff --git a/src/lib/ai-comment-generator.ts b/src/lib/ai-comment-generator.ts index 2f3a3d4..1a49258 100644 --- a/src/lib/ai-comment-generator.ts +++ b/src/lib/ai-comment-generator.ts @@ -1,5 +1,7 @@ // AI评论生成核心逻辑 +import { normalizeApiBaseUrl } from '@/lib/url'; + export interface AIComment { id: string; userName: string; @@ -164,7 +166,8 @@ export async function generateAIComments( const prompt = buildCommentPrompt(movieName, movieInfo, searchResults, count); // 3. 调用AI API - const response = await fetch(`${aiConfig.CustomBaseURL}/chat/completions`, { + const baseURL = normalizeApiBaseUrl(aiConfig.CustomBaseURL); + const response = await fetch(`${baseURL}/chat/completions`, { method: 'POST', headers: { Authorization: `Bearer ${aiConfig.CustomApiKey}`, diff --git a/src/lib/ai-orchestrator.ts b/src/lib/ai-orchestrator.ts index 66b90a2..41bfe60 100644 --- a/src/lib/ai-orchestrator.ts +++ b/src/lib/ai-orchestrator.ts @@ -6,6 +6,7 @@ import { fetchDoubanData as fetchDoubanAPI } from '@/lib/douban'; import { getNextApiKey } from '@/lib/tmdb.client'; +import { normalizeApiBaseUrl } from '@/lib/url'; export interface VideoContext { title?: string; @@ -468,7 +469,9 @@ ${availableSources.length === 0 ? '⚠️ 没有可用的数据源,请返回 } } else { // OpenAI 或 自定义 (OpenAI兼容格式) - const baseURL = config.baseURL || 'https://api.openai.com/v1'; + const baseURL = normalizeApiBaseUrl( + config.baseURL || 'https://api.openai.com/v1' + ); response = await fetch(`${baseURL}/chat/completions`, { method: 'POST', headers: { diff --git a/src/lib/config.ts b/src/lib/config.ts index c5827f4..3881f1f 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any, no-console, @typescript-eslint/no-non-null-assertion */ import { db } from '@/lib/db'; +import { normalizeApiBaseUrl } from '@/lib/url'; import { AdminConfig } from './admin.types'; @@ -1047,6 +1048,100 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig { } } + // API Base URL 统一去尾斜杠,避免拼接路径时出现 // + const site = adminConfig.SiteConfig; + if (site) { + site.DoubanProxy = normalizeApiBaseUrl(site.DoubanProxy); + site.DoubanImageProxy = normalizeApiBaseUrl(site.DoubanImageProxy); + site.DanmakuApiBase = normalizeApiBaseUrl(site.DanmakuApiBase); + site.TMDBProxy = normalizeApiBaseUrl(site.TMDBProxy); + site.TMDBReverseProxy = normalizeApiBaseUrl(site.TMDBReverseProxy); + site.BangumiApiBaseUrl = normalizeApiBaseUrl(site.BangumiApiBaseUrl); + site.BangumiImageBaseUrl = normalizeApiBaseUrl(site.BangumiImageBaseUrl); + site.BangumiProxy = normalizeApiBaseUrl(site.BangumiProxy); + site.PansouApiUrl = normalizeApiBaseUrl(site.PansouApiUrl); + site.MagnetProxy = normalizeApiBaseUrl(site.MagnetProxy); + site.MagnetMikanReverseProxy = normalizeApiBaseUrl( + site.MagnetMikanReverseProxy + ); + site.MagnetDmhyReverseProxy = normalizeApiBaseUrl( + site.MagnetDmhyReverseProxy + ); + site.MagnetAcgripReverseProxy = normalizeApiBaseUrl( + site.MagnetAcgripReverseProxy + ); + site.MagnetNyaaReverseProxy = normalizeApiBaseUrl( + site.MagnetNyaaReverseProxy + ); + site.OIDCIssuer = normalizeApiBaseUrl(site.OIDCIssuer); + } + + if (adminConfig.OpenListConfig) { + adminConfig.OpenListConfig.URL = normalizeApiBaseUrl( + adminConfig.OpenListConfig.URL + ); + adminConfig.OpenListConfig.OfflineDownloadURL = normalizeApiBaseUrl( + adminConfig.OpenListConfig.OfflineDownloadURL + ); + } + + if (adminConfig.MusicConfig) { + adminConfig.MusicConfig.BaseUrl = normalizeApiBaseUrl( + adminConfig.MusicConfig.BaseUrl + ); + } + + if (adminConfig.XiaoyaConfig) { + adminConfig.XiaoyaConfig.ServerURL = normalizeApiBaseUrl( + adminConfig.XiaoyaConfig.ServerURL + ); + } + + if (adminConfig.SuwayomiConfig) { + adminConfig.SuwayomiConfig.ServerURL = normalizeApiBaseUrl( + adminConfig.SuwayomiConfig.ServerURL + ); + } + + if (adminConfig.EmbyConfig) { + if (adminConfig.EmbyConfig.ServerURL) { + adminConfig.EmbyConfig.ServerURL = normalizeApiBaseUrl( + adminConfig.EmbyConfig.ServerURL + ); + } + if (Array.isArray(adminConfig.EmbyConfig.Sources)) { + adminConfig.EmbyConfig.Sources = adminConfig.EmbyConfig.Sources.map( + (source) => ({ + ...source, + ServerURL: normalizeApiBaseUrl(source.ServerURL), + }) + ); + } + } + + if (adminConfig.AIConfig) { + adminConfig.AIConfig.OpenAIBaseURL = normalizeApiBaseUrl( + adminConfig.AIConfig.OpenAIBaseURL + ); + adminConfig.AIConfig.CustomBaseURL = normalizeApiBaseUrl( + adminConfig.AIConfig.CustomBaseURL + ); + adminConfig.AIConfig.DecisionOpenAIBaseURL = normalizeApiBaseUrl( + adminConfig.AIConfig.DecisionOpenAIBaseURL + ); + adminConfig.AIConfig.DecisionCustomBaseURL = normalizeApiBaseUrl( + adminConfig.AIConfig.DecisionCustomBaseURL + ); + } + + if (adminConfig.TelegramConfig) { + adminConfig.TelegramConfig.apiBaseUrl = normalizeApiBaseUrl( + adminConfig.TelegramConfig.apiBaseUrl + ); + } + + // 注意:OPDS source.url 是完整目录 URL,保留末尾 / 以便相对路径解析 + return adminConfig; } diff --git a/src/lib/emby.client.ts b/src/lib/emby.client.ts index d69cab2..c73cd7c 100644 --- a/src/lib/emby.client.ts +++ b/src/lib/emby.client.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { normalizeApiBaseUrl } from '@/lib/url'; + interface EmbyConfig { ServerURL: string; ApiKey?: string; @@ -96,7 +98,7 @@ export class EmbyClient { private embyAuthorizationHeader: string; constructor(config: EmbyConfig) { - let serverUrl = config.ServerURL.replace(/\/$/, ''); + let serverUrl = normalizeApiBaseUrl(config.ServerURL); // 存储高级选项 this.removeEmbyPrefix = config.removeEmbyPrefix || false; diff --git a/src/lib/music-v2.ts b/src/lib/music-v2.ts index 399301f..db4fff2 100644 --- a/src/lib/music-v2.ts +++ b/src/lib/music-v2.ts @@ -1,4 +1,5 @@ import { getConfig } from '@/lib/config'; +import { normalizeApiBaseUrl } from '@/lib/url'; export const runtime = 'nodejs'; @@ -209,7 +210,9 @@ export async function getMusicV2Config() { const musicConfig = config?.MusicConfig; const enabled = musicConfig?.Enabled ?? false; - const baseUrl = (musicConfig?.BaseUrl || process.env.MUSIC_V2_BASE_URL || '').replace(/\/$/, ''); + const baseUrl = normalizeApiBaseUrl( + musicConfig?.BaseUrl || process.env.MUSIC_V2_BASE_URL || '' + ); const token = musicConfig?.Token || process.env.MUSIC_V2_TOKEN || ''; return { enabled, baseUrl, token }; diff --git a/src/lib/openlist.client.ts b/src/lib/openlist.client.ts index ee14daf..42f1c3f 100644 --- a/src/lib/openlist.client.ts +++ b/src/lib/openlist.client.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { normalizeApiBaseUrl } from '@/lib/url'; + // Token 内存缓存 const tokenCache = new Map(); @@ -34,12 +36,15 @@ export interface OpenListGetResponse { export class OpenListClient { private token = ''; + private baseURL: string; constructor( - private baseURL: string, + baseURL: string, private username: string, private password: string - ) {} + ) { + this.baseURL = normalizeApiBaseUrl(baseURL); + } /** * 使用账号密码登录获取Token @@ -49,7 +54,8 @@ export class OpenListClient { username: string, password: string ): Promise { - const response = await fetch(`${baseURL}/api/auth/login`, { + const normalizedBaseURL = normalizeApiBaseUrl(baseURL); + const response = await fetch(`${normalizedBaseURL}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/lib/pansou.client.ts b/src/lib/pansou.client.ts index b928d3d..25f1379 100644 --- a/src/lib/pansou.client.ts +++ b/src/lib/pansou.client.ts @@ -5,6 +5,8 @@ * 文档: https://github.com/fish2018/pansou */ +import { normalizeApiBaseUrl } from '@/lib/url'; + // Token 缓存 let cachedToken: string | null = null; let tokenExpiry: number | null = null; @@ -40,7 +42,8 @@ export async function loginPansou( password: string ): Promise { try { - const response = await fetch(`${apiUrl}/api/auth/login`, { + const baseUrl = normalizeApiBaseUrl(apiUrl); + const response = await fetch(`${baseUrl}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -111,9 +114,10 @@ export async function searchPansou( } ): Promise { try { + const baseUrl = normalizeApiBaseUrl(apiUrl); // 获取 Token(如果需要认证) const token = await getValidToken( - apiUrl, + baseUrl, options?.username, options?.password ); @@ -141,7 +145,7 @@ export async function searchPansou( body.cloud_types = options.cloudTypes; } - const response = await fetch(`${apiUrl}/api/search`, { + const response = await fetch(`${baseUrl}/api/search`, { method: 'POST', headers, body: JSON.stringify(body), @@ -201,7 +205,8 @@ export function clearPansouToken(): void { */ export async function checkPansouHealth(apiUrl: string): Promise { try { - const response = await fetch(`${apiUrl}/api/health`, { + const baseUrl = normalizeApiBaseUrl(apiUrl); + const response = await fetch(`${baseUrl}/api/health`, { method: 'GET', }); diff --git a/src/lib/suwayomi.client.ts b/src/lib/suwayomi.client.ts index d00baa0..d020195 100644 --- a/src/lib/suwayomi.client.ts +++ b/src/lib/suwayomi.client.ts @@ -13,6 +13,7 @@ import { MangaSearchResult, MangaSource, } from './manga.types'; +import { normalizeApiBaseUrl } from './url'; interface GraphQLResponse { data?: T; @@ -167,7 +168,7 @@ async function resolveSuwayomiConfig(options: SuwayomiClientOptions = {}): Promi throw new Error('Suwayomi 未配置,请先在管理面板或环境变量中设置服务地址'); } - const normalizedBaseUrl = serverUrl.replace(/\/$/, ''); + const normalizedBaseUrl = normalizeApiBaseUrl(serverUrl); return { serverBaseUrl: normalizedBaseUrl, diff --git a/src/lib/telegram.ts b/src/lib/telegram.ts index 8d8d01b..d718c9e 100644 --- a/src/lib/telegram.ts +++ b/src/lib/telegram.ts @@ -13,6 +13,7 @@ import { getConfig } from './config'; import { db, getStorage } from './db'; import { lockManager } from './lock'; import type { IStorage, Notification } from './types'; +import { normalizeApiBaseUrl } from './url'; import { getNotificationClickUrl } from './web-push'; export interface TelegramConfig { @@ -426,7 +427,7 @@ function isCloudflareEnvironment(): boolean { } function normalizeTelegramApiBaseUrl(input?: string | null): string { - const base = (input || 'https://api.telegram.org').trim().replace(/\/+$/, ''); + const base = normalizeApiBaseUrl(input || 'https://api.telegram.org'); return base || 'https://api.telegram.org'; } diff --git a/src/lib/url.ts b/src/lib/url.ts new file mode 100644 index 0000000..ed166bf --- /dev/null +++ b/src/lib/url.ts @@ -0,0 +1,10 @@ +/** + * 规范化 API Base URL:trim 并去除末尾斜杠,避免拼接路径时出现双斜杠。 + */ +export function normalizeApiBaseUrl( + url: string | undefined | null +): string { + return String(url || '') + .trim() + .replace(/\/+$/, ''); +} diff --git a/src/lib/xiaoya.client.ts b/src/lib/xiaoya.client.ts index c32cad7..727092b 100644 --- a/src/lib/xiaoya.client.ts +++ b/src/lib/xiaoya.client.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { normalizeApiBaseUrl } from '@/lib/url'; + // Token 内存缓存 const tokenCache = new Map(); @@ -17,13 +19,16 @@ export interface XiaoyaListResponse { export class XiaoyaClient { private token = ''; + private baseURL: string; constructor( - private baseURL: string, + baseURL: string, private username?: string, private password?: string, private configToken?: string - ) {} + ) { + this.baseURL = normalizeApiBaseUrl(baseURL); + } /** * 使用账号密码登录获取Token @@ -33,7 +38,8 @@ export class XiaoyaClient { username: string, password: string ): Promise { - const response = await fetch(`${baseURL}/api/auth/login`, { + const normalizedBaseURL = normalizeApiBaseUrl(baseURL); + const response = await fetch(`${normalizedBaseURL}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json',