feat(parcel-sync): server-side eTerra session + auto-connect on UAT typing
- Add session-store.ts: global singleton for shared eTerra session state with job tracking (registerJob/unregisterJob/getRunningJobs) - Add GET/POST /api/eterra/session: connect/disconnect with job-running guard - Export routes: credential fallback chain (body > session > env vars), register/unregister active jobs for disconnect protection - Login route: also creates server-side session - ConnectionPill: session-aware display with job count, no credential form - Auto-connect: triggers on first UAT keystroke via autoConnectAttempted ref - Session polling: all clients poll GET /api/eterra/session every 30s - Multi-client: any browser sees shared connection state
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user