diff --git a/src/app/api/eterra/export-bundle/route.ts b/src/app/api/eterra/export-bundle/route.ts index 633f0ee..6e8e8cf 100644 --- a/src/app/api/eterra/export-bundle/route.ts +++ b/src/app/api/eterra/export-bundle/route.ts @@ -9,6 +9,11 @@ import { clearProgress, setProgress, } from "@/modules/parcel-sync/services/progress-store"; +import { + getSessionCredentials, + registerJob, + unregisterJob, +} from "@/modules/parcel-sync/services/session-store"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; @@ -22,8 +27,14 @@ type ExportBundleRequest = { }; const validate = (body: ExportBundleRequest) => { - const username = String(body.username ?? "").trim(); - const password = String(body.password ?? "").trim(); + // Priority: request body > session store > env vars + const session = getSessionCredentials(); + const username = String( + body.username || session?.username || process.env.ETERRA_USERNAME || "", + ).trim(); + const password = String( + body.password || session?.password || process.env.ETERRA_PASSWORD || "", + ).trim(); const siruta = String(body.siruta ?? "").trim(); const jobId = body.jobId ? String(body.jobId).trim() : undefined; const mode = body.mode === "magic" ? "magic" : "base"; @@ -158,6 +169,7 @@ export async function POST(req: Request) { const body = (await req.json()) as ExportBundleRequest; const validated = validate(body); jobId = validated.jobId; + if (jobId) registerJob(jobId); pushProgress(); const terenuriLayer = findLayerById("TERENURI_ACTIVE"); @@ -816,6 +828,7 @@ export async function POST(req: Request) { validated.mode === "magic" ? `eterra_uat_${validated.siruta}_magic.zip` : `eterra_uat_${validated.siruta}_terenuri_cladiri.zip`; + if (jobId) unregisterJob(jobId); return new Response(new Uint8Array(zipBuffer), { headers: { "Content-Type": "application/zip", @@ -830,6 +843,7 @@ export async function POST(req: Request) { note = undefined; pushProgress(); scheduleClear(jobId); + if (jobId) unregisterJob(jobId); const lower = errMessage.toLowerCase(); const statusCode = lower.includes("login failed") || lower.includes("session") ? 401 : 400; diff --git a/src/app/api/eterra/export-layer-gpkg/route.ts b/src/app/api/eterra/export-layer-gpkg/route.ts index b275449..c546980 100644 --- a/src/app/api/eterra/export-layer-gpkg/route.ts +++ b/src/app/api/eterra/export-layer-gpkg/route.ts @@ -8,6 +8,11 @@ import { clearProgress, setProgress, } from "@/modules/parcel-sync/services/progress-store"; +import { + getSessionCredentials, + registerJob, + unregisterJob, +} from "@/modules/parcel-sync/services/session-store"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; @@ -21,8 +26,14 @@ type ExportLayerRequest = { }; const validate = (body: ExportLayerRequest) => { - const username = String(body.username ?? "").trim(); - const password = String(body.password ?? "").trim(); + // Priority: request body > session store > env vars + const session = getSessionCredentials(); + const username = String( + body.username || session?.username || process.env.ETERRA_USERNAME || "", + ).trim(); + const password = String( + body.password || session?.password || process.env.ETERRA_PASSWORD || "", + ).trim(); const siruta = String(body.siruta ?? "").trim(); const layerId = String(body.layerId ?? "").trim(); const jobId = body.jobId ? String(body.jobId).trim() : undefined; @@ -122,6 +133,7 @@ export async function POST(req: Request) { const body = (await req.json()) as ExportLayerRequest; const validated = validate(body); jobId = validated.jobId; + if (jobId) registerJob(jobId); pushProgress(); const layer = findLayerById(validated.layerId); @@ -245,6 +257,7 @@ export async function POST(req: Request) { pushProgress(); scheduleClear(jobId); + if (jobId) unregisterJob(jobId); const filename = `eterra_uat_${validated.siruta}_${layer.name}.gpkg`; return new Response(new Uint8Array(gpkg), { headers: { @@ -260,6 +273,7 @@ export async function POST(req: Request) { note = undefined; pushProgress(); scheduleClear(jobId); + if (jobId) unregisterJob(jobId); const lower = errMessage.toLowerCase(); const statusCode = lower.includes("login failed") || lower.includes("session") ? 401 : 400; diff --git a/src/app/api/eterra/login/route.ts b/src/app/api/eterra/login/route.ts index b8f3df6..dd3d67d 100644 --- a/src/app/api/eterra/login/route.ts +++ b/src/app/api/eterra/login/route.ts @@ -1,9 +1,14 @@ import { NextResponse } from "next/server"; import { EterraClient } from "@/modules/parcel-sync/services/eterra-client"; +import { createSession } from "@/modules/parcel-sync/services/session-store"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; +/** + * POST /api/eterra/login + * Legacy endpoint — kept for backward compat. Prefer /api/eterra/session. + */ export async function POST(req: Request) { try { const body = (await req.json()) as { username?: string; password?: string }; @@ -24,6 +29,7 @@ export async function POST(req: Request) { ); await EterraClient.create(username, password); + createSession(username, password); return NextResponse.json({ success: true }); } catch (error) { const message = error instanceof Error ? error.message : "Eroare server"; diff --git a/src/app/api/eterra/session/route.ts b/src/app/api/eterra/session/route.ts new file mode 100644 index 0000000..156569e --- /dev/null +++ b/src/app/api/eterra/session/route.ts @@ -0,0 +1,95 @@ +import { NextResponse } from "next/server"; +import { EterraClient } from "@/modules/parcel-sync/services/eterra-client"; +import { + createSession, + destroySession, + forceDestroySession, + getSessionCredentials, + getSessionStatus, +} from "@/modules/parcel-sync/services/session-store"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +/** + * GET /api/eterra/session — returns current server-side session status. + * Any client can call this to check if eTerra is connected. + */ +export async function GET() { + return NextResponse.json(getSessionStatus()); +} + +/** + * POST /api/eterra/session — connect or disconnect. + * + * Connect: { action: "connect", username?, password? } + * Disconnect: { action: "disconnect", force?: boolean } + */ +export async function POST(req: Request) { + try { + const body = (await req.json()) as { + action?: string; + username?: string; + password?: string; + force?: boolean; + }; + + const action = body.action ?? "connect"; + + if (action === "disconnect") { + if (body.force) { + forceDestroySession(); + return NextResponse.json({ success: true, disconnected: true }); + } + const result = destroySession(); + if (!result.destroyed) { + return NextResponse.json( + { success: false, error: result.reason }, + { status: 409 }, + ); + } + return NextResponse.json({ success: true, disconnected: true }); + } + + // Connect + const username = ( + body.username ?? + process.env.ETERRA_USERNAME ?? + "" + ).trim(); + const password = ( + body.password ?? + process.env.ETERRA_PASSWORD ?? + "" + ).trim(); + + if (!username || !password) { + return NextResponse.json( + { error: "Credențiale eTerra lipsă" }, + { status: 400 }, + ); + } + + // Check if already connected with same credentials + const existing = getSessionCredentials(); + if (existing && existing.username === username) { + // Already connected — verify session is still alive by pinging + try { + await EterraClient.create(username, password); + return NextResponse.json({ success: true, alreadyConnected: true }); + } catch { + // Session expired, re-login below + } + } + + // Attempt login + await EterraClient.create(username, password); + createSession(username, password); + + return NextResponse.json({ success: true }); + } catch (error) { + const message = error instanceof Error ? error.message : "Eroare server"; + const status = message.toLowerCase().includes("login") ? 401 : 500; + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/src/modules/parcel-sync/components/parcel-sync-module.tsx b/src/modules/parcel-sync/components/parcel-sync-module.tsx index 6df49ab..5272f21 100644 --- a/src/modules/parcel-sync/components/parcel-sync-module.tsx +++ b/src/modules/parcel-sync/components/parcel-sync-module.tsx @@ -24,10 +24,7 @@ import { Button } from "@/shared/components/ui/button"; import { Input } from "@/shared/components/ui/input"; import { Label } from "@/shared/components/ui/label"; import { Badge } from "@/shared/components/ui/badge"; -import { - Card, - CardContent, -} from "@/shared/components/ui/card"; +import { Card, CardContent } from "@/shared/components/ui/card"; import { Tabs, TabsContent, @@ -56,6 +53,14 @@ import type { ParcelFeature } from "../types"; type UatEntry = { siruta: string; name: string; county?: string }; +type SessionStatus = { + connected: boolean; + username?: string; + connectedAt?: string; + activeJobCount: number; + activeJobPhase?: string; +}; + type ExportProgress = { jobId: string; downloaded: number; @@ -100,30 +105,20 @@ function formatArea(val?: number | null) { /* ------------------------------------------------------------------ */ function ConnectionPill({ - connected, + session, connecting, connectionError, - connectedAt, - username, - password, - onUsernameChange, - onPasswordChange, - onConnect, onDisconnect, }: { - connected: boolean; + session: SessionStatus; connecting: boolean; connectionError: string; - connectedAt: Date | null; - username: string; - password: string; - onUsernameChange: (v: string) => void; - onPasswordChange: (v: string) => void; - onConnect: () => void; onDisconnect: () => void; }) { - const elapsed = connectedAt - ? Math.floor((Date.now() - connectedAt.getTime()) / 60_000) + const elapsed = session.connectedAt + ? Math.floor( + (Date.now() - new Date(session.connectedAt).getTime()) / 60_000, + ) : 0; const elapsedLabel = elapsed < 1 @@ -140,7 +135,7 @@ function ConnectionPill({ className={cn( "flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs font-medium transition-all", "hover:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", - connected + session.connected ? "border-emerald-200 bg-emerald-50/80 text-emerald-700 dark:border-emerald-800 dark:bg-emerald-950/40 dark:text-emerald-400" : connectionError ? "border-rose-200 bg-rose-50/80 text-rose-600 dark:border-rose-800 dark:bg-rose-950/40 dark:text-rose-400" @@ -149,7 +144,7 @@ function ConnectionPill({ > {connecting ? ( - ) : connected ? ( + ) : session.connected ? ( @@ -162,7 +157,7 @@ function ConnectionPill({ {connecting ? "Se conectează…" - : connected + : session.connected ? "eTerra" : connectionError ? "Eroare" @@ -176,7 +171,7 @@ function ConnectionPill({
Conexiune eTerra - {connected && ( + {session.connected && ( {elapsedLabel} )}
- {connected && username && ( + {session.connected && session.username && (

- {username} + {session.username}

)} {connectionError && ( -

{connectionError}

+

+ {connectionError} +

)} - {/* Credentials form */} - {!connected && ( -
-
- - onUsernameChange(e.target.value)} - autoComplete="username" - className="h-8 text-xs" - /> -
-
- - onPasswordChange(e.target.value)} - autoComplete="current-password" - className="h-8 text-xs" - /> -
- + {/* Info when not connected */} + {!session.connected && !connectionError && ( +
+

Conexiunea se face automat când începi să scrii un UAT.

+

+ Credențialele sunt preluate din configurarea serverului. +

)} - {/* Connected actions */} - {connected && ( + {/* Error detail */} + {!session.connected && connectionError && ( +
+

+ Conexiunea automată a eșuat. Verifică credențialele din + variabilele de mediu (ETERRA_USERNAME / ETERRA_PASSWORD). +

+
+ )} + + {/* Connected — active jobs info + disconnect */} + {session.connected && ( <> + {session.activeJobCount > 0 && ( +
+

+ + {session.activeJobCount} job + {session.activeJobCount > 1 ? "-uri" : ""} activ + {session.activeJobCount > 1 ? "e" : ""} + + {session.activeJobPhase && ( + + {" "} + — {session.activeJobPhase} + + )} +

+
+ )}
@@ -912,12 +935,12 @@ export function ParcelSyncModule() { {/* Tab 2: Layer catalog */} {/* ═══════════════════════════════════════════════════════ */} - {!sirutaValid || !connected ? ( + {!sirutaValid || !session.connected ? (

- {!connected + {!session.connected ? "Conectează-te la eTerra și selectează un UAT." : "Selectează un UAT pentru a vedea catalogul de layere."}

@@ -1045,7 +1068,7 @@ export function ParcelSyncModule() { {/* ═══════════════════════════════════════════════════════ */} {/* Hero buttons */} - {sirutaValid && connected ? ( + {sirutaValid && session.connected ? (