Compare commits
13 Commits
e49c0bbe45
...
d451331c66
Author | SHA1 | Date | |
---|---|---|---|
d451331c66 | |||
485cfc2d12 | |||
944c650ab3 | |||
dfc5587608 | |||
96bdbfda95 | |||
05a234b7a5 | |||
2f58c01c24 | |||
e92dde20ca | |||
63437d6dc7 | |||
cef9dae4d3 | |||
0d7b1355d5 | |||
a213ea85d0 | |||
5c43f6d72a |
@ -11,6 +11,7 @@ type ApiServiceDTO struct {
|
|||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
ClientID string `json:"client_id"`
|
ClientID string `json:"client_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
RedirectUris []string `json:"redirect_uris"`
|
RedirectUris []string `json:"redirect_uris"`
|
||||||
Scopes []string `json:"scopes"`
|
Scopes []string `json:"scopes"`
|
||||||
GrantTypes []string `json:"grant_types"`
|
GrantTypes []string `json:"grant_types"`
|
||||||
@ -24,11 +25,12 @@ func NewApiServiceDTO(service repository.ApiService) ApiServiceDTO {
|
|||||||
ID: service.ID,
|
ID: service.ID,
|
||||||
ClientID: service.ClientID,
|
ClientID: service.ClientID,
|
||||||
Name: service.Name,
|
Name: service.Name,
|
||||||
|
Description: service.Description.String,
|
||||||
RedirectUris: service.RedirectUris,
|
RedirectUris: service.RedirectUris,
|
||||||
Scopes: service.Scopes,
|
Scopes: service.Scopes,
|
||||||
GrantTypes: service.GrantTypes,
|
GrantTypes: service.GrantTypes,
|
||||||
CreatedAt: service.CreatedAt,
|
CreatedAt: service.CreatedAt,
|
||||||
UpdatedAt: service.UpdatedAt,
|
UpdatedAt: service.UpdatedAt,
|
||||||
IsActive: false,
|
IsActive: service.IsActive,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,8 @@ import (
|
|||||||
"gitea.local/admin/hspguard/internal/util"
|
"gitea.local/admin/hspguard/internal/util"
|
||||||
"gitea.local/admin/hspguard/internal/web"
|
"gitea.local/admin/hspguard/internal/web"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AdminHandler struct {
|
type AdminHandler struct {
|
||||||
@ -32,6 +34,7 @@ func (h *AdminHandler) RegisterRoutes(router chi.Router) {
|
|||||||
r.Use(authMiddleware.Runner, adminMiddleware.Runner)
|
r.Use(authMiddleware.Runner, adminMiddleware.Runner)
|
||||||
|
|
||||||
r.Get("/api-services", h.GetApiServices)
|
r.Get("/api-services", h.GetApiServices)
|
||||||
|
r.Get("/api-services/{id}", h.GetApiService)
|
||||||
r.Post("/api-services", h.AddApiService)
|
r.Post("/api-services", h.AddApiService)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -107,14 +110,24 @@ func (h *AdminHandler) AddApiService(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
service, err := h.repo.CreateApiService(r.Context(), repository.CreateApiServiceParams{
|
params := repository.CreateApiServiceParams{
|
||||||
ClientID: clientId,
|
ClientID: clientId,
|
||||||
ClientSecret: hashSecret,
|
ClientSecret: hashSecret,
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
RedirectUris: req.RedirectUris,
|
RedirectUris: req.RedirectUris,
|
||||||
Scopes: req.Scopes,
|
Scopes: req.Scopes,
|
||||||
GrantTypes: req.GrantTypes,
|
GrantTypes: req.GrantTypes,
|
||||||
})
|
IsActive: req.IsActive,
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Description != "" {
|
||||||
|
params.Description = pgtype.Text{
|
||||||
|
String: req.Description,
|
||||||
|
Valid: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service, err := h.repo.CreateApiService(r.Context(), params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
web.Error(w, "failed to create new api service", http.StatusInternalServerError)
|
web.Error(w, "failed to create new api service", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@ -124,7 +137,44 @@ func (h *AdminHandler) AddApiService(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
service.ClientSecret = clientSecret
|
service.ClientSecret = clientSecret
|
||||||
|
|
||||||
|
type ApiServiceCredentials struct {
|
||||||
|
ClientId string `json:"client_id"`
|
||||||
|
ClientSecret string `json:"client_secret"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Service ApiServiceDTO `json:"service"`
|
||||||
|
Credentials ApiServiceCredentials `json:"credentials"`
|
||||||
|
}
|
||||||
|
|
||||||
encoder := json.NewEncoder(w)
|
encoder := json.NewEncoder(w)
|
||||||
|
if err := encoder.Encode(Response{
|
||||||
|
Service: NewApiServiceDTO(service),
|
||||||
|
Credentials: ApiServiceCredentials{
|
||||||
|
ClientId: service.ClientID,
|
||||||
|
ClientSecret: service.ClientSecret,
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
web.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminHandler) GetApiService(w http.ResponseWriter, r *http.Request) {
|
||||||
|
serviceId := chi.URLParam(r, "id")
|
||||||
|
parsed, err := uuid.Parse(serviceId)
|
||||||
|
if err != nil {
|
||||||
|
web.Error(w, "service id provided is not a valid uuid", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
service, err := h.repo.GetApiServiceId(r.Context(), parsed)
|
||||||
|
if err != nil {
|
||||||
|
web.Error(w, "service with provided id not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(w)
|
||||||
|
|
||||||
if err := encoder.Encode(NewApiServiceDTO(service)); err != nil {
|
if err := encoder.Encode(NewApiServiceDTO(service)); err != nil {
|
||||||
web.Error(w, "failed to encode response", http.StatusInternalServerError)
|
web.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ package repository
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -96,6 +97,31 @@ func (q *Queries) GetApiServiceCID(ctx context.Context, clientID string) (ApiSer
|
|||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getApiServiceId = `-- name: GetApiServiceId :one
|
||||||
|
SELECT id, client_id, client_secret, name, redirect_uris, scopes, grant_types, created_at, updated_at, is_active, description FROM api_services
|
||||||
|
WHERE id = $1
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetApiServiceId(ctx context.Context, id uuid.UUID) (ApiService, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getApiServiceId, id)
|
||||||
|
var i ApiService
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ClientID,
|
||||||
|
&i.ClientSecret,
|
||||||
|
&i.Name,
|
||||||
|
&i.RedirectUris,
|
||||||
|
&i.Scopes,
|
||||||
|
&i.GrantTypes,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.IsActive,
|
||||||
|
&i.Description,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const listApiServices = `-- name: ListApiServices :many
|
const listApiServices = `-- name: ListApiServices :many
|
||||||
SELECT id, client_id, client_secret, name, redirect_uris, scopes, grant_types, created_at, updated_at, is_active, description FROM api_services
|
SELECT id, client_id, client_secret, name, redirect_uris, scopes, grant_types, created_at, updated_at, is_active, description FROM api_services
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
|
@ -11,6 +11,11 @@ WHERE client_id = $1
|
|||||||
AND is_active = true
|
AND is_active = true
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- name: GetApiServiceId :one
|
||||||
|
SELECT * FROM api_services
|
||||||
|
WHERE id = $1
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
-- name: ListApiServices :many
|
-- name: ListApiServices :many
|
||||||
SELECT * FROM api_services
|
SELECT * FROM api_services
|
||||||
ORDER BY created_at DESC;
|
ORDER BY created_at DESC;
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<div id="portal-root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -12,6 +12,7 @@ import PersonalInfoPage from "./pages/PersonalInfo";
|
|||||||
import ApiServicesPage from "./pages/ApiServices";
|
import ApiServicesPage from "./pages/ApiServices";
|
||||||
import AdminLayout from "./layout/AdminLayout";
|
import AdminLayout from "./layout/AdminLayout";
|
||||||
import ApiServiceCreatePage from "./pages/ApiServices/Create";
|
import ApiServiceCreatePage from "./pages/ApiServices/Create";
|
||||||
|
import ViewApiServicePage from "./pages/ApiServices/View";
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@ -39,6 +40,10 @@ const router = createBrowserRouter([
|
|||||||
children: [
|
children: [
|
||||||
{ index: true, element: <ApiServicesPage /> },
|
{ index: true, element: <ApiServicesPage /> },
|
||||||
{ path: "create", element: <ApiServiceCreatePage /> },
|
{ path: "create", element: <ApiServiceCreatePage /> },
|
||||||
|
{
|
||||||
|
path: "view/:serviceId",
|
||||||
|
element: <ViewApiServicePage />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { ApiService } from "@/types";
|
import type { ApiService, ApiServiceCredentials } from "@/types";
|
||||||
import { axios, handleApiError } from "..";
|
import { axios, handleApiError } from "..";
|
||||||
|
|
||||||
export interface FetchApiServicesResponse {
|
export interface FetchApiServicesResponse {
|
||||||
@ -16,3 +16,42 @@ export const getApiServices = async (): Promise<FetchApiServicesResponse> => {
|
|||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface CreateApiServiceRequest {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
redirect_uris: string[];
|
||||||
|
scopes: string[];
|
||||||
|
grant_types: string[];
|
||||||
|
is_active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateApiServiceResponse {
|
||||||
|
service: ApiService;
|
||||||
|
credentials: ApiServiceCredentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const postApiService = async (
|
||||||
|
req: CreateApiServiceRequest,
|
||||||
|
): Promise<CreateApiServiceResponse> => {
|
||||||
|
const response = await axios.post<CreateApiServiceResponse>(
|
||||||
|
"/api/v1/admin/api-services",
|
||||||
|
req,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status !== 200 && response.status !== 201)
|
||||||
|
throw await handleApiError(response);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getApiService = async (id: string): Promise<ApiService> => {
|
||||||
|
const response = await axios.get<ApiService>(
|
||||||
|
`/api/v1/admin/api-services/${id}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status !== 200 && response.status !== 201)
|
||||||
|
throw await handleApiError(response);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
@ -14,7 +14,7 @@ interface IBreadcrumbsProps {
|
|||||||
const Breadcrumbs: FC<IBreadcrumbsProps> = ({ items, className }) => {
|
const Breadcrumbs: FC<IBreadcrumbsProps> = ({ items, className }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${className ? `${className} ` : ""} flex flex-row p-3 gap-3 items-center text-gray-800 dark:text-gray-200`}
|
className={`${className ? `${className} ` : ""} flex flex-row p-1 sm:p-3 gap-3 items-center text-gray-800 dark:text-gray-200 text-sm sm:text-base`}
|
||||||
>
|
>
|
||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<>
|
<>
|
||||||
|
65
web/src/feature/ApiServiceCredentialsModal/index.tsx
Normal file
65
web/src/feature/ApiServiceCredentialsModal/index.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import { useAdmin } from "@/store/admin";
|
||||||
|
import type { ApiServiceCredentials } from "@/types";
|
||||||
|
|
||||||
|
const download = (credentials: ApiServiceCredentials) => {
|
||||||
|
const jsonStr = JSON.stringify(credentials, null, 2);
|
||||||
|
const blob = new Blob([jsonStr], { type: "application/json" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = `credentials.json`;
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ApiServiceCredentialsModal = () => {
|
||||||
|
const credentials = useAdmin((state) => state.createdCredentials);
|
||||||
|
const resetCredentials = useAdmin((state) => state.resetCredentials);
|
||||||
|
|
||||||
|
const portalRoot = document.getElementById("portal-root");
|
||||||
|
if (!portalRoot || !credentials) return null;
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div className="fixed z-50 inset-0 flex items-center justify-center bg-black/30 dark:bg-white/30 px-5">
|
||||||
|
<div className="rounded-2xl flex flex-col items-stretch bg-white dark:bg-black min-w-[300px] max-w-md w-full">
|
||||||
|
<div className="flex flex-row items-center justify-between p-4 border-b dark:border-gray-800 border-gray-300">
|
||||||
|
<p className="text-gray-800 dark:text-gray-200">New Credentials</p>
|
||||||
|
<Button variant="icon" onClick={resetCredentials}>
|
||||||
|
<X />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex flex-col gap-2 mb-4">
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 text-sm">
|
||||||
|
Client ID
|
||||||
|
</p>
|
||||||
|
<Input value={credentials.client_id} disabled />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2 mb-4">
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 text-sm">
|
||||||
|
Client Secret
|
||||||
|
</p>
|
||||||
|
<Input value={credentials.client_secret} disabled />
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex flex-row items-center justify-between">
|
||||||
|
<Button variant="contained" onClick={resetCredentials}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
<Button variant="outlined" onClick={() => download(credentials)}>
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
portalRoot,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ApiServiceCredentialsModal;
|
@ -1,7 +1,9 @@
|
|||||||
import Breadcrumbs from "@/components/ui/breadcrumbs";
|
import Breadcrumbs from "@/components/ui/breadcrumbs";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import type { FC } from "react";
|
import ApiServiceCredentialsModal from "@/feature/ApiServiceCredentialsModal";
|
||||||
|
import { useAdmin } from "@/store/admin";
|
||||||
|
import { useCallback, type FC } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
|
|
||||||
@ -26,13 +28,31 @@ const ApiServiceCreatePage: FC = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = (data: FormData) => {
|
const createApiService = useAdmin((state) => state.createApiService);
|
||||||
console.log("Form submitted:", data);
|
|
||||||
// handle create logic here (e.g. API call)
|
const credentials = useAdmin((state) => state.createdCredentials);
|
||||||
};
|
|
||||||
|
const onSubmit = useCallback(
|
||||||
|
(data: FormData) => {
|
||||||
|
console.log("Form submitted:", data);
|
||||||
|
createApiService({
|
||||||
|
name: data.name,
|
||||||
|
description: data.description ?? "",
|
||||||
|
redirect_uris: data.redirectUris.trim().split("\n"),
|
||||||
|
scopes: data.scopes.trim().split(" "),
|
||||||
|
grant_types: data.grantTypes
|
||||||
|
? data.grantTypes.trim().split(" ")
|
||||||
|
: ["authorization_code"],
|
||||||
|
is_active: data.enabled,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[createApiService],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
|
{credentials !== null && <ApiServiceCredentialsModal />}
|
||||||
|
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
items={[
|
items={[
|
||||||
{ href: "/admin", label: "Admin" },
|
{ href: "/admin", label: "Admin" },
|
||||||
|
198
web/src/pages/ApiServices/View/index.tsx
Normal file
198
web/src/pages/ApiServices/View/index.tsx
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
import Breadcrumbs from "@/components/ui/breadcrumbs";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { useAdmin } from "@/store/admin";
|
||||||
|
import { useEffect, type FC } from "react";
|
||||||
|
import { Link, useParams } from "react-router";
|
||||||
|
|
||||||
|
const InfoCard = ({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => (
|
||||||
|
<div className="border dark:border-gray-800 border-gray-300 rounded mb-4">
|
||||||
|
<div className="p-4 border-b dark:border-gray-800 border-gray-300">
|
||||||
|
<h2 className="text-gray-800 dark:text-gray-200 font-semibold text-lg">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ViewApiServicePage: FC = () => {
|
||||||
|
const { serviceId } = useParams();
|
||||||
|
const apiService = useAdmin((state) => state.viewApiService);
|
||||||
|
// const loading = useAdmin((state) => state.fetchingApiService);
|
||||||
|
|
||||||
|
const loadService = useAdmin((state) => state.fetchApiService);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof serviceId === "string") loadService(serviceId);
|
||||||
|
}, [loadService, serviceId]);
|
||||||
|
|
||||||
|
if (!apiService) {
|
||||||
|
return (
|
||||||
|
<div className="p-4 flex items-center justify-center h-[60vh]">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-2 border-blue-500 border-t-transparent mx-auto mb-3" />
|
||||||
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
|
Loading API Service...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dark:text-gray-200 text-gray-800 p-4">
|
||||||
|
<Breadcrumbs
|
||||||
|
items={[
|
||||||
|
{ href: "/admin", label: "Admin" },
|
||||||
|
{ href: "/admin/api-services", label: "API Services" },
|
||||||
|
{ label: "View API Service" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="p-4">
|
||||||
|
{/* 📋 Main Details */}
|
||||||
|
<InfoCard title="API Service Details">
|
||||||
|
<div className="flex flex-col gap-4 text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-900 dark:text-white">
|
||||||
|
Name:
|
||||||
|
</span>{" "}
|
||||||
|
{apiService.name}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-900 dark:text-white">
|
||||||
|
Description:
|
||||||
|
</span>{" "}
|
||||||
|
{apiService.description}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-900 dark:text-white">
|
||||||
|
Redirect URIs:
|
||||||
|
</span>
|
||||||
|
<ul className="list-disc list-inside ml-4 mt-1">
|
||||||
|
{apiService.redirect_uris.map((uri) => (
|
||||||
|
<li key={uri}>{uri}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-900 dark:text-white">
|
||||||
|
Scopes:
|
||||||
|
</span>
|
||||||
|
<div className="flex flex-wrap gap-2 mt-1">
|
||||||
|
{apiService.scopes.map((scope) => (
|
||||||
|
<span
|
||||||
|
key={scope}
|
||||||
|
className="bg-blue-100 dark:bg-blue-800/30 text-blue-700 dark:text-blue-300 text-xs font-medium px-2 py-1 rounded"
|
||||||
|
>
|
||||||
|
{scope}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-900 dark:text-white">
|
||||||
|
Grant Types:
|
||||||
|
</span>
|
||||||
|
<div className="flex flex-wrap gap-2 mt-1">
|
||||||
|
{apiService.grant_types.map((grant) => (
|
||||||
|
<span
|
||||||
|
key={grant}
|
||||||
|
className="bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-300 text-xs font-medium px-2 py-1 rounded"
|
||||||
|
>
|
||||||
|
{grant}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-900 dark:text-white">
|
||||||
|
Created At:
|
||||||
|
</span>{" "}
|
||||||
|
{new Date(apiService.created_at).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-900 dark:text-white">
|
||||||
|
Updated At:
|
||||||
|
</span>{" "}
|
||||||
|
{new Date(apiService.updated_at).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-900 dark:text-white">
|
||||||
|
Status:
|
||||||
|
</span>{" "}
|
||||||
|
<span
|
||||||
|
className={`font-semibold px-2 py-1 rounded ${
|
||||||
|
apiService.is_active
|
||||||
|
? "bg-green-200 text-green-800 dark:bg-green-700/20 dark:text-green-300"
|
||||||
|
: "bg-red-200 text-red-800 dark:bg-red-700/20 dark:text-red-300"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{apiService.is_active ? "Active" : "Inactive"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</InfoCard>
|
||||||
|
|
||||||
|
{/* 🔐 Credentials */}
|
||||||
|
<InfoCard title="Client Credentials">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1">
|
||||||
|
<span className="font-medium text-gray-900 dark:text-white">
|
||||||
|
Client ID:
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<Input value={apiService.client_id} disabled />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1">
|
||||||
|
<span className="font-medium text-gray-900 dark:text-white">
|
||||||
|
Client Secret:
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<Input value="***************" disabled />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button variant="outlined" onClick={() => {}}>
|
||||||
|
Regenerate Credentials
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</InfoCard>
|
||||||
|
|
||||||
|
{/* 🚀 Actions */}
|
||||||
|
<div className="flex flex-wrap gap-4 mt-6 justify-between items-center">
|
||||||
|
<Link to="/admin/api-services">
|
||||||
|
<Button variant="outlined">Back</Button>
|
||||||
|
</Link>
|
||||||
|
<div className="flex flex-row items-center gap-4">
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
className={
|
||||||
|
apiService.is_active
|
||||||
|
? "text-red-400 hover:text-red-500"
|
||||||
|
: "text-green-400 hover:text-green-500"
|
||||||
|
}
|
||||||
|
onClick={() => {}}
|
||||||
|
>
|
||||||
|
{apiService.is_active ? "Disable" : "Enable"}
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" disabled>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ViewApiServicePage;
|
@ -16,18 +16,20 @@ const ApiServicesPage: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-col items-stretch w-full h-full">
|
<div className="relative flex flex-col items-stretch w-full h-full">
|
||||||
<Breadcrumbs
|
<div className="p-4">
|
||||||
className="p-7 pb-2"
|
<Breadcrumbs
|
||||||
items={[
|
className="pb-2"
|
||||||
{
|
items={[
|
||||||
href: "/admin",
|
{
|
||||||
label: "Admin",
|
href: "/admin",
|
||||||
},
|
label: "Admin",
|
||||||
{
|
},
|
||||||
label: "API Services",
|
{
|
||||||
},
|
label: "API Services",
|
||||||
]}
|
},
|
||||||
/>
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="p-4 flex flex-row items-center justify-between">
|
<div className="p-4 flex flex-row items-center justify-between">
|
||||||
<p className="text-gray-800 dark:text-gray-300">Search...</p>
|
<p className="text-gray-800 dark:text-gray-300">Search...</p>
|
||||||
<Link to="/admin/api-services/create">
|
<Link to="/admin/api-services/create">
|
||||||
@ -47,7 +49,7 @@ const ApiServicesPage: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<thead className="bg-black/5 dark:bg-white/5">
|
<thead className="bg-black/5 dark:bg-white/5 text-nowrap">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-700 dark:text-white/70 border border-l-0 border-gray-300 dark:border-gray-700">
|
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-700 dark:text-white/70 border border-l-0 border-gray-300 dark:border-gray-700">
|
||||||
Name
|
Name
|
||||||
@ -84,7 +86,7 @@ const ApiServicesPage: FC = () => {
|
|||||||
>
|
>
|
||||||
<td className="px-6 py-4 text-sm font-medium text-blue-600 border border-gray-700">
|
<td className="px-6 py-4 text-sm font-medium text-blue-600 border border-gray-700">
|
||||||
<Link
|
<Link
|
||||||
to={`/services/${service.id}`}
|
to={`/admin/api-services/view/${service.id}`}
|
||||||
className="hover:underline hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
className="hover:underline hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||||
>
|
>
|
||||||
{service.name}
|
{service.name}
|
||||||
|
@ -1,18 +1,40 @@
|
|||||||
import { getApiServices } from "@/api/admin/apiServices";
|
import {
|
||||||
import type { ApiService } from "@/types";
|
getApiService,
|
||||||
|
getApiServices,
|
||||||
|
postApiService,
|
||||||
|
type CreateApiServiceRequest,
|
||||||
|
} from "@/api/admin/apiServices";
|
||||||
|
import type { ApiService, ApiServiceCredentials } from "@/types";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
|
||||||
interface IAdminState {
|
interface IAdminState {
|
||||||
apiServices: ApiService[];
|
apiServices: ApiService[];
|
||||||
loadingApiServices: boolean;
|
loadingApiServices: boolean;
|
||||||
|
|
||||||
|
createdCredentials: ApiServiceCredentials | null;
|
||||||
|
creatingApiService: boolean;
|
||||||
|
|
||||||
|
viewApiService: ApiService | null;
|
||||||
|
fetchingApiService: boolean;
|
||||||
|
|
||||||
fetchApiServices: () => Promise<void>;
|
fetchApiServices: () => Promise<void>;
|
||||||
|
fetchApiService: (id: string) => Promise<void>;
|
||||||
|
createApiService: (req: CreateApiServiceRequest) => Promise<void>;
|
||||||
|
resetCredentials: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAdmin = create<IAdminState>((set) => ({
|
export const useAdmin = create<IAdminState>((set) => ({
|
||||||
apiServices: [],
|
apiServices: [],
|
||||||
loadingApiServices: false,
|
loadingApiServices: false,
|
||||||
|
|
||||||
|
createdCredentials: null,
|
||||||
|
creatingApiService: false,
|
||||||
|
|
||||||
|
viewApiService: null,
|
||||||
|
fetchingApiService: false,
|
||||||
|
|
||||||
|
resetCredentials: () => set({ createdCredentials: null }),
|
||||||
|
|
||||||
fetchApiServices: async () => {
|
fetchApiServices: async () => {
|
||||||
set({ loadingApiServices: true });
|
set({ loadingApiServices: true });
|
||||||
|
|
||||||
@ -25,4 +47,30 @@ export const useAdmin = create<IAdminState>((set) => ({
|
|||||||
set({ loadingApiServices: false });
|
set({ loadingApiServices: false });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
fetchApiService: async (id: string) => {
|
||||||
|
set({ fetchingApiService: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await getApiService(id);
|
||||||
|
set({ viewApiService: response });
|
||||||
|
} catch (err) {
|
||||||
|
console.log("ERR: Failed to fetch services:", err);
|
||||||
|
} finally {
|
||||||
|
set({ fetchingApiService: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
createApiService: async (req: CreateApiServiceRequest) => {
|
||||||
|
set({ creatingApiService: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await postApiService(req);
|
||||||
|
set({ createdCredentials: response.credentials });
|
||||||
|
} catch (err) {
|
||||||
|
console.log("ERR: Failed to fetch services:", err);
|
||||||
|
} finally {
|
||||||
|
set({ creatingApiService: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
@ -14,6 +14,7 @@ export interface ApiService {
|
|||||||
id: string;
|
id: string;
|
||||||
client_id: string;
|
client_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
description: string;
|
||||||
redirect_uris: string[];
|
redirect_uris: string[];
|
||||||
scopes: string[];
|
scopes: string[];
|
||||||
grant_types: string[];
|
grant_types: string[];
|
||||||
@ -21,3 +22,8 @@ export interface ApiService {
|
|||||||
updated_at: string;
|
updated_at: string;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiServiceCredentials {
|
||||||
|
client_id: string;
|
||||||
|
client_secret: string;
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user