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, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
}
function decodeXml(value: string): string {
return value
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/&/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
- Validează payload-ul primit în API route înainte de
saveProperty. - Păstrează
IMOPEDIA = 1pentru ofertele care trebuie publicate. - Folosește
AGENTIAprimit de la echipa Imopedia, nu o valoare introdusă de utilizator în interfață. - Continuă cu imaginile doar după ce
savePropertyrăspunde cu succes; vezi Exemple media.