Connect Meshkee backend, blog and shared account session
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import { cache } from 'react';
|
||||
export const DOMAIN = 'sgemed.com';
|
||||
export const API_BASE = 'https://api.meshkee.com/api/v1';
|
||||
export const CUSTOMER_URL = 'https://customer.sgemed.com';
|
||||
export const CUSTOMER_PROFILE_URL = `${CUSTOMER_URL}/profile`;
|
||||
export const WEBSITE_MANAGEMENT_URL = 'https://business.sgemed.com/website';
|
||||
export class ApiError extends Error {
|
||||
constructor(public status: number) { super(`Meshkee request failed (${status})`); }
|
||||
}
|
||||
export async function api<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const response = await fetch(`${API_BASE}${path}`, { ...init, cache: 'no-store', signal: AbortSignal.timeout(12000) });
|
||||
if (!response.ok) throw new ApiError(response.status);
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
export const getTenant = cache(() => api<{id: number | string; nameFa: string}>(`/tenants/${DOMAIN}`));
|
||||
export async function tenantApi<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
await getTenant();
|
||||
return api<T>(`/tenants/${DOMAIN}${path}`, init);
|
||||
}
|
||||
export function safeUrl(value: unknown): string | undefined {
|
||||
if (typeof value !== 'string' || !value) return;
|
||||
try { const url = new URL(value); if (url.protocol === 'https:') return url.href; } catch {}
|
||||
}
|
||||
export async function getFavicon() {
|
||||
try {
|
||||
const data = await tenantApi<{faviconUrl: string | null}>('/website/favicon');
|
||||
return safeUrl(data.faviconUrl) || '/favicon.svg';
|
||||
} catch { return '/favicon.svg'; }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { cache } from 'react';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import { tenantApi, safeUrl } from './api';
|
||||
export type Blog = { id: string | number; title: string; slug?: string; excerpt?: string; summary?: string; content?: string; text?: string; coverUrl?: string; imageUrl?: string; publishedAt?: string };
|
||||
export type BlogList = { items: Blog[]; total: number; page: number; pageSize: number };
|
||||
export const getBlogs = cache(async (page: number) => {
|
||||
const data = await tenantApi<BlogList>(`/blogs?page=${page}&pageSize=12`);
|
||||
if (!Array.isArray(data.items) || typeof data.total !== 'number') throw new Error('Invalid blog list response');
|
||||
return data;
|
||||
});
|
||||
export const getBlog = cache(async (id: string) => {
|
||||
const data = await tenantApi<{blog: Blog}>(`/blogs/by-id/${encodeURIComponent(id)}`);
|
||||
if (!data.blog || !data.blog.id || typeof data.blog.title !== 'string') throw new Error('Invalid blog detail response');
|
||||
return data.blog;
|
||||
});
|
||||
export function titleSlug(title: string) { return title.trim().replace(/[^\p{L}\p{N}\s-]/gu, '').replace(/[\s-]+/g, '-') || 'article'; }
|
||||
export function blogHref(blog: Blog) { return `/blog/${encodeURIComponent(blog.id)}/${encodeURIComponent(titleSlug(blog.title))}`; }
|
||||
export function plainText(value: string) { return sanitizeHtml(value, {allowedTags: [], allowedAttributes: {}}); }
|
||||
export function summary(blog: Blog) { return plainText(blog.excerpt || blog.summary || blog.content || blog.text || '').slice(0,160); }
|
||||
export function cover(blog: Blog) { return safeUrl(blog.coverUrl || blog.imageUrl); }
|
||||
export function articleHtml(blog: Blog) {
|
||||
return sanitizeHtml(blog.content || blog.text || '', {
|
||||
allowedTags: ['p','br','h2','h3','h4','strong','b','em','i','ul','ol','li','blockquote','a','img','figure','figcaption','table','thead','tbody','tr','th','td','hr','pre','code'],
|
||||
allowedAttributes: {a:['href','title'],img:['src','alt','width','height']},
|
||||
allowedSchemes: ['https','http','mailto'], allowedSchemesByTag: {img:['https']},
|
||||
allowProtocolRelative: false, transformTags: {h1:'h2'},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export function normalizeCell(value: string) {
|
||||
const latin=value.replace(/[۰-۹]/g,c=>String('۰۱۲۳۴۵۶۷۸۹'.indexOf(c))).replace(/[٠-٩]/g,c=>String('٠١٢٣٤٥٦٧٨٩'.indexOf(c))).replace(/[\s()-]/g,'');
|
||||
if(latin.startsWith('09'))return '+98'+latin.slice(1);
|
||||
if(latin.startsWith('0098'))return '+'+latin.slice(2);
|
||||
return latin;
|
||||
}
|
||||
export function validateContact(input: Record<string,unknown>) {
|
||||
const fields=['title','name','email','cellNumber','text'] as const;
|
||||
const data=Object.fromEntries(fields.map(key=>[key,typeof input[key]==='string'?input[key].trim():''])) as Record<typeof fields[number],string>;
|
||||
const errors:Record<string,string>={};
|
||||
for(const key of ['title','name','text'] as const)if(!data[key])errors[key]='تکمیل این بخش ضروری است.';
|
||||
for(const [key,max] of Object.entries({title:200,name:120,email:254,cellNumber:25,text:5000}))if(data[key as keyof typeof data].length>max)errors[key]='متن واردشده بیش از حد طولانی است.';
|
||||
if(data.email&&!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email))errors.email='ایمیل معتبر وارد کنید.';
|
||||
data.cellNumber=normalizeCell(data.cellNumber);
|
||||
if(data.cellNumber&&!/^\+[1-9]\d{7,14}$/.test(data.cellNumber))errors.cellNumber='شماره را با ۰۹ یا کد کشور وارد کنید.';
|
||||
return {data,errors};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
export const ACCESS_COOKIE='meshkee_customer_access_token';
|
||||
export const REFRESH_COOKIE='meshkee_customer_refresh_token';
|
||||
export function cookieOptions(request:Request){
|
||||
const host=(request.headers.get('host')||'').split(':')[0];
|
||||
const production=host==='sgemed.com'||host.endsWith('.sgemed.com');
|
||||
return {path:'/',httpOnly:true,sameSite:'lax' as const,secure:production||new URL(request.url).protocol==='https:',...(production?{domain:'.sgemed.com'}:{})};
|
||||
}
|
||||
export function setSessionCookies(response:NextResponse,request:Request,tokens:{accessToken:string;refreshToken:string}){
|
||||
// Session lifetime remains controlled by the backend; never infer claims from an unverified JWT.
|
||||
const options=cookieOptions(request);
|
||||
response.cookies.set(ACCESS_COOKIE,tokens.accessToken,options);
|
||||
response.cookies.set(REFRESH_COOKIE,tokens.refreshToken,options);
|
||||
}
|
||||
export function sameOrigin(request:Request){
|
||||
const origin=request.headers.get('origin');
|
||||
if(!origin)return false;
|
||||
try{return new URL(origin).host===request.headers.get('host')}catch{return false}
|
||||
}
|
||||
Reference in New Issue
Block a user