diff --git a/api_services/api_builds/auth_service/Dockerfile b/api_services/api_builds/auth_service/Dockerfile index d0b222d..8db766a 100644 --- a/api_services/api_builds/auth_service/Dockerfile +++ b/api_services/api_builds/auth_service/Dockerfile @@ -19,10 +19,10 @@ COPY /api_services/schemas /schemas COPY /api_services/api_modules /api_modules COPY /api_services/api_middlewares /api_middlewares -COPY /api_services/api_builds/auth-service/endpoints /api_initializer/endpoints -COPY /api_services/api_builds/auth-service/events /api_initializer/events -COPY /api_services/api_builds/auth-service/validations /api_initializer/validations -COPY /api_services/api_builds/auth-service/index.py /api_initializer/index.py +COPY /api_services/api_builds/auth_service/endpoints /api_initializer/endpoints +COPY /api_services/api_builds/auth_service/events /api_initializer/events +COPY /api_services/api_builds/auth_service/validations /api_initializer/validations +# COPY /api_services/api_builds/auth_service/index.py /api_initializer/index.py # Set Python path to include app directory ENV PYTHONPATH=/ PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 diff --git a/api_services/api_builds/auth_service/endpoints/auth/router.py b/api_services/api_builds/auth_service/endpoints/auth/router.py index a550443..d2c2b63 100644 --- a/api_services/api_builds/auth_service/endpoints/auth/router.py +++ b/api_services/api_builds/auth_service/endpoints/auth/router.py @@ -14,7 +14,6 @@ from validations.request.auth.validations import ( RequestForgotPasswordPhone, RequestForgotPasswordEmail, RequestVerifyOTP, - RequestApplication, ) from events.auth.events import AuthHandlers from endpoints.index import endpoints_index diff --git a/api_services/api_modules/redis/redis_handlers.py b/api_services/api_modules/redis/redis_handlers.py index defb837..be0e1d6 100644 --- a/api_services/api_modules/redis/redis_handlers.py +++ b/api_services/api_modules/redis/redis_handlers.py @@ -1,7 +1,16 @@ from typing import Any, Union -from api_validations.token.validations import TokenDictType, EmployeeTokenObject, OccupantTokenObject, CompanyToken, OccupantToken, UserType +from api_validations.token.validations import ( + TokenDictType, + EmployeeTokenObject, + OccupantTokenObject, + CompanyToken, + OccupantToken, + UserType +) from api_controllers.redis.database import RedisActions +from api_modules.token.password_module import PasswordModule + from schemas import Users @@ -24,7 +33,7 @@ class RedisHandlers: @classmethod def get_object_from_redis(cls, access_token: str) -> TokenDictType: - redis_response = RedisActions.get_json(list_keys=[RedisHandlers.AUTH_TOKEN, access_token, "*"]) + redis_response = RedisActions.get_json(list_keys=[cls.AUTH_TOKEN, access_token, "*"]) if not redis_response.status: raise ValueError("EYS_0001") if redis_object := redis_response.first: @@ -33,21 +42,21 @@ class RedisHandlers: @classmethod def set_object_to_redis(cls, user: Users, token, header_info): - result_delete = RedisActions.delete(list_keys=[RedisHandlers.AUTH_TOKEN, "*", str(user.uu_id)]) + result_delete = RedisActions.delete(list_keys=[cls.AUTH_TOKEN, "*", str(user.uu_id)]) generated_access_token = PasswordModule.generate_access_token() - keys = [RedisHandlers.AUTH_TOKEN, generated_access_token, str(user.uu_id)] + keys = [cls.AUTH_TOKEN, generated_access_token, str(user.uu_id)] RedisActions.set_json(list_keys=keys, value={**token, **header_info}, expires={"hours": 1, "minutes": 30}) return generated_access_token @classmethod def update_token_at_redis(cls, token: str, add_payload: Union[CompanyToken, OccupantToken]): - if already_token_data := RedisActions.get_json(list_keys=[RedisHandlers.AUTH_TOKEN, token, "*"]).first: + if already_token_data := RedisActions.get_json(list_keys=[cls.AUTH_TOKEN, token, "*"]).first: already_token = cls.process_redis_object(already_token_data) if already_token.is_employee and isinstance(add_payload, CompanyToken): already_token.selected_company = add_payload elif already_token.is_occupant and isinstance(add_payload, OccupantToken): already_token.selected_occupant = add_payload - list_keys = [RedisHandlers.AUTH_TOKEN, token, str(already_token.user_uu_id)] + list_keys = [cls.AUTH_TOKEN, token, str(already_token.user_uu_id)] result = RedisActions.set_json(list_keys=list_keys, value=already_token.model_dump(), expires={"hours": 1, "minutes": 30}) return result.first raise ValueError("Something went wrong") diff --git a/docker-compose.yml b/docker-compose.yml index b2c3b33..e24512a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,36 @@ services: + client_frontend: + container_name: client_frontend + build: + context: . + dockerfile: web_services/client_frontend/Dockerfile + networks: + - wag-services + ports: + - "3000:3000" + environment: + - NODE_ENV=development + - WEB_BASE_URL=http://localhost:3000 + - API_BASE_URL=http://localhost:3000/api + cpus: 1 + mem_limit: 2048m + + management_frontend: + container_name: management_frontend + build: + context: . + dockerfile: web_services/management_frontend/Dockerfile + networks: + - wag-services + ports: + - "3001:3000" + environment: + - NODE_ENV=development + - WEB_BASE_URL=http://localhost:3000 + - API_BASE_URL=http://localhost:3000/api + cpus: 1 + mem_limit: 2048m + auth_service: container_name: auth_service build: @@ -6,6 +38,8 @@ services: dockerfile: api_services/api_builds/auth_service/Dockerfile env_file: - api_env.env + networks: + - wag-services environment: - API_PATH=app:app - API_HOST=0.0.0.0 @@ -28,6 +62,8 @@ services: dockerfile: api_services/api_builds/restriction_service/Dockerfile env_file: - api_env.env + networks: + - wag-services environment: - API_PATH=app:app - API_HOST=0.0.0.0 @@ -49,6 +85,8 @@ services: dockerfile: api_services/api_builds/management_service/Dockerfile env_file: - api_env.env + networks: + - wag-services environment: - API_PATH=app:app - API_HOST=0.0.0.0 diff --git a/web_services/client-frontend/.gitignore b/web_services/client_frontend/.gitignore similarity index 100% rename from web_services/client-frontend/.gitignore rename to web_services/client_frontend/.gitignore diff --git a/web_services/client_frontend/Dockerfile b/web_services/client_frontend/Dockerfile new file mode 100644 index 0000000..4fd59bc --- /dev/null +++ b/web_services/client_frontend/Dockerfile @@ -0,0 +1,21 @@ +FROM node:23-alpine + +WORKDIR / + +# Copy package.json and package-lock.json +COPY /web_services/client_frontend/package*.json ./web_services/client_frontend/ + +# Install dependencies +RUN cd ./web_services/client_frontend && npm install --legacy-peer-deps + +# Copy the rest of the application +COPY /web_services/client_frontend ./web_services/client_frontend + +## Build the Next.js app +#RUN cd ./web_services/client_frontend && npm run dev + +# Expose the port the app runs on +EXPOSE 3000 + +# Command to run the app +CMD ["sh", "-c", "cd /web_services/client_frontend && npm run dev"] diff --git a/web_services/client-frontend/README.md b/web_services/client_frontend/README.md similarity index 100% rename from web_services/client-frontend/README.md rename to web_services/client_frontend/README.md diff --git a/web_services/client-frontend/components.json b/web_services/client_frontend/components.json similarity index 100% rename from web_services/client-frontend/components.json rename to web_services/client_frontend/components.json diff --git a/web_services/client-frontend/eslint.config.mjs b/web_services/client_frontend/eslint.config.mjs similarity index 100% rename from web_services/client-frontend/eslint.config.mjs rename to web_services/client_frontend/eslint.config.mjs diff --git a/web_services/client-frontend/next.config.ts b/web_services/client_frontend/next.config.ts similarity index 100% rename from web_services/client-frontend/next.config.ts rename to web_services/client_frontend/next.config.ts diff --git a/web_services/client-frontend/package-lock.json b/web_services/client_frontend/package-lock.json similarity index 100% rename from web_services/client-frontend/package-lock.json rename to web_services/client_frontend/package-lock.json diff --git a/web_services/client-frontend/package.json b/web_services/client_frontend/package.json similarity index 100% rename from web_services/client-frontend/package.json rename to web_services/client_frontend/package.json diff --git a/web_services/client-frontend/postcss.config.mjs b/web_services/client_frontend/postcss.config.mjs similarity index 100% rename from web_services/client-frontend/postcss.config.mjs rename to web_services/client_frontend/postcss.config.mjs diff --git a/web_services/client_frontend/src/apicalls/api-fetcher.ts b/web_services/client_frontend/src/apicalls/api-fetcher.ts new file mode 100644 index 0000000..a37c7a8 --- /dev/null +++ b/web_services/client_frontend/src/apicalls/api-fetcher.ts @@ -0,0 +1,177 @@ +"use server"; +import { retrieveAccessToken } from "@/apicalls/mutual/cookies/token"; +import { + DEFAULT_RESPONSE, + defaultHeaders, + FetchOptions, + HttpMethod, + ApiResponse, + DEFAULT_TIMEOUT, +} from "./basics"; + +/** + * Creates a promise that rejects after a specified timeout + * @param ms Timeout in milliseconds + * @param controller AbortController to abort the fetch request + * @returns A promise that rejects after the timeout + */ +const createTimeoutPromise = ( + ms: number, + controller: AbortController +): Promise => { + return new Promise((_, reject) => { + setTimeout(() => { + controller.abort(); + reject(new Error(`Request timed out after ${ms}ms`)); + }, ms); + }); +}; + +/** + * Prepares a standardized API response + * @param response The response data + * @param statusCode HTTP status code + * @returns Standardized API response + */ +const prepareResponse = ( + response: T, + statusCode: number +): ApiResponse => { + try { + return { + status: statusCode, + data: response || ({} as T), + }; + } catch (error) { + console.error("Error preparing response:", error); + return { + ...DEFAULT_RESPONSE, + error: "Response parsing error", + } as ApiResponse; + } +}; + +/** + * Core fetch function with timeout and error handling + * @param url The URL to fetch + * @param options Fetch options + * @param headers Request headers + * @param payload Request payload + * @returns API response + */ +async function coreFetch( + url: string, + options: FetchOptions = {}, + headers: Record = defaultHeaders, + payload?: any +): Promise> { + const { method = "POST", cache = false, timeout = DEFAULT_TIMEOUT } = options; + + try { + const controller = new AbortController(); + const signal = controller.signal; + + const fetchOptions: RequestInit = { + method, + headers, + cache: cache ? "force-cache" : "no-cache", + signal, + }; + + // Add body if needed + if (method !== "GET" && payload) { + fetchOptions.body = JSON.stringify( + // Handle special case for updateDataWithToken + payload.payload ? payload.payload : payload + ); + } + + // Create timeout promise + const timeoutPromise = createTimeoutPromise(timeout, controller); + + // Race between fetch and timeout + const response = (await Promise.race([ + fetch(url, fetchOptions), + timeoutPromise, + ])) as Response; + + const responseJson = await response.json(); + + if (process.env.NODE_ENV !== "production") { + console.log("Fetching:", url, fetchOptions); + console.log("Response:", responseJson); + } + + return prepareResponse(responseJson, response.status); + } catch (error) { + console.error(`Fetch error (${url}):`, error); + return { + ...DEFAULT_RESPONSE, + error: error instanceof Error ? error.message : "Network error", + } as ApiResponse; + } +} + +/** + * Fetch data without authentication + */ +async function fetchData( + endpoint: string, + payload?: any, + method: HttpMethod = "POST", + cache: boolean = false, + timeout: number = DEFAULT_TIMEOUT +): Promise> { + return coreFetch( + endpoint, + { method, cache, timeout }, + defaultHeaders, + payload + ); +} + +/** + * Fetch data with authentication token + */ +async function fetchDataWithToken( + endpoint: string, + payload?: any, + method: HttpMethod = "POST", + cache: boolean = false, + timeout: number = DEFAULT_TIMEOUT +): Promise> { + const accessToken = (await retrieveAccessToken()) || ""; + const headers = { + ...defaultHeaders, + "eys-acs-tkn": accessToken, + }; + + return coreFetch(endpoint, { method, cache, timeout }, headers, payload); +} + +/** + * Update data with authentication token and UUID + */ +async function updateDataWithToken( + endpoint: string, + uuid: string, + payload?: any, + method: HttpMethod = "POST", + cache: boolean = false, + timeout: number = DEFAULT_TIMEOUT +): Promise> { + const accessToken = (await retrieveAccessToken()) || ""; + const headers = { + ...defaultHeaders, + "eys-acs-tkn": accessToken, + }; + + return coreFetch( + `${endpoint}/${uuid}`, + { method, cache, timeout }, + headers, + payload + ); +} + +export { fetchData, fetchDataWithToken, updateDataWithToken }; diff --git a/web_services/client_frontend/src/apicalls/basics.ts b/web_services/client_frontend/src/apicalls/basics.ts new file mode 100644 index 0000000..b74f7a2 --- /dev/null +++ b/web_services/client_frontend/src/apicalls/basics.ts @@ -0,0 +1,67 @@ +const formatServiceUrl = (url: string) => { + if (!url) return ""; + return url.startsWith("http") ? url : `http://${url}`; +}; + +const baseUrlAuth = formatServiceUrl( + process.env.NEXT_PUBLIC_AUTH_SERVICE_URL || "auth_service:8001" +); +const baseUrlPeople = formatServiceUrl( + process.env.NEXT_PUBLIC_VALIDATION_SERVICE_URL || "identity_service:8002" +); +const baseUrlApplication = formatServiceUrl( + process.env.NEXT_PUBLIC_MANAGEMENT_SERVICE_URL || "management_service:8004" +); + +// Types for better type safety +type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; + +interface ApiResponse { + status: number; + data: T; + error?: string; +} + +interface FetchOptions { + method?: HttpMethod; + cache?: boolean; + timeout?: number; +} + +const tokenSecret = process.env.TOKENSECRET_90 || ""; + +const cookieObject: any = { + httpOnly: true, + path: "/", + sameSite: "none", + secure: true, + maxAge: 3600, + priority: "high", +}; + +// Constants +const DEFAULT_TIMEOUT = 10000; // 10 seconds +const defaultHeaders = { + accept: "application/json", + language: "tr", + domain: "management.com.tr", + tz: "GMT+3", + "Content-type": "application/json", +}; +const DEFAULT_RESPONSE: ApiResponse = { + error: "Hata tipi belirtilmedi", + status: 500, + data: {}, +}; + +export type { HttpMethod, ApiResponse, FetchOptions }; +export { + DEFAULT_TIMEOUT, + DEFAULT_RESPONSE, + defaultHeaders, + baseUrlAuth, + baseUrlPeople, + baseUrlApplication, + tokenSecret, + cookieObject, +}; diff --git a/web_services/client_frontend/src/apicalls/custom/login/login.tsx b/web_services/client_frontend/src/apicalls/custom/login/login.tsx new file mode 100644 index 0000000..b551237 --- /dev/null +++ b/web_services/client_frontend/src/apicalls/custom/login/login.tsx @@ -0,0 +1,182 @@ +"use server"; +import NextCrypto from "next-crypto"; +import { fetchData, fetchDataWithToken } from "@/apicalls/api-fetcher"; +import { baseUrlAuth, cookieObject, tokenSecret } from "@/apicalls/basics"; +import { cookies } from "next/headers"; + +const loginEndpoint = `${baseUrlAuth}/authentication/login`; +const loginSelectEndpoint = `${baseUrlAuth}/authentication/select`; +const logoutEndpoint = `${baseUrlAuth}/authentication/logout`; + +console.log("loginEndpoint", loginEndpoint); +console.log("loginSelectEndpoint", loginSelectEndpoint); + +interface LoginViaAccessKeys { + accessKey: string; + password: string; + rememberMe: boolean; +} + +interface LoginSelectEmployee { + company_uu_id: string; +} + +interface LoginSelectOccupant { + build_living_space_uu_id: any; +} + +async function logoutActiveSession() { + const cookieStore = await cookies(); + const response = await fetchDataWithToken(logoutEndpoint, {}, "GET", false); + cookieStore.delete("accessToken"); + cookieStore.delete("accessObject"); + cookieStore.delete("userProfile"); + cookieStore.delete("userSelection"); + return response; +} + +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: any = response?.data; + const accessToken = await nextCrypto.encrypt(loginRespone.access_token); + const accessObject = await nextCrypto.encrypt( + JSON.stringify({ + userType: loginRespone.user_type, + selectionList: 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 companyUUID = payload.company_uu_id; + const selectResponse: any = await fetchDataWithToken( + loginSelectEndpoint, + { + company_uu_id: companyUUID, + }, + "POST", + false + ); + cookieStore.delete("userSelection"); + + if (selectResponse.status === 200 || selectResponse.status === 202) { + const usersSelection = await nextCrypto.encrypt( + JSON.stringify({ + selected: companyUUID, + user_type: "employee", + }) + ); + cookieStore.set({ + name: "userSelection", + value: usersSelection, + ...cookieObject, + }); + } + return selectResponse; +} + +async function loginSelectOccupant(payload: LoginSelectOccupant) { + const livingSpaceUUID = payload.build_living_space_uu_id; + const cookieStore = await cookies(); + const nextCrypto = new NextCrypto(tokenSecret); + const selectResponse: any = await fetchDataWithToken( + loginSelectEndpoint, + { + build_living_space_uu_id: livingSpaceUUID, + }, + "POST", + false + ); + cookieStore.delete("userSelection"); + + if (selectResponse.status === 200 || selectResponse.status === 202) { + const usersSelection = await nextCrypto.encrypt( + JSON.stringify({ + selected: livingSpaceUUID, + user_type: "occupant", + }) + ); + cookieStore.set({ + name: "userSelection", + value: usersSelection, + ...cookieObject, + }); + } + return selectResponse; +} + +export { + loginViaAccessKeys, + loginSelectEmployee, + loginSelectOccupant, + logoutActiveSession, +}; diff --git a/web_services/client_frontend/src/apicalls/mutual/cookies/token.tsx b/web_services/client_frontend/src/apicalls/mutual/cookies/token.tsx new file mode 100644 index 0000000..794ab5e --- /dev/null +++ b/web_services/client_frontend/src/apicalls/mutual/cookies/token.tsx @@ -0,0 +1,148 @@ +"use server"; +import { fetchDataWithToken } from "@/apicalls/api-fetcher"; +import { baseUrlAuth, tokenSecret } from "@/apicalls/basics"; +import { cookies } from "next/headers"; + +import NextCrypto from "next-crypto"; + +const checkToken = `${baseUrlAuth}/authentication/token/check`; +const pageValid = `${baseUrlAuth}/authentication/page/valid`; +const siteUrls = `${baseUrlAuth}/authentication/sites/list`; + +const nextCrypto = new NextCrypto(tokenSecret); + +async function checkAccessTokenIsValid() { + const response = await fetchDataWithToken(checkToken, {}, "GET", false); + return response?.status === 200 || response?.status === 202 ? true : false; +} + +async function retrievePageList() { + const response: any = await fetchDataWithToken(siteUrls, {}, "GET", false); + return response?.status === 200 || response?.status === 202 + ? response.data?.sites + : null; +} + +async function retrieveApplicationbyUrl(pageUrl: string) { + const response: any = await fetchDataWithToken( + pageValid, + { + page_url: pageUrl, + }, + "POST", + false + ); + return response?.status === 200 || response?.status === 202 + ? response.data?.application + : null; +} + +async function retrieveAccessToken() { + const cookieStore = await cookies(); + const encrpytAccessToken = cookieStore.get("accessToken")?.value || ""; + return encrpytAccessToken + ? await nextCrypto.decrypt(encrpytAccessToken) + : null; +} + +async function retrieveUserType() { + const cookieStore = await cookies(); + const encrpytaccessObject = cookieStore.get("accessObject")?.value || "{}"; + const decrpytUserType = JSON.parse( + (await nextCrypto.decrypt(encrpytaccessObject)) || "{}" + ); + return decrpytUserType ? decrpytUserType : null; +} + +async function retrieveAccessObjects() { + const cookieStore = await cookies(); + const encrpytAccessObject = cookieStore.get("accessObject")?.value || ""; + const decrpytAccessObject = await nextCrypto.decrypt(encrpytAccessObject); + return decrpytAccessObject ? JSON.parse(decrpytAccessObject) : null; +} + +async function retrieveUserSelection() { + const cookieStore = await cookies(); + const encrpytUserSelection = cookieStore.get("userSelection")?.value || ""; + + let objectUserSelection = {}; + let decrpytUserSelection: any = await nextCrypto.decrypt( + encrpytUserSelection + ); + decrpytUserSelection = decrpytUserSelection + ? JSON.parse(decrpytUserSelection) + : null; + console.log("decrpytUserSelection", decrpytUserSelection); + const userSelection = decrpytUserSelection?.selected; + const accessObjects = (await retrieveAccessObjects()) || {}; + console.log("accessObjects", accessObjects); + + if (decrpytUserSelection?.user_type === "employee") { + const companyList = accessObjects?.selectionList; + const selectedCompany = companyList.find( + (company: any) => company.uu_id === userSelection + ); + if (selectedCompany) { + objectUserSelection = { userType: "employee", selected: selectedCompany }; + } + } else if (decrpytUserSelection?.user_type === "occupant") { + const buildingsList = accessObjects?.selectionList; + + // Iterate through all buildings + if (buildingsList) { + // Loop through each building + for (const buildKey in buildingsList) { + const building = buildingsList[buildKey]; + + // Check if the building has occupants + if (building.occupants && building.occupants.length > 0) { + // Find the occupant with the matching build_living_space_uu_id + const occupant = building.occupants.find( + (occ: any) => occ.build_living_space_uu_id === userSelection + ); + + if (occupant) { + objectUserSelection = { + userType: "occupant", + selected: { + ...occupant, + buildName: building.build_name, + buildNo: building.build_no, + }, + }; + break; + } + } + } + } + } + return { + ...objectUserSelection, + }; +} + +// const avatarInfo = await retrieveAvatarInfo(); +// lang: avatarInfo?.data?.lang +// ? String(avatarInfo?.data?.lang).toLowerCase() +// : undefined, +// avatar: avatarInfo?.data?.avatar, +// fullName: avatarInfo?.data?.full_name, +// async function retrieveAvatarInfo() { +// const response = await fetchDataWithToken( +// `${baseUrlAuth}/authentication/avatar`, +// {}, +// "POST" +// ); +// return response; +// } + +export { + checkAccessTokenIsValid, + retrieveAccessToken, + retrieveUserType, + retrieveAccessObjects, + retrieveUserSelection, + retrieveApplicationbyUrl, + retrievePageList, + // retrieveavailablePages, +}; diff --git a/web_services/client_frontend/src/app/(AuthLayout)/auth/[lang]/login/page.tsx b/web_services/client_frontend/src/app/(AuthLayout)/auth/[lang]/login/page.tsx new file mode 100644 index 0000000..5d6e483 --- /dev/null +++ b/web_services/client_frontend/src/app/(AuthLayout)/auth/[lang]/login/page.tsx @@ -0,0 +1,6 @@ +'use server'; +import Login from "@/webPages/auth/Login/page"; + +const LoginPage = async () => { return }; + +export default LoginPage; diff --git a/web_services/client_frontend/src/app/(AuthLayout)/auth/[lang]/select/page.tsx b/web_services/client_frontend/src/app/(AuthLayout)/auth/[lang]/select/page.tsx new file mode 100644 index 0000000..78a49bb --- /dev/null +++ b/web_services/client_frontend/src/app/(AuthLayout)/auth/[lang]/select/page.tsx @@ -0,0 +1,19 @@ +"use server"; +import React from "react"; +import Select from "@/webPages/auth/Select/page"; + +import { redirect } from "next/navigation"; +import { checkAccessTokenIsValid, retrieveUserType } from "@/apicalls/mutual/cookies/token"; + +const SelectPage = async () => { + const token_is_valid = await checkAccessTokenIsValid(); + const selection = await retrieveUserType(); + const isEmployee = selection?.userType == "employee"; + const isOccupant = selection?.userType == "occupant"; + const selectionList = selection?.selectionList; + + if (!selectionList || !token_is_valid) { redirect("/auth/en/login") } + return + ) +} + +export { Input } diff --git a/web_services/client-frontend/src/components/ui/label.tsx b/web_services/client_frontend/src/components/mutual/shadcnui/label.tsx similarity index 100% rename from web_services/client-frontend/src/components/ui/label.tsx rename to web_services/client_frontend/src/components/mutual/shadcnui/label.tsx diff --git a/web_services/client-frontend/src/components/ui/menubar.tsx b/web_services/client_frontend/src/components/mutual/shadcnui/menubar.tsx similarity index 100% rename from web_services/client-frontend/src/components/ui/menubar.tsx rename to web_services/client_frontend/src/components/mutual/shadcnui/menubar.tsx diff --git a/web_services/client-frontend/src/components/ui/navigation-menu.tsx b/web_services/client_frontend/src/components/mutual/shadcnui/navigation-menu.tsx similarity index 100% rename from web_services/client-frontend/src/components/ui/navigation-menu.tsx rename to web_services/client_frontend/src/components/mutual/shadcnui/navigation-menu.tsx diff --git a/web_services/client-frontend/src/components/ui/popover.tsx b/web_services/client_frontend/src/components/mutual/shadcnui/popover.tsx similarity index 100% rename from web_services/client-frontend/src/components/ui/popover.tsx rename to web_services/client_frontend/src/components/mutual/shadcnui/popover.tsx diff --git a/web_services/client-frontend/src/components/ui/progress.tsx b/web_services/client_frontend/src/components/mutual/shadcnui/progress.tsx similarity index 100% rename from web_services/client-frontend/src/components/ui/progress.tsx rename to web_services/client_frontend/src/components/mutual/shadcnui/progress.tsx diff --git a/web_services/client-frontend/src/components/ui/radio-group.tsx b/web_services/client_frontend/src/components/mutual/shadcnui/radio-group.tsx similarity index 100% rename from web_services/client-frontend/src/components/ui/radio-group.tsx rename to web_services/client_frontend/src/components/mutual/shadcnui/radio-group.tsx diff --git a/web_services/client-frontend/src/components/ui/scroll-area.tsx b/web_services/client_frontend/src/components/mutual/shadcnui/scroll-area.tsx similarity index 100% rename from web_services/client-frontend/src/components/ui/scroll-area.tsx rename to web_services/client_frontend/src/components/mutual/shadcnui/scroll-area.tsx diff --git a/web_services/client-frontend/src/components/ui/select.tsx b/web_services/client_frontend/src/components/mutual/shadcnui/select.tsx similarity index 100% rename from web_services/client-frontend/src/components/ui/select.tsx rename to web_services/client_frontend/src/components/mutual/shadcnui/select.tsx diff --git a/web_services/client-frontend/src/components/ui/separator.tsx b/web_services/client_frontend/src/components/mutual/shadcnui/separator.tsx similarity index 100% rename from web_services/client-frontend/src/components/ui/separator.tsx rename to web_services/client_frontend/src/components/mutual/shadcnui/separator.tsx diff --git a/web_services/client-frontend/src/components/ui/sheet.tsx b/web_services/client_frontend/src/components/mutual/shadcnui/sheet.tsx similarity index 100% rename from web_services/client-frontend/src/components/ui/sheet.tsx rename to web_services/client_frontend/src/components/mutual/shadcnui/sheet.tsx diff --git a/web_services/client-frontend/src/components/ui/skeleton.tsx b/web_services/client_frontend/src/components/mutual/shadcnui/skeleton.tsx similarity index 100% rename from web_services/client-frontend/src/components/ui/skeleton.tsx rename to web_services/client_frontend/src/components/mutual/shadcnui/skeleton.tsx diff --git a/web_services/client-frontend/src/components/ui/slider.tsx b/web_services/client_frontend/src/components/mutual/shadcnui/slider.tsx similarity index 100% rename from web_services/client-frontend/src/components/ui/slider.tsx rename to web_services/client_frontend/src/components/mutual/shadcnui/slider.tsx diff --git a/web_services/client-frontend/src/components/ui/sonner.tsx b/web_services/client_frontend/src/components/mutual/shadcnui/sonner.tsx similarity index 100% rename from web_services/client-frontend/src/components/ui/sonner.tsx rename to web_services/client_frontend/src/components/mutual/shadcnui/sonner.tsx diff --git a/web_services/client-frontend/src/components/ui/switch.tsx b/web_services/client_frontend/src/components/mutual/shadcnui/switch.tsx similarity index 100% rename from web_services/client-frontend/src/components/ui/switch.tsx rename to web_services/client_frontend/src/components/mutual/shadcnui/switch.tsx diff --git a/web_services/client-frontend/src/components/ui/table.tsx b/web_services/client_frontend/src/components/mutual/shadcnui/table.tsx similarity index 100% rename from web_services/client-frontend/src/components/ui/table.tsx rename to web_services/client_frontend/src/components/mutual/shadcnui/table.tsx diff --git a/web_services/client-frontend/src/components/ui/tabs.tsx b/web_services/client_frontend/src/components/mutual/shadcnui/tabs.tsx similarity index 100% rename from web_services/client-frontend/src/components/ui/tabs.tsx rename to web_services/client_frontend/src/components/mutual/shadcnui/tabs.tsx diff --git a/web_services/client-frontend/src/components/ui/textarea.tsx b/web_services/client_frontend/src/components/mutual/shadcnui/textarea.tsx similarity index 100% rename from web_services/client-frontend/src/components/ui/textarea.tsx rename to web_services/client_frontend/src/components/mutual/shadcnui/textarea.tsx diff --git a/web_services/client-frontend/src/components/ui/toggle.tsx b/web_services/client_frontend/src/components/mutual/shadcnui/toggle.tsx similarity index 100% rename from web_services/client-frontend/src/components/ui/toggle.tsx rename to web_services/client_frontend/src/components/mutual/shadcnui/toggle.tsx diff --git a/web_services/client-frontend/src/components/ui/tooltip.tsx b/web_services/client_frontend/src/components/mutual/shadcnui/tooltip.tsx similarity index 100% rename from web_services/client-frontend/src/components/ui/tooltip.tsx rename to web_services/client_frontend/src/components/mutual/shadcnui/tooltip.tsx diff --git a/web_services/client_frontend/src/components/mutual/tableView/FullCardTableComp/component.tsx b/web_services/client_frontend/src/components/mutual/tableView/FullCardTableComp/component.tsx new file mode 100644 index 0000000..8ba4999 --- /dev/null +++ b/web_services/client_frontend/src/components/mutual/tableView/FullCardTableComp/component.tsx @@ -0,0 +1,88 @@ +'use client'; +import React, { useState, useEffect } from "react"; +import { apiPostFetcher } from "@/lib/fetcher"; +import { ApiPaginationRequestWithQuery } from "@/validations/mutual/api/requests/validations"; +import { TableComponentProps } from "@/validations/mutual/table/type"; +import ComponentTable from "../mutual/TableCardPlain"; +import PaginationComponent from "../mutual/UpperPagination"; +import LowerPagination from "../mutual/LowerPagination"; + +const TableCardComponent: React.FC = ({ + urls, + schemas, + translations, + columns, + initPagination, +}) => { + const defaultPagination = { + page: initPagination?.page || 1, + size: initPagination?.size || 10, + orderFields: initPagination?.orderFields || [], + orderTypes: initPagination?.orderTypes || [], + query: initPagination?.query || {}, + } + const [data, setData] = useState(null); + const [pagination, setPagination] = useState(defaultPagination); + const [apiPagination, setApiPagination] = useState({ + onPage: 1, + onPageCount: 10, + totalPage: 1, + totalCount: 1, + next: false, + back: false, + }); + const handleBack = async () => { + setPagination({ ...pagination, page: pagination.page > 1 ? pagination.page - 1 : pagination.page }); + await fetchData(); + } + const handleNext = async () => { + setPagination({ ...pagination, page: pagination.page < apiPagination.totalPage ? pagination.page + 1 : pagination.page }); + await fetchData(); + } + const fetchData = async () => { + const response = await apiPostFetcher({ + url: urls.list, + isNoCache: true, + body: { + page: pagination.page, + size: pagination.size, + orderFields: pagination.orderFields, + orderTypes: pagination.orderTypes, + query: pagination.query, + }, + }); + if (response && response.data) { + setData(response.data.data); + if (response.data.pagination) { + setApiPagination(response.data.pagination); + } + } + }; + useEffect(() => { fetchData() }, [pagination.page, pagination.size, pagination.orderFields, pagination.orderTypes]); + const upperPaginationProps = { + apiPagination, + pagination, + setPagination, + defaultPagination, + } + const lowerPaginationProps = { + pagination: apiPagination, + handleBack, + handleNext, + } + const tableProps = { + data, + columns, + translations, + } + + return ( +
+
+
+

