320 lines
9.1 KiB
TypeScript
320 lines
9.1 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
import { useEffect, useState } from "react";
|
|
import AuthShell from "@/components/AuthShell";
|
|
import { AuthField, AuthPasswordField } from "@/components/AuthFields";
|
|
import { postLoginHref, useAuth } from "@/components/AuthProvider";
|
|
import { ApiError, getAccessToken, getRefreshToken } from "@/lib/meshkee-api";
|
|
import { toE164CellNumber } from "@/lib/cellNumber";
|
|
import {
|
|
getAdminDashboardOrigin,
|
|
getCustomerDashboardOrigin,
|
|
hostAppFor,
|
|
} from "@/lib/host";
|
|
import styles from "@/components/AuthShell.module.css";
|
|
|
|
const websiteUrl =
|
|
process.env.NEXT_PUBLIC_WEBSITE_URL?.replace(/\/$/, "") ||
|
|
"http://localhost:3000";
|
|
|
|
const tenantDomain =
|
|
process.env.NEXT_PUBLIC_MESHKEE_TENANT_DOMAIN?.trim() ||
|
|
(() => {
|
|
try {
|
|
return new URL(websiteUrl).hostname;
|
|
} catch {
|
|
return "iranbay.com";
|
|
}
|
|
})();
|
|
|
|
type AuthMode = "login" | "register";
|
|
|
|
function isSafeWebsiteReturn(next: string) {
|
|
try {
|
|
const url = new URL(next);
|
|
const allowed = new URL(websiteUrl);
|
|
return url.origin === allowed.origin;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function websiteHandoffUrl(next: string) {
|
|
const access = getAccessToken();
|
|
const refresh = getRefreshToken();
|
|
if (!access || !refresh) return null;
|
|
|
|
const target = new URL(next);
|
|
const hash = new URLSearchParams({
|
|
access,
|
|
refresh,
|
|
next: `${target.pathname}${target.search}${target.hash}` || "/register",
|
|
}).toString();
|
|
|
|
return `${target.origin}/auth/handoff#${hash}`;
|
|
}
|
|
|
|
function splitName(fullName: string) {
|
|
const parts = fullName.trim().split(/\s+/).filter(Boolean);
|
|
const firstName = parts[0] || "";
|
|
const lastName = parts.length > 1 ? parts.slice(1).join(" ") : firstName;
|
|
return { firstName, lastName };
|
|
}
|
|
|
|
async function finishAuth(
|
|
dashboard: "super_admin" | "business" | "customer",
|
|
returnToWebsite: string | null,
|
|
router: ReturnType<typeof useRouter>,
|
|
) {
|
|
if (returnToWebsite) {
|
|
const handoff = websiteHandoffUrl(returnToWebsite);
|
|
if (handoff) {
|
|
window.location.href = handoff;
|
|
return;
|
|
}
|
|
}
|
|
|
|
const href = postLoginHref(dashboard);
|
|
if (href.startsWith("http")) {
|
|
window.location.href = href;
|
|
} else {
|
|
router.push(href);
|
|
}
|
|
}
|
|
|
|
export default function LoginForm() {
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const { login, register } = useAuth();
|
|
const [error, setError] = useState("");
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [hostReady, setHostReady] = useState(false);
|
|
const [app, setApp] = useState<"admin" | "customer" | "business">("admin");
|
|
const [adminLoginHref, setAdminLoginHref] = useState("");
|
|
|
|
const nextParam = searchParams.get("next");
|
|
const returnToWebsite =
|
|
nextParam && isSafeWebsiteReturn(nextParam) ? nextParam : null;
|
|
const modeParam = searchParams.get("mode");
|
|
const [mode, setMode] = useState<AuthMode>(() =>
|
|
modeParam === "register" ? "register" : "login",
|
|
);
|
|
|
|
useEffect(() => {
|
|
const host = window.location.host;
|
|
const next = hostAppFor(host);
|
|
setApp(next);
|
|
setHostReady(true);
|
|
setAdminLoginHref(`${getAdminDashboardOrigin()}/login`);
|
|
if (next === "business") {
|
|
const loginUrl = new URL(`${getCustomerDashboardOrigin()}/login`);
|
|
if (returnToWebsite) loginUrl.searchParams.set("next", returnToWebsite);
|
|
if (mode === "register") loginUrl.searchParams.set("mode", "register");
|
|
window.location.replace(loginUrl.toString());
|
|
}
|
|
}, [returnToWebsite, mode]);
|
|
|
|
useEffect(() => {
|
|
if (modeParam === "register") setMode("register");
|
|
if (modeParam === "login") setMode("login");
|
|
}, [modeParam]);
|
|
|
|
if (!hostReady || app === "business") {
|
|
return (
|
|
<div
|
|
style={{
|
|
minHeight: "100vh",
|
|
display: "grid",
|
|
placeItems: "center",
|
|
background: "#f6f6f6",
|
|
fontSize: 12,
|
|
letterSpacing: "0.2em",
|
|
textTransform: "uppercase",
|
|
color: "#8e8e8e",
|
|
}}
|
|
>
|
|
Redirecting to login…
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const isRegister = mode === "register" && app === "customer";
|
|
const titleLine1 = isRegister ? "Create" : "Login To";
|
|
const titleLine2 = app === "customer" ? "Account" : "IranBay";
|
|
|
|
return (
|
|
<AuthShell
|
|
titleLine1={titleLine1}
|
|
titleLine2={titleLine2}
|
|
footerNote={
|
|
isRegister ? (
|
|
<button
|
|
type="button"
|
|
className={styles.noteLink}
|
|
onClick={() => {
|
|
setError("");
|
|
setMode("login");
|
|
}}
|
|
>
|
|
Already have an account? Login
|
|
</button>
|
|
) : (
|
|
<Link href="/forgot-password">Forgot password?</Link>
|
|
)
|
|
}
|
|
>
|
|
<form
|
|
className={styles.form}
|
|
autoComplete="off"
|
|
key={isRegister ? "register" : "login"}
|
|
onSubmit={async (e) => {
|
|
e.preventDefault();
|
|
setError("");
|
|
setIsSubmitting(true);
|
|
|
|
const form = new FormData(e.currentTarget);
|
|
const phone = String(form.get("phone") ?? "");
|
|
const password = String(form.get("password") ?? "");
|
|
|
|
try {
|
|
if (isRegister) {
|
|
const name = String(form.get("name") ?? "");
|
|
const email = String(form.get("email") ?? "").trim();
|
|
const { firstName, lastName } = splitName(name);
|
|
|
|
if (firstName.length < 2) {
|
|
throw new ApiError("Please enter your full name.", 400);
|
|
}
|
|
|
|
const dashboard = await register({
|
|
cellNumber: toE164CellNumber(phone),
|
|
password,
|
|
firstName,
|
|
lastName: lastName.length >= 2 ? lastName : firstName,
|
|
email: email || undefined,
|
|
domain: tenantDomain,
|
|
});
|
|
await finishAuth(dashboard, returnToWebsite, router);
|
|
} else {
|
|
const dashboard = await login(toE164CellNumber(phone), password);
|
|
await finishAuth(dashboard, returnToWebsite, router);
|
|
}
|
|
} catch (err) {
|
|
if (err instanceof ApiError) {
|
|
setError(err.message);
|
|
} else {
|
|
setError(
|
|
isRegister
|
|
? "Unable to create account. Is Meshkee API running?"
|
|
: "Unable to sign in. Is Meshkee API running?",
|
|
);
|
|
}
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
}}
|
|
>
|
|
{error ? (
|
|
<p className={styles.error} role="alert">
|
|
{error}
|
|
{app === "customer" &&
|
|
adminLoginHref &&
|
|
error.includes("super admin access") ? (
|
|
<>
|
|
{" "}
|
|
<a href={adminLoginHref}>Go to admin login</a>
|
|
</>
|
|
) : null}
|
|
</p>
|
|
) : null}
|
|
|
|
{returnToWebsite ? (
|
|
<p className={styles.hint}>
|
|
{isRegister
|
|
? "Create an account to continue business registration"
|
|
: "Sign in to continue business registration"}
|
|
</p>
|
|
) : null}
|
|
|
|
{isRegister ? (
|
|
<AuthField
|
|
label="Name"
|
|
name="name"
|
|
autoComplete="name"
|
|
required
|
|
disabled={isSubmitting}
|
|
/>
|
|
) : null}
|
|
|
|
<AuthField
|
|
label={isRegister ? "Cell Number" : "Phone Number"}
|
|
name="phone"
|
|
type="tel"
|
|
autoComplete="off"
|
|
dir="ltr"
|
|
required
|
|
disabled={isSubmitting}
|
|
/>
|
|
|
|
{isRegister ? (
|
|
<AuthField
|
|
label="Email Address"
|
|
name="email"
|
|
type="email"
|
|
autoComplete="email"
|
|
required
|
|
disabled={isSubmitting}
|
|
/>
|
|
) : null}
|
|
|
|
<AuthPasswordField
|
|
label="Password"
|
|
name="password"
|
|
autoComplete={isRegister ? "new-password" : "current-password"}
|
|
required
|
|
disabled={isSubmitting}
|
|
/>
|
|
|
|
<div className={styles.actions}>
|
|
{app === "customer" ? (
|
|
isRegister ? (
|
|
<button
|
|
type="button"
|
|
className={styles.secondaryBtn}
|
|
disabled={isSubmitting}
|
|
onClick={() => {
|
|
setError("");
|
|
setMode("login");
|
|
}}
|
|
>
|
|
Login instead
|
|
</button>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
className={styles.secondaryBtn}
|
|
disabled={isSubmitting}
|
|
onClick={() => {
|
|
setError("");
|
|
setMode("register");
|
|
}}
|
|
>
|
|
Create account
|
|
</button>
|
|
)
|
|
) : null}
|
|
<button
|
|
type="submit"
|
|
className={`outline-btn ${styles.primaryBtn}`}
|
|
disabled={isSubmitting}
|
|
>
|
|
{isSubmitting ? "…" : isRegister ? "REGISTER" : "LOGIN"}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</AuthShell>
|
|
);
|
|
}
|