diff options
| author | Miqdad <ahmadmiqdad27@gmail.com> | 2025-09-30 22:42:46 +0700 |
|---|---|---|
| committer | Miqdad <ahmadmiqdad27@gmail.com> | 2025-09-30 22:42:46 +0700 |
| commit | 011c01a741f23734e4154342e9a560925687f152 (patch) | |
| tree | d874ad11b52eb9749a53af5d90958e3a76f219c0 | |
| parent | 352000c186a2765416cd2fab6693e68bb62e9e53 (diff) | |
<Miqdad> No auto loginrole
| -rw-r--r-- | app/lib/api/clearOdooSession.ts | 25 | ||||
| -rw-r--r-- | app/login/page.tsx | 45 | ||||
| -rw-r--r-- | app/page.tsx | 128 |
3 files changed, 114 insertions, 84 deletions
diff --git a/app/lib/api/clearOdooSession.ts b/app/lib/api/clearOdooSession.ts new file mode 100644 index 0000000..0ad9a5d --- /dev/null +++ b/app/lib/api/clearOdooSession.ts @@ -0,0 +1,25 @@ +export async function clearOdooSession(baseUrl: string) { + try { + if (baseUrl) { + await fetch(`${baseUrl}/web/session/destroy`, { + method: "POST", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: "{}", + }); + } + } catch { } + + // 2) hapus cookie session_id di browser + try { + const del = (name: string, domain?: string) => { + const d = domain ? `; domain=${domain}` : ""; + document.cookie = `${name}=; Max-Age=0; Path=/${d}`; + document.cookie = `${name}=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/${d}`; + }; + const host = window.location.hostname.replace(/^www\./, ""); + const parts = host.split("."); + const parent = parts.length >= 2 ? `.${parts.slice(-2).join(".")}` : undefined; + [undefined, host, parent].forEach(dom => del("session_id", dom)); + } catch { } +} diff --git a/app/login/page.tsx b/app/login/page.tsx index c0d7ab8..b4b0ed0 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -16,7 +16,7 @@ import { getAuth, setAuth } from "../lib/api/auth"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { setCookie } from "cookies-next"; - +import { clearOdooSession } from "../lib/api/clearOdooSession"; // Ambil tipe parameter untuk setAuth agar sesuai tepat dengan definisinya type AuthProps = Parameters<typeof setAuth>[0]; @@ -49,10 +49,16 @@ const Login = () => { const [loading, setLoading] = useState(false); useEffect(() => { + void clearOdooSession(process.env.NEXT_PUBLIC_ODOO_API_HOST ?? ""); const token = getAuth(); - if (token) router.push("/"); + if (token) router.replace("/"); }, [router]); + // useEffect(() => { + // const token = getAuth(); + // if (token) router.push("/"); + // }, [router]); + const validateInputs = (e: string, p: string, r: "" | Role) => { let ok = true; @@ -105,7 +111,8 @@ const Login = () => { const rawRole = fd.get("role"); const emailStr = typeof rawEmail === "string" ? rawEmail.trim() : ""; const passwordStr = typeof rawPassword === "string" ? rawPassword : ""; - const roleStr: "" | Role = rawRole === "driver" || rawRole === "dispatch" ? rawRole : ""; + const roleStr: "" | Role = + rawRole === "driver" || rawRole === "dispatch" ? rawRole : ""; if (!validateInputs(emailStr, passwordStr, roleStr)) return; @@ -125,9 +132,8 @@ const Login = () => { if (auth.user && typeof auth.user === "object") { setAuth(auth.user as AuthProps); } - // Simpan pilihan role agar bisa dipakai di halaman lain - setCookie("web_role", roleStr, { path: "/" }); - router.push("/"); + setCookie("web_role", roleStr, { path: "/", sameSite: "lax" }); + router.replace("/"); return; } @@ -140,7 +146,9 @@ const Login = () => { alert("Akun anda belum aktif"); break; default: - alert(res?.status?.description || "Login gagal. Periksa email/password."); + alert( + res?.status?.description || "Login gagal. Periksa email/password." + ); } } catch (error) { console.error(error); @@ -158,7 +166,11 @@ const Login = () => { <Typography component="h1" variant="h4" - sx={{ width: "100%", fontSize: "clamp(2rem, 10vw, 2.15rem)", mb: 4 }} + sx={{ + width: "100%", + fontSize: "clamp(2rem, 10vw, 2.15rem)", + mb: 4, + }} > Sign in </Typography> @@ -167,13 +179,19 @@ const Login = () => { component="form" onSubmit={handleSubmit} noValidate - sx={{ display: "flex", flexDirection: "column", width: "100%", gap: 2 }} + sx={{ + display: "flex", + flexDirection: "column", + width: "100%", + gap: 2, + }} > <FormControl> <FormLabel htmlFor="email">Email</FormLabel> <TextField error={emailError} helperText={emailErrorMessage} + disabled={loading} id="email" type="email" name="email" @@ -198,6 +216,7 @@ const Login = () => { <TextField error={passwordError} helperText={passwordErrorMessage} + disabled={loading} name="password" placeholder="••••••" type="password" @@ -220,6 +239,7 @@ const Login = () => { id="role" name="role" value={role} + disabled={loading} onChange={handleRoleChange} displayEmpty size="small" @@ -237,7 +257,12 @@ const Login = () => { )} </FormControl> - <Button type="submit" fullWidth variant="contained" disabled={loading}> + <Button + type="submit" + fullWidth + variant="contained" + disabled={loading} + > {loading ? "Loading..." : "Sign in"} </Button> </Box> diff --git a/app/page.tsx b/app/page.tsx index add141f..af5ada3 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -6,7 +6,14 @@ import SjCamera from "./lib/camera/component/sjCamera"; import DispatchCamera from "./lib/camera/component/dispatchCamera"; import useCameraStore from "./lib/camera/hooks/useCameraStore"; import Header from "./lib/camera/component/hedear"; -import { Button, FormControl, InputLabel, MenuItem, Select, FormHelperText } from "@mui/material"; +import { + Button, + FormControl, + InputLabel, + MenuItem, + Select, + FormHelperText, +} from "@mui/material"; import { SaveAsOutlined } from "@mui/icons-material"; import axios from "axios"; import odooApi from "./lib/api/odooApi"; @@ -14,18 +21,18 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { getAuth } from "./lib/api/auth"; import { getCookie } from "cookies-next"; +import { clearOdooSession } from "./lib/api/clearOdooSession"; type Role = "driver" | "dispatch"; type ShipMethod = "" | "self_pickup" | "indoteknik_delivery" | "ekspedisi"; export default function Home() { - const [isLogin, setIsLogin] = useState<boolean>(true); + const [isLogin, setIsLogin] = useState<boolean>(false); // start false biar nggak nge-flash const [isDriver, setIsDriver] = useState<boolean>(false); const [isDispatch, setIsDispatch] = useState<boolean>(false); const [shippingMethod, setShippingMethod] = useState<ShipMethod>(""); const [shipTouched, setShipTouched] = useState(false); - const { barcode, imageSj, @@ -40,18 +47,20 @@ export default function Home() { const [isLoading, setIsLoading] = useState<boolean>(false); const router = useRouter(); + // Single effect: auth gate + set role useEffect(() => { const auth = getAuth(); if (!auth) { - router.push("/login"); + void clearOdooSession(process.env.NEXT_PUBLIC_ODOO_API_HOST ?? ""); + router.replace("/login"); return; } - const roleCookie = (getCookie("web_role") as string | undefined)?.toLowerCase() as Role | undefined; - const role: Role | null = roleCookie === "driver" || roleCookie === "dispatch" ? roleCookie : null; - + const roleCookie = ( + getCookie("web_role") as string | undefined + )?.toLowerCase() as Role | undefined; + setIsDriver(roleCookie === "driver"); + setIsDispatch(roleCookie === "dispatch"); setIsLogin(true); - setIsDriver(role === "driver"); - setIsDispatch(role === "dispatch"); }, [router]); const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { @@ -72,7 +81,6 @@ export default function Home() { return; } - // Validasi foto sesuai role & shipping method if (isDispatch) { if (!imageDispatch) { @@ -80,7 +88,7 @@ export default function Home() { setIsLoading(false); return; } - // SJ opsional untuk self_pickup & ekspedisi → tidak divalidasi + // SJ opsional untuk self_pickup & ekspedisi } else { // Driver: SJ & Penerima wajib if (!imageSj || !imagePackage) { @@ -92,7 +100,9 @@ export default function Home() { try { const newSjImage = imageSj ? imageSj.replace(/^.*?,/, "") : undefined; - const newPackageImage = imagePackage ? imagePackage.replace(/^.*?,/, "") : undefined; + const newPackageImage = imagePackage + ? imagePackage.replace(/^.*?,/, "") + : undefined; const newDispatchImage = imageDispatch && imageDispatch.startsWith("data:") ? imageDispatch.replace(/^.*?,/, "") @@ -107,7 +117,6 @@ export default function Home() { if (submittedSj) data.sj_document = newSjImage!; if (submittedPackage) data.paket_document = newPackageImage!; if (submittedDispatch) data.dispatch_document = newDispatchImage!; - // (opsional) kirim shippingMethod jika backend mau simpan if (isDispatch && shippingMethod) data.shipping_method = shippingMethod; const response = (await odooApi( @@ -141,23 +150,23 @@ export default function Home() { } }; + // === UI helpers === // dispatch: SJ hanya utk self_pickup & ekspedisi const showSjForDispatch = - isDispatch && (shippingMethod === "self_pickup" || shippingMethod === "ekspedisi"); - + isDispatch && + (shippingMethod === "self_pickup" || shippingMethod === "ekspedisi"); // dispatch: kamera dispatch tampil utk semua method (asal sudah dipilih) const showDispatchForDispatch = isDispatch && shippingMethod !== ""; - // preview SJ: sembunyikan kalau dispatch belum pilih method const showSjPreview = !!imageSj && (!isDispatch || showSjForDispatch); - // preview Dispatch: sembunyikan kalau dispatch belum pilih method - const showDispatchPreview = !!imageDispatch && (!isDispatch || showDispatchForDispatch); + const showDispatchPreview = + !!imageDispatch && (!isDispatch || showDispatchForDispatch); return ( <div className="bg-white h-screen overflow-auto"> <Header /> - {isLogin && ( + {isLogin ? ( <div className="py-4 px-4 sm:px-96 pt-20"> <form onSubmit={handleSubmit}> <div> @@ -167,19 +176,32 @@ export default function Home() { {/* Shipping Method (khusus dispatch) */} {isDispatch && ( <div className="mt-4"> - <FormControl fullWidth size="small" required error={shipTouched && !shippingMethod}> - {/* shrink = kunci agar label tidak tumpuk dengan placeholder */} - <InputLabel id="shipping-label" shrink>Shipping Method</InputLabel> + <FormControl + fullWidth + size="small" + required + error={shipTouched && !shippingMethod} + > + <InputLabel id="shipping-label" shrink> + Shipping Method + </InputLabel> <Select labelId="shipping-label" id="shipping" label="Shipping Method" value={shippingMethod} displayEmpty - onChange={(e) => setShippingMethod(e.target.value as ShipMethod)} + onChange={(e) => + setShippingMethod(e.target.value as ShipMethod) + } onBlur={() => setShipTouched(true)} renderValue={(selected) => { - if (!selected) return <span style={{ opacity: 0.6 }}>Pilih Shipping Method</span>; + if (!selected) + return ( + <span style={{ opacity: 0.6 }}> + Pilih Shipping Method + </span> + ); const map: Record<string, string> = { self_pickup: "Self Pickup", indoteknik_delivery: "Indoteknik Delivery", @@ -192,17 +214,20 @@ export default function Home() { <em>Pilih Shipping Method</em> </MenuItem> <MenuItem value="self_pickup">Self Pickup</MenuItem> - <MenuItem value="indoteknik_delivery">Indoteknik Delivery</MenuItem> + <MenuItem value="indoteknik_delivery"> + Indoteknik Delivery + </MenuItem> <MenuItem value="ekspedisi">Ekspedisi</MenuItem> </Select> {shipTouched && !shippingMethod && ( - <FormHelperText>Wajib pilih shipping method.</FormHelperText> + <FormHelperText> + Wajib pilih shipping method. + </FormHelperText> )} </FormControl> </div> )} - <div className="h-4" /> <div className="flex justify-between"> @@ -210,19 +235,16 @@ export default function Home() { <> {showSjForDispatch && <SjCamera />} {showDispatchForDispatch && <DispatchCamera />} - {/* dispatch TIDAK perlu kamera penerima */} </> ) : ( <> {/* driver / non-dispatch */} <SjCamera /> <PackageCamera /> - {/* driver tidak menampilkan kamera dispatch */} </> )} </div> - <div className="h-2" /> {/* Preview SJ */} @@ -271,48 +293,7 @@ export default function Home() { </label> <div className="relative w-full h-[300px] border-2 border-gray-200 p-2 rounded-sm"> <Image - src={imageDispatch} - alt="Captured" - fill - unoptimized - className="p-2" - style={{ objectFit: "cover" }} - /> - </div> - </> - )} - - - <div className="h-2" /> - - {!isDispatch && imagePackage && ( - <> - <label className="block mt-2 text-sm font-medium text-gray-700 text-center"> - Gambar Foto Penerima - </label> - <div className="relative w-full h-[300px] border-2 border-gray-200 p-2 rounded-sm"> - <Image - src={imagePackage} - alt="Captured" - fill - unoptimized - className="p-2" - style={{ objectFit: "cover" }} - /> - </div> - </> - )} - - <div className="h-2" /> - - {!isDriver && imageDispatch && ( - <> - <label className="block mt-2 text-sm font-medium text-gray-700 text-center"> - Gambar Foto Dispatch - </label> - <div className="relative w-full h-[300px] border-2 border-gray-200 p-2 rounded-sm"> - <Image - src={imageDispatch} + src={imageDispatch!} alt="Captured" fill unoptimized @@ -336,8 +317,7 @@ export default function Home() { </Button> </form> </div> - )} - {!isLogin && ( + ) : ( <div className="py-4 px-4 sm:px-96 pt-20"> <div className="text-center"> <p className="text-2xl font-bold">Loading...</p> |
