first commit

This commit is contained in:
2024-11-07 17:44:29 +03:00
commit 643d6d8f65
247 changed files with 420800 additions and 0 deletions

25
api_services/__init__.py Normal file
View File

@@ -0,0 +1,25 @@
from .redis.conn import redis_cli
from .redis.functions import (
get_object_via_access_key,
get_object_via_user_uu_id,
)
from .redis.auth_actions.auth import (
save_access_token_to_redis,
update_selected_to_redis,
)
from .email.service import send_email
from .templates.password_templates import (
password_is_changed_template,
change_your_password_template,
)
__all__ = [
"redis_cli",
"send_email",
"get_object_via_access_key",
"get_object_via_user_uu_id",
"save_access_token_to_redis",
"update_selected_to_redis",
"password_is_changed_template",
"change_your_password_template",
]

View File

@@ -0,0 +1,202 @@
import textdistance
from unidecode import unidecode
from datetime import datetime
from databases import (
BuildIbanDescription,
BuildIbans,
BuildDecisionBook,
BuildLivingSpace,
AccountRecords,
)
from typing import Optional
from pydantic import BaseModel
class InsertBudgetRecord(BaseModel):
iban: str
bank_date: str = datetime.now().__str__()
receive_debit: str = "debit"
budget_type: str = "B"
currency_value: float = 0
balance: float = 0
bank_reference_code: str = ""
currency: str = "TL"
channel_branch: str = ""
process_name: str = ""
process_type: str = ""
process_comment: str = ""
add_xcomment: Optional[str] = None
project_no: Optional[str] = None
company_id: Optional[str] = None
customer_id: Optional[str] = None
send_person_id: Optional[int] = None
send_company_id: Optional[int] = None
build_id: Optional[int] = None
build_decision_book_id: Optional[int] = None
build_parts_id: Optional[int] = None
build_db_item_id: Optional[int] = None
dues_type: Optional[str] = "D"
period_time: Optional[str] = ""
approving_accounting_record: Optional[bool] = False
approving_accounting_person: Optional[str] = None
accounting_receipt_date: Optional[str] = "1900-01-01 00:00:00"
accounting_receipt_number: Optional[int] = 0
def strip_time_date(date_str):
return datetime.strptime(date_str, "%Y-%m-%d")
def strip_date_to_valid(date_str):
return datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S")
def find_iban_in_comment(iban: str, comment: str):
iban_results, iban_count = BuildIbanDescription.filter_by(iban=iban)
sm_dict_extended, sm_dict_digit = {}, {}
# is_reference_build = any(
# letter in comment.lower() for letter in ["no", "daire", "nolu"]
# )
if iban_count:
for iban_result in iban_results:
candidate_parts = comment.split(" ")
extended_candidate_parts, digit_part = [], []
for part in candidate_parts:
if part.lower() not in ["no", "daire", "nolu"]:
extended_candidate_parts.append(part)
# if part.isdigit():
# digit_part.append(part)
if extended_candidate_parts:
if all(
candidate_part.lower() in comment.lower()
for candidate_part in extended_candidate_parts
):
similarity_ratio = textdistance.jaro_winkler(
unidecode(str(iban_result.search_word)), comment
)
found = False
name_list = (
unidecode(str(iban_result.search_word))
.replace(".", " ")
.split(" ")
)
for name in name_list:
if len(name) > 3 and name.lower() in comment.lower():
found = True
break
if not found:
similarity_ratio = 0.1
sm_dict_extended[f"{iban_result.id}"] = similarity_ratio
if sm_dict_extended:
result = sorted(
sm_dict_extended.items(), key=lambda item: item[1], reverse=True
)[0]
if float(result[1]) >= 0.5:
iban_result = BuildIbanDescription.find_one(id=int(result[0]))
return {
"decision_book_project_id": iban_result.decision_book_project_id,
"company_id": iban_result.company_id,
"customer_id": iban_result.customer_id,
"build_parts_id": iban_result.build_parts_id,
"found_from": "Name",
"similarity": result[1],
}
return {
"decision_book_project_id": None,
"company_id": None,
"customer_id": None,
"build_parts_id": None,
"found_from": None,
"similarity": 0.0,
}
def parse_comment_with_name(iban: str, comment: str):
if "*" in comment:
b_comment, a_comment = (
unidecode(str(comment)).split("*")[0],
unidecode(str(comment)).split("*")[1],
)
a_result = find_iban_in_comment(iban, a_comment)
b_result = find_iban_in_comment(iban, b_comment)
if a_result["similarity"] > b_result["similarity"]:
a_result["send_person_id"] = a_result["customer_id"]
return a_result
else:
b_result["send_person_id"] = None
return b_result
else:
result = find_iban_in_comment(iban, comment)
result["send_person_id"] = result.get("customer_id", None)
return result
def wag_insert_budget_record(data):
similarity_result = parse_comment_with_name(data["iban"], data["process_comment"])
build_iban = BuildIbans.find_one(iban=data["iban"])
if payload := InsertBudgetRecord(**data):
payload_dict = payload.model_dump(exclude_unset=True, exclude_none=True)
decision_book, count = BuildDecisionBook.filter(
BuildDecisionBook.period_start_date
< strip_date_to_valid(payload_dict["bank_date"]),
BuildDecisionBook.period_stop_date
> strip_date_to_valid(payload_dict["bank_date"]),
)
payload_dict["build_id"] = getattr(
BuildIbans.find_one(iban=data["iban"]), "build_id", None
)
living_space, count = BuildLivingSpace.find_living_from_customer_id(
similarity_result.get("customer_id", None),
strip_date_to_valid(payload_dict["bank_date"]),
)
# living_space, count = BuildLivingSpace.filter(
# or_(
# BuildLivingSpace.owner_person_id
# == similarity_result.get("customer_id", None),
# BuildLivingSpace.life_person_id
# == similarity_result.get("customer_id", None),
# ),
# BuildLivingSpace.start_date
# < strip_date_to_valid(payload_dict["bank_date"]) - timedelta(days=30),
# BuildLivingSpace.stop_date
# > strip_date_to_valid(payload_dict["bank_date"]) + timedelta(days=30),
# BuildLivingSpace.active == True,
# BuildLivingSpace.deleted == False,
# )
payload_dict["build_decision_book_id"] = (
decision_book[0].id if decision_book else None
)
payload_dict["company_id"] = similarity_result.get("company_id", None)
payload_dict["customer_id"] = similarity_result.get("customer_id", None)
payload_dict["send_person_id"] = similarity_result.get("send_person_id", None)
payload_dict["build_parts_id"] = (
living_space[0].build_parts_id if living_space else None
)
payload_dict["bank_date_y"] = strip_date_to_valid(
payload_dict["bank_date"]
).year
payload_dict["bank_date_m"] = strip_date_to_valid(
payload_dict["bank_date"]
).month
payload_dict["bank_date_d"] = strip_date_to_valid(payload_dict["bank_date"]).day
payload_dict["bank_date_w"] = strip_date_to_valid(
payload_dict["bank_date"]
).isocalendar()[2]
payload_dict["build_id"] = build_iban.build_id if build_iban else None
payload_dict["replication_id"] = 55
payload_dict["receive_debit"] = (
"R" if payload_dict["currency_value"] < 0 else "D"
)
data, found = AccountRecords.find_or_create(
**payload_dict,
found_from=similarity_result.get("found_from", None),
similarity=similarity_result.get("similarity", 0.0),
)
data.payment_budget_record_close()
return data, found

View File

@@ -0,0 +1,9 @@
from redmail import EmailSender
from api_configs import EmailConfig
email_sender = EmailSender(
host=EmailConfig.EMAIL_HOST,
port=587,
username=EmailConfig.EMAIL_USERNAME,
password=EmailConfig.EMAIL_PASSWORD,
)

View File

@@ -0,0 +1,31 @@
from .config import email_sender
def send_email(
subject: str,
receivers: list,
text: str = "",
html: str = "",
cc: list = None,
bcc: list = None,
headers: dict = None,
attachments: dict = None,
) -> bool:
try:
email_sender.connect()
email_sender.send(
subject=subject,
receivers=receivers,
text=text,
html=html,
cc=cc,
bcc=bcc,
headers=headers or {},
attachments=attachments or {},
)
return True
except Exception as e:
print(f"Error raised at email send :{e}")
finally:
email_sender.close()
return False

View File

@@ -0,0 +1,219 @@
import json
import typing
from fastapi import status
from fastapi.exceptions import HTTPException
from api_configs import Auth
from databases import (
BuildLivingSpace,
BuildParts,
Companies,
Duties,
Departments,
Duty,
Employees,
Staff,
)
from api_objects import (
OccupantTokenObject,
EmployeeTokenObject,
UserType,
)
from api_services.redis.conn import redis_cli
from api_services.redis.functions import get_object_via_user_uu_id, get_object_via_access_key
def save_object_to_redis(
access_token, model_object: typing.Union[OccupantTokenObject, EmployeeTokenObject]
) -> bool:
try:
if redis_cli.set(
name=str(access_token) + ":" + str(model_object.user_uu_id),
value=model_object.model_dump_json(),
):
return access_token
except Exception as e:
print("Save Object to Redis Error: ", e)
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=dict(
message="Headers are not found in request. Invalid request object. Redis Error: Token is not saved."
),
)
def save_access_token_to_redis(
request, found_user, domain: str, access_token: str = None
):
if not found_user:
raise HTTPException(
status_code=400,
detail=dict(message="User is not found."),
headers=request.headers,
)
# Check user is already logged in or has a previous session
already_tokens = get_object_via_user_uu_id(user_id=found_user.uu_id)
for key in already_tokens or []:
token_user = json.loads(redis_cli.get(key).decode() or {})
if token_user.get("domain", "") == domain:
redis_cli.delete(key)
access_token = (
found_user.generate_access_token() if not access_token else access_token
)
# Prepare the user's details to save in Redis Session
if found_user.is_occupant: # Check if user is NOT an occupant
living_spaces: list[BuildLivingSpace] = BuildLivingSpace.filter_active(
BuildLivingSpace.person_id == found_user.person_id, filter_records=False
).data
if not living_spaces:
raise HTTPException(
status_code=400,
detail=dict(
message="NO Living Space is found. This user has no proper account set please contact the admin."
),
headers=request.headers,
)
occupants_selection_dict = {}
for living_space in living_spaces:
build_parts_selection = BuildParts.filter_active(
BuildParts.id == living_space.build_parts_id
)
if not build_parts_selection.data:
raise HTTPException(
status_code=400,
detail=dict(
message="No build Part is found for the living space. Please contact the admin."
),
headers=request.headers,
)
build_part = build_parts_selection.get(1)
occupant_dict = {
"uu_id": str(living_space.occupant_type_uu_id),
"id": living_space.occupant_type,
}
if not str(build_part.uu_id) in occupants_selection_dict:
occupants_selection_dict[str(build_part.uu_id)] = [occupant_dict]
elif str(build_part.uu_id) in occupants_selection_dict:
occupants_selection_dict[str(build_part.uu_id)].append(occupant_dict)
save_object_to_redis(
access_token=access_token,
model_object=OccupantTokenObject(
domain=domain,
user_type=UserType.occupant.value,
user_uu_id=str(found_user.uu_id),
credentials=found_user.credentials(),
user_id=found_user.id,
person_id=found_user.person_id,
person_uu_id=str(found_user.person.uu_id),
request=dict(request.headers),
available_occupants=occupants_selection_dict,
),
)
new_occupants_selection_dict = {}
for key, value in occupants_selection_dict.items():
new_occupants_selection_dict[key] = [
occupant.get("uu_id") for occupant in value
]
return dict(
user_type=UserType.occupant.name,
available_occupants=new_occupants_selection_dict,
)
list_employee = Employees.filter_active(Employees.people_id == found_user.person_id)
companies_uu_id_list, companies_id_list = [], []
duty_uu_id_list, duty_id_list = [], []
for employee in list_employee.data:
staff = Staff.find_one(id=employee.staff_id)
if duties := Duties.find_one(id=staff.duties_id):
if duty_found := Duty.find_one(id=duties.duties_id):
duty_uu_id_list.append(str(duty_found.uu_id))
duty_id_list.append(duty_found.id)
department = Departments.find_one(id=duties.department_id)
if company := Companies.find_one(id=department.company_id):
companies_uu_id_list.append(str(company.uu_id))
companies_id_list.append(company.id)
save_object_to_redis(
access_token=access_token,
model_object=EmployeeTokenObject(
domain=domain,
user_type=UserType.employee.value,
user_uu_id=str(found_user.uu_id),
credentials=found_user.credentials(),
user_id=found_user.id,
person_id=found_user.person_id,
person_uu_id=str(found_user.person.uu_id),
request=dict(request.headers),
companies_uu_id_list=companies_uu_id_list,
companies_id_list=companies_id_list,
duty_uu_id_list=duty_uu_id_list,
duty_id_list=duty_id_list,
),
)
return dict(
user_type=UserType.employee.name,
companies_uu_id_list=companies_uu_id_list,
)
def update_selected_to_redis(request, add_payload):
already_tokens = get_object_via_access_key(request=request)
if not hasattr(request, "headers"):
raise HTTPException(
status_code=401,
detail=dict(
message="Headers are not found in request. Invalid request object."
),
)
access_token = request.headers.get(Auth.ACCESS_TOKEN_TAG)
if already_tokens.user_type == UserType.occupant.value:
already_tokens.selected_occupant = add_payload.model_dump()
return save_object_to_redis(
access_token=access_token,
model_object=OccupantTokenObject(**already_tokens.model_dump()),
)
elif already_tokens.user_type == UserType.employee.value:
already_tokens.selected_company = add_payload.model_dump()
return save_object_to_redis(
access_token=access_token,
model_object=EmployeeTokenObject(**already_tokens.model_dump()),
)
raise HTTPException(
status_code=401,
detail=dict(
message="User type is not found in the token object. Please reach to your administrator."
),
)
def update_access_token_to_redis(request, add_payload):
already_tokens = get_object_via_access_key(request=request)
if not hasattr(request, "headers"):
raise HTTPException(
status_code=401,
detail=dict(
message="Headers are not found in request. Invalid request object."
),
)
payload = {**add_payload, **already_tokens}
access_token = request.headers.get(Auth.ACCESS_TOKEN_TAG)
if payload.get("user_type") == str(UserType.occupant.value):
return save_object_to_redis(
access_token=access_token,
model_object=OccupantTokenObject(**payload),
)
return save_object_to_redis(
access_token=access_token,
model_object=EmployeeTokenObject(**payload),
)

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,105 @@
from fastapi import HTTPException, status
from fastapi.requests import Request
from database_sql_models import Events
# url_that_not_requires_event_validation = [
# "/authentication/login",
# "/authentication/select",
# "/authentication/valid",
# "/authentication/refresh",
# "/authentication/change_password",
# "/authentication/create_password",
# "/authentication/disconnect",
# "/authentication/logout",
# "/authentication/refresher",
# "/authentication/forgot",
# "/authentication/avatar",
# ]
def parse_token_object_to_dict(request: Request): # from requests import Request
from api_services.redis.functions import get_object_via_access_key
from databases import EndpointRestriction
import api_events.events as events
if valid_token := get_object_via_access_key(request=request):
endpoint_name = str(request.url).replace(str(request.base_url), "/")
endpoint_active = EndpointRestriction.filter_active(
EndpointRestriction.endpoint_name.ilike(f"%{endpoint_name}%")
).data[0]
if not endpoint_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="This endpoint is not active for this user, please contact your responsible company for further information.",
)
if valid_token.user_type == 1:
if not valid_token.selected_company:
raise HTTPException(
status_code=status.HTTP_418_IM_A_TEAPOT,
detail="Selected company is not found in the token object.",
)
selected_event = Events.filter_active(
Events.endpoint_id == endpoint_active.id,
Events.id.in_(valid_token.selected_company.reachable_event_list_id),
)
if not selected_event.data:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="This endpoint requires event validation. Please contact your responsible company to use this event.",
)
selected_event = selected_event.data[0]
event_function_class = getattr(selected_event, "function_class", None)
event_function_code = getattr(selected_event, "function_code", None)
function_class = getattr(events, event_function_class, None)
active_function = getattr(
function_class,
function_class.__event_keys__.get(event_function_code, None),
None,
)
if not active_function:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="This endpoint requires event validation. Please contact your responsible company to use this event.",
)
valid_token.available_event = active_function
return valid_token
elif valid_token.user_type == 2:
if not valid_token.selected_occupant:
raise HTTPException(
status_code=status.HTTP_418_IM_A_TEAPOT,
detail="Selected occupant is not found in the token object.",
)
selected_event = Events.filter_active(
Events.endpoint_id == endpoint_active.id,
Events.id.in_(valid_token.selected_occupant.reachable_event_list_id),
)
if not selected_event.data:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="This endpoint requires event validation. Please contact your responsible company to use this event.",
)
selected_event = selected_event.data[0]
event_function_class = getattr(selected_event, "function_class", None)
event_function_code = getattr(selected_event, "function_code", None)
function_class = getattr(events, event_function_class, None)
active_function = getattr(
function_class,
function_class.__event_keys__.get(event_function_code, None),
None,
)
if not active_function:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="This endpoint requires event validation. Please contact your responsible company to use this event.",
)
valid_token.available_event = active_function
return valid_token
valid_token.available_event = None
return valid_token
user_type = "Company" if valid_token.user_type == 1 else "Occupant"
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Token of this user is not valid. Please login and refresh {user_type} selection.",
)

View File

@@ -0,0 +1,31 @@
from redis import Redis
# from configs import WagRedis
from api_configs import WagRedis
class RedisConn:
def __init__(self):
self.redis = Redis(
host=WagRedis.REDIS_HOST,
password=WagRedis.REDIS_PASSWORD,
port=WagRedis.REDIS_PORT,
db=WagRedis.REDIS_DB,
)
if not self.check_connection():
raise Exception("Connection error")
def check_connection(self):
return self.redis.ping()
def set_connection(self, host, password, port, db):
self.redis = Redis(host=host, password=password, port=port, db=db)
return self.redis
try:
redis_conn = RedisConn()
redis_cli = redis_conn.redis
except Exception as e:
print("Redis Connection Error", e)

View File

@@ -0,0 +1,71 @@
import json
import typing
from fastapi import status
from fastapi.exceptions import HTTPException
from .conn import redis_cli
from api_configs import Auth
from api_objects import EmployeeTokenObject, OccupantTokenObject
def get_object_via_access_key(
request,
) -> typing.Union[EmployeeTokenObject, OccupantTokenObject, None]:
if not hasattr(request, "headers"):
raise HTTPException(
status_code=401,
detail=dict(
message="Headers are not found in request. Invalid request object."
),
)
if not request.headers.get(Auth.ACCESS_TOKEN_TAG):
raise HTTPException(
status_code=401,
detail=dict(message="Unauthorized user, please login..."),
)
already_tokens = redis_cli.scan_iter(
match=str(request.headers.get(Auth.ACCESS_TOKEN_TAG) + ":*")
)
if already_tokens := list(already_tokens):
try:
if redis_object := json.loads(
redis_cli.get(already_tokens[0].decode()) or {}
):
if redis_object.get("user_type") == 1:
if not redis_object.get("selected_company", None):
redis_object["selected_company"] = None
return EmployeeTokenObject(**redis_object)
elif redis_object.get("user_type") == 2:
if not redis_object.get("selected_occupant", None):
redis_object["selected_occupant"] = None
return OccupantTokenObject(**redis_object)
raise HTTPException(
status_code=401,
detail=dict(
message="User type is not found in the token object. Please reach to your administrator."
),
)
except Exception as e:
raise HTTPException(
status_code=500,
detail={
"message": "Redis Service raised an exception.",
"error": str(e),
},
)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid credentials. Please login again.",
)
def get_object_via_user_uu_id(user_id: str) -> typing.Union[list, None]:
already_tokens = redis_cli.scan_iter(match=str("*:" + str(user_id)))
already_tokens = list(already_tokens)
if list(already_tokens):
return list(already_tokens)
return None

View File

@@ -0,0 +1,243 @@
import datetime
def change_your_password_template(**kwargs):
user_name, forgot_link, current_year = (
kwargs["user_name"],
kwargs["forgot_link"],
str(datetime.datetime.now().year),
)
template = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bootstrap demo</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
}
.email-container {
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
padding-bottom: 20px;
border-bottom: 1px solid #dddddd;
}
.header img {
max-width: 100px;
}
.content {
padding: 20px 0;
}
.footer {
text-align: center;
padding: 10px;
font-size: 12px;
color: #777777;
}
.btn-success {
color: #fff;
background-color: #198754;
border-color: #198754;
text-align: center;
text-decoration: none;
vertical-align: middle;
width: 150px;
height: 40px;
border-radius: 5px;
font-weight: 400;
padding: .375rem .75rem;
}
</style>
</head>
<body>
<div class="email-container">
<div class="header">
<img src="" alt="Company Logo">
<h2>Reset Password</h2>
</div>
<div class="content">
<p>Dear %s,</p>
<p>We have received a request to reset your password for your account with Let's Program Blog. To complete the password reset process, please click on the button below:</p>
<p>Please note that this link is only valid for a day only. If you did not request a password reset, please disregard this message.</p>
<a href="%s"><button type="button" class="btn-success">Reset Password</button></a>
</div>
<div class="footer">
<p>&copy; %s Evyos Ltd Şti. All rights reserved.</p>
</div>
</div>
</body>
</html>
""" % (
user_name,
forgot_link,
current_year,
)
return template
def password_is_changed_template(**kwargs):
user_name, current_year = kwargs["user_name"], str(datetime.datetime.now().year)
template = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Thank You for Changing Your Password</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
}
.email-container {
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
padding-bottom: 20px;
border-bottom: 1px solid #dddddd;
}
.header img {
max-width: 100px;
}
.content {
padding: 20px 0;
}
.footer {
text-align: center;
padding: 10px;
font-size: 12px;
color: #777777;
}
</style>
</head>
<body>
<div class="email-container">
<div class="header">
<img src="" alt="Company Logo">
<h2>Your Password has changed</h2>
</div>
<div class="content">
<p>Dear %s,</p>
<p>We wanted to let you know that your password has been successfully updated.
If you did not make this change or if you believe an unauthorized person has accessed your account,
please contact our support team immediately.</p>
<p>Thank you for helping us keep your account secure.</p>
</div>
<div class="footer">
<p>&copy; %s Evyos Ltd Şti. All rights reserved.</p>
</div>
</div>
</body>
</html>
""" % (
user_name,
current_year,
)
return template
def invalid_ip_or_address_found(**kwargs):
user_name, current_year, address = (
kwargs["user_name"],
str(datetime.datetime.now().year),
kwargs.get("address"),
)
template = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Thank You for Changing Your Password</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
}
.email-container {
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
padding-bottom: 20px;
border-bottom: 1px solid #dddddd;
}
.header img {
max-width: 100px;
}
.content {
padding: 20px 0;
}
.footer {
text-align: center;
padding: 10px;
font-size: 12px;
color: #777777;
}
</style>
</head>
<body>
<div class="email-container">
<div class="header">
<img src="" alt="Company Logo">
<h2>An Unknown login has been attempted</h2>
</div>
<div class="content">
<p>Dear %s,</p>
<p>We wanted to let you know that an unusual login attempt has been tried from address below.
If you have login from address below please ignore this message</p>
<p>Thank you for helping us keep your account secure.</p>
<h1>Address of ip attempt</h1>
<p>City : %s</p>
<p>Zip Code : %s</p>
<p>Country : %s</p>
<p>Region : %s</p>
<p>Region Name : %s</p>
<p>If you are not login from this address lets us now by clicking link below</p>
<a href="%s"><button type="button" class="btn-success">Reset Password</button></a>
</div>
<div class="footer">
<p>&copy; %s Evyos Ltd Şti. All rights reserved.</p>
</div>
</div>
</body>
</html>
""" % (
user_name,
address["city"],
address["zip"],
address["country"],
address["region"],
address["regionName"],
kwargs["notice_link"],
current_year,
)
return template