Skip to the content.

Exemplu Next.js și React

Acest exemplu arată o integrare minimă pentru o aplicație Next.js cu interfață React. Apelurile către API-ul SOAP Imopedia trebuie făcute server-side, într-un API route sau server action, nu direct din browser.

Motivul principal este securitatea: userul API, parola API și session_id nu trebuie expuse în codul React livrat clientului.

Variabile de mediu

În .env.local:

IMOPEDIA_SOAP_WSDL=https://www.imopedia.ro/soap.wsdl
IMOPEDIA_USER=user@example.com
IMOPEDIA_PASSWORD=parola
IMOPEDIA_AGENCY_ID=123

API route Next.js

Exemplu pentru App Router, în app/api/imopedia/save-property/route.ts:

import { NextResponse } from "next/server";

export const runtime = "nodejs";

const SOAP_WSDL =
  process.env.IMOPEDIA_SOAP_WSDL ?? "https://www.imopedia.ro/soap.wsdl";

const SOAP_ENVELOPE = "http://schemas.xmlsoap.org/soap/envelope/";
const SOAP_ENCODING = "http://schemas.xmlsoap.org/soap/encoding/";
const XML_SCHEMA_INSTANCE = "http://www.w3.org/2001/XMLSchema-instance";
const SOAP_NAMESPACE = "syncapi.imopedia.ro";

const OPTION_TYPES: Record<string, string> = {
  saveProperty: "SavePropertyType",
  saveAgent: "SaveAgentType",
};

type SoapValue =
  | string
  | number
  | boolean
  | null
  | undefined
  | SoapValue[]
  | { [name: string]: SoapValue };

type SoapParams = Record<string, SoapValue>;

export async function POST(request: Request) {
  const body = await request.json();

  const username = requireEnv("IMOPEDIA_USER");
  const password = requireEnv("IMOPEDIA_PASSWORD");
  const agencyId = Number(requireEnv("IMOPEDIA_AGENCY_ID"));

  const sessionId = await callSoap<string>("login", {
    username,
    password,
  });

  try {
    const property = {
      AGENTIA: agencyId,
      ID_LOCAL: body.idLocal,
      AGENT_ID: body.agentId,
      IMOPEDIA: 1,
      TIP_IMOBIL_REAL: body.tipImobilReal,
      DATA_APARITIE: body.dataAparitie,
      DATA_MODIFICARE: body.dataModificare,
      TITLU: body.titlu,
      T_V_TRANZ: body.tranzactie,
      T_V_PRET: body.pret,
      T_V_MONEDA: body.moneda ?? "EUR",
      JUDET: body.judet,
      JUDET_ID: body.judetId,
      ORAS: body.oras,
      ZONA: body.zona,
      CONTACT_PERS: body.contactPers,
      CONTACT_EMAIL: body.contactEmail,
      CONTACT_TEL: body.contactTel,
    };

    const result = await callSoap("saveProperty", {
      session_id: sessionId,
      options: property,
    });

    return NextResponse.json({ ok: true, result });
  } catch (error) {
    return NextResponse.json(
      {
        ok: false,
        error: error instanceof Error ? error.message : "Eroare necunoscută",
      },
      { status: 502 },
    );
  } finally {
    await callSoap("logout", { session_id: sessionId }).catch(() => null);
  }
}

async function callSoap<T = unknown>(
  operation: string,
  params: SoapParams,
): Promise<T> {
  const envelope = buildEnvelope(operation, params);
  const response = await fetch(SOAP_WSDL, {
    method: "POST",
    headers: {
      "Content-Type": "text/xml; charset=utf-8",
      SOAPAction: `${SOAP_WSDL}?method=${operation}`,
    },
    body: envelope,
  });

  const responseText = await response.text();
  if (!response.ok && !responseText) {
    throw new Error(`HTTP ${response.status} de la endpointul SOAP`);
  }

  return parseSoapResult<T>(operation, responseText);
}

