Initial commit: backend
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
import {
|
||||
BadGatewayException,
|
||||
Injectable,
|
||||
Logger,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
export type MeshkeeBusinessUpdate = {
|
||||
name?: string;
|
||||
slug?: string;
|
||||
nameFa?: string;
|
||||
about?: string;
|
||||
categoryIds?: number[];
|
||||
};
|
||||
|
||||
export type MeshkeeCreateBusinessInput = {
|
||||
name: string;
|
||||
nameFa: string;
|
||||
slug?: string;
|
||||
about?: string;
|
||||
categoryIds: number[];
|
||||
ownerFirstName: string;
|
||||
ownerLastName: string;
|
||||
ownerFirstNameEn: string;
|
||||
ownerLastNameEn: string;
|
||||
ownerCellNumber: string;
|
||||
ownerPassword: string;
|
||||
};
|
||||
|
||||
export type MeshkeeAuthUser = {
|
||||
id: string;
|
||||
cellNumber: string;
|
||||
email: string | null;
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
firstNameEn: string | null;
|
||||
lastNameEn: string | null;
|
||||
dashboard: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class MeshkeeClient {
|
||||
private readonly logger = new Logger(MeshkeeClient.name);
|
||||
|
||||
constructor(private readonly config: ConfigService) {}
|
||||
|
||||
private apiBase(): string {
|
||||
return (
|
||||
this.config.get<string>('MESHKEE_API_URL')?.replace(/\/$/, '') ||
|
||||
'http://localhost:3010/api/v1'
|
||||
);
|
||||
}
|
||||
|
||||
private authHeader(authorization?: string): string | null {
|
||||
if (authorization?.trim()) return authorization.trim();
|
||||
const token = this.config.get<string>('MESHKEE_API_TOKEN')?.trim();
|
||||
return token ? `Bearer ${token}` : null;
|
||||
}
|
||||
|
||||
private serviceAuth(): string {
|
||||
const auth = this.authHeader();
|
||||
if (!auth) {
|
||||
throw new UnauthorizedException(
|
||||
'MESHKEE_API_TOKEN is required for Meshkee admin operations',
|
||||
);
|
||||
}
|
||||
return auth;
|
||||
}
|
||||
|
||||
private async parseError(response: Response, fallback: string) {
|
||||
const text = await response.text().catch(() => '');
|
||||
try {
|
||||
const json = JSON.parse(text) as { message?: string | string[] };
|
||||
if (Array.isArray(json.message)) return json.message.join(', ');
|
||||
if (typeof json.message === 'string') return json.message;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return text.slice(0, 300) || fallback;
|
||||
}
|
||||
|
||||
async getMe(authorization: string): Promise<MeshkeeAuthUser> {
|
||||
const response = await fetch(`${this.apiBase()}/auth/me`, {
|
||||
headers: { Authorization: authorization },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new UnauthorizedException('Invalid or expired Meshkee session');
|
||||
}
|
||||
const payload = (await response.json()) as { user: MeshkeeAuthUser };
|
||||
return payload.user;
|
||||
}
|
||||
|
||||
async listBusinessCategories(authorization?: string) {
|
||||
const auth = this.authHeader(authorization) ?? this.serviceAuth();
|
||||
const response = await fetch(`${this.apiBase()}/business-categories`, {
|
||||
headers: { Authorization: auth },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new BadGatewayException(
|
||||
await this.parseError(response, 'Failed to list Meshkee categories'),
|
||||
);
|
||||
}
|
||||
const payload = (await response.json()) as {
|
||||
items?: Array<{ id: string | number; slug?: string }>;
|
||||
};
|
||||
return payload.items ?? [];
|
||||
}
|
||||
|
||||
async createBusiness(
|
||||
body: MeshkeeCreateBusinessInput,
|
||||
authorization?: string,
|
||||
) {
|
||||
const auth = this.authHeader(authorization) ?? this.serviceAuth();
|
||||
const response = await fetch(`${this.apiBase()}/businesses`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: auth,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new BadGatewayException(
|
||||
await this.parseError(response, 'Failed to create Meshkee business'),
|
||||
);
|
||||
}
|
||||
const data = (await response.json()) as {
|
||||
id: string | number;
|
||||
slug?: string;
|
||||
name?: string;
|
||||
};
|
||||
return { ...data, id: String(data.id) };
|
||||
}
|
||||
|
||||
async addDomain(businessId: string, host: string, authorization?: string) {
|
||||
const auth = this.authHeader(authorization) ?? this.serviceAuth();
|
||||
const response = await fetch(
|
||||
`${this.apiBase()}/businesses/${businessId}/domains`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: auth,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
host,
|
||||
isPrimary: true,
|
||||
updateDns: false,
|
||||
}),
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new BadGatewayException(
|
||||
await this.parseError(response, 'Failed to attach Meshkee domain'),
|
||||
);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/** Write profile / category fields to Meshkee (source of truth for content). */
|
||||
async updateBusiness(
|
||||
meshkeeBusinessId: string,
|
||||
body: MeshkeeBusinessUpdate,
|
||||
authorization?: string,
|
||||
): Promise<void> {
|
||||
const auth = this.authHeader(authorization);
|
||||
if (!auth) {
|
||||
this.logger.warn(
|
||||
`Skipping Meshkee update for business ${meshkeeBusinessId}: no auth token`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: Record<string, unknown> = {};
|
||||
if (body.name !== undefined) payload.name = body.name;
|
||||
if (body.slug !== undefined) payload.slug = body.slug;
|
||||
if (body.nameFa !== undefined) payload.nameFa = body.nameFa;
|
||||
if (body.about !== undefined) payload.about = body.about;
|
||||
if (body.categoryIds !== undefined) payload.categoryIds = body.categoryIds;
|
||||
|
||||
if (Object.keys(payload).length === 0) return;
|
||||
|
||||
const response = await fetch(
|
||||
`${this.apiBase()}/businesses/${meshkeeBusinessId}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
Authorization: auth,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => '');
|
||||
this.logger.error(
|
||||
`Meshkee update failed (${response.status}): ${text.slice(0, 300)}`,
|
||||
);
|
||||
throw new BadGatewayException(
|
||||
`Failed to update Meshkee business (${response.status})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user