+
-
{backgroundElements}
-
+
+ {" "}
+
-
-
-
- {t('forgotPassword.backToLogin')}
-
-
-
-
- {[1, 2].map((s) => (
- = s ? 'bg-blue-500' : 'bg-gray-700'}`} animate={{ scaleX: step >= s ? 1 : 0.5 }} />
- ))}
-
-
+
-
-
-
-
-
-
- {step === 1 ? t('register.tenant.step1Title') : t('register.tenant.step2Title')}
- {step === 1 ? t('register.tenant.step1Desc') : t('register.tenant.step2Desc')}
-
-
-
+
-
{ e.preventDefault(); handleNextStep(); } : handleSubmit} className="space-y-6">
- {step === 1 && (
- <>
-
-
-
-
-
-
-
-
{ setFormData({...formData, firstName: e.target.value}); setErrors({...errors, firstName: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.firstName ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.firstName')} />
-
- {errors.firstName &&
{errors.firstName}
}
-
-
-
-
{ setFormData({...formData, lastName: e.target.value}); setErrors({...errors, lastName: null}); }} className={`w-full px-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.lastName ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.lastName')} />
- {errors.lastName &&
{errors.lastName}
}
-
-
-
-
-
-
-
-
-
-
{ setFormData({...formData, email: e.target.value}); setErrors({...errors, email: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.email ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.emailPlaceholder')} />
-
- {errors.email && {errors.email}
}
-
-
-
-
-
-
-
{ setFormData({...formData, phone: e.target.value}); setErrors({...errors, phone: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.phone ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.phonePlaceholder')} />
-
- {errors.phone && {errors.phone}
}
-
-
-
-
-
-
-
{ setFormData({...formData, whatsapp: e.target.value}); setErrors({...errors, whatsapp: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.whatsapp ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.whatsappPlaceholder')} />
-
- {errors.whatsapp && {errors.whatsapp}
}
-
-
-
-
-
-
-
{ setFormData({...formData, phone2: e.target.value}); setErrors({...errors, phone2: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.phone2 ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.phoneSecondaryPlaceholder')} maxLength={7} />
-
- {errors.phone2 && {errors.phone2}
}
-
-
-
-
-
-
-
-
-
{ setFormData({...formData, nationalNumber: e.target.value}); setErrors({...errors, nationalNumber: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.nationalNumber ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.nationalNumberPlaceholder')} />
-
- {errors.nationalNumber && {errors.nationalNumber}
}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{ setFormData({...formData, password: e.target.value}); setErrors({...errors, password: null}); }} className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.password ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.passwordPlaceholder')} />
-
-
- {errors.password && {errors.password}
}
-
-
-
-
-
-
-
-
-
{ setFormData({...formData, confirmPassword: e.target.value}); setErrors({...errors, confirmPassword: null}); }} className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.confirmPassword ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.confirmPasswordPlaceholder')} />
-
- {formData.confirmPassword && (
{formData.password === formData.confirmPassword ? : }
)}
-
- {errors.confirmPassword && {errors.confirmPassword}
}
-
- >
- )}
-
- {step === 2 && (
- <>
-
- setFormData({...formData, agreeTerms: e.target.checked})} className="w-4 h-4 rounded border-gray-600 bg-white/5 text-blue-500 focus:ring-blue-500" required />
-
-
- >
- )}
-
+ { e.preventDefault(); handleNextStep(); }: handleSubmit }
+ className="space-y-6"
+ >
+ {step === 1 && }
+ {step === 2 && }
- {step === 1 ? (
- <>
-
-
- >
- ) : (
- <>
-
-
- >
- )}
+ {step === 1 ? : }
-
- {step === 1 && (
-
- {t('register.haveAccount')} {t('register.loginLink')}
-
- )}
+ {step === 1 && }
-
-
- {showOtpModal && (
-
-
-
-
-
{t('register.otpTitle')}
-
{t('register.otpSentTo')}
-
{formData.email}
-
-
-
-
-
-
setOtpCode(e.target.value)} className="w-full pr-12 pl-4 py-3 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 text-white text-center tracking-[0.5em] text-xl" placeholder="------" />
-
-
-
-
-
-
-
-
- )}
-
+
{showOtpModal && }
);
}
diff --git a/app/services/AuthService.js b/app/services/AuthService.js
index 35d8d6f..4de5b6e 100644
--- a/app/services/AuthService.js
+++ b/app/services/AuthService.js
@@ -1,4 +1,5 @@
import Cookies from "js-cookie";
+import { getCustomerByUserId, getOwnerByUserId, loginWithEmail, loginWithPhone } from "../utils/api";
/**
* AuthService
* Manages authentication tokens and user role detection via JWT decoding.
@@ -39,19 +40,21 @@ const AuthService = Object.freeze({
if (typeof window === "undefined") return null;
return Cookies.get(TOKEN_KEY);
},
-
deleteToken() {
- localStorage.removeItem(USER_KEY);
+ Cookies.remove(USER_KEY);
Cookies.remove(TOKEN_KEY);
},
-
/**
* Cache full user profile (from API)
* @param {object} user — { name, email, phone, ... }
*/
cacheUser(user) {
- if (!user) return;
- localStorage.setItem(USER_KEY, JSON.stringify(user));
+ Cookies.set(USER_KEY, JSON.stringify(user), {
+ expires: 7,
+ secure: true,
+ sameSite: "lax",
+ path: "/",
+ });
},
/**
@@ -59,8 +62,12 @@ const AuthService = Object.freeze({
* @returns {object|null}
*/
getCachedUser() {
+ const user = Cookies.get(USER_KEY);
+
+ if (!user) return null;
+
try {
- return JSON.parse(localStorage.getItem(USER_KEY));
+ return JSON.parse(user);
} catch {
return null;
}
@@ -88,7 +95,6 @@ const AuthService = Object.freeze({
getUser() {
const payload = this.decodeToken();
if (!payload) return null;
-
const cached = this.getCachedUser();
return {
@@ -96,7 +102,7 @@ const AuthService = Object.freeze({
name: cached?.name || payload["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"] || null,
email: cached?.email || payload["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"] || null,
phone: cached?.phone || payload["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/mobilephone"] || null,
- roles: this.getRoles(),
+ roles: cached?.roles,
};
},
@@ -107,7 +113,6 @@ const AuthService = Object.freeze({
getUserId() {
const user = this.getUser();
if (!user?.id) return null;
-
const parsedId = Number(user.id);
return Number.isFinite(parsedId) ? parsedId : user.id;
},
@@ -124,7 +129,21 @@ const AuthService = Object.freeze({
if (typeof roles === "string") return [roles];
return [];
},
+ async cacheCurrentUser(getOwnerByUserId, getCustomerByUserId) {
+ const authUser = this.getUser();
+ if (!authUser?.id) {
+ return;
+ }
+ const fetchFn = this.isOwner() ? getOwnerByUserId : getCustomerByUserId;
+ const profile = await fetchFn(authUser.id);
+ this.cacheUser({
+ name: profile.firstName + profile.lastName,
+ email: profile.email,
+ phone: profile.phoneNumber,
+ roles: this.getRoles(),
+ });
+ },
/**
* User has Owner role
* @returns {boolean}
@@ -164,6 +183,37 @@ const AuthService = Object.freeze({
isAuthenticated() {
return !!this.getToken();
},
+
+ async login({ loginMethod, credential, password }) {
+ const loginFn = loginMethod === "email" ? loginWithEmail : loginWithPhone;
+ const result = await loginFn(credential, password);
+ switch (result.status) {
+ case 200: {
+ const token = typeof result.data === "string" ? result.data : result.data.token;
+ this.addToken(token);
+ await this.cacheCurrentUser(getOwnerByUserId, getCustomerByUserId);
+ return {
+ type: "SUCCESS",
+ };
+ }
+
+ case 206: {
+ const token = typeof result.data === "string" ? result.data : result.data.token;
+ this.addToken(token);
+ if (loginMethod === "email") await sendEmailOTP();
+ else await sendPhoneOTP();
+ return {
+ type: "OTP_REQUIRED",
+ };
+ }
+
+ default:
+ return {
+ type: "ERROR",
+ result,
+ };
+ }
+ },
});
export default AuthService;
diff --git a/app/utils/api.js b/app/utils/api.js
index 693ffe4..aeb0e7a 100644
--- a/app/utils/api.js
+++ b/app/utils/api.js
@@ -1,6 +1,4 @@
-import { Toast } from "flowbite-react";
import AuthService from "../services/AuthService";
-
const API_BASE = process.env.NEXT_PUBLIC_API_URL || "https://45.93.137.91.nip.io/api";
const REPORT_API_BASE = process.env.NEXT_PUBLIC_REPORT_API_URL || "http://45.93.137.91/api";
@@ -82,7 +80,6 @@ async function apiFetch(endpoint, options = {}) {
}
} catch (error) {
if (error instanceof TypeError && error.message === "Failed to fetch") {
-
throw new Error(`تعذر الاتصال بالخادم. تحقق من اتصالك بالإنترنت أو حاول مرة أخرى لاحقاً. (${API_BASE}${endpoint})`);
}
throw error;
@@ -419,7 +416,6 @@ export async function uploadPicture(file) {
async function multipartAuthFetch(endpoint, formData) {
const token = AuthService.getToken();
-
const res = await fetch(`${API_BASE}${endpoint}`, {
method: "POST",
headers: {
@@ -450,7 +446,7 @@ async function multipartAuthFetch(endpoint, formData) {
};
}
-export async function addOwner(data, frontImage = null, backImage = null, licenseImage = null) {
+export async function addOwner(data, frontImage = null, backImage = null) {
const formData = new FormData();
formData.append("FirstName", data.firstName || data.FirstName || "");
@@ -471,8 +467,6 @@ export async function addOwner(data, frontImage = null, backImage = null, licens
if (frontImage) formData.append("FrontIdCarImagePath", frontImage);
if (backImage) formData.append("RearIdCarImagePath", backImage);
- if (licenseImage) formData.append("LicenseImagePath", licenseImage);
-
return multipartAuthFetch("/Owner/Add", formData);
}
diff --git a/app/validations/OwnerValidatoin.js b/app/validations/OwnerValidatoin.js
new file mode 100644
index 0000000..4010043
--- /dev/null
+++ b/app/validations/OwnerValidatoin.js
@@ -0,0 +1,54 @@
+// utils/registerValidation.js
+
+const validateEmail = (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email?.trim());
+
+// يدعم الأرقام مع البادئات اختياريًا
+const validatePhone = (phone) => /^(09|05)[0-9]{8}$/.test(phone?.trim());
+
+export const validateStep1 = (formData, setErrors, t) => {
+ const newErrors = {};
+
+ if (!formData.firstName?.trim()) newErrors.firstName = t("register.firstNameRequired");
+ if (!formData.lastName?.trim()) newErrors.lastName = t("register.lastNameRequired");
+
+ if (!formData.email?.trim()) {
+ newErrors.email = t("register.emailRequired");
+ } else if (!validateEmail(formData.email)) {
+ newErrors.email = t("register.emailInvalid");
+ }
+
+ if (!formData.phone?.trim()) {
+ newErrors.phone = t("register.phoneRequired");
+ } else if (!validatePhone(formData.phone)) {
+ newErrors.phone = `${t("register.phoneInvalid")} (${t("register.phoneMustStartWith")} 09/05)`;
+ }
+
+ if (!formData.whatsapp?.trim()) {
+ newErrors.whatsapp = t("register.whatsappRequired");
+ } else if (!validatePhone(formData.whatsapp)) {
+ newErrors.whatsapp = `${t("register.phoneInvalid")} (${t("register.phoneMustStartWith")} 09/05)`;
+ }
+
+ if (!formData.password) {
+ newErrors.password = t("register.passwordRequired");
+ } else if (formData.password.length < 6) {
+ newErrors.password = t("validation.passwordMinLength");
+ }
+
+ if (formData.password !== formData.confirmPassword) {
+ newErrors.confirmPassword = t("validation.passwordsMatch");
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+};
+
+export const validateStep2 = (idImages, setErrors, t) => {
+ const newErrors = {};
+
+ if (!idImages?.front) newErrors.front = t("register.frontIdRequired");
+ if (!idImages?.back) newErrors.back = t("register.backIdRequired");
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+};
diff --git a/public/NotUsed.txt b/public/NotUsed.txt
new file mode 100644
index 0000000..904ad1a
--- /dev/null
+++ b/public/NotUsed.txt
@@ -0,0 +1,169 @@
+: (
+ // /* OTP Verification Step */
+ //
+ //
+ //
+ //
+ // {t("sendOTPTo")}{" "}
+ //
+ // {formData.credential}
+ //
+ //
+ //
+
+ //
+ //
+ // {
+ // setOtpCode(e.target.value);
+ // if (otpError) setOtpError("");
+ // }}
+ // className={`w-full px-4 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white text-center text-2xl tracking-[0.5em] placeholder-gray-500 transition-all ${
+ // otpError ? "border-red-500" : "border-gray-700"
+ // }`}
+ // placeholder="______"
+ // maxLength={6}
+ // dir="ltr"
+ // />
+ // {otpError && (
+ //
+ // {otpError}
+ //
+ // )}
+ //
+
+ //
+ //
+ // {isLoading ? (
+ // <>
+ //
+ // {t("verifying")}
+ // >
+ // ) : isSuccess ? (
+ // <>
+ //
+ // {t("success")}
+ // >
+ // ) : (
+ // <>
+ //
+ // {t("verify")}
+ // >
+ // )}
+ //
+ //
+
+ //
+ //
+ //
+ //
+ //
+ <>>
+ // )}
+
+
+
+
+
+ // Send OTP
+ try {
+ if (loginMethod === "email") {
+ await sendEmailOTP();
+ } else {
+ await sendPhoneOTP();
+ }
+ } catch (otpErr) {
+ console.warn("[Login] OTP send failed, proceeding anyway:", otpErr);
+ }
+
+ setStep("otp");
+ } else if (result.status >= 500) {
+ console.error("[Login] Server error:", result.status, result.data);
+ toast.error(t("serverError"), {
+ style: { background: "#fee2e2", color: "#991b1b" },
+ });
+ } else if (result.status === 404) {
+ console.error("[Login] API endpoint not found:", result.status);
+ toast.error(t("apiNotFound"), {
+ style: { background: "#fee2e2", color: "#991b1b" },
+ });
+ } else if (result.status === 429) {
+ toast.error(t("tooManyRequests"), {
+ style: { background: "#fee2e2", color: "#991b1b" },
+ });
+ } else {
+ console.error("[Login] Unexpected status:", result.status, result.data);
+ toast.error(result.data?.message || result.data || t("invalidCredentials"), {
+ style: { background: "#fee2e2", color: "#991b1b" },
+ });
+ }
+ } catch (err) {
+ console.error("[Login] Error:", err);
+ toast.error(err.message || t("connectionError"), {
+ style: { background: "#fee2e2", color: "#991b1b" },
+ });
+ toast(t("otpRequired"), {
+ icon: "🔐",
+ style: { background: "#fef3c7", color: "#92400e" },
+ });
+
+ // Send OTP
+ try {
+ if (loginMethod === "email") {
+ await sendEmailOTP();
+ } else {
+ await sendPhoneOTP();
+ }
+ } catch (otpErr) {
+ console.warn("[Login] OTP send failed, proceeding anyway:", otpErr);
+ }
+
+ setStep("otp");
+ } else if (result.status >= 500) {
+ console.error("[Login] Server error:", result.status, result.data);
+ toast.error(t("serverError"), {
+ style: { background: "#fee2e2", color: "#991b1b" },
+ });
+ } else if (result.status === 404) {
+ console.error("[Login] API endpoint not found:", result.status);
+ toast.error(t("apiNotFound"), {
+ style: { background: "#fee2e2", color: "#991b1b" },
+ });
+ } else if (result.status === 429) {
+ toast.error(t("tooManyRequests"), {
+ style: { background: "#fee2e2", color: "#991b1b" },
+ });
+ } else {
+ console.error("[Login] Unexpected status:", result.status, result.data);
+ toast.error(result.data?.message || result.data || t("invalidCredentials"), {
+ style: { background: "#fee2e2", color: "#991b1b" },
+ });
+ }
+ } catch (err) {
+ console.error("[Login] Error:", err);
+ toast.error(err.message || t("connectionError"), {
+ style: { background: "#fee2e2", color: "#991b1b" },
+ });
+ } finally {
+ setIsLoading(false);
+ }
\ No newline at end of file