Post Data Page

+
+ ); +} + +export default TableCardComponent; \ No newline at end of file diff --git a/web_services/client_frontend/src/components/mutual/tableView/FullTableComp/component.tsx b/web_services/client_frontend/src/components/mutual/tableView/FullTableComp/component.tsx new file mode 100644 index 0000000..ef3b873 --- /dev/null +++ b/web_services/client_frontend/src/components/mutual/tableView/FullTableComp/component.tsx @@ -0,0 +1,104 @@ +'use client'; +import React, { useState, useEffect } from "react"; +import ComponentTable from "@/components/mutual/tableView/mutual/TablePlain"; +import PaginationComponent from "@/components/mutual/tableView/mutual/UpperPagination"; +import LowerPagination from "@/components/mutual/tableView/mutual/LowerPagination"; +import { apiPostFetcher } from "@/lib/fetcher"; +import { ApiPaginationRequestWithQuery } from "@/validations/mutual/api/requests/validations"; +import { TableComponentProps } from "@/validations/mutual/table/type"; + +const TableComponent: React.FC = ({ + urls, + schemas, + translations, + columns, + initPagination, + redirectUrls, + setSelectedRow, +}) => { + const defaultPagination = { + page: initPagination?.page || 1, + size: initPagination?.size || 10, + orderFields: initPagination?.orderFields || [], + orderTypes: initPagination?.orderTypes || [], + query: initPagination?.query || {}, + } + const [tableData, setTableData] = useState(null); + const [orgTableData, setOrgTableData] = useState(null); + const [tableColumns, setTableColumns] = useState([]); + const [pagination, setPagination] = useState(defaultPagination); + const [apiPagination, setApiPagination] = useState({ + onPage: 1, + onPageCount: 10, + totalPage: 1, + totalCount: 1, + next: false, + back: false, + }); + + const handleBack = async () => { + setPagination({ ...pagination, page: pagination.page > 1 ? pagination.page - 1 : pagination.page }); + await fetchData(); + } + const handleNext = async () => { + setPagination({ ...pagination, page: pagination.page < apiPagination.totalPage ? pagination.page + 1 : pagination.page }); + await fetchData(); + } + const fetchData = async () => { + const response = await apiPostFetcher({ + url: urls.list, + isNoCache: true, + body: { + page: pagination.page, + size: pagination.size, + orderFields: pagination.orderFields, + orderTypes: pagination.orderTypes, + query: pagination.query, + }, + }); + if (response && response.data) { + const oldData = response.data.data + setOrgTableData(oldData) + if (schemas.table) { + const newData = Object.entries(oldData).map(([key]) => { + return schemas.table.safeParse(oldData[key as keyof typeof oldData]).data + }) + setTableData(newData) + setTableColumns(columns.table) + } + if (response.data.pagination) { + setApiPagination(response.data.pagination); + } + } + }; + useEffect(() => { fetchData() }, [pagination.page, pagination.size, pagination.orderFields, pagination.orderTypes]); + + const upperPaginationProps = { + apiPagination, + pagination, + setPagination, + defaultPagination, + } + const lowerPaginationProps = { + pagination: apiPagination, + handleBack, + handleNext, + } + const tableProps = { + data: tableData, + orgData: orgTableData, + columns: tableColumns, + translations, + redirectUrls, + setSelectedRow, + } + return ( +
+
+
+
+
+ ); +} + +export default TableComponent; \ No newline at end of file diff --git a/web_services/client_frontend/src/components/mutual/tableView/mutual/CreateForm.tsx b/web_services/client_frontend/src/components/mutual/tableView/mutual/CreateForm.tsx new file mode 100644 index 0000000..7e33fd1 --- /dev/null +++ b/web_services/client_frontend/src/components/mutual/tableView/mutual/CreateForm.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import { CreateFormProps } from "@/validations/mutual/forms/type"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Form } from "@/components/mutual/shadcnui/form"; +import { renderInputsBySchema } from "@/lib/renderInputs"; +import { Button } from "@/components/mutual/shadcnui/button"; + +const CreateForm: React.FC = ({ schemas, labels, selectedRow }) => { + const createSchema = schemas.create + const findLabels = Object.entries(createSchema?.shape || {}).reduce((acc: any, [key, value]: any) => { + acc[key] = { + label: labels[key] || key, + description: value.description, + } + return acc + }, {}) + const handleSubmit = (data: any) => { + console.log(data) + } + const form = useForm({ + resolver: zodResolver(createSchema), + defaultValues: {}, + }); + + return ( +
+
Create Form
+
+ + {renderInputsBySchema(findLabels, form)} + +
+ +
selectedRow: {JSON.stringify(selectedRow)}
+
+ ) +} + +export default CreateForm diff --git a/web_services/client_frontend/src/components/mutual/tableView/mutual/LowerPagination.tsx b/web_services/client_frontend/src/components/mutual/tableView/mutual/LowerPagination.tsx new file mode 100644 index 0000000..d86e9a0 --- /dev/null +++ b/web_services/client_frontend/src/components/mutual/tableView/mutual/LowerPagination.tsx @@ -0,0 +1,24 @@ +'use client' +import { Button } from "@/components/mutual/shadcnui/button"; +import { LowerPaginationProps } from "@/validations/mutual/pagination/type"; + +const LowerPagination: React.FC = ({ + pagination, + handleBack, + handleNext, +}) => { + const paginationBackComponent = pagination.back ? ( + + ) : <>; + const paginationNextComponent = pagination.next ? ( + + ) : <>; + return ( +
+ {paginationBackComponent} + {paginationNextComponent} +
+ ) +} + +export default LowerPagination diff --git a/web_services/client_frontend/src/components/mutual/tableView/mutual/PaginationDetails.tsx b/web_services/client_frontend/src/components/mutual/tableView/mutual/PaginationDetails.tsx new file mode 100644 index 0000000..f7c0e3e --- /dev/null +++ b/web_services/client_frontend/src/components/mutual/tableView/mutual/PaginationDetails.tsx @@ -0,0 +1,34 @@ +import { Input } from "@/components/mutual/shadcnui/input"; +import { Label } from "@/components/mutual/shadcnui/label"; +import { PaginationDetailsProps } from "@/validations/mutual/pagination/type"; + +const PaginationDetails: React.FC = ({ + pagination, + setPagination, + defaultPagination, +}) => { + return ( +
+ + setPagination({ ...defaultPagination, size: Number(e.target.value) })} + /> + + setPagination({ ...defaultPagination, orderFields: e.target.value.split(",") })} + /> + + setPagination({ ...pagination, orderType: e.target.value.split(",") })} + /> +
+ ) +} + +export default PaginationDetails diff --git a/web_services/client_frontend/src/components/mutual/tableView/mutual/PaginationShow.tsx b/web_services/client_frontend/src/components/mutual/tableView/mutual/PaginationShow.tsx new file mode 100644 index 0000000..a480505 --- /dev/null +++ b/web_services/client_frontend/src/components/mutual/tableView/mutual/PaginationShow.tsx @@ -0,0 +1,15 @@ +import { PaginationShowProps } from "@/validations/mutual/pagination/type" + +const PaginationShow: React.FC = ({ apiPagination }) => { + return ( +
+

Page: {apiPagination.onPage}

+

Page Count: {apiPagination.onPageCount}

+

Total Page: {apiPagination.totalPage}

+

Total Count: {apiPagination.totalCount}

+

Next: {apiPagination.next ? "true" : "false"}

+

Back: {apiPagination.back ? "true" : "false"}

+
+ ) +} +export default PaginationShow \ No newline at end of file diff --git a/web_services/client_frontend/src/components/mutual/tableView/mutual/Search.tsx b/web_services/client_frontend/src/components/mutual/tableView/mutual/Search.tsx new file mode 100644 index 0000000..5c1c9c7 --- /dev/null +++ b/web_services/client_frontend/src/components/mutual/tableView/mutual/Search.tsx @@ -0,0 +1,24 @@ +import { Label } from "@/components/mutual/shadcnui/label"; +import { Textarea } from "@/components/mutual/shadcnui/textarea"; +import { SearchProps } from "@/validations/mutual/table/type"; + +const SearchBarComponent: React.FC = ({ pagination, setPagination }) => { + const handleSearch = (query: string) => { + setTimeout(() => { + setPagination({ ...pagination, query: {} }) + }, 2000) + } + return ( +
+
+ +