From 60507c87e0ee5477a9c5ebaabce4f88411bfdcec Mon Sep 17 00:00:00 2001 From: berkay Date: Sat, 5 Apr 2025 22:54:05 +0300 Subject: [PATCH] updated Service providers --- ApiServices/AuthService/README.md | 30 +++ .../AuthService/endpoints/auth/route.py | 41 +++- .../request/authentication/login_post.py | 4 + .../endpoints/test_template/route.py | 21 +- .../TemplateService/providers/__init__.py | 0 .../providers/token_provider.py | 210 ++++++++++++++++++ WebServices/client-frontend/package-lock.json | 66 +++++- WebServices/client-frontend/package.json | 14 +- .../src/apicalls/api-fetcher.tsx | 144 ++++++++++++ .../client-frontend/src/apicalls/basics.ts | 75 +++++++ .../src/apicalls/login/login.tsx | 172 ++++++++++++++ .../src/app/(AuthLayout)/auth/login/page.tsx | 16 ++ .../src/app/(AuthLayout)/auth/select/page.tsx | 7 + .../src/app/(AuthLayout)/layout.tsx | 34 +++ .../client-frontend/src/app/layout.tsx | 17 +- WebServices/client-frontend/src/app/page.tsx | 114 ++-------- .../src/components/auth/login.tsx | 147 ++++++++++++ WebServices/management-frontend/Dockerfile | 2 +- docker-compose.yml | 118 +++++----- package-lock.json | 75 +++++++ package.json | 10 + 21 files changed, 1134 insertions(+), 183 deletions(-) create mode 100644 ApiServices/TemplateService/providers/__init__.py create mode 100644 ApiServices/TemplateService/providers/token_provider.py create mode 100644 WebServices/client-frontend/src/apicalls/api-fetcher.tsx create mode 100644 WebServices/client-frontend/src/apicalls/basics.ts create mode 100644 WebServices/client-frontend/src/apicalls/login/login.tsx create mode 100644 WebServices/client-frontend/src/app/(AuthLayout)/auth/login/page.tsx create mode 100644 WebServices/client-frontend/src/app/(AuthLayout)/auth/select/page.tsx create mode 100644 WebServices/client-frontend/src/app/(AuthLayout)/layout.tsx create mode 100644 WebServices/client-frontend/src/components/auth/login.tsx create mode 100644 package-lock.json create mode 100644 package.json diff --git a/ApiServices/AuthService/README.md b/ApiServices/AuthService/README.md index e69de29..555f23f 100644 --- a/ApiServices/AuthService/README.md +++ b/ApiServices/AuthService/README.md @@ -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 +} \ No newline at end of file diff --git a/ApiServices/AuthService/endpoints/auth/route.py b/ApiServices/AuthService/endpoints/auth/route.py index a3208fe..6cd9e7e 100644 --- a/ApiServices/AuthService/endpoints/auth/route.py +++ b/ApiServices/AuthService/endpoints/auth/route.py @@ -14,6 +14,7 @@ from ApiServices.AuthService.validations.request.authentication.login_post impor RequestForgotPasswordPhone, RequestForgotPasswordEmail, RequestVerifyOTP, + RequestApplication, ) from ApiServices.AuthService.events.auth.auth import AuthHandlers @@ -367,7 +368,45 @@ def authentication_password_verify_otp( "tz": tz or "GMT+3", "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: return JSONResponse( content={"error": "EYS_0003"}, diff --git a/ApiServices/AuthService/validations/request/authentication/login_post.py b/ApiServices/AuthService/validations/request/authentication/login_post.py index c298948..838111c 100644 --- a/ApiServices/AuthService/validations/request/authentication/login_post.py +++ b/ApiServices/AuthService/validations/request/authentication/login_post.py @@ -14,6 +14,10 @@ class RequestVerifyOTP(BaseModel): otp: str +class RequestApplication(BaseModel): + page: str # /building/create + + class RequestSelectOccupant(BaseModel): company_uu_id: str diff --git a/ApiServices/TemplateService/endpoints/test_template/route.py b/ApiServices/TemplateService/endpoints/test_template/route.py index 43e46b7..dddf4d5 100644 --- a/ApiServices/TemplateService/endpoints/test_template/route.py +++ b/ApiServices/TemplateService/endpoints/test_template/route.py @@ -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 test_template_route = APIRouter(prefix="/test", tags=["Test"]) @@ -10,10 +12,25 @@ test_template_route = APIRouter(prefix="/test", tags=["Test"]) description="Test Template Route", 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 """ + 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_keys=[ "3f510dcf-9f84-4eb9-b919-f582f30adab1", diff --git a/ApiServices/TemplateService/providers/__init__.py b/ApiServices/TemplateService/providers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ApiServices/TemplateService/providers/token_provider.py b/ApiServices/TemplateService/providers/token_provider.py new file mode 100644 index 0000000..a969548 --- /dev/null +++ b/ApiServices/TemplateService/providers/token_provider.py @@ -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.") diff --git a/WebServices/client-frontend/package-lock.json b/WebServices/client-frontend/package-lock.json index 01dba39..2111f47 100644 --- a/WebServices/client-frontend/package-lock.json +++ b/WebServices/client-frontend/package-lock.json @@ -8,9 +8,15 @@ "name": "client-frontend", "version": "0.1.0", "dependencies": { + "@hookform/resolvers": "^5.0.1", + "flatpickr": "^4.6.13", "next": "15.2.4", + "next-crypto": "^1.0.8", "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": { "@tailwindcss/postcss": "^4", @@ -42,6 +48,17 @@ "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": { "version": "0.33.5", "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", @@ -509,6 +526,11 @@ "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": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -876,6 +898,11 @@ "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": { "version": "4.2.11", "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": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -1274,6 +1306,21 @@ "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": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", @@ -1339,6 +1386,15 @@ "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": { "version": "1.2.1", "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", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "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" + } } } } diff --git a/WebServices/client-frontend/package.json b/WebServices/client-frontend/package.json index 4449a92..dc07c72 100644 --- a/WebServices/client-frontend/package.json +++ b/WebServices/client-frontend/package.json @@ -9,16 +9,22 @@ "lint": "next lint" }, "dependencies": { + "@hookform/resolvers": "^5.0.1", + "flatpickr": "^4.6.13", + "next": "15.2.4", + "next-crypto": "^1.0.8", "react": "^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": { - "typescript": "^5", + "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "@tailwindcss/postcss": "^4", - "tailwindcss": "^4" + "tailwindcss": "^4", + "typescript": "^5" } } diff --git a/WebServices/client-frontend/src/apicalls/api-fetcher.tsx b/WebServices/client-frontend/src/apicalls/api-fetcher.tsx new file mode 100644 index 0000000..b65e888 --- /dev/null +++ b/WebServices/client-frontend/src/apicalls/api-fetcher.tsx @@ -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 }; diff --git a/WebServices/client-frontend/src/apicalls/basics.ts b/WebServices/client-frontend/src/apicalls/basics.ts new file mode 100644 index 0000000..17eea35 --- /dev/null +++ b/WebServices/client-frontend/src/apicalls/basics.ts @@ -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 }; diff --git a/WebServices/client-frontend/src/apicalls/login/login.tsx b/WebServices/client-frontend/src/apicalls/login/login.tsx new file mode 100644 index 0000000..2e6018a --- /dev/null +++ b/WebServices/client-frontend/src/apicalls/login/login.tsx @@ -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 }; diff --git a/WebServices/client-frontend/src/app/(AuthLayout)/auth/login/page.tsx b/WebServices/client-frontend/src/app/(AuthLayout)/auth/login/page.tsx new file mode 100644 index 0000000..a6daaa5 --- /dev/null +++ b/WebServices/client-frontend/src/app/(AuthLayout)/auth/login/page.tsx @@ -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 ( +
+ +
+ ); +} diff --git a/WebServices/client-frontend/src/app/(AuthLayout)/auth/select/page.tsx b/WebServices/client-frontend/src/app/(AuthLayout)/auth/select/page.tsx new file mode 100644 index 0000000..abe1a7d --- /dev/null +++ b/WebServices/client-frontend/src/app/(AuthLayout)/auth/select/page.tsx @@ -0,0 +1,7 @@ +import React from "react"; + +function SelectPage() { + return
SelectPage
; +} + +export default SelectPage; diff --git a/WebServices/client-frontend/src/app/(AuthLayout)/layout.tsx b/WebServices/client-frontend/src/app/(AuthLayout)/layout.tsx new file mode 100644 index 0000000..bf3041e --- /dev/null +++ b/WebServices/client-frontend/src/app/(AuthLayout)/layout.tsx @@ -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 ( + + +
+
+

Welcome to evyos

+

+ 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. +

+
+
+ Loading...
}>{children} +
+ + + + ); +} diff --git a/WebServices/client-frontend/src/app/layout.tsx b/WebServices/client-frontend/src/app/layout.tsx index f7fa87e..756fcce 100644 --- a/WebServices/client-frontend/src/app/layout.tsx +++ b/WebServices/client-frontend/src/app/layout.tsx @@ -1,17 +1,6 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; 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 = { title: "Create Next App", description: "Generated by create next app", @@ -24,11 +13,7 @@ export default function RootLayout({ }>) { return ( - - {children} - + {children} ); } diff --git a/WebServices/client-frontend/src/app/page.tsx b/WebServices/client-frontend/src/app/page.tsx index e68abe6..01a89f7 100644 --- a/WebServices/client-frontend/src/app/page.tsx +++ b/WebServices/client-frontend/src/app/page.tsx @@ -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 (
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - src/app/page.tsx - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
- -
- - Vercel logomark - Deploy now - - - Read our docs - -
-
- + {JSON.stringify({ data: data?.data })}
); } diff --git a/WebServices/client-frontend/src/components/auth/login.tsx b/WebServices/client-frontend/src/components/auth/login.tsx new file mode 100644 index 0000000..f88b95d --- /dev/null +++ b/WebServices/client-frontend/src/components/auth/login.tsx @@ -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; + +function Login() { + // Open transition for form login + const [isPending, startTransition] = useTransition(); + const [error, setError] = useState(null); + const [jsonText, setJsonText] = useState(null); + + const Router = useRouter(); + + const { + register, + formState: { errors }, + handleSubmit, + } = useForm({ + 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 ( + <> +
+
+

+ Login +

+
+
+ + + {errors.email && ( +

+ {errors.email.message} +

+ )} +
+ +
+ + + {errors.password && ( +

+ {errors.password.message} +

+ )} +
+ + {error && ( +
+

{error}

+
+ )} + + +
+
+ + {jsonText && ( +
+

+ Response Data +

+
+ {Object.entries(JSON.parse(jsonText)).map(([key, value]) => ( +
+ {key}: + + {typeof value === "object" + ? JSON.stringify(value) + : value?.toString() || "N/A"} + +
+ ))} +
+
+ )} +
+ + ); +} + +export default Login; diff --git a/WebServices/management-frontend/Dockerfile b/WebServices/management-frontend/Dockerfile index 470d8c2..e7b8a63 100644 --- a/WebServices/management-frontend/Dockerfile +++ b/WebServices/management-frontend/Dockerfile @@ -18,4 +18,4 @@ COPY WebServices/management-frontend ./WebServices/management-frontend EXPOSE 3000 # Command to run the app -CMD ["sh", "-c", "cd /WebServices/management-frontend && npm run dev"] \ No newline at end of file +CMD ["sh", "-c", "cd /WebServices/management-frontend && npm run dev"] diff --git a/docker-compose.yml b/docker-compose.yml index a9144ff..e5c0313 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,4 @@ services: - mongo_service: container_name: mongo_service image: "bitnami/mongodb:latest" @@ -59,70 +58,66 @@ services: - wag-services ports: - "3000:3000" - volumes: - - .WebServices/client-frontend:WebServices/client-frontend - - WebServices/client-frontend/node_modules - - WebServices/client-frontend/.next + # volumes: + # - client-frontend:/WebServices/client-frontend environment: - NODE_ENV=development - management_frontend: - container_name: management_frontend - build: - context: . - dockerfile: WebServices/management-frontend/Dockerfile - networks: - - wag-services - ports: - - "3001:3000" # Using different host port to avoid conflicts - volumes: - - .WebServices/management-frontend:WebServices/management-frontend - - WebServices/management-frontend/node_modules - - WebServices/management-frontend/.next - environment: - - NODE_ENV=development + # management_frontend: + # container_name: management_frontend + # build: + # context: . + # dockerfile: WebServices/management-frontend/Dockerfile + # networks: + # - wag-services + # ports: + # - "3001:3000" # Using different host port to avoid conflicts + # # volumes: + # # - management-frontend:/WebServices/management-frontend + # environment: + # - NODE_ENV=development -# initializer_service: -# container_name: initializer_service -# build: -# context: . -# dockerfile: ApiServices/InitialService/Dockerfile -# networks: -# - wag-services -# env_file: -# - api_env.env -# depends_on: -# - postgres-service -# - mongo_service -# - redis_service + # initializer_service: + # container_name: initializer_service + # build: + # context: . + # dockerfile: ApiServices/InitialService/Dockerfile + # networks: + # - wag-services + # env_file: + # - api_env.env + # depends_on: + # - postgres-service + # - mongo_service + # - redis_service -# template_service: -# container_name: template_service -# build: -# context: . -# dockerfile: ApiServices/TemplateService/Dockerfile -# networks: -# - wag-services -# env_file: -# - api_env.env -# environment: -# - API_PATH=app:app -# - API_HOST=0.0.0.0 -# - API_PORT=8000 -# - API_LOG_LEVEL=info -# - API_RELOAD=1 -# - API_ACCESS_TOKEN_TAG=1 -# - API_APP_NAME=evyos-template-api-gateway -# - API_TITLE=WAG API Template Api Gateway -# - 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_APP_URL=https://template_service -# ports: -# - "8000:8000" -# depends_on: -# - postgres-service -# - mongo_service -# - redis_service + # template_service: + # container_name: template_service + # build: + # context: . + # dockerfile: ApiServices/TemplateService/Dockerfile + # networks: + # - wag-services + # env_file: + # - api_env.env + # environment: + # - API_PATH=app:app + # - API_HOST=0.0.0.0 + # - API_PORT=8000 + # - API_LOG_LEVEL=info + # - API_RELOAD=1 + # - API_ACCESS_TOKEN_TAG=1 + # - API_APP_NAME=evyos-template-api-gateway + # - API_TITLE=WAG API Template Api Gateway + # - 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_APP_URL=https://template_service + # ports: + # - "8000:8000" + # depends_on: + # - postgres-service + # - mongo_service + # - redis_service auth_service: container_name: auth_service @@ -161,10 +156,11 @@ services: # networks: # - wag-services - networks: wag-services: volumes: postgres-data: mongodb-data: + client-frontend: + management-frontend: diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4b8e9f9 --- /dev/null +++ b/package-lock.json @@ -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" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..28ad0a7 --- /dev/null +++ b/package.json @@ -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" + } +}