Fix CustomerType and OwnerType enums: send int instead of string
Some checks failed
Build frontend / build (push) Failing after 45s

- CustomerType: PERSONAL=0, FAMILY=1 (was 'Personal', 'Family')
- OwnerType: PERSON=0, REAL_ESTATE_AGENCY=1 (was 'peerson', 'RealEstateAgency')
- Backend Type column is int(11), sending strings caused 415 errors
This commit is contained in:
Claw AI
2026-03-28 14:15:40 +00:00
parent 9cddee841b
commit 6394f1d71a
12 changed files with 166 additions and 100 deletions

View File

@ -36,6 +36,8 @@ import {
} from "lucide-react"; } from "lucide-react";
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import AuthService from "../services/AuthService";
import { UserRole, UserRoleLabels } from "./enums/UserRole";
import "./i18n/config"; import "./i18n/config";
export default function ClientLayout({ children }) { export default function ClientLayout({ children }) {
@ -55,11 +57,17 @@ export default function ClientLayout({ children }) {
setCurrentLanguage(savedLanguage); setCurrentLanguage(savedLanguage);
i18n.changeLanguage(savedLanguage); i18n.changeLanguage(savedLanguage);
const storedUser = localStorage.getItem("user"); // Load user from JWT via AuthService
if (storedUser) { const authUser = AuthService.getUser();
const userData = JSON.parse(storedUser); if (authUser) {
console.log("User data loaded:", userData); setUser({
setUser(userData); name: authUser.name || authUser.email,
email: authUser.email,
phone: authUser.phone,
role: AuthService.isOwner() ? UserRole.OWNER : UserRole.CUSTOMER,
});
} else {
setUser(null);
} }
if (savedLanguage === "ar") { if (savedLanguage === "ar") {
@ -104,7 +112,7 @@ export default function ClientLayout({ children }) {
}; };
const logout = () => { const logout = () => {
localStorage.removeItem("user"); AuthService.deleteToken();
setUser(null); setUser(null);
setShowUserMenu(false); setShowUserMenu(false);
window.location.href = "/"; window.location.href = "/";
@ -119,11 +127,10 @@ export default function ClientLayout({ children }) {
const isProfilePage = pathname === "/profile"; const isProfilePage = pathname === "/profile";
const isOwner = user?.role === "owner"; const isOwner = user?.role === UserRole.OWNER;
const isAdmin = user?.role === "admin"; const isAdmin = user?.role === UserRole.ADMIN;
const isCustomer = user?.role === UserRole.CUSTOMER;
console.log("User role:", user?.role); const isAuthenticated = !!user;
console.log("Is Admin:", isAdmin);
const getUserInitial = () => { const getUserInitial = () => {
if (user?.name) { if (user?.name) {
@ -295,11 +302,7 @@ export default function ClientLayout({ children }) {
{user?.email || ""} {user?.email || ""}
</p> </p>
<p className="text-xs text-amber-100 mt-1"> <p className="text-xs text-amber-100 mt-1">
{isOwner {UserRoleLabels[user?.role] || 'زائر'}
? "مالك عقار"
: isAdmin
? "مدير النظام"
: "مستأجر"}
</p> </p>
</div> </div>
</div> </div>
@ -486,7 +489,7 @@ export default function ClientLayout({ children }) {
</> </>
)} )}
{!isOwner && !isAdmin && user && ( {isCustomer && (
<> <>
<div className="border-t border-gray-100 my-2"></div> <div className="border-t border-gray-100 my-2"></div>

View File

@ -4,8 +4,8 @@
* Used in: Customer registration (Customer/Add) * Used in: Customer registration (Customer/Add)
*/ */
const CustomerType = Object.freeze({ const CustomerType = Object.freeze({
PERSONAL: 'Personal', PERSONAL: 0,
FAMILY: 'Family', FAMILY: 1,
}); });
// Map value → Arabic label // Map value → Arabic label

View File

@ -4,8 +4,8 @@
* Used in: Owner registration (Owner/Add) * Used in: Owner registration (Owner/Add)
*/ */
const OwnerType = Object.freeze({ const OwnerType = Object.freeze({
PERSON: 'peerson', PERSON: 0,
REAL_ESTATE_AGENCY: 'RealEstateAgency', REAL_ESTATE_AGENCY: 1,
}); });
// Map value → Arabic label // Map value → Arabic label

View File

@ -1,25 +1,26 @@
/** /**
* UserRole Enum * UserRole Enum
* User account roles in the system * User account roles in the system
* Used in: JWT payload, registration, routing * Derived from JWT token claims
*/ */
const UserRole = Object.freeze({ const UserRole = Object.freeze({
GUEST: 'guest',
CUSTOMER: 'customer',
OWNER: 'owner', OWNER: 'owner',
TENANT: 'tenant',
ADMIN: 'admin', ADMIN: 'admin',
}); });
// Map role → Arabic label
const UserRoleLabels = Object.freeze({ const UserRoleLabels = Object.freeze({
[UserRole.GUEST]: 'زائر',
[UserRole.CUSTOMER]: 'مستأجر',
[UserRole.OWNER]: 'مالك عقار', [UserRole.OWNER]: 'مالك عقار',
[UserRole.TENANT]: 'مستأجر',
[UserRole.ADMIN]: 'مدير النظام', [UserRole.ADMIN]: 'مدير النظام',
}); });
// Map role → color theme (used in UI)
const UserRoleColors = Object.freeze({ const UserRoleColors = Object.freeze({
[UserRole.GUEST]: 'gray',
[UserRole.CUSTOMER]: 'blue',
[UserRole.OWNER]: 'amber', [UserRole.OWNER]: 'amber',
[UserRole.TENANT]: 'blue',
[UserRole.ADMIN]: 'red', [UserRole.ADMIN]: 'red',
}); });

View File

@ -93,24 +93,12 @@ export default function LoginPage() {
AuthService.addToken(token); AuthService.addToken(token);
console.log('[Login] Token stored via AuthService'); console.log('[Login] Token stored via AuthService');
// Decode token to get user info (basic JWT decode) // Decode token to get user info via AuthService
try { const authUser = AuthService.getUser();
const payload = JSON.parse(atob(token.split('.')[1])); const userRole = authUser?.roles?.includes('Owner') ? 'owner'
const user = { : authUser?.roles?.includes('Admin') ? 'admin'
name: payload.name || payload.unique_name || formData.credential, : 'customer';
email: payload.email || (loginMethod === 'email' ? formData.credential : ''), console.log('[Login] User role:', userRole);
phone: payload.phone || (loginMethod === 'phone' ? formData.credential : ''),
role: payload.role || payload.Role || 'customer',
};
localStorage.setItem('user', JSON.stringify(user));
console.log('[Login] User stored:', user);
} catch (decodeErr) {
console.warn('[Login] Could not decode JWT, storing credential as user');
localStorage.setItem('user', JSON.stringify({
name: formData.credential,
role: 'customer',
}));
}
setIsSuccess(true); setIsSuccess(true);
toast.success('تم تسجيل الدخول بنجاح!', { toast.success('تم تسجيل الدخول بنجاح!', {
@ -118,9 +106,7 @@ export default function LoginPage() {
}); });
setTimeout(() => { setTimeout(() => {
const user = JSON.parse(localStorage.getItem('user') || '{}'); if (userRole === 'admin') {
console.log('[Login] Redirecting user:', user);
if (user.role === 'admin') {
router.push('/admin'); router.push('/admin');
} else { } else {
router.push('/'); router.push('/');
@ -190,11 +176,6 @@ export default function LoginPage() {
console.log('[OTP] Token stored via AuthService'); console.log('[OTP] Token stored via AuthService');
} }
localStorage.setItem('user', JSON.stringify({
name: formData.credential,
role: 'customer',
}));
setIsSuccess(true); setIsSuccess(true);
toast.success('تم التحقق بنجاح!', { toast.success('تم التحقق بنجاح!', {
style: { background: '#dcfce7', color: '#166534' }, style: { background: '#dcfce7', color: '#166534' },

View File

@ -33,6 +33,7 @@ import {
Building Building
} from 'lucide-react'; } from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast'; import toast, { Toaster } from 'react-hot-toast';
import AuthService from '../../../services/AuthService';
import Image from 'next/image'; import Image from 'next/image';
const OwnerBookingCalendar = ({ property, onDateSelect, selectedDates }) => { const OwnerBookingCalendar = ({ property, onDateSelect, selectedDates }) => {
@ -425,8 +426,8 @@ export default function OwnerBookingsPage() {
useEffect(() => { useEffect(() => {
const storedUser = localStorage.getItem('user'); const storedUser = localStorage.getItem('user');
if (storedUser) { // User loaded via AuthService
const userData = JSON.parse(storedUser); // Handled above
if (userData.role !== 'owner') { if (userData.role !== 'owner') {
router.push('/'); router.push('/');
} else { } else {

View File

@ -36,6 +36,7 @@ import {
Calendar as CalendarIcon Calendar as CalendarIcon
} from 'lucide-react'; } from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast'; import toast, { Toaster } from 'react-hot-toast';
import AuthService from '../../../services/AuthService';
const MonthlyCalendar = ({ properties, selectedPropertyId, onDateClick, onPropertySelect }) => { const MonthlyCalendar = ({ properties, selectedPropertyId, onDateClick, onPropertySelect }) => {
const [currentMonth, setCurrentMonth] = useState(new Date()); const [currentMonth, setCurrentMonth] = useState(new Date());
@ -484,8 +485,8 @@ export default function OwnerCalendarPage() {
useEffect(() => { useEffect(() => {
const storedUser = localStorage.getItem('user'); const storedUser = localStorage.getItem('user');
if (storedUser) { // User loaded via AuthService
const userData = JSON.parse(storedUser); // Handled above
if (userData.role !== 'owner') { if (userData.role !== 'owner') {
router.push('/'); router.push('/');
} else { } else {

View File

@ -28,6 +28,7 @@ import {
XCircle XCircle
} from 'lucide-react'; } from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast'; import toast, { Toaster } from 'react-hot-toast';
import AuthService from '../../../services/AuthService';
const StatCard = ({ title, value, change, icon: Icon, color, trend }) => { const StatCard = ({ title, value, change, icon: Icon, color, trend }) => {
return ( return (
@ -237,8 +238,8 @@ export default function OwnerProfitsPage() {
useEffect(() => { useEffect(() => {
const storedUser = localStorage.getItem('user'); const storedUser = localStorage.getItem('user');
if (storedUser) { // User loaded via AuthService
const userData = JSON.parse(storedUser); // Handled above
if (userData.role !== 'owner') { if (userData.role !== 'owner') {
router.push('/'); router.push('/');
} else { } else {

View File

@ -45,6 +45,7 @@ import {
X X
} from 'lucide-react'; } from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast'; import toast, { Toaster } from 'react-hot-toast';
import AuthService from '../../../services/AuthService';
const DeleteConfirmationModal = ({ isOpen, onClose, onConfirm, propertyTitle }) => { const DeleteConfirmationModal = ({ isOpen, onClose, onConfirm, propertyTitle }) => {
if (!isOpen) return null; if (!isOpen) return null;
@ -693,8 +694,8 @@ export default function OwnerPropertiesPage() {
useEffect(() => { useEffect(() => {
const storedUser = localStorage.getItem('user'); const storedUser = localStorage.getItem('user');
if (storedUser) { // User loaded via AuthService
const userData = JSON.parse(storedUser); // Handled above
if (userData.role !== 'owner') { if (userData.role !== 'owner') {
router.push('/'); router.push('/');
} else { } else {

View File

@ -30,6 +30,7 @@ import Link from 'next/link';
import Image from 'next/image'; import Image from 'next/image';
import { getRentProperties, getSaleProperties } from './utils/api'; import { getRentProperties, getSaleProperties } from './utils/api';
import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from './enums'; import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from './enums';
import AuthService from './services/AuthService';
// Map API property data to the format the UI expects // Map API property data to the format the UI expects
// API returns { propertyInformationId, deposit, monthlyRent, dailyRent, rating, propertyInformation: {...}, ... } // API returns { propertyInformationId, deposit, monthlyRent, dailyRent, rating, propertyInformation: {...}, ... }
@ -180,9 +181,13 @@ export default function HomePage() {
// Fetch properties from API on mount // Fetch properties from API on mount
useEffect(() => { useEffect(() => {
const storedUser = localStorage.getItem('user'); const authUser = AuthService.getUser();
if (storedUser) { if (authUser) {
setUser(JSON.parse(storedUser)); setUser({
name: authUser.name || authUser.email,
email: authUser.email,
role: AuthService.isOwner() ? 'owner' : 'customer',
});
} }
async function fetchProperties() { async function fetchProperties() {
@ -226,7 +231,7 @@ export default function HomePage() {
}, []); }, []);
const logout = () => { const logout = () => {
localStorage.removeItem('user'); AuthService.deleteToken();
setUser(null); setUser(null);
setShowUserMenu(false); setShowUserMenu(false);
}; };

View File

@ -27,6 +27,7 @@ import {
Pencil Pencil
} from 'lucide-react'; } from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast'; import toast, { Toaster } from 'react-hot-toast';
import AuthService from '../services/AuthService';
export default function ProfilePage() { export default function ProfilePage() {
const router = useRouter(); const router = useRouter();
@ -62,9 +63,14 @@ export default function ProfilePage() {
}; };
useEffect(() => { useEffect(() => {
const storedUser = localStorage.getItem('user'); const authUser = AuthService.getUser();
if (storedUser) { if (authUser) {
const userData = JSON.parse(storedUser); const userData = {
name: authUser.name || '',
email: authUser.email || '',
phone: authUser.phone || '',
role: AuthService.isOwner() ? 'owner' : 'customer',
};
setUser(userData); setUser(userData);
const savedProfile = localStorage.getItem('userProfile'); const savedProfile = localStorage.getItem('userProfile');
@ -167,7 +173,6 @@ export default function ProfilePage() {
if (field === 'name') { if (field === 'name') {
const updatedUser = { ...user, name: value }; const updatedUser = { ...user, name: value };
localStorage.setItem('user', JSON.stringify(updatedUser));
setUser(updatedUser); setUser(updatedUser);
} }

View File

@ -1,50 +1,117 @@
/** /**
* AuthService * AuthService
* Manages authentication tokens securely using localStorage. * Manages authentication tokens and user role detection via JWT decoding.
*
* Methods:
* addToken(token) — store JWT token
* getToken() — retrieve JWT token
* deleteToken() — remove JWT token
* *
* Usage: * Roles (from JWT claims):
* import AuthService from '@/app/services/AuthService'; * - Owner: roles array contains "Owner"
* AuthService.addToken(token); * - Customer: authenticated but no "Owner" role
* const token = AuthService.getToken(); * - Guest: no token
* AuthService.deleteToken(); *
* Methods:
* addToken(token) — store JWT token
* getToken() — retrieve JWT token
* deleteToken() — remove JWT token
* decodeToken() — decode JWT payload
* getUser() — get decoded user info
* getRoles() — get roles array from JWT
* isOwner() — check if user has Owner role
* isCustomer() — check if user is authenticated but not Owner
* isGuest() — check if no token exists
* isAuthenticated() — check if token exists
*/ */
const TOKEN_KEY = 'auth_token'; const TOKEN_KEY = 'auth_token';
const AuthService = Object.freeze({ const AuthService = Object.freeze({
/**
* Store token in localStorage
* @param {string} token — JWT string
*/
addToken(token) { addToken(token) {
if (!token || typeof token !== 'string') { if (!token || typeof token !== 'string') return;
console.error('[AuthService] addToken: invalid token', token);
return;
}
localStorage.setItem(TOKEN_KEY, token); localStorage.setItem(TOKEN_KEY, token);
console.log('[AuthService] Token stored');
}, },
/**
* Retrieve token from localStorage
* @returns {string|null}
*/
getToken() { getToken() {
const token = localStorage.getItem(TOKEN_KEY); return localStorage.getItem(TOKEN_KEY);
console.log('[AuthService] getToken:', token ? '***exists***' : null);
return token;
}, },
/**
* Remove token from localStorage
*/
deleteToken() { deleteToken() {
localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(TOKEN_KEY);
console.log('[AuthService] Token deleted'); },
/**
* Decode JWT payload (base64)
* @returns {object|null}
*/
decodeToken() {
const token = this.getToken();
if (!token) return null;
try {
const payload = token.split('.')[1];
return JSON.parse(atob(payload));
} catch {
return null;
}
},
/**
* Extract user info from JWT
* @returns {object|null} — { id, name, email, phone, roles }
*/
getUser() {
const payload = this.decodeToken();
if (!payload) return null;
return {
id: payload['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'] || payload.sub || null,
name: payload['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'] || null,
email: payload['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'] || null,
phone: payload['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/mobilephone'] || null,
roles: this.getRoles(),
};
},
/**
* Get roles array from JWT
* @returns {string[]}
*/
getRoles() {
const payload = this.decodeToken();
if (!payload) return [];
const roles = payload['http://schemas.microsoft.com/ws/2008/06/identity/claims/role'];
if (Array.isArray(roles)) return roles;
if (typeof roles === 'string') return [roles];
return [];
},
/**
* User has Owner role
* @returns {boolean}
*/
isOwner() {
const roles = this.getRoles();
return roles.includes('Owner');
},
/**
* Authenticated user without Owner role (i.e. customer)
* @returns {boolean}
*/
isCustomer() {
return this.isAuthenticated() && !this.isOwner();
},
/**
* No token — guest user
* @returns {boolean}
*/
isGuest() {
return !this.getToken();
},
/**
* Token exists
* @returns {boolean}
*/
isAuthenticated() {
return !!this.getToken();
}, },
}); });