function buildEnvelope(operation: string, params: SoapParams): string {
  const body = Object.entries(params)
    .map(([name, value]) =>
      serializeElement(name, value, {
        optionType: name === "options" ? OPTION_TYPES[operation] : undefined,
      }),
    )
    .join("");

  return [
    '<?xml version="1.0" encoding="UTF-8"?>',
    `<soapenv:Envelope xmlns:soapenv="${SOAP_ENVELOPE}" xmlns:soapenc="${SOAP_ENCODING}" xmlns:xsi="${XML_SCHEMA_INSTANCE}" xmlns:tns="${SOAP_NAMESPACE}">`,
    "<soapenv:Body>",
    `<tns:${operation} soapenv:encodingStyle="${SOAP_ENCODING}">`,
    body,
    `</tns:${operation}>`,
    "</soapenv:Body>",
    "</soapenv:Envelope>",
  ].join("");
}

function serializeElement(
  name: string,
  value: SoapValue,
  options: { optionType?: string } = {},
): string {
  const typeAttribute = options.optionType
    ? ` xsi:type="tns:${options.optionType}"`
    : "";

  if (value === null || value === undefined) {
    return `<${name}${typeAttribute} />`;
  }

  if (Array.isArray(value)) {
    return `<${name}${typeAttribute}>${value
      .map((item) => serializeElement("item", item))
      .join("")}</${name}>`;
  }

  if (typeof value === "object") {
    const inner = Object.entries(value)
      .map(([childName, childValue]) => serializeElement(childName, childValue))
      .join("");
    return `<${name}${typeAttribute}>${inner}</${name}>`;
  }

  return `<${name}${typeAttribute}>${escapeXml(String(value))}</${name}>`;
}

function parseSoapResult<T>(operation: string, xml: string): T {
  const fault = xml.match(/<faultstring[^>]*>([\s\S]*?)<\/faultstring>/i);
  if (fault) {
    throw new Error(`${operation}: ${decodeXml(fault[1])}`);
  }

  const resultXml = extractElement(xml, `${operation}Return`);
  if (resultXml === null) {
    return null as T;
  }

  const entries = [
    ...resultXml.matchAll(
      /<[^:>]*:?item[^>]*>\s*<[^:>]*:?key[^>]*>([\s\S]*?)<\/[^:>]*:?key>\s*<[^:>]*:?value[^>]*>([\s\S]*?)<\/[^:>]*:?value>\s*<\/[^:>]*:?item>/gi,
    ),
  ];
  if (entries.length > 0) {
    return Object.fromEntries(
      entries.map((entry) => [decodeXml(entry[1]), decodeXml(stripTags(entry[2]))]),
    ) as T;
  }

  return decodeXml(stripTags(resultXml)) as T;
}

function extractElement(xml: string, name: string): string | null {
  const match = xml.match(new RegExp(`<[^:>]*:?${name}[^>]*>([\\s\\S]*?)<\\/[^:>]*:?${name}>`, "i"));
  return match ? match[1] : null;
}

function stripTags(value: string): string {
  return value.replace(/<[^>]+>/g, "").trim();
}

function escapeXml(value: string): string {
  return value
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
}

function decodeXml(value: string): string {
  return value
    .replace(/&lt;/g, "<")
    .replace(/&gt;/g, ">")
    .replace(/&quot;/g, '"')
    .replace(/&apos;/g, "'")
    .replace(/&amp;/g, "&")
    .trim();
}

function requireEnv(name: string): string {
  const value = process.env[name];
  if (!value) {
    throw new Error(`Lipsește variabila de mediu ${name}`);
  }
  return value;
}

Apel din componenta React

Componenta React trimite datele către API route-ul intern. Credențialele Imopedia nu ajung în browser.

"use client";

export function PublishPropertyButton() {
  async function publishProperty() {
    const response = await fetch("/api/imopedia/save-property", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        idLocal: 67654,
        agentId: 45,
        tipImobilReal: 1,
        dataAparitie: "2026-05-28",
        dataModificare: "2026-05-28",
        titlu: "Apartament 2 camere - exemplu",
        tranzactie: 1,
        pret: 120000,
        moneda: "EUR",
        judet: "Bucuresti",
        judetId: 10,
        oras: "Bucuresti",
        zona: "Central",
        contactPers: "Agent Exemplu",
        contactEmail: "agent@example.com",
        contactTel: "+40 700 000 000",
      }),
    });

    const result = await response.json();
    console.log(result);
  }

  return <button onClick={publishProperty}>Publică oferta</button>;
}

Recomandări