Initial commit: Negin Dental Clinic website
Next.js (App Router) clone of negindentalclinic.ir, wired to the Meshkee Website API (blog, contact form, favicon), with on-page SEO and a scroll-reveal animation sequence on the homepage. See context.md for full project context. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
// Lightweight client-side session bridge for the Meshkee customer account.
|
||||
//
|
||||
// This storefront has no login form of its own — signing in happens on the
|
||||
// separate customer portal (CUSTOMER_PORTAL_URL). What's implemented here is
|
||||
// the "come back logged in" half of that flow:
|
||||
//
|
||||
// 1. The customer portal sends the shopper back to this site with
|
||||
// `?token=<accessToken>&refreshToken=<refreshToken>` in the URL.
|
||||
// 2. On first render we pick those up, store them, and strip them from
|
||||
// the address bar.
|
||||
// 3. From then on we call `GET /auth/me` with the stored access token to
|
||||
// know who's signed in (and to detect an admin, for "مدیریت وبسایت").
|
||||
//
|
||||
// NOTE: the exact hand-back mechanism isn't published in the public API
|
||||
// docs (auth is plain Bearer-JWT, not a shared cookie), so this is the
|
||||
// standard "redirect back with tokens in the query string" pattern. If
|
||||
// Meshkee's customer portal actually bridges sessions a different way
|
||||
// (shared cookie, postMessage, …), swap the implementation in this file —
|
||||
// everything that reads auth state goes through the hooks below.
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { API_BASE } from "./meshkee";
|
||||
|
||||
const ACCESS_TOKEN_KEY = "meshkee_access_token";
|
||||
const REFRESH_TOKEN_KEY = "meshkee_refresh_token";
|
||||
|
||||
export function getStoredAccessToken() {
|
||||
if (typeof window === "undefined") return null;
|
||||
try {
|
||||
return window.localStorage.getItem(ACCESS_TOKEN_KEY);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function storeTokens({ accessToken, refreshToken }) {
|
||||
try {
|
||||
if (accessToken) window.localStorage.setItem(ACCESS_TOKEN_KEY, accessToken);
|
||||
if (refreshToken) window.localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken);
|
||||
} catch {
|
||||
/* localStorage unavailable (private mode, etc.) — session just won't persist */
|
||||
}
|
||||
}
|
||||
|
||||
export function clearStoredTokens() {
|
||||
try {
|
||||
window.localStorage.removeItem(ACCESS_TOKEN_KEY);
|
||||
window.localStorage.removeItem(REFRESH_TOKEN_KEY);
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
}
|
||||
|
||||
/** Picks up `?token=&refreshToken=` from a customer-portal hand-back, once. */
|
||||
function captureTokenFromUrl() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const token = params.get("token") || params.get("accessToken");
|
||||
const refreshToken = params.get("refreshToken");
|
||||
if (!token) return false;
|
||||
|
||||
storeTokens({ accessToken: token, refreshToken });
|
||||
params.delete("token");
|
||||
params.delete("accessToken");
|
||||
params.delete("refreshToken");
|
||||
const query = params.toString();
|
||||
const cleanUrl = window.location.pathname + (query ? `?${query}` : "") + window.location.hash;
|
||||
window.history.replaceState({}, "", cleanUrl);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function fetchCurrentUser(accessToken) {
|
||||
const res = await fetch(`${API_BASE}/auth/me`, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json().catch(() => null);
|
||||
return data?.user ?? data ?? null;
|
||||
}
|
||||
|
||||
/** True when the account object looks like a business admin, not a regular customer. */
|
||||
export function isAdminUser(user) {
|
||||
if (!user) return false;
|
||||
const role = String(user.role || user.type || "").toLowerCase();
|
||||
return Boolean(user.isAdmin) || role.includes("admin") || role.includes("owner");
|
||||
}
|
||||
|
||||
/**
|
||||
* Session state for the header: `{ status: 'loading' | 'guest' | 'in', user, logout }`.
|
||||
*/
|
||||
export function useMeshkeeSession() {
|
||||
const [state, setState] = useState({ status: "loading", user: null });
|
||||
|
||||
const load = useCallback(async () => {
|
||||
captureTokenFromUrl();
|
||||
const token = getStoredAccessToken();
|
||||
if (!token) {
|
||||
setState({ status: "guest", user: null });
|
||||
return;
|
||||
}
|
||||
const user = await fetchCurrentUser(token);
|
||||
if (user) {
|
||||
setState({ status: "in", user });
|
||||
} else {
|
||||
clearStoredTokens();
|
||||
setState({ status: "guest", user: null });
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
clearStoredTokens();
|
||||
setState({ status: "guest", user: null });
|
||||
}, []);
|
||||
|
||||
return { ...state, logout };
|
||||
}
|
||||
Reference in New Issue
Block a user