destinyebuka commited on
Commit
3ee4cc7
·
1 Parent(s): 5381654
app/i18n/email_translations.py CHANGED
@@ -22,6 +22,20 @@ EMAIL_TRANSLATIONS = {
22
  "otp_reset_expiry": "This code will expire in 15 minutes.",
23
  "otp_reset_footer": "If you didn't request a password reset, please ignore this email and your password will remain unchanged.",
24
  "otp_reset_brand": "Lojiz - Find Your Perfect Home",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  },
26
 
27
  "fr": {
@@ -42,6 +56,20 @@ EMAIL_TRANSLATIONS = {
42
  "otp_reset_expiry": "Ce code expirera dans 15 minutes.",
43
  "otp_reset_footer": "Si vous n'avez pas demandé de réinitialisation de mot de passe, veuillez ignorer cet e-mail et votre mot de passe restera inchangé.",
44
  "otp_reset_brand": "Lojiz - Trouvez votre maison parfaite",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  },
46
 
47
  "es": {
@@ -62,6 +90,20 @@ EMAIL_TRANSLATIONS = {
62
  "otp_reset_expiry": "Este código expirará en 15 minutos.",
63
  "otp_reset_footer": "Si no solicitaste un restablecimiento de contraseña, por favor ignora este correo y tu contraseña permanecerá sin cambios.",
64
  "otp_reset_brand": "Lojiz - Encuentra tu hogar perfecto",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  },
66
 
67
  "ar": {
@@ -82,6 +124,20 @@ EMAIL_TRANSLATIONS = {
82
  "otp_reset_expiry": "سينتهي صلاحية هذا الرمز خلال 15 دقيقة.",
83
  "otp_reset_footer": "إذا لم تطلب إعادة تعيين كلمة المرور، يرجى تجاهل هذا البريد الإلكتروني وستبقى كلمة مرورك دون تغيير.",
84
  "otp_reset_brand": "Lojiz - اعثر على منزلك المثالي",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  },
86
 
87
  "pt": {
@@ -102,6 +158,20 @@ EMAIL_TRANSLATIONS = {
102
  "otp_reset_expiry": "Este código expirará em 15 minutos.",
103
  "otp_reset_footer": "Se você não solicitou uma redefinição de senha, por favor ignore este e-mail e sua senha permanecerá inalterada.",
104
  "otp_reset_brand": "Lojiz - Encontre sua casa perfeita",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  },
106
  }
107
 
 
22
  "otp_reset_expiry": "This code will expire in 15 minutes.",
23
  "otp_reset_footer": "If you didn't request a password reset, please ignore this email and your password will remain unchanged.",
24
  "otp_reset_brand": "Lojiz - Find Your Perfect Home",
25
+
26
+ # Email Update OTP
27
+ "otp_email_update_subject": "Verify Your New Email Address",
28
+ "otp_email_update_greeting": "Hello!",
29
+ "otp_email_update_message": "You requested to update your email address on Lojiz. Please use the verification code below to confirm this change:",
30
+ "otp_email_update_expiry": "This code will expire in 15 minutes.",
31
+ "otp_email_update_footer": "If you didn't request this change, please ignore this email. Your account email will not be changed.",
32
+
33
+ # Phone Update OTP (sent via email)
34
+ "otp_phone_update_subject": "Verify Your New Phone Number",
35
+ "otp_phone_update_greeting": "Hello!",
36
+ "otp_phone_update_message": "You requested to update your phone number on Lojiz. Please use the verification code below to confirm this change:",
37
+ "otp_phone_update_expiry": "This code will expire in 15 minutes.",
38
+ "otp_phone_update_footer": "If you didn't request this change, please ignore this email. Your phone number will not be changed.",
39
  },
40
 
41
  "fr": {
 
56
  "otp_reset_expiry": "Ce code expirera dans 15 minutes.",
57
  "otp_reset_footer": "Si vous n'avez pas demandé de réinitialisation de mot de passe, veuillez ignorer cet e-mail et votre mot de passe restera inchangé.",
58
  "otp_reset_brand": "Lojiz - Trouvez votre maison parfaite",
59
+
60
+ # Email Update OTP
61
+ "otp_email_update_subject": "Vérifiez votre nouvelle adresse e-mail",
62
+ "otp_email_update_greeting": "Bonjour !",
63
+ "otp_email_update_message": "Vous avez demandé à modifier votre adresse e-mail sur Lojiz. Veuillez utiliser le code ci-dessous pour confirmer ce changement :",
64
+ "otp_email_update_expiry": "Ce code expirera dans 15 minutes.",
65
+ "otp_email_update_footer": "Si vous n'avez pas demandé ce changement, veuillez ignorer cet e-mail.",
66
+
67
+ # Phone Update OTP
68
+ "otp_phone_update_subject": "Vérifiez votre nouveau numéro de téléphone",
69
+ "otp_phone_update_greeting": "Bonjour !",
70
+ "otp_phone_update_message": "Vous avez demandé à modifier votre numéro de téléphone sur Lojiz. Veuillez utiliser le code ci-dessous pour confirmer ce changement :",
71
+ "otp_phone_update_expiry": "Ce code expirera dans 15 minutes.",
72
+ "otp_phone_update_footer": "Si vous n'avez pas demandé ce changement, veuillez ignorer cet e-mail.",
73
  },
74
 
75
  "es": {
 
90
  "otp_reset_expiry": "Este código expirará en 15 minutos.",
91
  "otp_reset_footer": "Si no solicitaste un restablecimiento de contraseña, por favor ignora este correo y tu contraseña permanecerá sin cambios.",
92
  "otp_reset_brand": "Lojiz - Encuentra tu hogar perfecto",
93
+
94
+ # Email Update OTP
95
+ "otp_email_update_subject": "Verifica tu nueva dirección de correo",
96
+ "otp_email_update_greeting": "¡Hola!",
97
+ "otp_email_update_message": "Solicitaste actualizar tu dirección de correo en Lojiz. Usa el código a continuación para confirmar el cambio:",
98
+ "otp_email_update_expiry": "Este código expirará en 15 minutos.",
99
+ "otp_email_update_footer": "Si no solicitaste este cambio, ignora este correo.",
100
+
101
+ # Phone Update OTP
102
+ "otp_phone_update_subject": "Verifica tu nuevo número de teléfono",
103
+ "otp_phone_update_greeting": "¡Hola!",
104
+ "otp_phone_update_message": "Solicitaste actualizar tu número de teléfono en Lojiz. Usa el código a continuación para confirmar el cambio:",
105
+ "otp_phone_update_expiry": "Este código expirará en 15 minutos.",
106
+ "otp_phone_update_footer": "Si no solicitaste este cambio, ignora este correo.",
107
  },
108
 
109
  "ar": {
 
124
  "otp_reset_expiry": "سينتهي صلاحية هذا الرمز خلال 15 دقيقة.",
125
  "otp_reset_footer": "إذا لم تطلب إعادة تعيين كلمة المرور، يرجى تجاهل هذا البريد الإلكتروني وستبقى كلمة مرورك دون تغيير.",
126
  "otp_reset_brand": "Lojiz - اعثر على منزلك المثالي",
127
+
128
+ # Email Update OTP
129
+ "otp_email_update_subject": "تحقق من عنوان بريدك الإلكتروني الجديد",
130
+ "otp_email_update_greeting": "مرحباً!",
131
+ "otp_email_update_message": "لقد طلبت تحديث عنوان بريدك الإلكتروني على Lojiz. يرجى استخدام رمز التحقق أدناه لتأكيد هذا التغيير:",
132
+ "otp_email_update_expiry": "سينتهي صلاحية هذا الرمز خلال 15 دقيقة.",
133
+ "otp_email_update_footer": "إذا ل�� تطلب هذا التغيير، يرجى تجاهل هذا البريد الإلكتروني.",
134
+
135
+ # Phone Update OTP
136
+ "otp_phone_update_subject": "تحقق من رقم هاتفك الجديد",
137
+ "otp_phone_update_greeting": "مرحباً!",
138
+ "otp_phone_update_message": "لقد طلبت تحديث رقم هاتفك على Lojiz. يرجى استخدام رمز التحقق أدناه لتأكيد هذا التغيير:",
139
+ "otp_phone_update_expiry": "سينتهي صلاحية هذا الرمز خلال 15 دقيقة.",
140
+ "otp_phone_update_footer": "إذا لم تطلب هذا التغيير، يرجى تجاهل هذا البريد الإلكتروني.",
141
  },
142
 
143
  "pt": {
 
158
  "otp_reset_expiry": "Este código expirará em 15 minutos.",
159
  "otp_reset_footer": "Se você não solicitou uma redefinição de senha, por favor ignore este e-mail e sua senha permanecerá inalterada.",
160
  "otp_reset_brand": "Lojiz - Encontre sua casa perfeita",
161
+
162
+ # Email Update OTP
163
+ "otp_email_update_subject": "Verifique seu novo endereço de e-mail",
164
+ "otp_email_update_greeting": "Olá!",
165
+ "otp_email_update_message": "Você solicitou a atualização do seu endereço de e-mail no Lojiz. Use o código abaixo para confirmar a alteração:",
166
+ "otp_email_update_expiry": "Este código expirará em 15 minutos.",
167
+ "otp_email_update_footer": "Se você não solicitou esta alteração, ignore este e-mail.",
168
+
169
+ # Phone Update OTP
170
+ "otp_phone_update_subject": "Verifique seu novo número de telefone",
171
+ "otp_phone_update_greeting": "Olá!",
172
+ "otp_phone_update_message": "Você solicitou a atualização do seu número de telefone no Lojiz. Use o código abaixo para confirmar a alteração:",
173
+ "otp_phone_update_expiry": "Este código expirará em 15 minutos.",
174
+ "otp_phone_update_footer": "Se você não solicitou esta alteração, ignore este e-mail.",
175
  },
176
  }
177
 
app/routes/contact_update.py ADDED
@@ -0,0 +1,430 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # app/routes/contact_update.py - Update Email & Phone with OTP
3
+ # ============================================================
4
+
5
+ import logging
6
+ from fastapi import APIRouter, HTTPException, status, Depends, Request
7
+ from pydantic import BaseModel, EmailStr, Field
8
+ from typing import Optional
9
+ from app.middleware import limiter, AUTH_RATE_LIMIT
10
+ from app.guards.jwt_guard import get_current_user
11
+ from app.services.otp_service import otp_service
12
+ from app.database import get_db
13
+ from app.core.rate_limiter import get_rate_limiter, RateLimitExceeded
14
+ from datetime import datetime
15
+
16
+ router = APIRouter(tags=["Contact Update"])
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ # ============================================================
21
+ # DTOs
22
+ # ============================================================
23
+
24
+ class SendEmailOtpDto(BaseModel):
25
+ """Request OTP to update email"""
26
+ new_email: EmailStr
27
+ language: str = Field(default="en", pattern="^(en|fr|es|ar|pt)$")
28
+
29
+
30
+ class VerifyEmailOtpDto(BaseModel):
31
+ """Verify OTP and update email"""
32
+ new_email: EmailStr
33
+ code: str = Field(..., min_length=6, max_length=6)
34
+ language: str = Field(default="en", pattern="^(en|fr|es|ar|pt)$")
35
+
36
+
37
+ class SendPhoneOtpDto(BaseModel):
38
+ """Request OTP to update phone"""
39
+ new_phone: str = Field(..., min_length=5, max_length=20)
40
+ language: str = Field(default="en", pattern="^(en|fr|es|ar|pt)$")
41
+
42
+
43
+ class VerifyPhoneOtpDto(BaseModel):
44
+ """Verify OTP and update phone"""
45
+ new_phone: str = Field(..., min_length=5, max_length=20)
46
+ code: str = Field(..., min_length=6, max_length=6)
47
+ language: str = Field(default="en", pattern="^(en|fr|es|ar|pt)$")
48
+
49
+
50
+ # ============================================================
51
+ # RATE LIMITING HELPER
52
+ # ============================================================
53
+
54
+ async def check_contact_rate_limit(identifier: str, operation: str, request: Request):
55
+ """Check rate limit for contact update operations"""
56
+ rate_limiter = get_rate_limiter()
57
+ allowed, info = await rate_limiter.is_allowed(
58
+ user_id=identifier,
59
+ operation=operation,
60
+ ip_address=request.client.host if request.client else None,
61
+ )
62
+ if not allowed:
63
+ raise HTTPException(
64
+ status_code=status.HTTP_429_TOO_MANY_REQUESTS,
65
+ detail=f"Too many {operation} attempts. Please try again later.",
66
+ headers={"Retry-After": str(info.get("user", {}).get("reset_in", 60))},
67
+ )
68
+
69
+
70
+ # ============================================================
71
+ # EMAIL UPDATE ENDPOINTS
72
+ # ============================================================
73
+
74
+ @router.post("/email/send-otp", status_code=status.HTTP_200_OK)
75
+ @limiter.limit(AUTH_RATE_LIMIT)
76
+ async def send_email_update_otp(
77
+ dto: SendEmailOtpDto,
78
+ request: Request,
79
+ current_user: dict = Depends(get_current_user),
80
+ ):
81
+ """
82
+ Step 1: Send OTP to new email address.
83
+ Requires authentication. Checks that the new email is not already in use.
84
+ """
85
+ user_id = current_user.get("user_id")
86
+ new_email = dto.new_email.lower()
87
+
88
+ logger.info(f"Email update OTP requested by user {user_id} for {new_email}")
89
+
90
+ db = await get_db()
91
+ users_collection = db["users"]
92
+
93
+ # Check if new email is same as current
94
+ current_email = current_user.get("email", "")
95
+ if current_email and current_email.lower() == new_email:
96
+ raise HTTPException(
97
+ status_code=status.HTTP_400_BAD_REQUEST,
98
+ detail="New email is the same as your current email.",
99
+ )
100
+
101
+ # Check if email already taken by another user
102
+ existing = await users_collection.find_one({"email": new_email})
103
+ if existing:
104
+ raise HTTPException(
105
+ status_code=status.HTTP_409_CONFLICT,
106
+ detail="This email is already registered to another account.",
107
+ )
108
+
109
+ # Send OTP to the NEW email
110
+ try:
111
+ await otp_service.send_otp(new_email, "email_update", dto.language)
112
+ except Exception as e:
113
+ logger.error(f"Failed to send email update OTP: {e}")
114
+ raise HTTPException(
115
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
116
+ detail="Failed to send verification email. Please try again.",
117
+ )
118
+
119
+ return {
120
+ "success": True,
121
+ "message": "Verification code sent to your new email address.",
122
+ "data": {"email": new_email},
123
+ }
124
+
125
+
126
+ @router.post("/email/verify-otp", status_code=status.HTTP_200_OK)
127
+ async def verify_email_update_otp(
128
+ dto: VerifyEmailOtpDto,
129
+ request: Request,
130
+ current_user: dict = Depends(get_current_user),
131
+ ):
132
+ """
133
+ Step 2: Verify OTP and update email.
134
+ On success, updates the user's email in the database.
135
+ """
136
+ user_id = current_user.get("user_id")
137
+ new_email = dto.new_email.lower()
138
+
139
+ await check_contact_rate_limit(user_id, "verify_email_otp", request)
140
+
141
+ logger.info(f"Email update OTP verification by user {user_id} for {new_email}")
142
+
143
+ # Verify OTP
144
+ await otp_service.verify_otp(new_email, dto.code, "email_update", dto.language)
145
+
146
+ db = await get_db()
147
+ users_collection = db["users"]
148
+
149
+ # Double-check email not taken (race condition guard)
150
+ existing = await users_collection.find_one({"email": new_email})
151
+ if existing and str(existing["_id"]) != user_id:
152
+ raise HTTPException(
153
+ status_code=status.HTTP_409_CONFLICT,
154
+ detail="This email was just registered by another account.",
155
+ )
156
+
157
+ # Update user email
158
+ from bson import ObjectId
159
+ await users_collection.update_one(
160
+ {"_id": ObjectId(user_id)},
161
+ {
162
+ "$set": {
163
+ "email": new_email,
164
+ "isEmailVerified": True,
165
+ "updatedAt": datetime.utcnow(),
166
+ }
167
+ },
168
+ )
169
+
170
+ # Clean up OTP
171
+ await otp_service.delete_otp(new_email, "email_update")
172
+
173
+ logger.info(f"Email updated successfully for user {user_id}: {new_email}")
174
+
175
+ return {
176
+ "success": True,
177
+ "message": "Email updated successfully!",
178
+ "data": {"email": new_email},
179
+ }
180
+
181
+
182
+ @router.post("/email/resend-otp", status_code=status.HTTP_200_OK)
183
+ @limiter.limit(AUTH_RATE_LIMIT)
184
+ async def resend_email_update_otp(
185
+ dto: SendEmailOtpDto,
186
+ request: Request,
187
+ current_user: dict = Depends(get_current_user),
188
+ ):
189
+ """Resend OTP for email update."""
190
+ new_email = dto.new_email.lower()
191
+
192
+ # Check if previous OTP has expired
193
+ is_expired = await otp_service.is_otp_expired(new_email, "email_update")
194
+ if not is_expired:
195
+ from app.i18n.error_translations import get_error_message
196
+ raise HTTPException(
197
+ status_code=status.HTTP_400_BAD_REQUEST,
198
+ detail=get_error_message(dto.language, "otp_still_valid"),
199
+ )
200
+
201
+ try:
202
+ await otp_service.send_otp(new_email, "email_update", dto.language)
203
+ except Exception as e:
204
+ logger.error(f"Failed to resend email update OTP: {e}")
205
+ raise HTTPException(
206
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
207
+ detail="Failed to resend verification email.",
208
+ )
209
+
210
+ return {
211
+ "success": True,
212
+ "message": "New verification code sent to your email.",
213
+ }
214
+
215
+
216
+ # ============================================================
217
+ # PHONE UPDATE ENDPOINTS
218
+ # ============================================================
219
+
220
+ @router.post("/phone/send-otp", status_code=status.HTTP_200_OK)
221
+ @limiter.limit(AUTH_RATE_LIMIT)
222
+ async def send_phone_update_otp(
223
+ dto: SendPhoneOtpDto,
224
+ request: Request,
225
+ current_user: dict = Depends(get_current_user),
226
+ ):
227
+ """
228
+ Step 1: Send OTP to new phone number.
229
+ Requires authentication. Checks that the new phone is not already in use.
230
+
231
+ NOTE: SMS delivery depends on SMS provider being configured.
232
+ Currently sends via email if phone user has an email on file.
233
+ """
234
+ user_id = current_user.get("user_id")
235
+ new_phone = dto.new_phone.strip()
236
+
237
+ logger.info(f"Phone update OTP requested by user {user_id} for {new_phone}")
238
+
239
+ db = await get_db()
240
+ users_collection = db["users"]
241
+
242
+ # Check if same as current
243
+ current_phone = current_user.get("phone", "")
244
+ if current_phone and current_phone == new_phone:
245
+ raise HTTPException(
246
+ status_code=status.HTTP_400_BAD_REQUEST,
247
+ detail="New phone number is the same as your current number.",
248
+ )
249
+
250
+ # Check if phone already taken
251
+ existing = await users_collection.find_one({"phone": new_phone})
252
+ if existing:
253
+ raise HTTPException(
254
+ status_code=status.HTTP_409_CONFLICT,
255
+ detail="This phone number is already registered to another account.",
256
+ )
257
+
258
+ # For phone OTP, we send to the user's email since SMS is not implemented
259
+ # The OTP is keyed by the new phone number for verification
260
+ from bson import ObjectId
261
+ user = await users_collection.find_one({"_id": ObjectId(user_id)})
262
+ user_email = user.get("email") if user else None
263
+
264
+ if not user_email:
265
+ raise HTTPException(
266
+ status_code=status.HTTP_400_BAD_REQUEST,
267
+ detail="You need a verified email on your account to update your phone number.",
268
+ )
269
+
270
+ # Generate OTP keyed to new phone, but send via email
271
+ otp_code = await otp_service.generate_otp(new_phone, "phone_update")
272
+ try:
273
+ await otp_service._send_otp_via_email(user_email, otp_code, "phone_update", dto.language)
274
+ except Exception as e:
275
+ logger.error(f"Failed to send phone update OTP: {e}")
276
+ raise HTTPException(
277
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
278
+ detail="Failed to send verification code. Please try again.",
279
+ )
280
+
281
+ return {
282
+ "success": True,
283
+ "message": "Verification code sent to your email address.",
284
+ "data": {"phone": new_phone},
285
+ }
286
+
287
+
288
+ @router.post("/phone/verify-otp", status_code=status.HTTP_200_OK)
289
+ async def verify_phone_update_otp(
290
+ dto: VerifyPhoneOtpDto,
291
+ request: Request,
292
+ current_user: dict = Depends(get_current_user),
293
+ ):
294
+ """
295
+ Step 2: Verify OTP and update phone number.
296
+ """
297
+ user_id = current_user.get("user_id")
298
+ new_phone = dto.new_phone.strip()
299
+
300
+ await check_contact_rate_limit(user_id, "verify_phone_otp", request)
301
+
302
+ logger.info(f"Phone update OTP verification by user {user_id} for {new_phone}")
303
+
304
+ # Verify OTP
305
+ await otp_service.verify_otp(new_phone, dto.code, "phone_update", dto.language)
306
+
307
+ db = await get_db()
308
+ users_collection = db["users"]
309
+
310
+ # Double-check phone not taken
311
+ existing = await users_collection.find_one({"phone": new_phone})
312
+ if existing and str(existing["_id"]) != user_id:
313
+ raise HTTPException(
314
+ status_code=status.HTTP_409_CONFLICT,
315
+ detail="This phone number was just registered by another account.",
316
+ )
317
+
318
+ # Update user phone
319
+ from bson import ObjectId
320
+ await users_collection.update_one(
321
+ {"_id": ObjectId(user_id)},
322
+ {
323
+ "$set": {
324
+ "phone": new_phone,
325
+ "isPhoneVerified": True,
326
+ "updatedAt": datetime.utcnow(),
327
+ }
328
+ },
329
+ )
330
+
331
+ # Clean up OTP
332
+ await otp_service.delete_otp(new_phone, "phone_update")
333
+
334
+ logger.info(f"Phone updated successfully for user {user_id}: {new_phone}")
335
+
336
+ return {
337
+ "success": True,
338
+ "message": "Phone number updated successfully!",
339
+ "data": {"phone": new_phone},
340
+ }
341
+
342
+
343
+ @router.post("/phone/resend-otp", status_code=status.HTTP_200_OK)
344
+ @limiter.limit(AUTH_RATE_LIMIT)
345
+ async def resend_phone_update_otp(
346
+ dto: SendPhoneOtpDto,
347
+ request: Request,
348
+ current_user: dict = Depends(get_current_user),
349
+ ):
350
+ """Resend OTP for phone update (sent via email)."""
351
+ new_phone = dto.new_phone.strip()
352
+
353
+ is_expired = await otp_service.is_otp_expired(new_phone, "phone_update")
354
+ if not is_expired:
355
+ from app.i18n.error_translations import get_error_message
356
+ raise HTTPException(
357
+ status_code=status.HTTP_400_BAD_REQUEST,
358
+ detail=get_error_message(dto.language, "otp_still_valid"),
359
+ )
360
+
361
+ user_id = current_user.get("user_id")
362
+ db = await get_db()
363
+ users_collection = db["users"]
364
+ from bson import ObjectId
365
+ user = await users_collection.find_one({"_id": ObjectId(user_id)})
366
+ user_email = user.get("email") if user else None
367
+
368
+ if not user_email:
369
+ raise HTTPException(
370
+ status_code=status.HTTP_400_BAD_REQUEST,
371
+ detail="You need a verified email to update your phone number.",
372
+ )
373
+
374
+ otp_code = await otp_service.generate_otp(new_phone, "phone_update")
375
+ try:
376
+ await otp_service._send_otp_via_email(user_email, otp_code, "phone_update", dto.language)
377
+ except Exception as e:
378
+ logger.error(f"Failed to resend phone update OTP: {e}")
379
+ raise HTTPException(
380
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
381
+ detail="Failed to resend verification code.",
382
+ )
383
+
384
+ return {
385
+ "success": True,
386
+ "message": "New verification code sent to your email.",
387
+ }
388
+
389
+
390
+ # ============================================================
391
+ # ACCOUNT DELETION
392
+ # ============================================================
393
+
394
+ @router.delete("/account", status_code=status.HTTP_200_OK)
395
+ async def delete_account(
396
+ request: Request,
397
+ current_user: dict = Depends(get_current_user),
398
+ ):
399
+ """
400
+ Permanently delete user account and all associated data.
401
+ Requires authentication. This action is irreversible.
402
+ """
403
+ user_id = current_user.get("user_id")
404
+ logger.warning(f"Account deletion requested by user {user_id}")
405
+
406
+ from bson import ObjectId
407
+ db = await get_db()
408
+ oid = ObjectId(user_id)
409
+
410
+ # Delete user's data across collections
411
+ await db["users"].delete_one({"_id": oid})
412
+ await db["listings"].delete_many({"landlord_id": user_id})
413
+ await db["otps"].delete_many({"identifier": current_user.get("email", "")})
414
+ await db["conversations"].delete_many({
415
+ "$or": [{"user_id": user_id}, {"landlord_id": user_id}]
416
+ })
417
+ await db["bookings"].delete_many({
418
+ "$or": [{"renter_id": user_id}, {"landlord_id": user_id}]
419
+ })
420
+ await db["reviews"].delete_many({
421
+ "$or": [{"reviewer_id": user_id}, {"landlord_id": user_id}]
422
+ })
423
+ await db["wishlists"].delete_many({"user_id": user_id})
424
+
425
+ logger.info(f"Account and associated data deleted for user {user_id}")
426
+
427
+ return {
428
+ "success": True,
429
+ "message": "Your account has been permanently deleted.",
430
+ }
app/routes/legal.py ADDED
@@ -0,0 +1,948 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # app/routes/legal.py – Localized legal HTML pages
3
+ # Serves Privacy Policy & Terms of Service as styled HTML
4
+ # ============================================================
5
+
6
+ from fastapi import APIRouter, Query
7
+ from fastapi.responses import HTMLResponse
8
+ from datetime import datetime
9
+
10
+ router = APIRouter()
11
+
12
+ # ── Brand colors ────────────────────────────────────────────
13
+ GRADIENT_START = "#764BA2"
14
+ GRADIENT_END = "#FF305A"
15
+ BRAND_COLOR = "#FF305A"
16
+
17
+ SUPPORTED_LANGS = {"en", "fr", "es", "ar", "pt"}
18
+ LAST_UPDATED = "2026-03-01"
19
+
20
+
21
+ def _base_template(title: str, content: str, lang: str) -> str:
22
+ """Wrap content in a branded, responsive HTML shell."""
23
+ is_rtl = lang == "ar"
24
+ dir_attr = ' dir="rtl"' if is_rtl else ""
25
+ year = datetime.now().year
26
+
27
+ return f"""<!DOCTYPE html>
28
+ <html lang="{lang}"{dir_attr}>
29
+ <head>
30
+ <meta charset="UTF-8">
31
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
32
+ <title>{title}</title>
33
+ <style>
34
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
35
+ body {{
36
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
37
+ background: #f8f8fc;
38
+ color: #2c2c2e;
39
+ line-height: 1.7;
40
+ -webkit-font-smoothing: antialiased;
41
+ }}
42
+ .header {{
43
+ background: linear-gradient(135deg, {GRADIENT_START}, {GRADIENT_END});
44
+ padding: 48px 24px 36px;
45
+ text-align: center;
46
+ }}
47
+ .header h1 {{
48
+ color: #fff;
49
+ font-size: 24px;
50
+ font-weight: 700;
51
+ letter-spacing: -0.3px;
52
+ margin-bottom: 8px;
53
+ }}
54
+ .header .updated {{
55
+ color: rgba(255,255,255,0.75);
56
+ font-size: 13px;
57
+ font-weight: 400;
58
+ }}
59
+ .content {{
60
+ max-width: 680px;
61
+ margin: -20px auto 0;
62
+ background: #fff;
63
+ border-radius: 20px 20px 0 0;
64
+ padding: 32px 24px 40px;
65
+ min-height: 60vh;
66
+ }}
67
+ h2 {{
68
+ font-size: 18px;
69
+ font-weight: 700;
70
+ color: {GRADIENT_START};
71
+ margin: 28px 0 12px;
72
+ letter-spacing: -0.2px;
73
+ }}
74
+ h2:first-child {{ margin-top: 0; }}
75
+ h3 {{
76
+ font-size: 15px;
77
+ font-weight: 600;
78
+ color: #1a1a2e;
79
+ margin: 20px 0 8px;
80
+ }}
81
+ p {{
82
+ font-size: 14px;
83
+ color: #4a4a4e;
84
+ margin-bottom: 14px;
85
+ }}
86
+ ul {{
87
+ padding-{"right" if is_rtl else "left"}: 20px;
88
+ margin-bottom: 14px;
89
+ }}
90
+ li {{
91
+ font-size: 14px;
92
+ color: #4a4a4e;
93
+ margin-bottom: 8px;
94
+ line-height: 1.6;
95
+ }}
96
+ .highlight {{
97
+ background: linear-gradient(135deg, rgba(118,75,162,0.08), rgba(255,48,90,0.06));
98
+ border-{"right" if is_rtl else "left"}: 3px solid {BRAND_COLOR};
99
+ padding: 14px 16px;
100
+ border-radius: 0 12px 12px 0;
101
+ margin: 16px 0;
102
+ font-size: 13.5px;
103
+ color: #3a3a3c;
104
+ }}
105
+ .contact-box {{
106
+ background: linear-gradient(135deg, {GRADIENT_START}, {GRADIENT_END});
107
+ border-radius: 16px;
108
+ padding: 24px;
109
+ margin-top: 32px;
110
+ text-align: center;
111
+ }}
112
+ .contact-box p {{
113
+ color: rgba(255,255,255,0.9);
114
+ font-size: 14px;
115
+ margin-bottom: 8px;
116
+ }}
117
+ .contact-box a {{
118
+ color: #fff;
119
+ font-weight: 600;
120
+ text-decoration: none;
121
+ border-bottom: 1px solid rgba(255,255,255,0.4);
122
+ }}
123
+ .footer {{
124
+ text-align: center;
125
+ padding: 24px;
126
+ font-size: 12px;
127
+ color: #8e8e93;
128
+ }}
129
+ @media (max-width: 480px) {{
130
+ .header {{ padding: 40px 20px 28px; }}
131
+ .header h1 {{ font-size: 21px; }}
132
+ .content {{ padding: 24px 18px 32px; }}
133
+ h2 {{ font-size: 16px; }}
134
+ }}
135
+ </style>
136
+ </head>
137
+ <body>
138
+ {content}
139
+ <div class="footer">&copy; {year} Lojiz. All rights reserved.</div>
140
+ </body>
141
+ </html>"""
142
+
143
+
144
+ # ══════════════════════════════════════════════════════════════
145
+ # PRIVACY POLICY
146
+ # ══════════════════════════════════════════════════════════════
147
+
148
+ PRIVACY_CONTENT = {
149
+ "en": {
150
+ "title": "Privacy Policy",
151
+ "updated": "Last updated",
152
+ "sections": """
153
+ <div class="header">
154
+ <h1>Privacy Policy</h1>
155
+ <div class="updated">Last updated: {date}</div>
156
+ </div>
157
+ <div class="content">
158
+ <h2>1. Information We Collect</h2>
159
+ <p>At Lojiz, we collect information to provide you with the best property rental experience. This includes:</p>
160
+ <ul>
161
+ <li><strong>Account Information:</strong> Name, email address, phone number, and profile picture when you create an account.</li>
162
+ <li><strong>Property Data:</strong> Listings you create, search history, saved properties, and booking details.</li>
163
+ <li><strong>Usage Data:</strong> How you interact with our app, including pages viewed, features used, and time spent.</li>
164
+ <li><strong>Device Information:</strong> Device type, operating system, and app version for optimization.</li>
165
+ <li><strong>Location Data:</strong> With your permission, we collect location to show nearby properties.</li>
166
+ </ul>
167
+
168
+ <h2>2. How We Use Your Information</h2>
169
+ <p>We use your information to:</p>
170
+ <ul>
171
+ <li>Provide and improve our property rental platform</li>
172
+ <li>Facilitate communication between renters and landlords</li>
173
+ <li>Power AIDA, our AI assistant, to help you find properties</li>
174
+ <li>Send relevant notifications about listings and bookings</li>
175
+ <li>Process payments securely through Stripe</li>
176
+ <li>Prevent fraud and ensure platform safety</li>
177
+ </ul>
178
+
179
+ <div class="highlight">We never sell your personal data to third parties. Your information is used solely to enhance your Lojiz experience.</div>
180
+
181
+ <h2>3. AI Assistant (AIDA)</h2>
182
+ <p>AIDA uses your search preferences and conversation history to provide personalized property recommendations. Conversations are processed securely and are not shared with other users or third parties.</p>
183
+
184
+ <h2>4. Data Sharing</h2>
185
+ <p>We may share limited information with:</p>
186
+ <ul>
187
+ <li><strong>Landlords/Renters:</strong> Contact details when you initiate a booking or inquiry.</li>
188
+ <li><strong>Payment Processors:</strong> Stripe processes payments; we don't store full card details.</li>
189
+ <li><strong>Service Providers:</strong> Hosting, analytics, and email services that help us operate.</li>
190
+ <li><strong>Legal Requirements:</strong> When required by law or to protect rights and safety.</li>
191
+ </ul>
192
+
193
+ <h2>5. Data Security</h2>
194
+ <p>We protect your data with:</p>
195
+ <ul>
196
+ <li>End-to-end encryption for messages</li>
197
+ <li>Secure JWT authentication</li>
198
+ <li>Regular security audits</li>
199
+ <li>Encrypted data storage</li>
200
+ </ul>
201
+
202
+ <h2>6. Your Rights</h2>
203
+ <p>You have the right to:</p>
204
+ <ul>
205
+ <li>Access and download your personal data</li>
206
+ <li>Correct inaccurate information</li>
207
+ <li>Delete your account and associated data</li>
208
+ <li>Opt out of analytics and marketing communications</li>
209
+ <li>Withdraw consent for location tracking at any time</li>
210
+ </ul>
211
+
212
+ <h2>7. Data Retention</h2>
213
+ <p>We retain your data for as long as your account is active. After account deletion, we remove personal data within 30 days, except where legal obligations require longer retention.</p>
214
+
215
+ <h2>8. Children's Privacy</h2>
216
+ <p>Lojiz is not intended for users under 18. We do not knowingly collect data from minors.</p>
217
+
218
+ <h2>9. Changes to This Policy</h2>
219
+ <p>We may update this policy periodically. Significant changes will be notified via the app or email.</p>
220
+
221
+ <div class="contact-box">
222
+ <p>Questions about your privacy?</p>
223
+ <a href="mailto:privacy@lojiz.com">privacy@lojiz.com</a>
224
+ </div>
225
+ </div>"""
226
+ },
227
+ "fr": {
228
+ "title": "Politique de Confidentialité",
229
+ "updated": "Dernière mise à jour",
230
+ "sections": """
231
+ <div class="header">
232
+ <h1>Politique de Confidentialité</h1>
233
+ <div class="updated">Dernière mise à jour : {date}</div>
234
+ </div>
235
+ <div class="content">
236
+ <h2>1. Informations que nous collectons</h2>
237
+ <p>Chez Lojiz, nous collectons des informations pour vous offrir la meilleure expérience de location. Cela inclut :</p>
238
+ <ul>
239
+ <li><strong>Informations de compte :</strong> Nom, adresse e-mail, numéro de téléphone et photo de profil lors de la création de votre compte.</li>
240
+ <li><strong>Données immobilières :</strong> Annonces créées, historique de recherche, propriétés sauvegardées et détails de réservation.</li>
241
+ <li><strong>Données d'utilisation :</strong> Comment vous interagissez avec notre application, pages consultées et fonctionnalités utilisées.</li>
242
+ <li><strong>Informations sur l'appareil :</strong> Type d'appareil, système d'exploitation et version de l'application.</li>
243
+ <li><strong>Données de localisation :</strong> Avec votre permission, pour afficher les propriétés à proximité.</li>
244
+ </ul>
245
+
246
+ <h2>2. Comment nous utilisons vos informations</h2>
247
+ <p>Nous utilisons vos informations pour :</p>
248
+ <ul>
249
+ <li>Fournir et améliorer notre plateforme de location</li>
250
+ <li>Faciliter la communication entre locataires et propriétaires</li>
251
+ <li>Alimenter AIDA, notre assistante IA, pour vous aider à trouver des propriétés</li>
252
+ <li>Envoyer des notifications pertinentes sur les annonces et réservations</li>
253
+ <li>Traiter les paiements de manière sécurisée via Stripe</li>
254
+ <li>Prévenir la fraude et assurer la sécurité de la plateforme</li>
255
+ </ul>
256
+
257
+ <div class="highlight">Nous ne vendons jamais vos données personnelles à des tiers. Vos informations sont utilisées uniquement pour améliorer votre expérience Lojiz.</div>
258
+
259
+ <h2>3. Assistante IA (AIDA)</h2>
260
+ <p>AIDA utilise vos préférences de recherche et l'historique de vos conversations pour fournir des recommandations personnalisées. Les conversations sont traitées de manière sécurisée et ne sont pas partagées.</p>
261
+
262
+ <h2>4. Partage des données</h2>
263
+ <p>Nous pouvons partager des informations limitées avec :</p>
264
+ <ul>
265
+ <li><strong>Propriétaires/Locataires :</strong> Coordonnées lors d'une réservation ou demande.</li>
266
+ <li><strong>Processeurs de paiement :</strong> Stripe traite les paiements ; nous ne stockons pas les détails complets de carte.</li>
267
+ <li><strong>Prestataires de services :</strong> Hébergement, analytique et services e-mail.</li>
268
+ <li><strong>Obligations légales :</strong> Lorsque la loi l'exige.</li>
269
+ </ul>
270
+
271
+ <h2>5. Sécurité des données</h2>
272
+ <p>Nous protégeons vos données avec :</p>
273
+ <ul>
274
+ <li>Chiffrement de bout en bout pour les messages</li>
275
+ <li>Authentification JWT sécurisée</li>
276
+ <li>Audits de sécurité réguliers</li>
277
+ <li>Stockage de données chiffré</li>
278
+ </ul>
279
+
280
+ <h2>6. Vos droits</h2>
281
+ <p>Vous avez le droit de :</p>
282
+ <ul>
283
+ <li>Accéder et télécharger vos données personnelles</li>
284
+ <li>Corriger les informations inexactes</li>
285
+ <li>Supprimer votre compte et les données associées</li>
286
+ <li>Refuser l'analytique et les communications marketing</li>
287
+ <li>Retirer votre consentement pour le suivi de localisation</li>
288
+ </ul>
289
+
290
+ <h2>7. Conservation des données</h2>
291
+ <p>Nous conservons vos données tant que votre compte est actif. Après suppression, nous effaçons les données personnelles sous 30 jours, sauf obligation légale.</p>
292
+
293
+ <h2>8. Confidentialité des enfants</h2>
294
+ <p>Lojiz n'est pas destiné aux utilisateurs de moins de 18 ans. Nous ne collectons pas sciemment de données de mineurs.</p>
295
+
296
+ <h2>9. Modifications de cette politique</h2>
297
+ <p>Nous pouvons mettre à jour cette politique périodiquement. Les changements importants seront notifiés via l'application ou par e-mail.</p>
298
+
299
+ <div class="contact-box">
300
+ <p>Questions sur votre confidentialité ?</p>
301
+ <a href="mailto:privacy@lojiz.com">privacy@lojiz.com</a>
302
+ </div>
303
+ </div>"""
304
+ },
305
+ "es": {
306
+ "title": "Política de Privacidad",
307
+ "updated": "Última actualización",
308
+ "sections": """
309
+ <div class="header">
310
+ <h1>Política de Privacidad</h1>
311
+ <div class="updated">Última actualización: {date}</div>
312
+ </div>
313
+ <div class="content">
314
+ <h2>1. Información que recopilamos</h2>
315
+ <p>En Lojiz, recopilamos información para brindarte la mejor experiencia de alquiler. Esto incluye:</p>
316
+ <ul>
317
+ <li><strong>Información de cuenta:</strong> Nombre, correo electrónico, número de teléfono y foto de perfil al crear tu cuenta.</li>
318
+ <li><strong>Datos de propiedades:</strong> Anuncios creados, historial de búsqueda, propiedades guardadas y detalles de reservas.</li>
319
+ <li><strong>Datos de uso:</strong> Cómo interactúas con nuestra aplicación, páginas visitadas y funciones utilizadas.</li>
320
+ <li><strong>Información del dispositivo:</strong> Tipo de dispositivo, sistema operativo y versión de la aplicación.</li>
321
+ <li><strong>Datos de ubicación:</strong> Con tu permiso, para mostrar propiedades cercanas.</li>
322
+ </ul>
323
+
324
+ <h2>2. Cómo usamos tu información</h2>
325
+ <p>Usamos tu información para:</p>
326
+ <ul>
327
+ <li>Proporcionar y mejorar nuestra plataforma de alquiler</li>
328
+ <li>Facilitar la comunicación entre inquilinos y propietarios</li>
329
+ <li>Potenciar AIDA, nuestra asistente de IA, para ayudarte a encontrar propiedades</li>
330
+ <li>Enviar notificaciones relevantes sobre anuncios y reservas</li>
331
+ <li>Procesar pagos de forma segura a través de Stripe</li>
332
+ <li>Prevenir fraudes y garantizar la seguridad de la plataforma</li>
333
+ </ul>
334
+
335
+ <div class="highlight">Nunca vendemos tus datos personales a terceros. Tu información se utiliza únicamente para mejorar tu experiencia en Lojiz.</div>
336
+
337
+ <h2>3. Asistente de IA (AIDA)</h2>
338
+ <p>AIDA utiliza tus preferencias de búsqueda y el historial de conversaciones para ofrecer recomendaciones personalizadas. Las conversaciones se procesan de forma segura y no se comparten.</p>
339
+
340
+ <h2>4. Compartir datos</h2>
341
+ <ul>
342
+ <li><strong>Propietarios/Inquilinos:</strong> Datos de contacto al iniciar una reserva o consulta.</li>
343
+ <li><strong>Procesadores de pago:</strong> Stripe procesa los pagos; no almacenamos datos completos de tarjetas.</li>
344
+ <li><strong>Proveedores de servicios:</strong> Alojamiento, analítica y servicios de correo electrónico.</li>
345
+ <li><strong>Requisitos legales:</strong> Cuando lo exija la ley.</li>
346
+ </ul>
347
+
348
+ <h2>5. Seguridad de datos</h2>
349
+ <ul>
350
+ <li>Cifrado de extremo a extremo para mensajes</li>
351
+ <li>Autenticación JWT segura</li>
352
+ <li>Auditorías de seguridad regulares</li>
353
+ <li>Almacenamiento de datos cifrado</li>
354
+ </ul>
355
+
356
+ <h2>6. Tus derechos</h2>
357
+ <ul>
358
+ <li>Acceder y descargar tus datos personales</li>
359
+ <li>Corregir información inexacta</li>
360
+ <li>Eliminar tu cuenta y datos asociados</li>
361
+ <li>Rechazar analíticas y comunicaciones de marketing</li>
362
+ <li>Retirar el consentimiento para el seguimiento de ubicación</li>
363
+ </ul>
364
+
365
+ <h2>7. Retención de datos</h2>
366
+ <p>Conservamos tus datos mientras tu cuenta esté activa. Después de la eliminación, borramos los datos personales en 30 días, salvo obligación legal.</p>
367
+
368
+ <h2>8. Privacidad de menores</h2>
369
+ <p>Lojiz no está destinado a usuarios menores de 18 años.</p>
370
+
371
+ <h2>9. Cambios en esta política</h2>
372
+ <p>Podemos actualizar esta política periódicamente. Los cambios importantes se notificarán a través de la aplicación o correo electrónico.</p>
373
+
374
+ <div class="contact-box">
375
+ <p>¿Preguntas sobre tu privacidad?</p>
376
+ <a href="mailto:privacy@lojiz.com">privacy@lojiz.com</a>
377
+ </div>
378
+ </div>"""
379
+ },
380
+ "ar": {
381
+ "title": "سياسة الخصوصية",
382
+ "updated": "آخر تحديث",
383
+ "sections": """
384
+ <div class="header">
385
+ <h1>سياسة الخصوصية</h1>
386
+ <div class="updated">آخر تحديث: {date}</div>
387
+ </div>
388
+ <div class="content">
389
+ <h2>1. المعلومات التي نجمعها</h2>
390
+ <p>في Lojiz، نجمع المعلومات لتقديم أفضل تجربة تأجير لك. يشمل ذلك:</p>
391
+ <ul>
392
+ <li><strong>معلومات الحساب:</strong> الاسم والبريد الإلكتروني ورقم الهاتف وصورة الملف الشخصي عند إنشاء حسابك.</li>
393
+ <li><strong>بيانات العقارات:</strong> الإعلانات المنشأة وسجل البحث والعقارات المحفوظة وتفاصيل الحجز.</li>
394
+ <li><strong>بيانات الاستخدام:</strong> كيفية تفاعلك مع تطبيقنا والصفحات المعروضة والميزات المستخدمة.</li>
395
+ <li><strong>معلومات الجهاز:</strong> نوع الجهاز ونظام التشغيل وإصدار التطبيق.</li>
396
+ <li><strong>بيانات الموقع:</strong> بإذنك، لعرض العقارات القريبة.</li>
397
+ </ul>
398
+
399
+ <h2>2. كيف نستخدم معلوماتك</h2>
400
+ <ul>
401
+ <li>توفير وتحسين منصة التأجير</li>
402
+ <li>تسهيل التواصل بين المستأجرين والملاك</li>
403
+ <li>تشغيل AIDA، مساعدتنا الذكية، لمساعدتك في العثور على عقارات</li>
404
+ <li>إرسال إشعارات ذات صلة حول الإعلانات والحجوزات</li>
405
+ <li>معالجة المدفوعات بأمان عبر Stripe</li>
406
+ <li>منع الاحتيال وضمان سلامة المنصة</li>
407
+ </ul>
408
+
409
+ <div class="highlight">نحن لا نبيع بياناتك الشخصية أبداً لأطراف ثالثة. تُستخدم معلوماتك فقط لتحسين تجربتك في Lojiz.</div>
410
+
411
+ <h2>3. المساعدة الذكية (AIDA)</h2>
412
+ <p>تستخدم AIDA تفضيلات البحث وسجل المحادثات لتقديم توصيات مخصصة. تتم معالجة المحادثات بشكل آمن ولا تتم مشاركتها.</p>
413
+
414
+ <h2>4. مشاركة البيانات</h2>
415
+ <ul>
416
+ <li><strong>الملاك/المستأجرون:</strong> بيانات الاتصال عند بدء حجز أو استفسار.</li>
417
+ <li><strong>معالجو الدفع:</strong> يعالج Stripe المدفوعات؛ لا نخزن تفاصيل البطاقة الكاملة.</li>
418
+ <li><strong>مقدمو الخدمات:</strong> الاستضافة والتحليلات وخدمات البريد الإلكتروني.</li>
419
+ <li><strong>المتطلبات القانونية:</strong> عندما يتطلب القانون ذلك.</li>
420
+ </ul>
421
+
422
+ <h2>5. أمان البيانات</h2>
423
+ <ul>
424
+ <li>تشفير من طرف إلى طرف للرسائل</li>
425
+ <li>مصادقة JWT آمنة</li>
426
+ <li>تدقيقات أمنية منتظمة</li>
427
+ <li>تخزين بيانات مشفر</li>
428
+ </ul>
429
+
430
+ <h2>6. حقوقك</h2>
431
+ <ul>
432
+ <li>الوصول إلى بياناتك الشخصية وتحميلها</li>
433
+ <li>تصحيح المعلومات غير الدقيقة</li>
434
+ <li>حذف حسابك والبيانات المرتبطة</li>
435
+ <li>رفض التحليلات والاتصالات التسويقية</li>
436
+ <li>سحب الموافقة على تتبع الموقع</li>
437
+ </ul>
438
+
439
+ <h2>7. الاحتفاظ بالبيانات</h2>
440
+ <p>نحتفظ ببياناتك طالما حسابك نشط. بعد الحذف، نزيل البيانات الشخصية خلال 30 يوماً، ما لم تتطلب الالتزامات القانونية خلاف ذلك.</p>
441
+
442
+ <h2>8. خصوصية الأطفال</h2>
443
+ <p>Lojiz غير مخصص للمستخدمين دون 18 عاماً.</p>
444
+
445
+ <h2>9. التغييرات على هذه السياسة</h2>
446
+ <p>قد نحدّث هذه السياسة بشكل دوري. سيتم إخطارك بالتغييرات المهمة عبر التطبيق أو البريد الإلكتروني.</p>
447
+
448
+ <div class="contact-box">
449
+ <p>أسئلة حول خصوصيتك؟</p>
450
+ <a href="mailto:privacy@lojiz.com">privacy@lojiz.com</a>
451
+ </div>
452
+ </div>"""
453
+ },
454
+ "pt": {
455
+ "title": "Política de Privacidade",
456
+ "updated": "Última atualização",
457
+ "sections": """
458
+ <div class="header">
459
+ <h1>Política de Privacidade</h1>
460
+ <div class="updated">Última atualização: {date}</div>
461
+ </div>
462
+ <div class="content">
463
+ <h2>1. Informações que coletamos</h2>
464
+ <p>Na Lojiz, coletamos informações para oferecer a melhor experiência de aluguel. Isso inclui:</p>
465
+ <ul>
466
+ <li><strong>Informações da conta:</strong> Nome, e-mail, número de telefone e foto de perfil ao criar sua conta.</li>
467
+ <li><strong>Dados de imóveis:</strong> Anúncios criados, histórico de busca, imóveis salvos e detalhes de reservas.</li>
468
+ <li><strong>Dados de uso:</strong> Como você interage com nosso aplicativo, páginas visitadas e recursos utilizados.</li>
469
+ <li><strong>Informações do dispositivo:</strong> Tipo de dispositivo, sistema operacional e versão do aplicativo.</li>
470
+ <li><strong>Dados de localização:</strong> Com sua permissão, para mostrar imóveis próximos.</li>
471
+ </ul>
472
+
473
+ <h2>2. Como usamos suas informações</h2>
474
+ <ul>
475
+ <li>Fornecer e melhorar nossa plataforma de aluguel</li>
476
+ <li>Facilitar a comunicação entre inquilinos e proprietários</li>
477
+ <li>Alimentar a AIDA, nossa assistente de IA, para ajudá-lo a encontrar imóveis</li>
478
+ <li>Enviar notificações relevantes sobre anúncios e reservas</li>
479
+ <li>Processar pagamentos com segurança via Stripe</li>
480
+ <li>Prevenir fraudes e garantir a segurança da plataforma</li>
481
+ </ul>
482
+
483
+ <div class="highlight">Nunca vendemos seus dados pessoais a terceiros. Suas informações são usadas exclusivamente para melhorar sua experiência na Lojiz.</div>
484
+
485
+ <h2>3. Assistente de IA (AIDA)</h2>
486
+ <p>A AIDA usa suas preferências de busca e histórico de conversas para fornecer recomendações personalizadas. As conversas são processadas com segurança e não são compartilhadas.</p>
487
+
488
+ <h2>4. Compartilhamento de dados</h2>
489
+ <ul>
490
+ <li><strong>Proprietários/Inquilinos:</strong> Dados de contato ao iniciar uma reserva ou consulta.</li>
491
+ <li><strong>Processadores de pagamento:</strong> O Stripe processa pagamentos; não armazenamos dados completos do cartão.</li>
492
+ <li><strong>Provedores de serviço:</strong> Hospedagem, análises e serviços de e-mail.</li>
493
+ <li><strong>Requisitos legais:</strong> Quando exigido por lei.</li>
494
+ </ul>
495
+
496
+ <h2>5. Segurança dos dados</h2>
497
+ <ul>
498
+ <li>Criptografia de ponta a ponta para mensagens</li>
499
+ <li>Autenticação JWT segura</li>
500
+ <li>Auditorias de segurança regulares</li>
501
+ <li>Armazenamento de dados criptografado</li>
502
+ </ul>
503
+
504
+ <h2>6. Seus direitos</h2>
505
+ <ul>
506
+ <li>Acessar e baixar seus dados pessoais</li>
507
+ <li>Corrigir informações imprecisas</li>
508
+ <li>Excluir sua conta e dados associados</li>
509
+ <li>Recusar análises e comunicações de marketing</li>
510
+ <li>Retirar o consentimento para rastreamento de localização</li>
511
+ </ul>
512
+
513
+ <h2>7. Retenção de dados</h2>
514
+ <p>Mantemos seus dados enquanto sua conta estiver ativa. Após a exclusão, removemos os dados pessoais em 30 dias, exceto quando obrigações legais exigirem retenção mais longa.</p>
515
+
516
+ <h2>8. Privacidade de menores</h2>
517
+ <p>A Lojiz não é destinada a usuários menores de 18 anos.</p>
518
+
519
+ <h2>9. Alterações nesta política</h2>
520
+ <p>Podemos atualizar esta política periodicamente. Mudanças significativas serão notificadas pelo aplicativo ou e-mail.</p>
521
+
522
+ <div class="contact-box">
523
+ <p>Dúvidas sobre sua privacidade?</p>
524
+ <a href="mailto:privacy@lojiz.com">privacy@lojiz.com</a>
525
+ </div>
526
+ </div>"""
527
+ },
528
+ }
529
+
530
+
531
+ # ══════════════════════════════════════════════════════════════
532
+ # TERMS OF SERVICE
533
+ # ══════════════════════════════════════════════════════════════
534
+
535
+ TERMS_CONTENT = {
536
+ "en": {
537
+ "title": "Terms of Service",
538
+ "sections": """
539
+ <div class="header">
540
+ <h1>Terms of Service</h1>
541
+ <div class="updated">Last updated: {date}</div>
542
+ </div>
543
+ <div class="content">
544
+ <h2>1. Acceptance of Terms</h2>
545
+ <p>By accessing or using Lojiz, you agree to be bound by these Terms of Service. If you do not agree, please do not use our platform.</p>
546
+
547
+ <h2>2. Eligibility</h2>
548
+ <p>You must be at least 18 years old to use Lojiz. By creating an account, you represent that you meet this requirement.</p>
549
+
550
+ <h2>3. Account Responsibilities</h2>
551
+ <ul>
552
+ <li>You are responsible for maintaining the confidentiality of your account credentials.</li>
553
+ <li>You must provide accurate and current information.</li>
554
+ <li>You are responsible for all activities under your account.</li>
555
+ <li>Notify us immediately of any unauthorized access.</li>
556
+ </ul>
557
+
558
+ <h2>4. Property Listings</h2>
559
+ <h3>For Landlords:</h3>
560
+ <ul>
561
+ <li>You must have legal authority to list the property.</li>
562
+ <li>Listings must be accurate, truthful, and up-to-date.</li>
563
+ <li>Photos must represent the actual property.</li>
564
+ <li>Pricing must be transparent with no hidden fees.</li>
565
+ </ul>
566
+ <h3>For Renters:</h3>
567
+ <ul>
568
+ <li>Use the platform respectfully and in good faith.</li>
569
+ <li>Do not submit fraudulent bookings or inquiries.</li>
570
+ <li>Respect property rules set by landlords.</li>
571
+ </ul>
572
+
573
+ <div class="highlight">Lojiz acts as a platform connecting landlords and renters. We do not own, manage, or control any listed properties.</div>
574
+
575
+ <h2>5. AI Assistant (AIDA)</h2>
576
+ <p>AIDA provides property recommendations and assistance. While we strive for accuracy, AIDA's suggestions are informational and do not constitute professional real estate advice. Always verify property details independently.</p>
577
+
578
+ <h2>6. Payments & Fees</h2>
579
+ <ul>
580
+ <li>Payments are processed securely through Stripe.</li>
581
+ <li>Service fees may apply to bookings.</li>
582
+ <li>Cancellation and refund policies are specified per listing.</li>
583
+ <li>Landlords receive payouts according to their Stripe configuration.</li>
584
+ </ul>
585
+
586
+ <h2>7. Prohibited Conduct</h2>
587
+ <p>You may not:</p>
588
+ <ul>
589
+ <li>Post false, misleading, or discriminatory content</li>
590
+ <li>Harass, threaten, or abuse other users</li>
591
+ <li>Circumvent the platform to avoid fees</li>
592
+ <li>Use automated tools to scrape or manipulate the platform</li>
593
+ <li>Violate any applicable laws or regulations</li>
594
+ </ul>
595
+
596
+ <h2>8. Content Ownership</h2>
597
+ <p>You retain ownership of content you post (photos, descriptions). By posting, you grant Lojiz a non-exclusive license to display and promote your listings on the platform.</p>
598
+
599
+ <h2>9. Limitation of Liability</h2>
600
+ <p>Lojiz is provided "as is". We are not liable for:</p>
601
+ <ul>
602
+ <li>Property conditions or landlord/renter behavior</li>
603
+ <li>Disputes between users</li>
604
+ <li>Service interruptions or data loss</li>
605
+ <li>Third-party service failures</li>
606
+ </ul>
607
+
608
+ <h2>10. Account Termination</h2>
609
+ <p>We may suspend or terminate accounts that violate these terms. You may delete your account at any time through the app settings.</p>
610
+
611
+ <h2>11. Dispute Resolution</h2>
612
+ <p>Disputes will be resolved through good-faith negotiation first. If unresolved, disputes will be settled through binding arbitration.</p>
613
+
614
+ <h2>12. Governing Law</h2>
615
+ <p>These terms are governed by applicable laws. Any legal proceedings shall be conducted in the appropriate jurisdiction.</p>
616
+
617
+ <h2>13. Changes to Terms</h2>
618
+ <p>We may modify these terms at any time. Continued use after changes constitutes acceptance. Material changes will be notified via the app.</p>
619
+
620
+ <div class="contact-box">
621
+ <p>Questions about these terms?</p>
622
+ <a href="mailto:legal@lojiz.com">legal@lojiz.com</a>
623
+ </div>
624
+ </div>"""
625
+ },
626
+ "fr": {
627
+ "title": "Conditions d'Utilisation",
628
+ "sections": """
629
+ <div class="header">
630
+ <h1>Conditions d'Utilisation</h1>
631
+ <div class="updated">Dernière mise à jour : {date}</div>
632
+ </div>
633
+ <div class="content">
634
+ <h2>1. Acceptation des conditions</h2>
635
+ <p>En accédant ou en utilisant Lojiz, vous acceptez d'être lié par ces conditions. Si vous n'êtes pas d'accord, veuillez ne pas utiliser notre plateforme.</p>
636
+
637
+ <h2>2. Éligibilité</h2>
638
+ <p>Vous devez avoir au moins 18 ans pour utiliser Lojiz. En créant un compte, vous déclarez remplir cette condition.</p>
639
+
640
+ <h2>3. Responsabilités du compte</h2>
641
+ <ul>
642
+ <li>Vous êtes responsable de la confidentialité de vos identifiants.</li>
643
+ <li>Vous devez fournir des informations exactes et à jour.</li>
644
+ <li>Vous êtes responsable de toutes les activités sous votre compte.</li>
645
+ <li>Signalez immédiatement tout accès non autorisé.</li>
646
+ </ul>
647
+
648
+ <h2>4. Annonces immobilières</h2>
649
+ <h3>Pour les propriétaires :</h3>
650
+ <ul>
651
+ <li>Vous devez avoir l'autorité légale pour lister la propriété.</li>
652
+ <li>Les annonces doivent être exactes, véridiques et à jour.</li>
653
+ <li>Les photos doivent représenter la propriété réelle.</li>
654
+ <li>Les prix doivent être transparents sans frais cachés.</li>
655
+ </ul>
656
+ <h3>Pour les locataires :</h3>
657
+ <ul>
658
+ <li>Utilisez la plateforme respectueusement et de bonne foi.</li>
659
+ <li>Ne soumettez pas de réservations ou demandes frauduleuses.</li>
660
+ <li>Respectez les règles de propriété définies par les propriétaires.</li>
661
+ </ul>
662
+
663
+ <div class="highlight">Lojiz agit comme une plateforme connectant propriétaires et locataires. Nous ne possédons, ne gérons ni ne contrôlons les propriétés listées.</div>
664
+
665
+ <h2>5. Assistante IA (AIDA)</h2>
666
+ <p>AIDA fournit des recommandations et une assistance immobilière. Ses suggestions sont informatives et ne constituent pas un conseil immobilier professionnel.</p>
667
+
668
+ <h2>6. Paiements et frais</h2>
669
+ <ul>
670
+ <li>Les paiements sont traités de manière sécurisée via Stripe.</li>
671
+ <li>Des frais de service peuvent s'appliquer aux réservations.</li>
672
+ <li>Les politiques d'annulation et de remboursement sont spécifiées par annonce.</li>
673
+ <li>Les propriétaires reçoivent les paiements selon leur configuration Stripe.</li>
674
+ </ul>
675
+
676
+ <h2>7. Conduite interdite</h2>
677
+ <ul>
678
+ <li>Publier du contenu faux, trompeur ou discriminatoire</li>
679
+ <li>Harceler, menacer ou abuser d'autres utilisateurs</li>
680
+ <li>Contourner la plateforme pour éviter les frais</li>
681
+ <li>Utiliser des outils automatisés pour extraire ou manipuler la plateforme</li>
682
+ <li>Violer les lois ou réglementations applicables</li>
683
+ </ul>
684
+
685
+ <h2>8. Propriété du contenu</h2>
686
+ <p>Vous conservez la propriété du contenu que vous publiez. En publiant, vous accordez à Lojiz une licence non exclusive pour afficher et promouvoir vos annonces.</p>
687
+
688
+ <h2>9. Limitation de responsabilité</h2>
689
+ <p>Lojiz est fourni « tel quel ». Nous ne sommes pas responsables des conditions des propriétés, des litiges entre utilisateurs ou des interruptions de service.</p>
690
+
691
+ <h2>10. Résiliation de compte</h2>
692
+ <p>Nous pouvons suspendre ou résilier les comptes en violation. Vous pouvez supprimer votre compte à tout moment via les paramètres.</p>
693
+
694
+ <h2>11. Résolution des litiges</h2>
695
+ <p>Les litiges seront résolus par négociation de bonne foi d'abord, puis par arbitrage contraignant si nécessaire.</p>
696
+
697
+ <h2>12. Modifications des conditions</h2>
698
+ <p>Nous pouvons modifier ces conditions à tout moment. L'utilisation continue après les modifications constitue une acceptation.</p>
699
+
700
+ <div class="contact-box">
701
+ <p>Questions sur ces conditions ?</p>
702
+ <a href="mailto:legal@lojiz.com">legal@lojiz.com</a>
703
+ </div>
704
+ </div>"""
705
+ },
706
+ "es": {
707
+ "title": "Términos de Servicio",
708
+ "sections": """
709
+ <div class="header">
710
+ <h1>Términos de Servicio</h1>
711
+ <div class="updated">Última actualización: {date}</div>
712
+ </div>
713
+ <div class="content">
714
+ <h2>1. Aceptación de los términos</h2>
715
+ <p>Al acceder o usar Lojiz, aceptas estar sujeto a estos Términos de Servicio. Si no estás de acuerdo, no uses nuestra plataforma.</p>
716
+
717
+ <h2>2. Elegibilidad</h2>
718
+ <p>Debes tener al menos 18 años para usar Lojiz.</p>
719
+
720
+ <h2>3. Responsabilidades de la cuenta</h2>
721
+ <ul>
722
+ <li>Eres responsable de la confidencialidad de tus credenciales.</li>
723
+ <li>Debes proporcionar información precisa y actual.</li>
724
+ <li>Eres responsable de todas las actividades bajo tu cuenta.</li>
725
+ <li>Notifícanos inmediatamente de cualquier acceso no autorizado.</li>
726
+ </ul>
727
+
728
+ <h2>4. Anuncios de propiedades</h2>
729
+ <h3>Para propietarios:</h3>
730
+ <ul>
731
+ <li>Debes tener autoridad legal para listar la propiedad.</li>
732
+ <li>Los anuncios deben ser precisos, veraces y actualizados.</li>
733
+ <li>Las fotos deben representar la propiedad real.</li>
734
+ <li>Los precios deben ser transparentes sin cargos ocultos.</li>
735
+ </ul>
736
+ <h3>Para inquilinos:</h3>
737
+ <ul>
738
+ <li>Usa la plataforma respetuosamente y de buena fe.</li>
739
+ <li>No envíes reservas o consultas fraudulentas.</li>
740
+ </ul>
741
+
742
+ <div class="highlight">Lojiz actúa como plataforma que conecta propietarios e inquilinos. No poseemos, gestionamos ni controlamos las propiedades listadas.</div>
743
+
744
+ <h2>5. Asistente de IA (AIDA)</h2>
745
+ <p>AIDA proporciona recomendaciones e información. Sus sugerencias no constituyen asesoramiento inmobiliario profesional.</p>
746
+
747
+ <h2>6. Pagos y tarifas</h2>
748
+ <ul>
749
+ <li>Los pagos se procesan de forma segura a través de Stripe.</li>
750
+ <li>Pueden aplicarse tarifas de servicio a las reservas.</li>
751
+ <li>Las políticas de cancelación se especifican por anuncio.</li>
752
+ </ul>
753
+
754
+ <h2>7. Conducta prohibida</h2>
755
+ <ul>
756
+ <li>Publicar contenido falso, engañoso o discriminatorio</li>
757
+ <li>Acosar, amenazar o abusar de otros usuarios</li>
758
+ <li>Eludir la plataforma para evitar tarifas</li>
759
+ <li>Violar leyes o regulaciones aplicables</li>
760
+ </ul>
761
+
762
+ <h2>8. Propiedad del contenido</h2>
763
+ <p>Conservas la propiedad del contenido que publicas. Al publicar, otorgas a Lojiz una licencia no exclusiva para mostrar tus anuncios.</p>
764
+
765
+ <h2>9. Limitación de responsabilidad</h2>
766
+ <p>Lojiz se proporciona "tal cual". No somos responsables de las condiciones de las propiedades ni de disputas entre usuarios.</p>
767
+
768
+ <h2>10. Terminación de cuenta</h2>
769
+ <p>Podemos suspender cuentas que violen estos términos. Puedes eliminar tu cuenta en cualquier momento.</p>
770
+
771
+ <h2>11. Cambios en los términos</h2>
772
+ <p>Podemos modificar estos términos en cualquier momento. El uso continuado constituye aceptación.</p>
773
+
774
+ <div class="contact-box">
775
+ <p>¿Preguntas sobre estos términos?</p>
776
+ <a href="mailto:legal@lojiz.com">legal@lojiz.com</a>
777
+ </div>
778
+ </div>"""
779
+ },
780
+ "ar": {
781
+ "title": "شروط الخدمة",
782
+ "sections": """
783
+ <div class="header">
784
+ <h1>شروط الخدمة</h1>
785
+ <div class="updated">آخر تحديث: {date}</div>
786
+ </div>
787
+ <div class="content">
788
+ <h2>1. قبول الشروط</h2>
789
+ <p>باستخدام Lojiz، فإنك تواف�� على الالتزام بشروط الخدمة هذه. إذا لم توافق، يرجى عدم استخدام منصتنا.</p>
790
+
791
+ <h2>2. الأهلية</h2>
792
+ <p>يجب أن يكون عمرك 18 عاماً على الأقل لاستخدام Lojiz.</p>
793
+
794
+ <h2>3. مسؤوليات الحساب</h2>
795
+ <ul>
796
+ <li>أنت مسؤول عن الحفاظ على سرية بيانات اعتماد حسابك.</li>
797
+ <li>يجب تقديم معلومات دقيقة وحديثة.</li>
798
+ <li>أنت مسؤول عن جميع الأنشطة تحت حسابك.</li>
799
+ <li>أبلغنا فوراً عن أي وصول غير مصرح به.</li>
800
+ </ul>
801
+
802
+ <h2>4. إعلانات العقارات</h2>
803
+ <h3>للملاك:</h3>
804
+ <ul>
805
+ <li>يجب أن تملك السلطة القانونية لإدراج العقار.</li>
806
+ <li>يجب أن تكون الإعلانات دقيقة وصادقة ومحدثة.</li>
807
+ <li>يجب أن تمثل الصور العقار الفعلي.</li>
808
+ <li>يجب أن تكون الأسعار شفافة بدون رسوم مخفية.</li>
809
+ </ul>
810
+ <h3>للمستأجرين:</h3>
811
+ <ul>
812
+ <li>استخدم المنصة باحترام وحسن نية.</li>
813
+ <li>لا تقدم حجوزات أو استفسارات احتيالية.</li>
814
+ </ul>
815
+
816
+ <div class="highlight">Lojiz هي منصة تربط الملاك بالمستأجرين. نحن لا نملك أو ندير أو نتحكم في أي عقارات مدرجة.</div>
817
+
818
+ <h2>5. المساعدة الذكية (AIDA)</h2>
819
+ <p>تقدم AIDA توصيات ومعلومات. اقتراحاتها إرشادية ولا تشكل نصيحة عقارية مهنية.</p>
820
+
821
+ <h2>6. المدفوعات والرسوم</h2>
822
+ <ul>
823
+ <li>تتم معالجة المدفوعات بأمان عبر Stripe.</li>
824
+ <li>قد تنطبق رسوم خدمة على الحجوزات.</li>
825
+ <li>سياسات الإلغاء والاسترداد محددة لكل إعلان.</li>
826
+ </ul>
827
+
828
+ <h2>7. السلوك المحظور</h2>
829
+ <ul>
830
+ <li>نشر محتوى كاذب أو مضلل أو تمييزي</li>
831
+ <li>مضايقة أو تهديد أو إساءة معاملة المستخدمين الآخرين</li>
832
+ <li>التحايل على المنصة لتجنب الرسوم</li>
833
+ <li>انتهاك القوانين أو اللوائح المعمول بها</li>
834
+ </ul>
835
+
836
+ <h2>8. ملكية المحتوى</h2>
837
+ <p>تحتفظ بملكية المحتوى الذي تنشره. بالنشر، تمنح Lojiz ترخيصاً غير حصري لعرض إعلاناتك.</p>
838
+
839
+ <h2>9. تحديد المسؤولية</h2>
840
+ <p>يتم توفير Lojiz "كما هي". لسنا مسؤولين عن حالة العقارات أو النزاعات بين المستخدمين.</p>
841
+
842
+ <h2>10. إنهاء الحساب</h2>
843
+ <p>يمكننا تعليق الحسابات المخالفة. يمكنك حذف حسابك في أي وقت من الإعدادات.</p>
844
+
845
+ <h2>11. التغييرات على الشروط</h2>
846
+ <p>يمكننا تعديل هذه الشروط في أي وقت. الاستمرار في الاستخدام يعني القبول.</p>
847
+
848
+ <div class="contact-box">
849
+ <p>أسئلة حول هذه الشروط؟</p>
850
+ <a href="mailto:legal@lojiz.com">legal@lojiz.com</a>
851
+ </div>
852
+ </div>"""
853
+ },
854
+ "pt": {
855
+ "title": "Termos de Serviço",
856
+ "sections": """
857
+ <div class="header">
858
+ <h1>Termos de Serviço</h1>
859
+ <div class="updated">Última atualização: {date}</div>
860
+ </div>
861
+ <div class="content">
862
+ <h2>1. Aceitação dos termos</h2>
863
+ <p>Ao acessar ou usar a Lojiz, você concorda em estar vinculado a estes Termos de Serviço. Se não concordar, não use nossa plataforma.</p>
864
+
865
+ <h2>2. Elegibilidade</h2>
866
+ <p>Você deve ter pelo menos 18 anos para usar a Lojiz.</p>
867
+
868
+ <h2>3. Responsabilidades da conta</h2>
869
+ <ul>
870
+ <li>Você é responsável pela confidencialidade de suas credenciais.</li>
871
+ <li>Deve fornecer informações precisas e atuais.</li>
872
+ <li>É responsável por todas as atividades em sua conta.</li>
873
+ <li>Notifique-nos imediatamente sobre qualquer acesso não autorizado.</li>
874
+ </ul>
875
+
876
+ <h2>4. Anúncios de imóveis</h2>
877
+ <h3>Para proprietários:</h3>
878
+ <ul>
879
+ <li>Você deve ter autoridade legal para listar o imóvel.</li>
880
+ <li>Os anúncios devem ser precisos, verdadeiros e atualizados.</li>
881
+ <li>As fotos devem representar o imóvel real.</li>
882
+ <li>Os preços devem ser transparentes sem taxas ocultas.</li>
883
+ </ul>
884
+ <h3>Para inquilinos:</h3>
885
+ <ul>
886
+ <li>Use a plataforma com respeito e boa-fé.</li>
887
+ <li>Não envie reservas ou consultas fraudulentas.</li>
888
+ </ul>
889
+
890
+ <div class="highlight">A Lojiz atua como plataforma conectando proprietários e inquilinos. Não possuímos, gerenciamos ou controlamos os imóveis listados.</div>
891
+
892
+ <h2>5. Assistente de IA (AIDA)</h2>
893
+ <p>A AIDA fornece recomendações e informações. Suas sugestões não constituem consultoria imobiliária profissional.</p>
894
+
895
+ <h2>6. Pagamentos e taxas</h2>
896
+ <ul>
897
+ <li>Pagamentos processados com segurança via Stripe.</li>
898
+ <li>Taxas de serviço podem ser aplicadas às reservas.</li>
899
+ <li>Políticas de cancelamento especificadas por anúncio.</li>
900
+ </ul>
901
+
902
+ <h2>7. Conduta proibida</h2>
903
+ <ul>
904
+ <li>Publicar conteúdo falso, enganoso ou discriminatório</li>
905
+ <li>Assediar, ameaçar ou abusar de outros usuários</li>
906
+ <li>Contornar a plataforma para evitar taxas</li>
907
+ <li>Violar leis ou regulamentos aplicáveis</li>
908
+ </ul>
909
+
910
+ <h2>8. Propriedade do conteúdo</h2>
911
+ <p>Você mantém a propriedade do conteúdo publicado. Ao publicar, concede à Lojiz uma licença não exclusiva para exibir seus anúncios.</p>
912
+
913
+ <h2>9. Limitação de responsabilidade</h2>
914
+ <p>A Lojiz é fornecida "como está". Não somos responsáveis pelas condições dos imóveis ou disputas entre usuários.</p>
915
+
916
+ <h2>10. Encerramento de conta</h2>
917
+ <p>Podemos suspender contas que violem estes termos. Você pode excluir sua conta a qualquer momento.</p>
918
+
919
+ <h2>11. Alterações nos termos</h2>
920
+ <p>Podemos modificar estes termos a qualquer momento. O uso continuado constitui aceitação.</p>
921
+
922
+ <div class="contact-box">
923
+ <p>Dúvidas sobre estes termos?</p>
924
+ <a href="mailto:legal@lojiz.com">legal@lojiz.com</a>
925
+ </div>
926
+ </div>"""
927
+ },
928
+ }
929
+
930
+
931
+ # ══════════════════════════════════════════════════════════════
932
+ # ENDPOINTS
933
+ # ══════════════════════════════════════════════════════════════
934
+
935
+ @router.get("/privacy-policy", response_class=HTMLResponse)
936
+ async def privacy_policy(lang: str = Query("en", regex="^(en|fr|es|ar|pt)$")):
937
+ """Serve localized Privacy Policy as styled HTML."""
938
+ content = PRIVACY_CONTENT.get(lang, PRIVACY_CONTENT["en"])
939
+ html_body = content["sections"].replace("{date}", LAST_UPDATED)
940
+ return HTMLResponse(_base_template(content["title"], html_body, lang))
941
+
942
+
943
+ @router.get("/terms-of-service", response_class=HTMLResponse)
944
+ async def terms_of_service(lang: str = Query("en", regex="^(en|fr|es|ar|pt)$")):
945
+ """Serve localized Terms of Service as styled HTML."""
946
+ content = TERMS_CONTENT.get(lang, TERMS_CONTENT["en"])
947
+ html_body = content["sections"].replace("{date}", LAST_UPDATED)
948
+ return HTMLResponse(_base_template(content["title"], html_body, lang))
app/routes/notifications.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # app/routes/notifications.py – Notification preferences + FCM
3
+ # ============================================================
4
+
5
+ import logging
6
+ from fastapi import APIRouter, HTTPException, status, Depends, Request
7
+ from pydantic import BaseModel, Field
8
+ from typing import Optional
9
+ from app.guards.jwt_guard import get_current_user
10
+ from app.database import get_db
11
+ from datetime import datetime
12
+ from bson import ObjectId
13
+
14
+ router = APIRouter(tags=["Notifications"])
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ # ============================================================
19
+ # DTOs
20
+ # ============================================================
21
+
22
+ class NotificationPrefsDto(BaseModel):
23
+ """Notification preference toggles"""
24
+ master_enabled: bool = True
25
+ messages: bool = True
26
+ property_alerts: bool = True
27
+ booking_updates: bool = True
28
+ promotions: bool = False
29
+ push_enabled: bool = False
30
+ email_enabled: bool = False
31
+ voice_notes_enabled: bool = False
32
+
33
+
34
+ class FCMTokenDto(BaseModel):
35
+ """Register or update FCM device token"""
36
+ token: str = Field(..., min_length=10, max_length=500)
37
+ platform: str = Field(..., pattern="^(android|ios)$")
38
+ device_id: Optional[str] = None
39
+
40
+
41
+ # ============================================================
42
+ # NOTIFICATION PREFERENCES
43
+ # ============================================================
44
+
45
+ @router.get("/preferences", status_code=status.HTTP_200_OK)
46
+ async def get_notification_preferences(
47
+ request: Request,
48
+ current_user: dict = Depends(get_current_user),
49
+ ):
50
+ """Fetch user's notification preferences."""
51
+ user_id = current_user.get("user_id")
52
+
53
+ db = await get_db()
54
+ prefs = await db["notification_prefs"].find_one({"user_id": user_id})
55
+
56
+ if not prefs:
57
+ # Return defaults if no prefs saved yet
58
+ return {
59
+ "success": True,
60
+ "data": NotificationPrefsDto().model_dump(),
61
+ }
62
+
63
+ # Remove mongo fields
64
+ prefs.pop("_id", None)
65
+ prefs.pop("user_id", None)
66
+ prefs.pop("updatedAt", None)
67
+
68
+ return {
69
+ "success": True,
70
+ "data": prefs,
71
+ }
72
+
73
+
74
+ @router.put("/preferences", status_code=status.HTTP_200_OK)
75
+ async def update_notification_preferences(
76
+ dto: NotificationPrefsDto,
77
+ request: Request,
78
+ current_user: dict = Depends(get_current_user),
79
+ ):
80
+ """Update user's notification preferences. Synced across devices."""
81
+ user_id = current_user.get("user_id")
82
+
83
+ db = await get_db()
84
+ prefs_data = {
85
+ **dto.model_dump(),
86
+ "user_id": user_id,
87
+ "updatedAt": datetime.utcnow(),
88
+ }
89
+
90
+ await db["notification_prefs"].update_one(
91
+ {"user_id": user_id},
92
+ {"$set": prefs_data},
93
+ upsert=True,
94
+ )
95
+
96
+ logger.info(f"Notification prefs updated for user {user_id}")
97
+
98
+ return {
99
+ "success": True,
100
+ "message": "Notification preferences saved.",
101
+ "data": dto.model_dump(),
102
+ }
103
+
104
+
105
+ # ============================================================
106
+ # FCM TOKEN REGISTRATION
107
+ # ============================================================
108
+
109
+ @router.post("/fcm-token", status_code=status.HTTP_200_OK)
110
+ async def register_fcm_token(
111
+ dto: FCMTokenDto,
112
+ request: Request,
113
+ current_user: dict = Depends(get_current_user),
114
+ ):
115
+ """
116
+ Register or update FCM device token for push notifications.
117
+ Each user can have multiple tokens (one per device).
118
+ """
119
+ user_id = current_user.get("user_id")
120
+
121
+ db = await get_db()
122
+ token_data = {
123
+ "user_id": user_id,
124
+ "token": dto.token,
125
+ "platform": dto.platform,
126
+ "device_id": dto.device_id,
127
+ "active": True,
128
+ "updatedAt": datetime.utcnow(),
129
+ }
130
+
131
+ # Upsert by token (same device updates, new device inserts)
132
+ await db["fcm_tokens"].update_one(
133
+ {"token": dto.token},
134
+ {"$set": token_data},
135
+ upsert=True,
136
+ )
137
+
138
+ logger.info(f"FCM token registered for user {user_id} ({dto.platform})")
139
+
140
+ return {
141
+ "success": True,
142
+ "message": "Push notification token registered.",
143
+ }
144
+
145
+
146
+ @router.delete("/fcm-token", status_code=status.HTTP_200_OK)
147
+ async def unregister_fcm_token(
148
+ dto: FCMTokenDto,
149
+ request: Request,
150
+ current_user: dict = Depends(get_current_user),
151
+ ):
152
+ """Unregister FCM token (e.g., on logout)."""
153
+ user_id = current_user.get("user_id")
154
+
155
+ db = await get_db()
156
+ await db["fcm_tokens"].delete_one({
157
+ "user_id": user_id,
158
+ "token": dto.token,
159
+ })
160
+
161
+ logger.info(f"FCM token unregistered for user {user_id}")
162
+
163
+ return {
164
+ "success": True,
165
+ "message": "Push notification token removed.",
166
+ }
app/services/email_notification_service.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # app/services/email_notification_service.py
3
+ # Email notifications for bookings & property alerts only
4
+ # Uses Resend API (same as OTP emails)
5
+ # ============================================================
6
+
7
+ import logging
8
+ from typing import Optional
9
+ from app.database import get_db
10
+ from app.config import settings
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ # Categories that support email notifications
15
+ # Messages are excluded — email is only for bookings & alerts
16
+ EMAIL_ALLOWED_CATEGORIES = {"booking_updates", "property_alerts", "promotions"}
17
+
18
+
19
+ class EmailNotificationService:
20
+ """Send email notifications via Resend for bookings and alerts."""
21
+
22
+ @staticmethod
23
+ async def should_send_email(user_id: str, category: str) -> bool:
24
+ """Check if user has email notifications enabled for this category."""
25
+ # Never send email for messages
26
+ if category not in EMAIL_ALLOWED_CATEGORIES:
27
+ return False
28
+
29
+ db = await get_db()
30
+ prefs = await db["notification_prefs"].find_one({"user_id": user_id})
31
+
32
+ if not prefs:
33
+ return False # Default: email off
34
+
35
+ if not prefs.get("master_enabled", True):
36
+ return False
37
+
38
+ if not prefs.get("email_enabled", False):
39
+ return False
40
+
41
+ # Check category-specific toggle
42
+ return prefs.get(category, True)
43
+
44
+ @staticmethod
45
+ async def get_user_email(user_id: str) -> Optional[str]:
46
+ """Get user's email address from the database."""
47
+ db = await get_db()
48
+ user = await db["users"].find_one({"_id": user_id})
49
+ if not user:
50
+ from bson import ObjectId
51
+ user = await db["users"].find_one({"_id": ObjectId(user_id)})
52
+ return user.get("email") if user else None
53
+
54
+ @staticmethod
55
+ async def send_email(
56
+ user_id: str,
57
+ subject: str,
58
+ body_html: str,
59
+ category: str = "booking_updates",
60
+ ) -> bool:
61
+ """
62
+ Send an email notification to a user.
63
+
64
+ Only sends if:
65
+ - Category is allowed (bookings, property alerts, promotions)
66
+ - User has master + email toggles enabled
67
+ - User has the specific category enabled
68
+ """
69
+ if not await EmailNotificationService.should_send_email(user_id, category):
70
+ logger.debug(f"Email skipped (prefs) for user {user_id}, category={category}")
71
+ return False
72
+
73
+ email = await EmailNotificationService.get_user_email(user_id)
74
+ if not email:
75
+ logger.debug(f"Email skipped (no email) for user {user_id}")
76
+ return False
77
+
78
+ try:
79
+ import resend
80
+ resend.api_key = settings.RESEND_API_KEY
81
+
82
+ params = {
83
+ "from": f"{settings.RESEND_FROM_NAME} <{settings.RESEND_FROM_EMAIL}>",
84
+ "to": email,
85
+ "subject": subject,
86
+ "html": _wrap_email_template(subject, body_html),
87
+ }
88
+ resp = resend.Emails.send(params)
89
+ if resp.get("error"):
90
+ raise Exception(resp["error"].get("message", "Send failed"))
91
+
92
+ logger.info(f"Email notification sent to {email} ({category})")
93
+ return True
94
+
95
+ except ImportError:
96
+ logger.warning("resend package not installed")
97
+ return False
98
+ except Exception as e:
99
+ logger.error(f"Email notification failed for {email}: {e}")
100
+ return False
101
+
102
+ @staticmethod
103
+ async def notify_user(
104
+ user_id: str,
105
+ title: str,
106
+ body: str,
107
+ category: str = "booking_updates",
108
+ ) -> bool:
109
+ """
110
+ High-level: send a simple email notification.
111
+ Builds a clean HTML email from title + body text.
112
+ """
113
+ body_html = f"<p style='font-size:16px;color:#333;line-height:1.6;'>{body}</p>"
114
+ return await EmailNotificationService.send_email(
115
+ user_id=user_id,
116
+ subject=title,
117
+ body_html=body_html,
118
+ category=category,
119
+ )
120
+
121
+
122
+ def _wrap_email_template(title: str, content_html: str) -> str:
123
+ """Wrap content in Lojiz branded email template."""
124
+ return f"""<!DOCTYPE html>
125
+ <html>
126
+ <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"></head>
127
+ <body style="margin:0;padding:0;background:#f5f5fa;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
128
+ <table width="100%" cellpadding="0" cellspacing="0" style="background:#f5f5fa;padding:32px 16px;">
129
+ <tr><td align="center">
130
+ <table width="100%" style="max-width:520px;background:#ffffff;border-radius:16px;overflow:hidden;box-shadow:0 2px 12px rgba(0,0,0,0.08);">
131
+ <!-- Header gradient -->
132
+ <tr><td style="background:linear-gradient(135deg,#764BA2,#FF305A);padding:28px 24px;text-align:center;">
133
+ <h1 style="margin:0;color:#ffffff;font-size:22px;font-weight:700;letter-spacing:-0.3px;">Lojiz</h1>
134
+ </td></tr>
135
+ <!-- Content -->
136
+ <tr><td style="padding:28px 24px;">
137
+ <h2 style="margin:0 0 16px;font-size:18px;font-weight:600;color:#1a1025;">{title}</h2>
138
+ {content_html}
139
+ </td></tr>
140
+ <!-- Footer -->
141
+ <tr><td style="padding:20px 24px;border-top:1px solid #eee;text-align:center;">
142
+ <p style="margin:0;font-size:12px;color:#999;">&copy; 2026 Lojiz. All rights reserved.</p>
143
+ <p style="margin:8px 0 0;font-size:11px;color:#bbb;">You received this because email notifications are enabled in your Lojiz settings.</p>
144
+ </td></tr>
145
+ </table>
146
+ </td></tr>
147
+ </table>
148
+ </body>
149
+ </html>"""
150
+
151
+
152
+ email_notification_service = EmailNotificationService()
app/services/otp_service.py CHANGED
@@ -105,8 +105,14 @@ class OTPService:
105
  try:
106
  template = self._generate_email_template(otp, purpose, language)
107
 
108
- # Get localized subject
109
- subject_key = "otp_verify_subject" if purpose == "signup" else "otp_reset_subject"
 
 
 
 
 
 
110
  subject = get_translation(language, subject_key)
111
 
112
  params = {
@@ -144,17 +150,19 @@ class OTPService:
144
  current_year = datetime.now().year
145
  expiry_minutes = settings.OTP_EXPIRY_MINUTES
146
 
147
- # Get localized strings
148
- if purpose == "signup":
149
- subject_line = get_translation(language, "otp_verify_subject").replace("Verify Your Lojiz Account", "Verify Your Account").replace("Vérifiez votre compte Lojiz", "Vérifiez votre compte")
150
- greeting = get_translation(language, "otp_verify_greeting")
151
- intro_text = get_translation(language, "otp_verify_message")
152
- footer_text = get_translation(language, "otp_verify_footer")
153
- else:
154
- subject_line = get_translation(language, "otp_reset_subject").replace("Reset Your Lojiz Password", "Reset Your Password").replace("Réinitialisez votre mot de passe Lojiz", "Réinitialisez votre mot de passe")
155
- greeting = get_translation(language, "otp_reset_greeting")
156
- intro_text = get_translation(language, "otp_reset_message")
157
- footer_text = get_translation(language, "otp_reset_footer")
 
 
158
 
159
  # Check if RTL language (Arabic)
160
  is_rtl = language == "ar"
@@ -200,7 +208,7 @@ class OTPService:
200
  <p class="intro">{intro_text}</p>
201
  <div class="otp-box">
202
  <div class="otp-code">{otp}</div>
203
- <div class="otp-expiry">{get_translation(language, "otp_verify_expiry" if purpose == "signup" else "otp_reset_expiry")}</div>
204
  </div>
205
  <p class="warning">{footer_text}</p>
206
  </div>
 
105
  try:
106
  template = self._generate_email_template(otp, purpose, language)
107
 
108
+ # Get localized subject based on purpose
109
+ subject_key_map = {
110
+ "signup": "otp_verify_subject",
111
+ "password_reset": "otp_reset_subject",
112
+ "email_update": "otp_email_update_subject",
113
+ "phone_update": "otp_phone_update_subject",
114
+ }
115
+ subject_key = subject_key_map.get(purpose, "otp_verify_subject")
116
  subject = get_translation(language, subject_key)
117
 
118
  params = {
 
150
  current_year = datetime.now().year
151
  expiry_minutes = settings.OTP_EXPIRY_MINUTES
152
 
153
+ # Get localized strings based on purpose
154
+ purpose_key_map = {
155
+ "signup": "otp_verify",
156
+ "password_reset": "otp_reset",
157
+ "email_update": "otp_email_update",
158
+ "phone_update": "otp_phone_update",
159
+ }
160
+ key_prefix = purpose_key_map.get(purpose, "otp_verify")
161
+
162
+ subject_line = get_translation(language, f"{key_prefix}_subject")
163
+ greeting = get_translation(language, f"{key_prefix}_greeting")
164
+ intro_text = get_translation(language, f"{key_prefix}_message")
165
+ footer_text = get_translation(language, f"{key_prefix}_footer")
166
 
167
  # Check if RTL language (Arabic)
168
  is_rtl = language == "ar"
 
208
  <p class="intro">{intro_text}</p>
209
  <div class="otp-box">
210
  <div class="otp-code">{otp}</div>
211
+ <div class="otp-expiry">{get_translation(language, f"{key_prefix}_expiry")}</div>
212
  </div>
213
  <p class="warning">{footer_text}</p>
214
  </div>
app/services/push_service.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # app/services/push_service.py – FCM Push Notification Service
3
+ # ============================================================
4
+ import os
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Optional
8
+ from app.database import get_db
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ # Try to initialize Firebase Admin
13
+ _firebase_app = None
14
+ try:
15
+ import firebase_admin
16
+ from firebase_admin import credentials, messaging
17
+
18
+ if not firebase_admin._apps:
19
+ # 1) Try explicit path from env
20
+ sa_path = os.getenv("FIREBASE_SERVICE_ACCOUNT_PATH", "")
21
+ if sa_path:
22
+ # Resolve relative to project root (where main.py lives)
23
+ full_path = Path(__file__).resolve().parents[2] / sa_path
24
+ if full_path.exists():
25
+ cred = credentials.Certificate(str(full_path))
26
+ _firebase_app = firebase_admin.initialize_app(cred)
27
+ logger.info(f"Firebase Admin SDK initialized from {full_path}")
28
+ else:
29
+ logger.warning(f"Firebase key not found at {full_path}")
30
+ # 2) Fallback: GOOGLE_APPLICATION_CREDENTIALS env var
31
+ if not _firebase_app and os.getenv("GOOGLE_APPLICATION_CREDENTIALS"):
32
+ cred = credentials.ApplicationDefault()
33
+ _firebase_app = firebase_admin.initialize_app(cred)
34
+ logger.info("Firebase Admin SDK initialized via GOOGLE_APPLICATION_CREDENTIALS")
35
+
36
+ if not _firebase_app:
37
+ logger.warning("No Firebase credentials found. Push notifications disabled.")
38
+ except ImportError:
39
+ logger.warning("firebase-admin not installed. Run: pip install firebase-admin")
40
+ except Exception as e:
41
+ logger.warning(f"Firebase Admin SDK init failed: {e}")
42
+
43
+
44
+ class PushService:
45
+ """Send push notifications via Firebase Cloud Messaging."""
46
+
47
+ @staticmethod
48
+ async def get_user_tokens(user_id: str) -> list[str]:
49
+ """Get all active FCM tokens for a user."""
50
+ db = await get_db()
51
+ cursor = db["fcm_tokens"].find({
52
+ "user_id": user_id,
53
+ "active": True,
54
+ })
55
+ tokens = []
56
+ async for doc in cursor:
57
+ tokens.append(doc["token"])
58
+ return tokens
59
+
60
+ @staticmethod
61
+ async def should_notify(user_id: str, category: str) -> bool:
62
+ """
63
+ Check if user has enabled notifications for this category.
64
+ Categories: messages, property_alerts, booking_updates, promotions
65
+ """
66
+ db = await get_db()
67
+ prefs = await db["notification_prefs"].find_one({"user_id": user_id})
68
+
69
+ if not prefs:
70
+ defaults = {
71
+ "messages": True,
72
+ "property_alerts": True,
73
+ "booking_updates": True,
74
+ "promotions": False,
75
+ }
76
+ return defaults.get(category, True)
77
+
78
+ if not prefs.get("master_enabled", True):
79
+ return False
80
+ if not prefs.get("push_enabled", False):
81
+ return False
82
+
83
+ return prefs.get(category, True)
84
+
85
+ @staticmethod
86
+ async def send_to_user(
87
+ user_id: str,
88
+ title: str,
89
+ body: str,
90
+ category: str = "messages",
91
+ data: Optional[dict] = None,
92
+ ) -> int:
93
+ """
94
+ Send push notification to all of a user's devices.
95
+ Returns number of devices successfully notified.
96
+ """
97
+ if not _firebase_app:
98
+ logger.debug(f"Push skipped (Firebase not configured): {title}")
99
+ return 0
100
+
101
+ if not await PushService.should_notify(user_id, category):
102
+ logger.debug(f"Push skipped (user opted out of {category}): {user_id}")
103
+ return 0
104
+
105
+ tokens = await PushService.get_user_tokens(user_id)
106
+ if not tokens:
107
+ return 0
108
+
109
+ from firebase_admin import messaging
110
+
111
+ notification = messaging.Notification(title=title, body=body)
112
+
113
+ android_config = messaging.AndroidConfig(
114
+ priority="high",
115
+ notification=messaging.AndroidNotification(
116
+ sound="default",
117
+ channel_id="lojiz_default",
118
+ ),
119
+ )
120
+ apns_config = messaging.APNSConfig(
121
+ payload=messaging.APNSPayload(
122
+ aps=messaging.Aps(sound="default", badge=1),
123
+ ),
124
+ )
125
+
126
+ success_count = 0
127
+ stale_tokens = []
128
+
129
+ for token in tokens:
130
+ message = messaging.Message(
131
+ notification=notification,
132
+ android=android_config,
133
+ apns=apns_config,
134
+ token=token,
135
+ data=data or {},
136
+ )
137
+ try:
138
+ messaging.send(message)
139
+ success_count += 1
140
+ except messaging.UnregisteredError:
141
+ stale_tokens.append(token)
142
+ except Exception as e:
143
+ logger.error(f"FCM send failed for token {token[:20]}...: {e}")
144
+
145
+ # Clean up stale tokens
146
+ if stale_tokens:
147
+ db = await get_db()
148
+ await db["fcm_tokens"].delete_many({"token": {"$in": stale_tokens}})
149
+ logger.info(f"Cleaned {len(stale_tokens)} stale FCM tokens for user {user_id}")
150
+
151
+ logger.info(f"Push sent to {success_count}/{len(tokens)} devices for user {user_id}")
152
+ return success_count
153
+
154
+ @staticmethod
155
+ async def send_to_many(
156
+ user_ids: list[str],
157
+ title: str,
158
+ body: str,
159
+ category: str = "messages",
160
+ data: Optional[dict] = None,
161
+ ) -> int:
162
+ """Send push notification to multiple users."""
163
+ total = 0
164
+ for user_id in user_ids:
165
+ total += await PushService.send_to_user(user_id, title, body, category, data)
166
+ return total
167
+
168
+ @staticmethod
169
+ async def notify_user(
170
+ user_id: str,
171
+ title: str,
172
+ body: str,
173
+ category: str = "messages",
174
+ data: Optional[dict] = None,
175
+ ) -> dict:
176
+ """
177
+ Unified notification: sends push + email (for allowed categories).
178
+ Email is only sent for booking_updates, property_alerts, promotions.
179
+ Messages never trigger email.
180
+ """
181
+ push_count = await PushService.send_to_user(
182
+ user_id, title, body, category, data,
183
+ )
184
+
185
+ email_sent = False
186
+ try:
187
+ from app.services.email_notification_service import email_notification_service
188
+ email_sent = await email_notification_service.notify_user(
189
+ user_id, title, body, category,
190
+ )
191
+ except Exception as e:
192
+ logger.error(f"Email notification error for user {user_id}: {e}")
193
+
194
+ return {"push_devices": push_count, "email_sent": email_sent}
195
+
196
+
197
+ push_service = PushService()
main.py CHANGED
@@ -358,10 +358,25 @@ app.include_router(wishlist_router, prefix="/api/wishlist", tags=["Wishlist"])
358
  app.include_router(booking_router, prefix="/api/bookings", tags=["Bookings"])
359
  app.include_router(stripe_connect.router, prefix="/api/stripe", tags=["Stripe Connect"])
360
 
 
 
 
 
 
361
  from app.routes.quota import router as quota_router
362
  app.include_router(quota_router, prefix="/api/quota", tags=["Quota"])
363
  logger.info("✅ Quota router registered at /api/quota")
364
 
 
 
 
 
 
 
 
 
 
 
365
  logger.info("✅ Conversations router registered at /api/conversations")
366
  logger.info("✅ Wishlist router registered at /api/wishlist")
367
  logger.info("✅ Bookings router registered at /api/bookings")
 
358
  app.include_router(booking_router, prefix="/api/bookings", tags=["Bookings"])
359
  app.include_router(stripe_connect.router, prefix="/api/stripe", tags=["Stripe Connect"])
360
 
361
+ # Contact Update (Email & Phone)
362
+ from app.routes.contact_update import router as contact_update_router
363
+ app.include_router(contact_update_router, prefix="/api/contact", tags=["Contact Update"])
364
+ logger.info("✅ Contact Update router registered at /api/contact")
365
+
366
  from app.routes.quota import router as quota_router
367
  app.include_router(quota_router, prefix="/api/quota", tags=["Quota"])
368
  logger.info("✅ Quota router registered at /api/quota")
369
 
370
+ # Legal Pages (Privacy Policy & Terms of Service)
371
+ from app.routes.legal import router as legal_router
372
+ app.include_router(legal_router, prefix="/api/legal", tags=["Legal"])
373
+ logger.info("✅ Legal pages router registered at /api/legal")
374
+
375
+ # Notification Preferences + FCM
376
+ from app.routes.notifications import router as notifications_router
377
+ app.include_router(notifications_router, prefix="/api/notifications", tags=["Notifications"])
378
+ logger.info("✅ Notifications router registered at /api/notifications")
379
+
380
  logger.info("✅ Conversations router registered at /api/conversations")
381
  logger.info("✅ Wishlist router registered at /api/wishlist")
382
  logger.info("✅ Bookings router registered at /api/bookings")
push/louezmoi-5d0f0-firebase-adminsdk-fbsvc-7411d58511.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "type": "service_account",
3
+ "project_id": "louezmoi-5d0f0",
4
+ "private_key_id": "7411d5851127bbf9c774a497cab76dee887d1b01",
5
+ "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDMQkVUUz9uDYPG\nD5Ch3PBUEgWDb5DTmIftsKWVa4jFQQIfkXcBFw7VWRjIUK5opbEJq/iJnsCIEkiy\nN7wu/LdTed6a/xoY3RNPUPHuDG/cJd1tThdZK10aunz460yGG1mjlKLPryqmp5WY\nhpnofmS2gFMymGCp4Kb0CtqRDgBVCAkJd5NXPpkU3iGnPo9hUENjtvwMhRK/FzLn\nJyDmSyoq7yR5Xdd1uZJa7VKzyNoqxOL9PUQSB/lI0Y8UKSdiKSBNSw5MAcapX7TJ\n/gQLSRqEUxMbEgMYT0aScDY5gl0fUBg02ASIgepTu11eIGfm6IHK0lfl4aU2MtMn\nWsKMjNLzAgMBAAECggEAAax1VviLp4Iy1zefbsQZsAapxubuH927F2aAJNZJu5wJ\nwWp2uCqRM4xUWKfYlgDtj2+6TJ5hY3wK3nL4dsYZVnTELy7x7sw48UhvgwzVCc/B\nnbirFk71p1dp2LxM1zh4Jo/8OYls5wqufh50OmR2vMwWYm6/NAUkORGxssiTlh7D\n+WZeep48XI/izUGLasEd1nq3mdi1YyktN1cRCd8BAARDDmELUZZOg3eH+TrM8rAv\nomyvBRBKAkeqcTGstEoxTtCF1ecFV5aaXV/Lx43QqYEKg5b8I/sVoEF/iqCFKIwf\ng2dmgN3F2ho7EWc6m73kZ8Yj8j95hV5/IsyBa9fvEQKBgQD45FUbnr6pGpkHRXmp\nmAI4amKxBWSLivbMjay8F9EBaB/ugJLRmxbmssxgpd2mt9H1YJz57/1GTTIESfj9\nlnpUOkk1luNZNUbRaraP4kr+xs2j/ZbxXECl7fDEFbfzovzxyH/K+CGravbo8NRL\nfUUXo76ZMgU/iGcVou3fl59W+wKBgQDSF59tPSojFVM7NUTQqwRYZUfcKNOqEQ6U\n0m5LkenGEdIJVMCt1gv6bIRe3HKBZgN1qdKcP+2Voa1/5Fop0AXAoSa4kHrLjd3M\nBg4sfCK2heLc7/RJDP4hMgZ0drYD6VwtzK8IVZt+X26QQFgGBUm4fois4PLpzjRg\n5Grf7hWSaQKBgQCC6V6Wm4W/Xn0VJQbcHFc2o0Zrw+FhwGMVroMnoANQERuSkFAp\nLcD68RqszzFpqpAl135ZvFBktN/U/6+Wepq3SL0PEeFdH7ezGMUTozw9DAZH9m95\nXkOmwyN7j8zkulr/Na157c+P4UAjv+B6IFbCG196JqQ6CZ3m9H7Xt2GZFwKBgA+M\nM/4vkpi6EUjGbmtK3K12AVVnqBJj5MJauFLbX4OK+hI6tW+n8Yb81Ob4RXkSBZuF\nj9KEYJ68MkzOp2WEwYg55NWvy4aY3xskL5RXM3JyghF7qIZncb8fJp8j0eCEOpAh\nH6rsOu+lPFdsi5cWeRgwNQkmNJPc8L9i6A3eXrZRAoGAAjJuAGNUO+YiMq2LjiHJ\nEGFjbdiPKiKpItkWsqf/OkJ8uEOaYEvYsjV+x53p0f09hyTTgBM+QV9lKWqcBp35\nZlDGZmd+TNT1rgGadh5smCe7Vgzh4jzjZwA3GvYky3lBk+iKI9HeOlidlHTPJCwm\nh1js/XYyzwagdUkpqtIBrtw=\n-----END PRIVATE KEY-----\n",
6
+ "client_email": "firebase-adminsdk-fbsvc@louezmoi-5d0f0.iam.gserviceaccount.com",
7
+ "client_id": "109525111802797881931",
8
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
9
+ "token_uri": "https://oauth2.googleapis.com/token",
10
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
11
+ "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40louezmoi-5d0f0.iam.gserviceaccount.com",
12
+ "universe_domain": "googleapis.com"
13
+ }