Files
dashboards/components/LoginForm.tsx
T

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>
);
}