updated last web service
This commit is contained in:
parent
df3f59bd8e
commit
0cd0eb0f22
|
|
@ -0,0 +1,30 @@
|
|||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /
|
||||
|
||||
# Install system dependencies and Poetry
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends gcc && rm -rf /var/lib/apt/lists/* && pip install --no-cache-dir poetry
|
||||
|
||||
# Copy Poetry configuration
|
||||
COPY /pyproject.toml ./pyproject.toml
|
||||
|
||||
# Configure Poetry and install dependencies with optimizations
|
||||
RUN poetry config virtualenvs.create false && poetry install --no-interaction --no-ansi --no-root --only main && pip cache purge && rm -rf ~/.cache/pypoetry
|
||||
|
||||
# Copy application code
|
||||
COPY /ServicesApi/Initializer /Initializer
|
||||
COPY /ServicesApi/Controllers /Controllers
|
||||
COPY /ServicesApi/Validations /Validations
|
||||
COPY /ServicesApi/Schemas /Schemas
|
||||
COPY /ServicesApi/Extensions /Extensions
|
||||
|
||||
COPY /ServicesApi/Builds/TestApi/endpoints /endpoints
|
||||
COPY /ServicesApi/Builds/TestApi/events /events
|
||||
# COPY /api_services/api_builds/test_api/validations /api_initializer/validations
|
||||
# COPY /api_services/api_builds/test_api/index.py /api_initializer/index.py
|
||||
|
||||
# Set Python path to include app directory
|
||||
ENV PYTHONPATH=/ PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
# Run the application using the configured uvicorn server
|
||||
CMD ["poetry", "run", "python", "/Initializer/app.py"]
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
from fastapi import APIRouter
|
||||
from .tester.router import tester_endpoint_route
|
||||
|
||||
|
||||
def get_routes() -> list[APIRouter]:
|
||||
return [tester_endpoint_route]
|
||||
|
||||
|
||||
def get_safe_endpoint_urls() -> list[tuple[str, str]]:
|
||||
return [
|
||||
("/", "GET"),
|
||||
("/docs", "GET"),
|
||||
("/redoc", "GET"),
|
||||
("/openapi.json", "GET"),
|
||||
("/metrics", "GET"),
|
||||
("/tester/list", "POST"),
|
||||
]
|
||||
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import datetime
|
||||
|
||||
from typing import Any
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel
|
||||
from Validations.response import PaginateOnly, Pagination, PaginationResult, EndpointResponse
|
||||
from Validations.defaults.validations import CommonHeaders
|
||||
from Schemas import AccountRecords
|
||||
|
||||
|
||||
tester_endpoint_route = APIRouter(prefix="/tester", tags=["Tester Cluster"])
|
||||
|
||||
|
||||
class TestList(BaseModel):
|
||||
uu_id: str
|
||||
bank_date: datetime.datetime
|
||||
currency_value: float
|
||||
process_name: str
|
||||
|
||||
|
||||
tester_list = "TestList"
|
||||
@tester_endpoint_route.post(
|
||||
path="/list",
|
||||
description="List all tester endpoint",
|
||||
operation_id="4c38fab8-9b66-41cd-b87a-41175c9eea48",
|
||||
)
|
||||
def tester_list_route(
|
||||
list_options: PaginateOnly,
|
||||
headers: CommonHeaders = Depends(CommonHeaders.as_dependency),
|
||||
):
|
||||
with AccountRecords.new_session() as db_session:
|
||||
AccountRecords.set_session(db_session)
|
||||
tester_list = AccountRecords.query.filter(AccountRecords.currency_value > 0)
|
||||
pagination = Pagination(data=tester_list, base_query=AccountRecords.query.filter())
|
||||
pagination.change(**list_options.model_dump())
|
||||
pagination_result = PaginationResult(data=tester_list, pagination=pagination, response_model=TestList)
|
||||
return EndpointResponse(message="MSG0003-LIST", pagination_result=pagination_result).response
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
|
||||
__all__ = []
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
from contextlib import contextmanager
|
||||
from functools import lru_cache
|
||||
from typing import Generator
|
||||
from api_controllers.postgres.config import postgres_configs
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import declarative_base, sessionmaker, scoped_session, Session
|
||||
|
||||
from Controllers.Postgres.config import postgres_configs
|
||||
|
||||
|
||||
# Configure the database engine with proper pooling
|
||||
engine = create_engine(
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from sqlalchemy_mixins.repr import ReprMixin
|
|||
from sqlalchemy_mixins.smartquery import SmartQueryMixin
|
||||
from sqlalchemy_mixins.activerecord import ActiveRecordMixin
|
||||
|
||||
from api_controllers.postgres.engine import get_db, Base
|
||||
from Controllers.Postgres.engine import get_db, Base
|
||||
|
||||
|
||||
T = TypeVar("CrudMixin", bound="CrudMixin")
|
||||
|
|
@ -72,7 +72,7 @@ class BasicMixin(Base, ActiveRecordMixin, SerializeMixin, ReprMixin, SmartQueryM
|
|||
return True, str(arrow.get(str(val)).format("YYYY-MM-DD HH:mm:ss"))
|
||||
elif isinstance(val, bool):
|
||||
return True, bool(val)
|
||||
elif isinstance(val, (float, Decimal)):
|
||||
elif isinstance(val, float) or isinstance(val, Decimal):
|
||||
return True, round(float(val), 3)
|
||||
elif isinstance(val, int):
|
||||
return True, int(val)
|
||||
|
|
@ -81,7 +81,6 @@ class BasicMixin(Base, ActiveRecordMixin, SerializeMixin, ReprMixin, SmartQueryM
|
|||
elif val is None:
|
||||
return True, None
|
||||
return False, None
|
||||
|
||||
except Exception as e:
|
||||
err = e
|
||||
return False, None
|
||||
|
|
@ -160,7 +159,6 @@ class BasicMixin(Base, ActiveRecordMixin, SerializeMixin, ReprMixin, SmartQueryM
|
|||
return {}
|
||||
|
||||
|
||||
|
||||
class CrudMixin(BasicMixin):
|
||||
"""
|
||||
Base mixin providing CRUD operations and common fields for PostgreSQL models.
|
||||
|
|
@ -272,4 +270,3 @@ class CrudCollection(CrudMixin):
|
|||
is_email_send: Mapped[bool] = mapped_column(
|
||||
Boolean, server_default="0", comment="Email sent flag"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
from fastapi import Request, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from config import api_config
|
||||
|
||||
from endpoints.routes import get_safe_endpoint_urls
|
||||
|
||||
|
||||
async def token_middleware(request: Request, call_next):
|
||||
base_url = request.url.path
|
||||
safe_endpoints = [_[0] for _ in get_safe_endpoint_urls()]
|
||||
if base_url in safe_endpoints:
|
||||
return await call_next(request)
|
||||
|
||||
token = request.headers.get(api_config.ACCESS_TOKEN_TAG, None)
|
||||
if not token:
|
||||
return JSONResponse(content={"error": "EYS_0002"}, status_code=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import enum
|
||||
|
||||
from typing import Optional, Union, Dict, Any, List
|
||||
from pydantic import BaseModel
|
||||
|
||||
from api_controllers.redis.database import RedisActions
|
||||
from api_validations.token.validations import (
|
||||
TokenDictType,
|
||||
OccupantTokenObject,
|
||||
EmployeeTokenObject,
|
||||
UserType,
|
||||
)
|
||||
|
||||
class TokenProvider:
|
||||
|
||||
AUTH_TOKEN: str = "AUTH_TOKEN"
|
||||
|
||||
@classmethod
|
||||
def convert_redis_object_to_token(cls, redis_object: Dict[str, Any]) -> TokenDictType:
|
||||
"""
|
||||
Process Redis object and return appropriate token object.
|
||||
"""
|
||||
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_login_token_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.convert_redis_object_to_token(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.convert_redis_object_to_token(all_record))
|
||||
return list_of_token_dict
|
||||
raise ValueError("Token not found in Redis. Please check the token or user_uu_id.")
|
||||
|
||||
@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.convert_redis_object_to_token(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.convert_redis_object_to_token(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, token: TokenDictType):
|
||||
"""
|
||||
Retrieve application code from the token object or list of token objects.
|
||||
"""
|
||||
if isinstance(token, EmployeeTokenObject):
|
||||
if application_codes := token.selected_company.reachable_app_codes.get(page_url, None):
|
||||
return application_codes
|
||||
elif isinstance(token, OccupantTokenObject):
|
||||
if application_codes := token.selected_occupant.reachable_app_codes.get(page_url, None):
|
||||
return application_codes
|
||||
raise ValueError("Invalid token type or no application code found.")
|
||||
|
||||
@classmethod
|
||||
def retrieve_event_codes(cls, endpoint_code: str, token: TokenDictType) -> str:
|
||||
"""
|
||||
Retrieve event code from the token object or list of token objects.
|
||||
"""
|
||||
if isinstance(token, EmployeeTokenObject):
|
||||
if event_codes := token.selected_company.reachable_event_codes.get(endpoint_code, None):
|
||||
return event_codes
|
||||
elif isinstance(token, OccupantTokenObject):
|
||||
if event_codes := token.selected_occupant.reachable_event_codes.get(endpoint_code, None):
|
||||
return event_codes
|
||||
raise ValueError("Invalid token type or no event code found.")
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import uvicorn
|
||||
|
||||
from api_initializer.config import api_config
|
||||
from api_initializer.create_app import create_app
|
||||
from config import api_config
|
||||
from create_app import create_app
|
||||
|
||||
# from prometheus_fastapi_instrumentator import Instrumentator
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import events
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
|
@ -6,9 +8,8 @@ from config import api_config
|
|||
from open_api_creator import create_openapi_schema
|
||||
from create_route import RouteRegisterController
|
||||
|
||||
from api_middlewares.token_middleware import token_middleware
|
||||
from Extensions.Middlewares.token_middleware import token_middleware
|
||||
from endpoints.routes import get_routes
|
||||
import events
|
||||
|
||||
|
||||
cluster_is_set = False
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class RouteRegisterController:
|
|||
|
||||
@staticmethod
|
||||
def add_router_with_event_to_database(router: APIRouter):
|
||||
from schemas import EndpointRestriction
|
||||
from Schemas import EndpointRestriction
|
||||
|
||||
with EndpointRestriction.new_session() as db_session:
|
||||
EndpointRestriction.set_session(db_session)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from ApiControllers.postgres.mixin import CrudCollection
|
||||
from Controllers.Postgres.mixin import CrudCollection
|
||||
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||
from sqlalchemy import (
|
||||
String,
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ class EndpointResponse(BaseModel):
|
|||
return {
|
||||
"completed": self.completed,
|
||||
"message": self.message,
|
||||
"data": result_data,
|
||||
"pagination": pagination_dict,
|
||||
"data": result_data,
|
||||
}
|
||||
|
||||
model_config = {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ class Pagination:
|
|||
MIN_SIZE = default_paginate_config.MIN_SIZE
|
||||
MAX_SIZE = default_paginate_config.MAX_SIZE
|
||||
|
||||
def __init__(self, data: PostgresResponse):
|
||||
def __init__(self, data: Query, base_query: Query):
|
||||
self.base_query = base_query
|
||||
self.query = data
|
||||
self.size: int = self.DEFAULT_SIZE
|
||||
self.page: int = 1
|
||||
|
|
@ -60,7 +61,7 @@ class Pagination:
|
|||
"""Update page counts and validate current page."""
|
||||
if self.query:
|
||||
self.total_count = self.query.count()
|
||||
self.all_count = self.query.count()
|
||||
self.all_count = self.base_query.count()
|
||||
|
||||
self.size = (
|
||||
self.size
|
||||
|
|
@ -151,24 +152,14 @@ class PaginationResult:
|
|||
Ordered query object.
|
||||
"""
|
||||
if not len(self.order_by) == len(self.pagination.orderType):
|
||||
raise ValueError(
|
||||
"Order by fields and order types must have the same length."
|
||||
)
|
||||
raise ValueError("Order by fields and order types must have the same length.")
|
||||
order_criteria = zip(self.order_by, self.pagination.orderType)
|
||||
for field, direction in order_criteria:
|
||||
if hasattr(self._query.column_descriptions[0]["entity"], field):
|
||||
if direction.lower().startswith("d"):
|
||||
self._query = self._query.order_by(
|
||||
desc(
|
||||
getattr(self._query.column_descriptions[0]["entity"], field)
|
||||
)
|
||||
)
|
||||
self._query = self._query.order_by(desc(getattr(self._query.column_descriptions[0]["entity"], field)))
|
||||
else:
|
||||
self._query = self._query.order_by(
|
||||
asc(
|
||||
getattr(self._query.column_descriptions[0]["entity"], field)
|
||||
)
|
||||
)
|
||||
self._query = self._query.order_by(asc(getattr(self._query.column_descriptions[0]["entity"], field)))
|
||||
return self._query
|
||||
|
||||
@property
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import { fetchTest } from "@/fetchers/custom/test/fetch";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
return NextResponse.json({ status: 200, data: { message: "Test" } });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ status: 500, message: "No data is found" });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json();
|
||||
try {
|
||||
const data = await fetchTest({
|
||||
page: body.page,
|
||||
size: body.size,
|
||||
orderField: body.orderField,
|
||||
orderType: body.orderType,
|
||||
query: body.query,
|
||||
});
|
||||
return NextResponse.json({ status: 200, data });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ status: 500, message: "No data is found" });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
'use client';
|
||||
import React, { useState } from "react";
|
||||
import { apiPostFetcher, apiGetFetcher } from "@/lib/fetcher"
|
||||
import { API_BASE_URL } from "@/config/config"
|
||||
import { Button } from "@/components/mutual/ui/button"
|
||||
import { Input } from "@/components/mutual/ui/input";
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/mutual/ui/form"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import * as z from "zod"
|
||||
|
||||
// Define the form schema with Zod
|
||||
const formSchema = z.object({
|
||||
page: z.number().min(1, "Page must be at least 1"),
|
||||
size: z.number().min(1, "Size must be at least 1"),
|
||||
orderField: z.array(z.string()),
|
||||
orderType: z.array(z.string()),
|
||||
query: z.any()
|
||||
});
|
||||
|
||||
// Define the type for our form values
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
export default function TestPage() {
|
||||
const [testPostResult, setTestPostResult] = useState({});
|
||||
const [testGetResult, setTestGetResult] = useState({});
|
||||
|
||||
// Initialize the form
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
page: 1,
|
||||
size: 10,
|
||||
orderField: [],
|
||||
orderType: [],
|
||||
query: {}
|
||||
}
|
||||
});
|
||||
|
||||
const testPost = async (values: FormValues) => {
|
||||
const result = await apiPostFetcher({
|
||||
url: `${API_BASE_URL}/test`,
|
||||
body: values,
|
||||
isNoCache: true
|
||||
})
|
||||
setTestPostResult(result);
|
||||
}
|
||||
|
||||
const testGet = async () => {
|
||||
const result = await apiGetFetcher({
|
||||
url: `${API_BASE_URL}/test`,
|
||||
isNoCache: true
|
||||
})
|
||||
setTestGetResult(result);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container flex flex-col gap-4 mx-auto my-10">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(testPost)} className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="page"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Page</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" placeholder="Page" {...field} onChange={e => field.onChange(Number(e.target.value))} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="size"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Size</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" placeholder="Size" {...field} onChange={e => field.onChange(Number(e.target.value))} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="orderField"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Order Field</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Order Field"
|
||||
value={field.value.join(",")}
|
||||
onChange={e => field.onChange(e.target.value.split(","))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>Comma-separated list of fields</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="orderType"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Order Type</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Order Type"
|
||||
value={field.value.join(",")}
|
||||
onChange={e => field.onChange(e.target.value.split(","))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>Comma-separated list of types (asc/desc)</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="query"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Query</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Query"
|
||||
value={JSON.stringify(field.value)}
|
||||
onChange={e => {
|
||||
try {
|
||||
field.onChange(JSON.parse(e.target.value))
|
||||
} catch (error) {
|
||||
// Handle JSON parse error
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>JSON format</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Test Post</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
<Button onClick={testGet}>Test Get</Button>
|
||||
{/* <div>{JSON.stringify(testPostResult?.data?.data || [])}</div> */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{(testPostResult?.data?.data?.map((item: any) => (
|
||||
<div className="flex flex-col gap-2" key={item.uu_id}>
|
||||
<span key={`${item.uu_id}-uu_id`} >UUID:{item.uu_id}</span>
|
||||
<span key={`${item.uu_id}-name`}>Name:{item.process_name}</span>
|
||||
<span key={`${item.uu_id}-bank_date`}>Bank Date:{item.bank_date}</span>
|
||||
<span key={`${item.uu_id}-currency_value`}>Currency Value:{item.currency_value}</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import * as React from "react"
|
|||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
import { buttonVariants } from "@/components/mutual/ui/button"
|
||||
|
||||
function AlertDialog({
|
||||
...props
|
||||
|
|
@ -5,7 +5,7 @@ import { ChevronLeft, ChevronRight } from "lucide-react"
|
|||
import { DayPicker } from "react-day-picker"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
import { buttonVariants } from "@/components/mutual/ui/button"
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
|
|
@ -11,7 +11,7 @@ import {
|
|||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
} from "@/components/mutual/ui/dialog"
|
||||
|
||||
function Command({
|
||||
className,
|
||||
|
|
@ -14,7 +14,7 @@ import {
|
|||
} from "react-hook-form"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Label } from "@/components/mutual/ui/label"
|
||||
|
||||
const Form = FormProvider
|
||||
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
"use server";
|
||||
import { fetchData } from "@/fetchers/fecther";
|
||||
import { urlTesterList } from "@/fetchers/index";
|
||||
|
||||
export const fetchTest = async ({
|
||||
page,
|
||||
size,
|
||||
orderField,
|
||||
orderType,
|
||||
query,
|
||||
}: {
|
||||
page?: number;
|
||||
size?: number;
|
||||
orderField?: string[];
|
||||
orderType?: string[];
|
||||
query?: any;
|
||||
}) => {
|
||||
try {
|
||||
const response = await fetchData(urlTesterList, { method: "POST", cache: false, payload: { page, size, orderField, orderType, query } });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return {
|
||||
completed: false,
|
||||
data: null,
|
||||
message: "No data is found",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
@ -1,36 +1,36 @@
|
|||
import { formatServiceUrl } from "./utils";
|
||||
|
||||
const baseUrlAuth = formatServiceUrl(
|
||||
process.env.NEXT_PUBLIC_AUTH_SERVICE_URL || "auth_service:8001"
|
||||
process.env.NEXT_PUBLIC_AUTH_SERVICE_URL || "localhost:8001"
|
||||
);
|
||||
const baseUrlRestriction = formatServiceUrl(
|
||||
process.env.NEXT_PUBLIC_RESTRICTION_SERVICE_URL || "restriction_service:8002"
|
||||
process.env.NEXT_PUBLIC_RESTRICTION_SERVICE_URL || "localhost:8002"
|
||||
);
|
||||
const baseUrlApplication = formatServiceUrl(
|
||||
process.env.NEXT_PUBLIC_MANAGEMENT_SERVICE_URL || "management_service:8003"
|
||||
process.env.NEXT_PUBLIC_MANAGEMENT_SERVICE_URL || "localhost:8003"
|
||||
);
|
||||
const baseUrlAccount = formatServiceUrl(
|
||||
process.env.NEXT_PUBLIC_ACCOUNT_SERVICE_URL || "account_service:8004"
|
||||
process.env.NEXT_PUBLIC_ACCOUNT_SERVICE_URL || "localhost:8004"
|
||||
);
|
||||
const baseUrlBuilding = formatServiceUrl(
|
||||
process.env.NEXT_PUBLIC_BUILDING_SERVICE_URL || "building_service:8006"
|
||||
process.env.NEXT_PUBLIC_BUILDING_SERVICE_URL || "localhost:8006"
|
||||
);
|
||||
const baseUrlPeople = formatServiceUrl(
|
||||
process.env.NEXT_PUBLIC_VALIDATION_SERVICE_URL || "validation_service:8009"
|
||||
process.env.NEXT_PUBLIC_VALIDATION_SERVICE_URL || "localhost:8009"
|
||||
);
|
||||
const baseUrlTester = formatServiceUrl(
|
||||
process.env.NEXT_PUBLIC_TESTER_SERVICE_URL || "localhost:8005"
|
||||
);
|
||||
|
||||
const urlCheckToken = `${baseUrlAuth}/authentication/token/check`;
|
||||
const urlPageValid = `${baseUrlRestriction}/restrictions/page/valid`;
|
||||
const urlSiteUrls = `${baseUrlRestriction}/restrictions/sites/list`;
|
||||
|
||||
const urlTesterList = `${baseUrlTester}/tester/list`;
|
||||
|
||||
export {
|
||||
baseUrlAuth,
|
||||
baseUrlPeople,
|
||||
baseUrlApplication,
|
||||
baseUrlAccount,
|
||||
baseUrlBuilding,
|
||||
baseUrlRestriction,
|
||||
urlCheckToken,
|
||||
urlPageValid,
|
||||
urlSiteUrls,
|
||||
// For test use only
|
||||
urlTesterList,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { cookies } from "next/headers";
|
|||
import { fetchDataWithToken } from "@/fetchers/fecther";
|
||||
import { urlCheckToken, urlPageValid, urlSiteUrls } from "@/fetchers/index";
|
||||
import { nextCrypto } from "@/fetchers/base";
|
||||
import { AuthError } from "@/validations/mutual/context/validations";
|
||||
import { AuthError } from "@/fetchers/types/context";
|
||||
import { fetchResponseStatus } from "@/fetchers/utils";
|
||||
|
||||
async function checkAccessTokenIsValid() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,165 @@
|
|||
import { NextRequest } from "next/server";
|
||||
import {
|
||||
successResponse,
|
||||
errorResponse,
|
||||
paginationResponse,
|
||||
createResponse,
|
||||
updateResponse,
|
||||
deleteResponse,
|
||||
} from "./responseHandlers";
|
||||
import { withErrorHandling, validateRequiredFields } from "./requestHandlers";
|
||||
import {
|
||||
ApiHandler,
|
||||
PaginationParams,
|
||||
ListFunction,
|
||||
CreateFunction,
|
||||
UpdateFunction,
|
||||
DeleteFunction,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* Generic list operation handler
|
||||
* @param request NextRequest object
|
||||
* @param body Request body
|
||||
* @param listFunction The function to call to get the list data
|
||||
*/
|
||||
export async function handleListOperation(
|
||||
request: NextRequest,
|
||||
body: any,
|
||||
listFunction: ListFunction
|
||||
) {
|
||||
const page = body.page || 1;
|
||||
const size = body.size || 10;
|
||||
const orderField = body.orderField || ["uu_id"];
|
||||
const orderType = body.orderType || ["asc"];
|
||||
const query = body.query || {};
|
||||
const response = await listFunction({
|
||||
page,
|
||||
size,
|
||||
orderField,
|
||||
orderType,
|
||||
query,
|
||||
} as PaginationParams);
|
||||
return paginationResponse(response.data, response.pagination);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic create operation handler
|
||||
* @param request NextRequest object
|
||||
* @param body Request body
|
||||
* @param createFunction The function to call to create the item
|
||||
* @param requiredFields Array of required field names
|
||||
*/
|
||||
export async function handleCreateOperation(
|
||||
body: any,
|
||||
createFunction?: CreateFunction,
|
||||
requiredFields: string[] = []
|
||||
) {
|
||||
if (requiredFields.length > 0) {
|
||||
const validation = validateRequiredFields(body, requiredFields);
|
||||
if (!validation.valid) {
|
||||
return errorResponse(validation.error as string, 400);
|
||||
}
|
||||
}
|
||||
|
||||
if (createFunction) {
|
||||
const result = await createFunction(body);
|
||||
return createResponse(result);
|
||||
}
|
||||
|
||||
return createResponse({
|
||||
uuid: Math.floor(Math.random() * 1000),
|
||||
...body,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic update operation handler
|
||||
* @param request NextRequest object
|
||||
* @param body Request body
|
||||
* @param updateFunction The function to call to update the item
|
||||
*/
|
||||
export async function handleUpdateOperation(
|
||||
request: NextRequest,
|
||||
body: any,
|
||||
updateFunction?: UpdateFunction
|
||||
) {
|
||||
const uuid = request.nextUrl.searchParams.get("uuid");
|
||||
if (!uuid) {
|
||||
return errorResponse("UUID not found", 400);
|
||||
}
|
||||
if (updateFunction) {
|
||||
const result = await updateFunction(body, uuid);
|
||||
return updateResponse(result);
|
||||
}
|
||||
return updateResponse(body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic delete operation handler
|
||||
* @param request NextRequest object
|
||||
* @param deleteFunction The function to call to delete the item
|
||||
*/
|
||||
export async function handleDeleteOperation(
|
||||
request: NextRequest,
|
||||
deleteFunction?: DeleteFunction
|
||||
) {
|
||||
const uuid = request.nextUrl.searchParams.get("uuid");
|
||||
if (!uuid) {
|
||||
return errorResponse("UUID not found", 400);
|
||||
}
|
||||
|
||||
if (deleteFunction) {
|
||||
await deleteFunction(uuid);
|
||||
}
|
||||
return deleteResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a wrapped list handler with error handling
|
||||
* @param listFunction The function to call to get the list data
|
||||
*/
|
||||
export function createListHandler(listFunction: ListFunction) {
|
||||
return withErrorHandling((request: NextRequest, body: any) =>
|
||||
handleListOperation(request, body, listFunction)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a wrapped create handler with error handling
|
||||
* @param createFunction The function to call to create the item
|
||||
* @param requiredFields Array of required field names
|
||||
*/
|
||||
export function createCreateHandler(
|
||||
createFunction?: CreateFunction,
|
||||
requiredFields: string[] = []
|
||||
) {
|
||||
// This handler only takes the body parameter, not the request
|
||||
return withErrorHandling((body: any) => {
|
||||
// Ensure we're only passing the actual body data to the create function
|
||||
if (body && typeof body === 'object' && body.body) {
|
||||
return handleCreateOperation(body.body, createFunction, requiredFields);
|
||||
}
|
||||
return handleCreateOperation(body, createFunction, requiredFields);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a wrapped update handler with error handling
|
||||
* @param updateFunction The function to call to update the item
|
||||
*/
|
||||
export function createUpdateHandler(updateFunction?: UpdateFunction) {
|
||||
return withErrorHandling((request: NextRequest, body: any) =>
|
||||
handleUpdateOperation(request, body, updateFunction)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a wrapped delete handler with error handling
|
||||
* @param deleteFunction The function to call to delete the item
|
||||
*/
|
||||
export function createDeleteHandler(deleteFunction?: DeleteFunction) {
|
||||
return withErrorHandling((request: NextRequest) =>
|
||||
handleDeleteOperation(request, deleteFunction)
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
export {
|
||||
successResponse,
|
||||
errorResponse,
|
||||
paginationResponse,
|
||||
createResponse,
|
||||
updateResponse,
|
||||
deleteResponse,
|
||||
} from "./responseHandlers";
|
||||
export { withErrorHandling, validateRequiredFields } from "./requestHandlers";
|
||||
export {
|
||||
createListHandler,
|
||||
createCreateHandler,
|
||||
createUpdateHandler,
|
||||
createDeleteHandler,
|
||||
} from "./apiOperations";
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import { NextRequest } from "next/server";
|
||||
import { errorResponse } from "./responseHandlers";
|
||||
import { ValidationResult, ApiHandler, ApiHandlerBodyOnly, ApiHandlerWithRequest } from "./types";
|
||||
|
||||
/**
|
||||
* Safely parse JSON request body with error handling
|
||||
* @param request NextRequest object
|
||||
* @returns Parsed request body or null if parsing fails
|
||||
*/
|
||||
export async function parseRequestBody(request: NextRequest) {
|
||||
try {
|
||||
return await request.json();
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for API route handlers with built-in error handling
|
||||
* @param handler The handler function to wrap
|
||||
*/
|
||||
export function withErrorHandling(
|
||||
handler: ApiHandler
|
||||
) {
|
||||
return async (request: NextRequest) => {
|
||||
try {
|
||||
const body = await parseRequestBody(request);
|
||||
|
||||
if (body === null) {
|
||||
return errorResponse("Invalid request body", 400);
|
||||
}
|
||||
|
||||
// Check handler parameter count to determine if it needs request object
|
||||
// If handler has only 1 parameter, it's likely a create operation that only needs body
|
||||
if (handler.length === 1) {
|
||||
// Cast to the appropriate handler type
|
||||
return await (handler as ApiHandlerBodyOnly)(body);
|
||||
} else {
|
||||
// Otherwise pass both request and body (for list, update, delete operations)
|
||||
return await (handler as ApiHandlerWithRequest)(request, body);
|
||||
}
|
||||
} catch (error: any) {
|
||||
return errorResponse(
|
||||
error.message || "Internal Server Error",
|
||||
error.status || 500
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that required fields are present in the request body
|
||||
* @param body Request body
|
||||
* @param requiredFields Array of required field names
|
||||
* @returns Object with validation result and error message if validation fails
|
||||
*/
|
||||
export function validateRequiredFields(body: any, requiredFields: string[]): ValidationResult {
|
||||
const missingFields = requiredFields.filter(field =>
|
||||
body[field] === undefined || body[field] === null || body[field] === ''
|
||||
);
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Missing required fields: ${missingFields.join(', ')}`
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import { ApiResponse, PaginationResponse, PaginatedApiResponse } from "./types";
|
||||
|
||||
/**
|
||||
* Standard success response handler
|
||||
* @param data The data to return in the response
|
||||
* @param status HTTP status code (default: 200)
|
||||
*/
|
||||
export function successResponse<T>(data: T, status: number = 200) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data,
|
||||
} as ApiResponse<T>,
|
||||
{ status }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard error response handler
|
||||
* @param message Error message
|
||||
* @param status HTTP status code (default: 500)
|
||||
*/
|
||||
export function errorResponse(message: string, status: number = 500) {
|
||||
console.error(`API error: ${message}`);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: message
|
||||
} as ApiResponse<never>,
|
||||
{ status }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard pagination response format
|
||||
* @param data Array of items to return
|
||||
* @param pagination Pagination information
|
||||
*/
|
||||
export function paginationResponse<T>(data: T[], pagination: PaginationResponse | null) {
|
||||
return NextResponse.json({
|
||||
data: data || [],
|
||||
pagination: pagination || {
|
||||
page: 1,
|
||||
size: 10,
|
||||
totalCount: 0,
|
||||
totalItems: 0,
|
||||
totalPages: 0,
|
||||
pageCount: 0,
|
||||
orderField: ["name"],
|
||||
orderType: ["asc"],
|
||||
query: {},
|
||||
next: false,
|
||||
back: false,
|
||||
},
|
||||
} as PaginatedApiResponse<T>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create response handler
|
||||
* @param data The created item data
|
||||
*/
|
||||
export function createResponse<T>(data: T) {
|
||||
return successResponse(
|
||||
{
|
||||
...data as any,
|
||||
createdAt: new Date().toISOString(),
|
||||
} as T,
|
||||
201
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update response handler
|
||||
* @param data The updated item data
|
||||
*/
|
||||
export function updateResponse<T>(data: T) {
|
||||
return successResponse(
|
||||
{
|
||||
...data as any,
|
||||
updatedAt: new Date().toISOString(),
|
||||
} as T
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete response handler
|
||||
*/
|
||||
export function deleteResponse() {
|
||||
return successResponse({ message: "Item deleted successfully" }, 204);
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
/**
|
||||
* Type definitions for API utilities
|
||||
*/
|
||||
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
/**
|
||||
* Validation result interface
|
||||
*/
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pagination parameters interface
|
||||
*/
|
||||
export interface PaginationParams {
|
||||
page: number;
|
||||
size: number;
|
||||
orderField: string[];
|
||||
orderType: string[];
|
||||
query: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pagination response interface
|
||||
*/
|
||||
export interface PaginationResponse {
|
||||
page: number;
|
||||
size: number;
|
||||
totalCount: number;
|
||||
totalItems: number;
|
||||
totalPages: number;
|
||||
pageCount: number;
|
||||
orderField: string[];
|
||||
orderType: string[];
|
||||
query: Record<string, any>;
|
||||
next: boolean;
|
||||
back: boolean;
|
||||
}
|
||||
|
||||
export const defaultPaginationResponse: PaginationResponse = {
|
||||
page: 1,
|
||||
size: 10,
|
||||
totalCount: 0,
|
||||
totalItems: 0,
|
||||
totalPages: 0,
|
||||
pageCount: 0,
|
||||
orderField: ["uu_id"],
|
||||
orderType: ["asc"],
|
||||
query: {},
|
||||
next: false,
|
||||
back: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* API response interface
|
||||
*/
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginated API response interface
|
||||
*/
|
||||
export interface PaginatedApiResponse<T> {
|
||||
data: T[];
|
||||
pagination: PaginationResponse;
|
||||
}
|
||||
|
||||
export const collectPaginationFromApiResponse = (
|
||||
response: PaginatedApiResponse<any>
|
||||
): PaginationResponse => {
|
||||
return {
|
||||
page: response.pagination?.page || 1,
|
||||
size: response.pagination?.size || 10,
|
||||
totalCount: response.pagination?.totalCount || 0,
|
||||
totalItems: response.pagination?.totalItems || 0,
|
||||
totalPages: response.pagination?.totalPages || 0,
|
||||
pageCount: response.pagination?.pageCount || 0,
|
||||
orderField: response.pagination?.orderField || ["uu_id"],
|
||||
orderType: response.pagination?.orderType || ["asc"],
|
||||
query: response.pagination?.query || {},
|
||||
next: response.pagination?.next || false,
|
||||
back: response.pagination?.back || false,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* API handler function types
|
||||
*/
|
||||
export type ApiHandlerWithRequest = (request: NextRequest, body: any) => Promise<Response>;
|
||||
export type ApiHandlerBodyOnly = (body: any) => Promise<Response>;
|
||||
export type ApiHandler = ApiHandlerWithRequest | ApiHandlerBodyOnly;
|
||||
|
||||
/**
|
||||
* List function type
|
||||
*/
|
||||
export type ListFunction = (
|
||||
params: PaginationParams
|
||||
) => Promise<PaginatedApiResponse<any>>;
|
||||
|
||||
/**
|
||||
* Create function type
|
||||
*/
|
||||
export type CreateFunction = (data: any) => Promise<any>;
|
||||
|
||||
/**
|
||||
* Update function type
|
||||
*/
|
||||
export type UpdateFunction = (id: any, data: any) => Promise<any>;
|
||||
|
||||
/**
|
||||
* Delete function type
|
||||
*/
|
||||
export type DeleteFunction = (id: any) => Promise<any>;
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
interface FetcherRequest {
|
||||
url: string;
|
||||
isNoCache: boolean;
|
||||
}
|
||||
|
||||
interface PostFetcherRequest<T> extends FetcherRequest {
|
||||
body: Record<string, T>;
|
||||
}
|
||||
|
||||
interface GetFetcherRequest extends FetcherRequest {
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface DeleteFetcherRequest extends GetFetcherRequest {}
|
||||
interface PutFetcherRequest<T> extends PostFetcherRequest<T> {}
|
||||
interface PatchFetcherRequest<T> extends PostFetcherRequest<T> {}
|
||||
|
||||
interface FetcherRespose {
|
||||
success: boolean;
|
||||
}
|
||||
interface PaginationResponse {
|
||||
onPage: number;
|
||||
onPageCount: number;
|
||||
totalPage: number;
|
||||
totalCount: number;
|
||||
next: boolean;
|
||||
back: boolean;
|
||||
}
|
||||
|
||||
interface FetcherDataResponse<T> extends FetcherRespose {
|
||||
data: Record<string, T> | null;
|
||||
pagination?: PaginationResponse;
|
||||
}
|
||||
|
||||
export type {
|
||||
FetcherRequest,
|
||||
PostFetcherRequest,
|
||||
GetFetcherRequest,
|
||||
DeleteFetcherRequest,
|
||||
PutFetcherRequest,
|
||||
PatchFetcherRequest,
|
||||
FetcherRespose,
|
||||
FetcherDataResponse,
|
||||
};
|
||||
|
|
@ -4,7 +4,7 @@ import * as React from "react"
|
|||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
import { buttonVariants } from "@/components/mutual/ui/button"
|
||||
|
||||
function AlertDialog({
|
||||
...props
|
||||
|
|
@ -5,7 +5,7 @@ import { ChevronLeft, ChevronRight } from "lucide-react"
|
|||
import { DayPicker } from "react-day-picker"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
import { buttonVariants } from "@/components/mutual/ui/button"
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
|
|
@ -11,7 +11,7 @@ import {
|
|||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
} from "@/components/mutual/ui/dialog"
|
||||
|
||||
function Command({
|
||||
className,
|
||||
|
|
@ -14,7 +14,7 @@ import {
|
|||
} from "react-hook-form"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Label } from "@/components/mutual/ui/label"
|
||||
|
||||
const Form = FormProvider
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue