updated Service providers
This commit is contained in:
parent
fa4df11323
commit
60507c87e0
|
|
@ -0,0 +1,30 @@
|
||||||
|
ToDo List:
|
||||||
|
|
||||||
|
Redis Items:
|
||||||
|
|
||||||
|
Internal serves from Class inside API:
|
||||||
|
- [ ] Save Events activities as [
|
||||||
|
{"endpoint_code-UUID1": ["event-UUID1", "event-UUID2", "event-UUID3"]},
|
||||||
|
{"endpoint_code-UUID2": ["event-UUID4", "event-UUID5", "event-UUID6"]}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
- [ ] Save Application activities as [
|
||||||
|
{"app1": ["appCode-UUID1", "appCode-UUID2", "appCode-UUID3"]},
|
||||||
|
{"app2": ["appCode-UUID4", "appCode-UUID5", "appCode-UUID6"]}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
Serve Only from Redis:
|
||||||
|
- [] Endpoint: "authentication/page/valid"
|
||||||
|
send data: {"page": "app1"}
|
||||||
|
result: {"page": "appCode-UUID4"}
|
||||||
|
|
||||||
|
|
||||||
|
Client side Frontend:
|
||||||
|
On Redirect [send=> authentication/page/valid]
|
||||||
|
Retrieve available application UUIDs from API
|
||||||
|
Redirect to related page with UUID
|
||||||
|
{
|
||||||
|
"appCode-UUID1": App1.tsx
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ from ApiServices.AuthService.validations.request.authentication.login_post impor
|
||||||
RequestForgotPasswordPhone,
|
RequestForgotPasswordPhone,
|
||||||
RequestForgotPasswordEmail,
|
RequestForgotPasswordEmail,
|
||||||
RequestVerifyOTP,
|
RequestVerifyOTP,
|
||||||
|
RequestApplication,
|
||||||
)
|
)
|
||||||
from ApiServices.AuthService.events.auth.auth import AuthHandlers
|
from ApiServices.AuthService.events.auth.auth import AuthHandlers
|
||||||
|
|
||||||
|
|
@ -367,7 +368,45 @@ def authentication_password_verify_otp(
|
||||||
"tz": tz or "GMT+3",
|
"tz": tz or "GMT+3",
|
||||||
"token": token,
|
"token": token,
|
||||||
}
|
}
|
||||||
print('Token&OTP : ', data.otp, data.token)
|
print("Token&OTP : ", data.otp, data.token)
|
||||||
|
if not domain or not language:
|
||||||
|
return JSONResponse(
|
||||||
|
content={"error": "EYS_0003"},
|
||||||
|
status_code=status.HTTP_406_NOT_ACCEPTABLE,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
return JSONResponse(
|
||||||
|
content={},
|
||||||
|
status_code=status.HTTP_202_ACCEPTED,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@auth_route.post(
|
||||||
|
path="/page/valid",
|
||||||
|
summary="Verify if page is valid returns application avaliable",
|
||||||
|
description="Verify if page is valid returns application avaliable",
|
||||||
|
)
|
||||||
|
def authentication_page_valid(
|
||||||
|
request: Request,
|
||||||
|
data: RequestApplication,
|
||||||
|
language: str = Header(None, alias="language"),
|
||||||
|
domain: str = Header(None, alias="domain"),
|
||||||
|
tz: str = Header(None, alias="timezone"),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Verify if page is valid returns application that can user reach
|
||||||
|
page: { url = /building/create}
|
||||||
|
result: { "application": "4c11f5ef-0bbd-41ac-925e-f79d9aac2b0e" }
|
||||||
|
"""
|
||||||
|
token = request.headers.get(api_config.ACCESS_TOKEN_TAG, None)
|
||||||
|
headers = {
|
||||||
|
"language": language or "",
|
||||||
|
"domain": domain or "",
|
||||||
|
"eys-ext": f"{str(uuid.uuid4())}",
|
||||||
|
"tz": tz or "GMT+3",
|
||||||
|
"token": token,
|
||||||
|
}
|
||||||
if not domain or not language:
|
if not domain or not language:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={"error": "EYS_0003"},
|
content={"error": "EYS_0003"},
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,10 @@ class RequestVerifyOTP(BaseModel):
|
||||||
otp: str
|
otp: str
|
||||||
|
|
||||||
|
|
||||||
|
class RequestApplication(BaseModel):
|
||||||
|
page: str # /building/create
|
||||||
|
|
||||||
|
|
||||||
class RequestSelectOccupant(BaseModel):
|
class RequestSelectOccupant(BaseModel):
|
||||||
|
|
||||||
company_uu_id: str
|
company_uu_id: str
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
from fastapi import APIRouter, Request, Response
|
import uuid
|
||||||
|
from fastapi import APIRouter, Request, Response, Header
|
||||||
|
|
||||||
|
from ApiServices.TemplateService.config import api_config
|
||||||
from ApiServices.TemplateService.events.template.event import template_event_cluster
|
from ApiServices.TemplateService.events.template.event import template_event_cluster
|
||||||
|
|
||||||
test_template_route = APIRouter(prefix="/test", tags=["Test"])
|
test_template_route = APIRouter(prefix="/test", tags=["Test"])
|
||||||
|
|
@ -10,10 +12,25 @@ test_template_route = APIRouter(prefix="/test", tags=["Test"])
|
||||||
description="Test Template Route",
|
description="Test Template Route",
|
||||||
operation_id="bb20c8c6-a289-4cab-9da7-34ca8a36c8e5",
|
operation_id="bb20c8c6-a289-4cab-9da7-34ca8a36c8e5",
|
||||||
)
|
)
|
||||||
def test_template(request: Request, response: Response):
|
def test_template(
|
||||||
|
request: Request,
|
||||||
|
response: Response,
|
||||||
|
language: str = Header(None, alias="language"),
|
||||||
|
domain: str = Header(None, alias="domain"),
|
||||||
|
tz: str = Header(None, alias="timezone"),
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Test Template Route
|
Test Template Route
|
||||||
"""
|
"""
|
||||||
|
event_code = "bb20c8c6-a289-4cab-9da7-34ca8a36c8e5"
|
||||||
|
token = request.headers.get(api_config.ACCESS_TOKEN_TAG, None)
|
||||||
|
headers = {
|
||||||
|
"language": language or "",
|
||||||
|
"domain": domain or "",
|
||||||
|
"eys-ext": f"{str(uuid.uuid4())}",
|
||||||
|
"tz": tz or "GMT+3",
|
||||||
|
"token": token,
|
||||||
|
}
|
||||||
event_cluster_matched = template_event_cluster.match_event(
|
event_cluster_matched = template_event_cluster.match_event(
|
||||||
event_keys=[
|
event_keys=[
|
||||||
"3f510dcf-9f84-4eb9-b919-f582f30adab1",
|
"3f510dcf-9f84-4eb9-b919-f582f30adab1",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,210 @@
|
||||||
|
import enum
|
||||||
|
|
||||||
|
from typing import Optional, Union, Dict, Any, List
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from Controllers.Redis.database import RedisActions
|
||||||
|
from ..config import api_config
|
||||||
|
|
||||||
|
|
||||||
|
class UserType(enum.Enum):
|
||||||
|
|
||||||
|
employee = 1
|
||||||
|
occupant = 2
|
||||||
|
|
||||||
|
|
||||||
|
class Credentials(BaseModel):
|
||||||
|
|
||||||
|
person_id: int
|
||||||
|
person_name: str
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationToken(BaseModel):
|
||||||
|
# Application Token Object -> is the main object for the user
|
||||||
|
|
||||||
|
user_type: int = UserType.occupant.value
|
||||||
|
credential_token: str = ""
|
||||||
|
|
||||||
|
user_uu_id: str
|
||||||
|
user_id: int
|
||||||
|
|
||||||
|
person_id: int
|
||||||
|
person_uu_id: str
|
||||||
|
|
||||||
|
request: Optional[dict] = None # Request Info of Client
|
||||||
|
expires_at: Optional[float] = None # Expiry timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class OccupantToken(BaseModel):
|
||||||
|
|
||||||
|
# Selection of the occupant type for a build part is made by the user
|
||||||
|
|
||||||
|
living_space_id: int # Internal use
|
||||||
|
living_space_uu_id: str # Outer use
|
||||||
|
|
||||||
|
occupant_type_id: int
|
||||||
|
occupant_type_uu_id: str
|
||||||
|
occupant_type: str
|
||||||
|
|
||||||
|
build_id: int
|
||||||
|
build_uuid: str
|
||||||
|
build_part_id: int
|
||||||
|
build_part_uuid: str
|
||||||
|
|
||||||
|
responsible_company_id: Optional[int] = None
|
||||||
|
responsible_company_uuid: Optional[str] = None
|
||||||
|
responsible_employee_id: Optional[int] = None
|
||||||
|
responsible_employee_uuid: Optional[str] = None
|
||||||
|
|
||||||
|
reachable_event_codes: Optional[dict[str, list[str]]] = (
|
||||||
|
None # ID list of reachable modules
|
||||||
|
)
|
||||||
|
reachable_app_codes: Optional[dict[str, list[str]]] = (
|
||||||
|
None # ID list of reachable modules
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CompanyToken(BaseModel):
|
||||||
|
|
||||||
|
# Selection of the company for an employee is made by the user
|
||||||
|
company_id: int
|
||||||
|
company_uu_id: str
|
||||||
|
|
||||||
|
department_id: int # ID list of departments
|
||||||
|
department_uu_id: str # ID list of departments
|
||||||
|
|
||||||
|
duty_id: int
|
||||||
|
duty_uu_id: str
|
||||||
|
|
||||||
|
staff_id: int
|
||||||
|
staff_uu_id: str
|
||||||
|
|
||||||
|
employee_id: int
|
||||||
|
employee_uu_id: str
|
||||||
|
|
||||||
|
bulk_duties_id: int
|
||||||
|
|
||||||
|
reachable_event_codes: Optional[dict[str, list[str]]] = (
|
||||||
|
None # ID list of reachable modules
|
||||||
|
)
|
||||||
|
reachable_app_codes: Optional[dict[str, list[str]]] = (
|
||||||
|
None # ID list of reachable modules
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OccupantTokenObject(ApplicationToken):
|
||||||
|
# Occupant Token Object -> Requires selection of the occupant type for a specific build part
|
||||||
|
|
||||||
|
available_occupants: dict = None
|
||||||
|
selected_occupant: Optional[OccupantToken] = None # Selected Occupant Type
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_employee(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_occupant(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class EmployeeTokenObject(ApplicationToken):
|
||||||
|
# Full hierarchy Employee[staff_id] -> Staff -> Duty -> Department -> Company
|
||||||
|
|
||||||
|
companies_id_list: List[int] # List of company objects
|
||||||
|
companies_uu_id_list: List[str] # List of company objects
|
||||||
|
|
||||||
|
duty_id_list: List[int] # List of duty objects
|
||||||
|
duty_uu_id_list: List[str] # List of duty objects
|
||||||
|
|
||||||
|
selected_company: Optional[CompanyToken] = None # Selected Company Object
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_employee(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_occupant(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
TokenDictType = Union[EmployeeTokenObject, OccupantTokenObject]
|
||||||
|
|
||||||
|
|
||||||
|
class TokenProvider:
|
||||||
|
|
||||||
|
AUTH_KEY: str = api_config.ACCESS_TOKEN_TAG
|
||||||
|
AUTH_TOKEN: str = "AUTH_TOKEN"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def process_redis_object(cls, redis_object: Dict[str, Any]) -> TokenDictType:
|
||||||
|
"""
|
||||||
|
Process Redis object and return appropriate token object.
|
||||||
|
"""
|
||||||
|
if not redis_object.get("selected_company"):
|
||||||
|
redis_object["selected_company"] = None
|
||||||
|
if not redis_object.get("selected_occupant"):
|
||||||
|
redis_object["selected_occupant"] = None
|
||||||
|
if redis_object.get("user_type") == UserType.employee.value:
|
||||||
|
return EmployeeTokenObject(**redis_object)
|
||||||
|
elif redis_object.get("user_type") == UserType.occupant.value:
|
||||||
|
return OccupantTokenObject(**redis_object)
|
||||||
|
raise ValueError("Invalid user type")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_dict_from_redis(
|
||||||
|
cls, token: Optional[str] = None, user_uu_id: Optional[str] = None
|
||||||
|
) -> Union[TokenDictType, List[TokenDictType]]:
|
||||||
|
"""
|
||||||
|
Retrieve token object from Redis using token and user_uu_id
|
||||||
|
"""
|
||||||
|
token_to_use, user_uu_id_to_use = token or "*", user_uu_id or "*"
|
||||||
|
list_of_token_dict, auth_key_list = [], [
|
||||||
|
cls.AUTH_TOKEN,
|
||||||
|
token_to_use,
|
||||||
|
user_uu_id_to_use,
|
||||||
|
]
|
||||||
|
if token:
|
||||||
|
result = RedisActions.get_json(list_keys=auth_key_list, limit=1)
|
||||||
|
if first_record := result.first:
|
||||||
|
return cls.process_redis_object(first_record)
|
||||||
|
elif user_uu_id:
|
||||||
|
result = RedisActions.get_json(list_keys=auth_key_list)
|
||||||
|
if all_records := result.all:
|
||||||
|
for all_record in all_records:
|
||||||
|
list_of_token_dict.append(cls.process_redis_object(all_record))
|
||||||
|
return list_of_token_dict
|
||||||
|
raise ValueError(
|
||||||
|
"Token not found in Redis. Please check the token or user_uu_id."
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def retrieve_application_codes(
|
||||||
|
cls, page_url: str, tokens: Union[TokenDictType, List[TokenDictType]]
|
||||||
|
):
|
||||||
|
if isinstance(tokens, TokenDictType):
|
||||||
|
if application_codes := tokens.reachable_app_codes.get(page_url, None):
|
||||||
|
return application_codes
|
||||||
|
elif isinstance(tokens, list):
|
||||||
|
retrieved_event_apps = []
|
||||||
|
for token in tokens:
|
||||||
|
if application_codes := token.reachable_app_codes.get(page_url, None):
|
||||||
|
retrieved_event_apps.append(application_codes)
|
||||||
|
return retrieved_event_apps
|
||||||
|
raise ValueError("Invalid token type or no application codes found.")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def retrieve_event_codes(
|
||||||
|
cls, endpoint_code: str, tokens: Union[TokenDictType, List[TokenDictType]]
|
||||||
|
):
|
||||||
|
if isinstance(tokens, TokenDictType):
|
||||||
|
if event_codes := tokens.reachable_event_codes.get(endpoint_code, None):
|
||||||
|
return event_codes
|
||||||
|
elif isinstance(tokens, List):
|
||||||
|
retrieved_event_codes = []
|
||||||
|
for token in tokens:
|
||||||
|
if isinstance(token, TokenDictType):
|
||||||
|
if event_codes := token.reachable_event_codes.get(
|
||||||
|
endpoint_code, None
|
||||||
|
):
|
||||||
|
retrieved_event_codes.append(event_codes)
|
||||||
|
return retrieved_event_codes
|
||||||
|
raise ValueError("Invalid token type or no event codes found.")
|
||||||
|
|
@ -8,9 +8,15 @@
|
||||||
"name": "client-frontend",
|
"name": "client-frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^5.0.1",
|
||||||
|
"flatpickr": "^4.6.13",
|
||||||
"next": "15.2.4",
|
"next": "15.2.4",
|
||||||
|
"next-crypto": "^1.0.8",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0",
|
||||||
|
"react-hook-form": "^7.55.0",
|
||||||
|
"sonner": "^2.0.3",
|
||||||
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
|
@ -42,6 +48,17 @@
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@hookform/resolvers": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/utils": "^0.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react-hook-form": "^7.55.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@img/sharp-darwin-arm64": {
|
"node_modules/@img/sharp-darwin-arm64": {
|
||||||
"version": "0.33.5",
|
"version": "0.33.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
|
||||||
|
|
@ -509,6 +526,11 @@
|
||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@standard-schema/utils": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="
|
||||||
|
},
|
||||||
"node_modules/@swc/counter": {
|
"node_modules/@swc/counter": {
|
||||||
"version": "0.1.3",
|
"version": "0.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||||
|
|
@ -876,6 +898,11 @@
|
||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/flatpickr": {
|
||||||
|
"version": "4.6.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz",
|
||||||
|
"integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw=="
|
||||||
|
},
|
||||||
"node_modules/graceful-fs": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
|
|
@ -1195,6 +1222,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/next-crypto": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/next-crypto/-/next-crypto-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-6VcrH+xFuuCRGCdDMjFFibhJ97c4s+J/6SEV73RUYJhh38MDW4WXNZNTWIMZBq0B29LOIfAQ0XA37xGUZZCCjA=="
|
||||||
|
},
|
||||||
"node_modules/next/node_modules/postcss": {
|
"node_modules/next/node_modules/postcss": {
|
||||||
"version": "8.4.31",
|
"version": "8.4.31",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||||
|
|
@ -1274,6 +1306,21 @@
|
||||||
"react": "^19.1.0"
|
"react": "^19.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-hook-form": {
|
||||||
|
"version": "7.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.55.0.tgz",
|
||||||
|
"integrity": "sha512-XRnjsH3GVMQz1moZTW53MxfoWN7aDpUg/GpVNc4A3eXRVNdGXfbzJ4vM4aLQ8g6XCUh1nIbx70aaNCl7kxnjog==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/react-hook-form"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/scheduler": {
|
"node_modules/scheduler": {
|
||||||
"version": "0.26.0",
|
"version": "0.26.0",
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
||||||
|
|
@ -1339,6 +1386,15 @@
|
||||||
"is-arrayish": "^0.3.1"
|
"is-arrayish": "^0.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sonner": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
|
@ -1415,6 +1471,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "3.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
|
||||||
|
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,16 +9,22 @@
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^5.0.1",
|
||||||
|
"flatpickr": "^4.6.13",
|
||||||
|
"next": "15.2.4",
|
||||||
|
"next-crypto": "^1.0.8",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"next": "15.2.4"
|
"react-hook-form": "^7.55.0",
|
||||||
|
"sonner": "^2.0.3",
|
||||||
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"@tailwindcss/postcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"tailwindcss": "^4"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
"use server";
|
||||||
|
// import { retrieveAccessToken } from "@/apicalls/cookies/token";
|
||||||
|
|
||||||
|
const defaultHeaders = {
|
||||||
|
accept: "application/json",
|
||||||
|
language: "tr",
|
||||||
|
domain: "evyos.com.tr",
|
||||||
|
tz: "GMT+3",
|
||||||
|
"Content-type": "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
const DefaultResponse = {
|
||||||
|
error: "Hata tipi belirtilmedi",
|
||||||
|
status: "500",
|
||||||
|
data: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const cacheList = ["no-cache", "no-store", "force-cache", "only-if-cached"];
|
||||||
|
|
||||||
|
const prepareResponse = (response: any, statusCode: number) => {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: statusCode,
|
||||||
|
data: response || {},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error preparing response:", error);
|
||||||
|
return {
|
||||||
|
...DefaultResponse,
|
||||||
|
error: "Response parsing error",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchData = async (
|
||||||
|
endpoint: string,
|
||||||
|
payload: any,
|
||||||
|
method: string = "POST",
|
||||||
|
cache: boolean = false
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const headers = {
|
||||||
|
...defaultHeaders,
|
||||||
|
};
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
cache: cache ? "force-cache" : "no-cache",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (method !== "GET" && payload) {
|
||||||
|
fetchOptions.body = JSON.stringify(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(endpoint, fetchOptions);
|
||||||
|
const responseJson = await response.json();
|
||||||
|
console.log("Fetching:", endpoint, fetchOptions);
|
||||||
|
console.log("Response:", responseJson);
|
||||||
|
return prepareResponse(responseJson, response.status);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Fetch error:", error);
|
||||||
|
return {
|
||||||
|
...DefaultResponse,
|
||||||
|
error: error instanceof Error ? error.message : "Network error",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDataWithToken = async (
|
||||||
|
endpoint: string,
|
||||||
|
uuid: string,
|
||||||
|
payload: any,
|
||||||
|
method: string = "POST",
|
||||||
|
cache: boolean = false
|
||||||
|
) => {
|
||||||
|
// const accessToken = (await retrieveAccessToken()) || "";
|
||||||
|
const headers = {
|
||||||
|
...defaultHeaders,
|
||||||
|
// "evyos-session-key": accessToken,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
cache: cache ? "force-cache" : "no-cache",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (method !== "GET" && payload) {
|
||||||
|
fetchOptions.body = JSON.stringify(payload.payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${endpoint}/${uuid}`, fetchOptions);
|
||||||
|
const responseJson = await response.json();
|
||||||
|
console.log("Fetching:", `${endpoint}/${uuid}`, fetchOptions);
|
||||||
|
console.log("Response:", responseJson);
|
||||||
|
return prepareResponse(responseJson, response.status);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Update error:", error);
|
||||||
|
return {
|
||||||
|
...DefaultResponse,
|
||||||
|
error: error instanceof Error ? error.message : "Network error",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchDataWithToken = async (
|
||||||
|
endpoint: string,
|
||||||
|
payload: any,
|
||||||
|
method: string = "POST",
|
||||||
|
cache: boolean = false
|
||||||
|
) => {
|
||||||
|
// const accessToken = (await retrieveAccessToken()) || "";
|
||||||
|
const headers = {
|
||||||
|
...defaultHeaders,
|
||||||
|
// "evyos-session-key": accessToken,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
cache: cache ? "force-cache" : "no-cache",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (method !== "GET" && payload) {
|
||||||
|
fetchOptions.body = JSON.stringify(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(endpoint, fetchOptions);
|
||||||
|
const responseJson = await response.json();
|
||||||
|
console.log("Fetching:", endpoint, fetchOptions);
|
||||||
|
console.log("Response:", responseJson);
|
||||||
|
return prepareResponse(responseJson, response.status);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Fetch with token error:", error);
|
||||||
|
return {
|
||||||
|
...DefaultResponse,
|
||||||
|
error: error instanceof Error ? error.message : "Network error",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { fetchData, fetchDataWithToken, updateDataWithToken };
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
const formatServiceUrl = (url: string) => {
|
||||||
|
if (!url) return "";
|
||||||
|
return url.startsWith("http") ? url : `http://${url}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const baseUrlAuth = formatServiceUrl(
|
||||||
|
process.env.NEXT_PUBLIC_AUTH_SERVICE_URL || "auth_service:8001"
|
||||||
|
);
|
||||||
|
// export const baseUrlValidation = formatServiceUrl(
|
||||||
|
// process.env.NEXT_PUBLIC_VALIDATION_SERVICE_URL || "validationservice:8888"
|
||||||
|
// );
|
||||||
|
// export const baseUrlEvent = formatServiceUrl(
|
||||||
|
// process.env.NEXT_PUBLIC_EVENT_SERVICE_URL || "eventservice:8888"
|
||||||
|
// );
|
||||||
|
export const tokenSecret = process.env.TOKENSECRET_90 || "";
|
||||||
|
|
||||||
|
export const cookieObject: any = {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "none",
|
||||||
|
secure: true,
|
||||||
|
maxAge: 3600,
|
||||||
|
priority: "high",
|
||||||
|
};
|
||||||
|
|
||||||
|
interface FilterListInterface {
|
||||||
|
page?: number | null | undefined;
|
||||||
|
size?: number | null | undefined;
|
||||||
|
orderField?: string | null | undefined;
|
||||||
|
orderType?: string | null | undefined;
|
||||||
|
includeJoins?: any[] | null | undefined;
|
||||||
|
query?: any | null | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FilterList {
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
orderField: string;
|
||||||
|
orderType: string;
|
||||||
|
includeJoins: any[];
|
||||||
|
query: any;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
page = 1,
|
||||||
|
size = 5,
|
||||||
|
orderField = "id",
|
||||||
|
orderType = "asc",
|
||||||
|
includeJoins = [],
|
||||||
|
query = {},
|
||||||
|
}: FilterListInterface = {}) {
|
||||||
|
this.page = page ?? 1;
|
||||||
|
this.size = size ?? 5;
|
||||||
|
this.orderField = orderField ?? "uu_id";
|
||||||
|
this.orderType = orderType ?? "asc";
|
||||||
|
this.orderType = this.orderType.startsWith("a") ? "asc" : "desc";
|
||||||
|
this.includeJoins = includeJoins ?? [];
|
||||||
|
this.query = query ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
|
filter() {
|
||||||
|
return {
|
||||||
|
page: this.page,
|
||||||
|
size: this.size,
|
||||||
|
orderField: this.orderField,
|
||||||
|
orderType: this.orderType,
|
||||||
|
includeJoins: this.includeJoins,
|
||||||
|
query: this.query,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultFilterList = new FilterList({});
|
||||||
|
|
||||||
|
export { FilterList, defaultFilterList };
|
||||||
|
export type { FilterListInterface };
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
"use server";
|
||||||
|
import NextCrypto from "next-crypto";
|
||||||
|
|
||||||
|
import { fetchData, fetchDataWithToken } from "../api-fetcher";
|
||||||
|
import { baseUrlAuth, cookieObject, tokenSecret } from "../basics";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
|
||||||
|
// import { setAvailableEvents } from "../events/available";
|
||||||
|
|
||||||
|
const loginEndpoint = `${baseUrlAuth}/authentication/login`;
|
||||||
|
const loginSelectEndpoint = `${baseUrlAuth}/authentication/select`;
|
||||||
|
|
||||||
|
console.log("loginEndpoint", loginEndpoint);
|
||||||
|
console.log("loginSelectEndpoint", loginSelectEndpoint);
|
||||||
|
|
||||||
|
interface LoginViaAccessKeys {
|
||||||
|
accessKey: string;
|
||||||
|
password: string;
|
||||||
|
rememberMe: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoginSelectEmployee {
|
||||||
|
company_uu_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoginSelectOccupant {
|
||||||
|
selectedBuilding: any;
|
||||||
|
build_part_uu_id: string;
|
||||||
|
occupant_uu_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loginViaAccessKeys(payload: LoginViaAccessKeys) {
|
||||||
|
try {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const nextCrypto = new NextCrypto(tokenSecret);
|
||||||
|
|
||||||
|
const response = await fetchData(
|
||||||
|
loginEndpoint,
|
||||||
|
{
|
||||||
|
access_key: payload.accessKey,
|
||||||
|
password: payload.password,
|
||||||
|
remember_me: payload.rememberMe,
|
||||||
|
},
|
||||||
|
"POST",
|
||||||
|
false
|
||||||
|
);
|
||||||
|
console.log("response", response);
|
||||||
|
if (response.status === 200 || response.status === 202) {
|
||||||
|
const loginRespone = response?.data;
|
||||||
|
const accessToken = await nextCrypto.encrypt(loginRespone.access_token);
|
||||||
|
const accessObject = await nextCrypto.encrypt(
|
||||||
|
JSON.stringify(loginRespone.selection_list)
|
||||||
|
);
|
||||||
|
const userProfile = await nextCrypto.encrypt(
|
||||||
|
JSON.stringify(loginRespone.user)
|
||||||
|
);
|
||||||
|
|
||||||
|
cookieStore.set({
|
||||||
|
name: "accessToken",
|
||||||
|
value: accessToken,
|
||||||
|
...cookieObject,
|
||||||
|
});
|
||||||
|
|
||||||
|
cookieStore.set({
|
||||||
|
name: "accessObject",
|
||||||
|
value: accessObject,
|
||||||
|
...cookieObject,
|
||||||
|
});
|
||||||
|
cookieStore.set({
|
||||||
|
name: "userProfile",
|
||||||
|
value: JSON.stringify(userProfile),
|
||||||
|
...cookieObject,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
completed: true,
|
||||||
|
message: "Login successful",
|
||||||
|
status: 200,
|
||||||
|
data: loginRespone,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("JSON parse error:", error);
|
||||||
|
return {
|
||||||
|
completed: false,
|
||||||
|
message: "Login NOT successful",
|
||||||
|
status: 401,
|
||||||
|
data: "{}",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
completed: false,
|
||||||
|
// error: response.error || "Login failed",
|
||||||
|
// message: response.message || "Authentication failed",
|
||||||
|
status: response.status || 500,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Login error:", error);
|
||||||
|
return {
|
||||||
|
completed: false,
|
||||||
|
// error: error instanceof Error ? error.message : "Login error",
|
||||||
|
// message: "An error occurred during login",
|
||||||
|
status: 500,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loginSelectEmployee(payload: LoginSelectEmployee) {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const nextCrypto = new NextCrypto(tokenSecret);
|
||||||
|
|
||||||
|
const selectResponse: any = await fetchDataWithToken(
|
||||||
|
loginSelectEndpoint,
|
||||||
|
{
|
||||||
|
company_uu_id: payload.company_uu_id,
|
||||||
|
},
|
||||||
|
"POST",
|
||||||
|
false
|
||||||
|
);
|
||||||
|
if (selectResponse.status === 200) {
|
||||||
|
const usersSelection = await nextCrypto.encrypt(
|
||||||
|
JSON.stringify({
|
||||||
|
company_uu_id: payload.company_uu_id,
|
||||||
|
user_type: "employee",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
cookieStore.set({
|
||||||
|
name: "userSelection",
|
||||||
|
value: usersSelection,
|
||||||
|
...cookieObject,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return selectResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loginSelectOccupant(payload: LoginSelectOccupant) {
|
||||||
|
const selectedBuilding = payload.selectedBuilding;
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const nextCrypto = new NextCrypto(tokenSecret);
|
||||||
|
const selectResponse: any = await fetchDataWithToken(
|
||||||
|
loginSelectEndpoint,
|
||||||
|
{
|
||||||
|
build_part_uu_id: payload.build_part_uu_id,
|
||||||
|
occupant_uu_id: payload.occupant_uu_id,
|
||||||
|
},
|
||||||
|
"POST",
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectResponse.status === 200) {
|
||||||
|
const usersSelection = await nextCrypto.encrypt(
|
||||||
|
JSON.stringify({
|
||||||
|
company_uu_id: {
|
||||||
|
build_part_uu_id: payload.build_part_uu_id,
|
||||||
|
occupant_uu_id: payload.occupant_uu_id,
|
||||||
|
build_id: selectedBuilding,
|
||||||
|
},
|
||||||
|
user_type: "occupant",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
cookieStore.set({
|
||||||
|
name: "userSelection",
|
||||||
|
value: usersSelection,
|
||||||
|
...cookieObject,
|
||||||
|
});
|
||||||
|
// await setAvailableEvents();
|
||||||
|
}
|
||||||
|
return selectResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { loginViaAccessKeys, loginSelectEmployee, loginSelectOccupant };
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import React from "react";
|
||||||
|
import Login from "@/components/auth/login";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "WAG Login",
|
||||||
|
description: "Login to WAG system",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-100">
|
||||||
|
<Login />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
function SelectPage() {
|
||||||
|
return <div>SelectPage</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectPage;
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "WAG Frontend",
|
||||||
|
description: "WAG Frontend Application",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="en" suppressHydrationWarning>
|
||||||
|
<body>
|
||||||
|
<div className="min-h-screen flex">
|
||||||
|
<div className="w-1/3 bg-purple-600 p-8 text-white">
|
||||||
|
<h1 className="text-4xl font-bold mb-4">Welcome to evyos</h1>
|
||||||
|
<p className="text-lg">
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
|
||||||
|
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
|
||||||
|
enim ad minim veniam, quis nostrud exercitation ullamco laboris
|
||||||
|
nisi ut aliquip ex ea commodo consequat.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-2/3">
|
||||||
|
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,6 @@
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
const geistSans = Geist({
|
|
||||||
variable: "--font-geist-sans",
|
|
||||||
subsets: ["latin"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
|
||||||
variable: "--font-geist-mono",
|
|
||||||
subsets: ["latin"],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "Create Next App",
|
||||||
description: "Generated by create next app",
|
description: "Generated by create next app",
|
||||||
|
|
@ -24,11 +13,7 @@ export default function RootLayout({
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body
|
<body>{children}</body>
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,103 +1,23 @@
|
||||||
import Image from "next/image";
|
export default async function Home() {
|
||||||
|
const result = await fetch("http://auth_service:8001/authentication/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
language: "tr",
|
||||||
|
domain: "evyos.com.tr",
|
||||||
|
tz: "GMT+3",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
access_key: "karatay.berkay.sup@evyos.com.tr",
|
||||||
|
password: "string",
|
||||||
|
remember_me: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const data = await result.json();
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
||||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
{JSON.stringify({ data: data?.data })}
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
|
||||||
src="/next.svg"
|
|
||||||
alt="Next.js logo"
|
|
||||||
width={180}
|
|
||||||
height={38}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
|
|
||||||
<li className="mb-2 tracking-[-.01em]">
|
|
||||||
Get started by editing{" "}
|
|
||||||
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
|
|
||||||
src/app/page.tsx
|
|
||||||
</code>
|
|
||||||
.
|
|
||||||
</li>
|
|
||||||
<li className="tracking-[-.01em]">
|
|
||||||
Save and see your changes instantly.
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
|
||||||
<a
|
|
||||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel logomark"
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
/>
|
|
||||||
Deploy now
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Read our docs
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
aria-hidden
|
|
||||||
src="/file.svg"
|
|
||||||
alt="File icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Learn
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
aria-hidden
|
|
||||||
src="/window.svg"
|
|
||||||
alt="Window icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Examples
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
||||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
aria-hidden
|
|
||||||
src="/globe.svg"
|
|
||||||
alt="Globe icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Go to nextjs.org →
|
|
||||||
</a>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
"use client";
|
||||||
|
import { useState, useTransition } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { loginViaAccessKeys } from "@/apicalls/login/login";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const loginSchema = z.object({
|
||||||
|
email: z.string().email("Invalid email address"),
|
||||||
|
password: z.string().min(5, "Password must be at least 5 characters"),
|
||||||
|
});
|
||||||
|
|
||||||
|
type LoginFormData = z.infer<typeof loginSchema>;
|
||||||
|
|
||||||
|
function Login() {
|
||||||
|
// Open transition for form login
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [jsonText, setJsonText] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const Router = useRouter();
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
formState: { errors },
|
||||||
|
handleSubmit,
|
||||||
|
} = useForm<LoginFormData>({
|
||||||
|
resolver: zodResolver(loginSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (data: LoginFormData) => {
|
||||||
|
try {
|
||||||
|
startTransition(() => {
|
||||||
|
try {
|
||||||
|
loginViaAccessKeys({
|
||||||
|
accessKey: data.email,
|
||||||
|
password: data.password,
|
||||||
|
rememberMe: false,
|
||||||
|
})
|
||||||
|
.then((result: any) => {
|
||||||
|
const dataResponse = result?.data;
|
||||||
|
if (dataResponse?.access_token) {
|
||||||
|
setJsonText(JSON.stringify(dataResponse));
|
||||||
|
setTimeout(() => {
|
||||||
|
Router.push("/auth/select");
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
return dataResponse;
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
} catch (error) {}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Login error:", error);
|
||||||
|
setError("An error occurred during login");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex h-full min-h-[inherit] flex-col items-center justify-center gap-4">
|
||||||
|
<div className="w-full max-w-md rounded-lg bg-white p-8 shadow-md">
|
||||||
|
<h2 className="mb-6 text-center text-2xl font-bold text-gray-900">
|
||||||
|
Login
|
||||||
|
</h2>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="email"
|
||||||
|
className="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
{...register("email")}
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500"
|
||||||
|
/>
|
||||||
|
{errors.email && (
|
||||||
|
<p className="mt-1 text-sm text-red-600">
|
||||||
|
{errors.email.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="password"
|
||||||
|
className="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
{...register("password")}
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500"
|
||||||
|
/>
|
||||||
|
{errors.password && (
|
||||||
|
<p className="mt-1 text-sm text-red-600">
|
||||||
|
{errors.password.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="rounded-md bg-red-50 p-4">
|
||||||
|
<p className="text-sm text-red-700">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isPending}
|
||||||
|
className="w-full rounded-md bg-indigo-600 px-4 py-2 text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isPending ? "Logging in..." : "Login"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{jsonText && (
|
||||||
|
<div className="w-full max-w-md rounded-lg bg-white p-8 shadow-md">
|
||||||
|
<h2 className="mb-4 text-center text-xl font-bold text-gray-900">
|
||||||
|
Response Data
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{Object.entries(JSON.parse(jsonText)).map(([key, value]) => (
|
||||||
|
<div key={key} className="flex items-start gap-2">
|
||||||
|
<strong className="text-gray-700">{key}:</strong>
|
||||||
|
<span className="text-gray-600">
|
||||||
|
{typeof value === "object"
|
||||||
|
? JSON.stringify(value)
|
||||||
|
: value?.toString() || "N/A"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Login;
|
||||||
|
|
@ -18,4 +18,4 @@ COPY WebServices/management-frontend ./WebServices/management-frontend
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
# Command to run the app
|
# Command to run the app
|
||||||
CMD ["sh", "-c", "cd /WebServices/management-frontend && npm run dev"]
|
CMD ["sh", "-c", "cd /WebServices/management-frontend && npm run dev"]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
services:
|
services:
|
||||||
|
|
||||||
mongo_service:
|
mongo_service:
|
||||||
container_name: mongo_service
|
container_name: mongo_service
|
||||||
image: "bitnami/mongodb:latest"
|
image: "bitnami/mongodb:latest"
|
||||||
|
|
@ -59,70 +58,66 @@ services:
|
||||||
- wag-services
|
- wag-services
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
volumes:
|
# volumes:
|
||||||
- .WebServices/client-frontend:WebServices/client-frontend
|
# - client-frontend:/WebServices/client-frontend
|
||||||
- WebServices/client-frontend/node_modules
|
|
||||||
- WebServices/client-frontend/.next
|
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=development
|
- NODE_ENV=development
|
||||||
|
|
||||||
management_frontend:
|
# management_frontend:
|
||||||
container_name: management_frontend
|
# container_name: management_frontend
|
||||||
build:
|
# build:
|
||||||
context: .
|
# context: .
|
||||||
dockerfile: WebServices/management-frontend/Dockerfile
|
# dockerfile: WebServices/management-frontend/Dockerfile
|
||||||
networks:
|
# networks:
|
||||||
- wag-services
|
# - wag-services
|
||||||
ports:
|
# ports:
|
||||||
- "3001:3000" # Using different host port to avoid conflicts
|
# - "3001:3000" # Using different host port to avoid conflicts
|
||||||
volumes:
|
# # volumes:
|
||||||
- .WebServices/management-frontend:WebServices/management-frontend
|
# # - management-frontend:/WebServices/management-frontend
|
||||||
- WebServices/management-frontend/node_modules
|
# environment:
|
||||||
- WebServices/management-frontend/.next
|
# - NODE_ENV=development
|
||||||
environment:
|
|
||||||
- NODE_ENV=development
|
|
||||||
|
|
||||||
# initializer_service:
|
# initializer_service:
|
||||||
# container_name: initializer_service
|
# container_name: initializer_service
|
||||||
# build:
|
# build:
|
||||||
# context: .
|
# context: .
|
||||||
# dockerfile: ApiServices/InitialService/Dockerfile
|
# dockerfile: ApiServices/InitialService/Dockerfile
|
||||||
# networks:
|
# networks:
|
||||||
# - wag-services
|
# - wag-services
|
||||||
# env_file:
|
# env_file:
|
||||||
# - api_env.env
|
# - api_env.env
|
||||||
# depends_on:
|
# depends_on:
|
||||||
# - postgres-service
|
# - postgres-service
|
||||||
# - mongo_service
|
# - mongo_service
|
||||||
# - redis_service
|
# - redis_service
|
||||||
|
|
||||||
# template_service:
|
# template_service:
|
||||||
# container_name: template_service
|
# container_name: template_service
|
||||||
# build:
|
# build:
|
||||||
# context: .
|
# context: .
|
||||||
# dockerfile: ApiServices/TemplateService/Dockerfile
|
# dockerfile: ApiServices/TemplateService/Dockerfile
|
||||||
# networks:
|
# networks:
|
||||||
# - wag-services
|
# - wag-services
|
||||||
# env_file:
|
# env_file:
|
||||||
# - api_env.env
|
# - api_env.env
|
||||||
# environment:
|
# environment:
|
||||||
# - API_PATH=app:app
|
# - API_PATH=app:app
|
||||||
# - API_HOST=0.0.0.0
|
# - API_HOST=0.0.0.0
|
||||||
# - API_PORT=8000
|
# - API_PORT=8000
|
||||||
# - API_LOG_LEVEL=info
|
# - API_LOG_LEVEL=info
|
||||||
# - API_RELOAD=1
|
# - API_RELOAD=1
|
||||||
# - API_ACCESS_TOKEN_TAG=1
|
# - API_ACCESS_TOKEN_TAG=1
|
||||||
# - API_APP_NAME=evyos-template-api-gateway
|
# - API_APP_NAME=evyos-template-api-gateway
|
||||||
# - API_TITLE=WAG API Template Api Gateway
|
# - API_TITLE=WAG API Template Api Gateway
|
||||||
# - API_FORGOT_LINK=https://template_service/forgot-password
|
# - API_FORGOT_LINK=https://template_service/forgot-password
|
||||||
# - API_DESCRIPTION=This api is serves as web template api gateway only to evyos web services.
|
# - API_DESCRIPTION=This api is serves as web template api gateway only to evyos web services.
|
||||||
# - API_APP_URL=https://template_service
|
# - API_APP_URL=https://template_service
|
||||||
# ports:
|
# ports:
|
||||||
# - "8000:8000"
|
# - "8000:8000"
|
||||||
# depends_on:
|
# depends_on:
|
||||||
# - postgres-service
|
# - postgres-service
|
||||||
# - mongo_service
|
# - mongo_service
|
||||||
# - redis_service
|
# - redis_service
|
||||||
|
|
||||||
auth_service:
|
auth_service:
|
||||||
container_name: auth_service
|
container_name: auth_service
|
||||||
|
|
@ -161,10 +156,11 @@ services:
|
||||||
# networks:
|
# networks:
|
||||||
# - wag-services
|
# - wag-services
|
||||||
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
wag-services:
|
wag-services:
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
mongodb-data:
|
mongodb-data:
|
||||||
|
client-frontend:
|
||||||
|
management-frontend:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
{
|
||||||
|
"name": "prod-wag-backend-automate-services",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^5.0.1",
|
||||||
|
"flatpickr": "^4.6.13",
|
||||||
|
"next-crypto": "^1.0.8",
|
||||||
|
"react-hook-form": "^7.55.0",
|
||||||
|
"sonner": "^2.0.3",
|
||||||
|
"zod": "^3.24.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@hookform/resolvers": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/utils": "^0.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react-hook-form": "^7.55.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@standard-schema/utils": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="
|
||||||
|
},
|
||||||
|
"node_modules/flatpickr": {
|
||||||
|
"version": "4.6.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz",
|
||||||
|
"integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw=="
|
||||||
|
},
|
||||||
|
"node_modules/next-crypto": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/next-crypto/-/next-crypto-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-6VcrH+xFuuCRGCdDMjFFibhJ97c4s+J/6SEV73RUYJhh38MDW4WXNZNTWIMZBq0B29LOIfAQ0XA37xGUZZCCjA=="
|
||||||
|
},
|
||||||
|
"node_modules/react-hook-form": {
|
||||||
|
"version": "7.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.55.0.tgz",
|
||||||
|
"integrity": "sha512-XRnjsH3GVMQz1moZTW53MxfoWN7aDpUg/GpVNc4A3eXRVNdGXfbzJ4vM4aLQ8g6XCUh1nIbx70aaNCl7kxnjog==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/react-hook-form"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sonner": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "3.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
|
||||||
|
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^5.0.1",
|
||||||
|
"flatpickr": "^4.6.13",
|
||||||
|
"next-crypto": "^1.0.8",
|
||||||
|
"react-hook-form": "^7.55.0",
|
||||||
|
"sonner": "^2.0.3",
|
||||||
|
"zod": "^3.24.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue