Try
dfssdfsfdsf
This commit is contained in:
@@ -1,29 +1,92 @@
|
||||
import { getApiJwt } from './appwrite'
|
||||
|
||||
/**
|
||||
* Replace every `/api/api` in the path with `/api` until stable (avoids Express catch-all 404).
|
||||
* Call on every outgoing API URL before `fetch`.
|
||||
*/
|
||||
export function collapseDoubleApi(url: string): string {
|
||||
if (!url) return url
|
||||
if (/^https?:\/\//i.test(url)) {
|
||||
try {
|
||||
const u = new URL(url)
|
||||
let p = u.pathname
|
||||
let prev = ''
|
||||
while (p !== prev && p.includes('/api/api')) {
|
||||
prev = p
|
||||
p = p.replace(/\/api\/api/g, '/api')
|
||||
}
|
||||
// z. B. //api → /api (sonst 404 auf dem Server)
|
||||
p = p.replace(/\/+/g, '/')
|
||||
if (p !== '/' && !p.startsWith('/')) {
|
||||
p = `/${p}`
|
||||
}
|
||||
u.pathname = p
|
||||
return u.toString()
|
||||
} catch {
|
||||
return url
|
||||
}
|
||||
}
|
||||
let s = url
|
||||
let prev = ''
|
||||
while (s !== prev && s.includes('/api/api')) {
|
||||
prev = s
|
||||
s = s.replace(/\/api\/api/g, '/api')
|
||||
}
|
||||
s = s.replace(/\/+/g, '/')
|
||||
return s
|
||||
}
|
||||
|
||||
/**
|
||||
* `import.meta.env.VITE_DEV_BACKEND_ORIGIN` (from Vite define) must be origin ONLY, e.g. `http://127.0.0.1:3030`.
|
||||
* Do NOT set a trailing `/api` in .env / .env.local (`VITE_DEV_API_ORIGIN`) — the client appends `/api` once.
|
||||
*/
|
||||
function stripTrailingApiFromOrigin(origin: string): string {
|
||||
let o = origin.replace(/\/+$/, '')
|
||||
while (o.endsWith('/api')) {
|
||||
o = o.slice(0, -4).replace(/\/+$/, '')
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoints in this file are paths like `/subscription/status`.
|
||||
* Express mounts the API under `/api`, so the base must end with `/api`.
|
||||
* If VITE_API_URL is `http://localhost:3000` (missing /api), requests would 404.
|
||||
*/
|
||||
function resolveApiBase(): string {
|
||||
const raw = (import.meta.env.VITE_API_URL as string | undefined)?.trim()
|
||||
if (!raw) return '/api'
|
||||
/** Empty / unset → use Vite define (VITE_DEV_BACKEND_ORIGIN) or relative /api */
|
||||
const hasExplicitViteApiUrl = Boolean(raw)
|
||||
|
||||
if (import.meta.env.DEV && !hasExplicitViteApiUrl) {
|
||||
const origin = (
|
||||
import.meta.env as { VITE_DEV_BACKEND_ORIGIN?: string }
|
||||
).VITE_DEV_BACKEND_ORIGIN?.trim()
|
||||
if (origin) {
|
||||
const o = stripTrailingApiFromOrigin(origin).replace(/\/+$/, '')
|
||||
const withApi = `${o}/api`
|
||||
return collapseDoubleApi(withApi)
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasExplicitViteApiUrl) return collapseDoubleApi('/api')
|
||||
|
||||
if (!raw) return collapseDoubleApi('/api')
|
||||
|
||||
if (raw.startsWith('/')) {
|
||||
const p = raw.replace(/\/+$/, '') || '/api'
|
||||
return p
|
||||
return collapseDoubleApi(p)
|
||||
}
|
||||
|
||||
if (!/^https?:\/\//i.test(raw)) {
|
||||
const p = raw.replace(/^\/+/, '').replace(/\/+$/, '') || 'api'
|
||||
return p.startsWith('api') ? `/${p}` : `/api`
|
||||
const rel = p.startsWith('api') ? `/${p}` : `/api`
|
||||
return collapseDoubleApi(rel)
|
||||
}
|
||||
|
||||
const normalized = raw.replace(/\/+$/, '')
|
||||
const normalized = collapseDoubleApi(raw.replace(/\/+$/, '').trim())
|
||||
try {
|
||||
const u = new URL(normalized)
|
||||
const path = u.pathname.replace(/\/$/, '') || '/'
|
||||
// Same host as Vite + /v1 = Appwrite-Proxy; never append /api (would hit /v1/api/… → 404)
|
||||
const path = u.pathname.replace(/\/+/g, '/').replace(/\/$/, '') || '/'
|
||||
const localVite =
|
||||
/^(localhost|127\.0\.0\.1)$/i.test(u.hostname) &&
|
||||
(u.port === '5173' || (u.port === '' && u.hostname === 'localhost'))
|
||||
@@ -31,31 +94,40 @@ function resolveApiBase(): string {
|
||||
return '/api'
|
||||
}
|
||||
if (path === '/' || path === '') {
|
||||
return `${normalized}/api`
|
||||
const originOnly = normalized.replace(/\/+$/, '')
|
||||
return collapseDoubleApi(`${originOnly}/api`)
|
||||
}
|
||||
if (path.endsWith('/api')) {
|
||||
return normalized
|
||||
return collapseDoubleApi(normalized)
|
||||
}
|
||||
return `${normalized}/api`
|
||||
const originOnly = normalized.replace(/\/+$/, '')
|
||||
return collapseDoubleApi(`${originOnly}/api`)
|
||||
} catch {
|
||||
return '/api'
|
||||
}
|
||||
}
|
||||
|
||||
const API_BASE = resolveApiBase()
|
||||
export const API_BASE = collapseDoubleApi(resolveApiBase())
|
||||
|
||||
/** Root-relative or absolute API URL; avoids `api/foo` (relative to current route). */
|
||||
/** Join API base (ends with `/api`) and endpoint (`/email/...`). If endpoint starts with `/api`, strip it once so we never produce `/api/api`. */
|
||||
function joinApiUrl(base: string, endpoint: string): string {
|
||||
const ep = endpoint.startsWith('/') ? endpoint : `/${endpoint}`
|
||||
let ep = endpoint.startsWith('/') ? endpoint : `/${endpoint}`
|
||||
if (ep === '/api' || ep.startsWith('/api/')) {
|
||||
ep = ep === '/api' ? '/' : ep.slice(4)
|
||||
if (ep !== '' && !ep.startsWith('/')) {
|
||||
ep = `/${ep}`
|
||||
}
|
||||
}
|
||||
if (/^https?:\/\//i.test(base)) {
|
||||
return `${base.replace(/\/+$/, '')}${ep}`
|
||||
const joined = `${base.replace(/\/+$/, '')}${ep}`
|
||||
return collapseDoubleApi(joined)
|
||||
}
|
||||
let b = base.trim()
|
||||
if (!b.startsWith('/')) {
|
||||
b = `/${b.replace(/^\/+/, '')}`
|
||||
}
|
||||
b = b.replace(/\/+$/, '') || '/api'
|
||||
return `${b}${ep}`
|
||||
return collapseDoubleApi(`${b}${ep}`)
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
@@ -86,7 +158,7 @@ async function fetchApi<T>(
|
||||
headers['Authorization'] = `Bearer ${jwt}`
|
||||
}
|
||||
|
||||
const urlJoined = joinApiUrl(API_BASE, endpoint)
|
||||
const urlJoined = collapseDoubleApi(joinApiUrl(API_BASE, endpoint))
|
||||
|
||||
const response = await fetch(urlJoined, {
|
||||
...options,
|
||||
@@ -100,12 +172,16 @@ async function fetchApi<T>(
|
||||
: { success: false as const, error: undefined }
|
||||
|
||||
if (!isJson) {
|
||||
const devHint =
|
||||
import.meta.env.DEV && response.status === 404
|
||||
? ` API_BASE=${API_BASE}`
|
||||
: ''
|
||||
return {
|
||||
error: {
|
||||
code: response.status === 404 ? 'NOT_FOUND' : 'INVALID_RESPONSE',
|
||||
message:
|
||||
response.status === 404
|
||||
? 'API 404: backend unreachable or wrong port — check server is running and VITE_DEV_API_ORIGIN matches PORT.'
|
||||
? `API 404: Backend antwortet nicht (falscher Port oder alter Prozess). Server starten: cd server && npm run start. PORT wie in server/.env.${devHint}`
|
||||
: `Expected JSON, got ${ct || 'unknown'} (HTTP ${response.status})`,
|
||||
},
|
||||
}
|
||||
@@ -122,10 +198,15 @@ async function fetchApi<T>(
|
||||
|
||||
return { success: true, data: data.data ?? data }
|
||||
} catch (error) {
|
||||
const base = error instanceof Error ? error.message : 'Network error'
|
||||
const devHint =
|
||||
import.meta.env.DEV
|
||||
? ` API_BASE=${API_BASE} — Ist der Server an diesem Port gestartet? (cd server && npm run start)`
|
||||
: ''
|
||||
return {
|
||||
error: {
|
||||
code: 'NETWORK_ERROR',
|
||||
message: error instanceof Error ? error.message : 'Network error',
|
||||
message: `${base}${devHint}`,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -217,6 +298,30 @@ export const api = {
|
||||
})
|
||||
},
|
||||
|
||||
async recoverEmails(accountId: string) {
|
||||
return fetchApi<{
|
||||
recovered: number
|
||||
folders: Array<{ folder: string; count: number }>
|
||||
message: string
|
||||
}>(`/email/recover/${accountId}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
},
|
||||
|
||||
/** Move mail from sort-related folders (Junk, Archive, MailFlow/…) back to INBOX and strip $MailFlow-* keywords (IMAP only). */
|
||||
async reSortEmails(accountId: string) {
|
||||
return fetchApi<{
|
||||
recovered: number
|
||||
folders: Array<{ folder: string; count: number }>
|
||||
mailFlowKeywordsStripped: number
|
||||
message: string
|
||||
}>(`/email/re-sort/${accountId}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
},
|
||||
|
||||
async sortDemo(count: number = 10) {
|
||||
return fetchApi<{
|
||||
sorted: number
|
||||
@@ -514,6 +619,17 @@ export const api = {
|
||||
return fetchApi<{ isAdmin: boolean }>('/me')
|
||||
},
|
||||
|
||||
async resetSortData(email: string) {
|
||||
return fetchApi<{
|
||||
reset: boolean
|
||||
deleted?: { stats: number; digests: number; usage: number }
|
||||
imapCleared?: number
|
||||
}>('/admin/reset-user-sort-data', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email }),
|
||||
})
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// NAME LABELS (Workers – Admin only)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user