From 03bf6550518ad6fa0236c0aa399f60f299cebd2a Mon Sep 17 00:00:00 2001 From: LandaMm Date: Sun, 15 Jun 2025 19:36:02 +0200 Subject: [PATCH] feat: service sessions feature --- web/src/App.tsx | 11 +- web/src/api/admin/sessions.ts | 21 +- web/src/hooks/barItems.tsx | 8 +- web/src/pages/Admin/ServiceSessions/index.tsx | 196 ++++++++++++++++++ web/src/pages/Admin/UserSessions/index.tsx | 4 +- web/src/store/admin/serviceSessions.ts | 59 ++++++ 6 files changed, 291 insertions(+), 8 deletions(-) create mode 100644 web/src/pages/Admin/ServiceSessions/index.tsx create mode 100644 web/src/store/admin/serviceSessions.ts diff --git a/web/src/App.tsx b/web/src/App.tsx index d49f361..fd0ca70 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -25,7 +25,8 @@ import VerifyEmailPage from "./pages/Verify/Email"; import VerifyEmailOtpPage from "./pages/Verify/Email/OTP"; import VerifyAvatarPage from "./pages/Verify/Avatar"; import VerifyReviewPage from "./pages/Verify/Review"; -import AdminSessionsPage from "./pages/Admin/UserSessions"; +import AdminUserSessionsPage from "./pages/Admin/UserSessions"; +import AdminServiceSessionsPage from "./pages/Admin/ServiceSessions"; const router = createBrowserRouter([ { @@ -84,7 +85,13 @@ const router = createBrowserRouter([ }, { path: "user-sessions", - children: [{ index: true, element: }], + children: [{ index: true, element: }], + }, + { + path: "service-sessions", + children: [ + { index: true, element: }, + ], }, ], }, diff --git a/web/src/api/admin/sessions.ts b/web/src/api/admin/sessions.ts index 7b6c978..c540eb8 100644 --- a/web/src/api/admin/sessions.ts +++ b/web/src/api/admin/sessions.ts @@ -40,11 +40,15 @@ export const adminRevokeUserSessionApi = async ( }; export interface FetchServiceSessionsRequest { - limit: number; - offset: number; + page: number; + size: number; } -export type FetchServiceSessionsResponse = ServiceSession[]; +export interface FetchServiceSessionsResponse { + items: ServiceSession[]; + page: number; + total_pages: number; +} export const adminGetServiceSessionsApi = async ( req: FetchServiceSessionsRequest, @@ -61,3 +65,14 @@ export const adminGetServiceSessionsApi = async ( return response.data; }; + +export const adminRevokeServiceSessionApi = async ( + sessionId: string, +): Promise => { + const response = await axios.patch( + `/api/v1/admin/service-sessions/revoke/${sessionId}`, + ); + + if (response.status !== 200 && response.status !== 201) + throw await handleApiError(response); +}; diff --git a/web/src/hooks/barItems.tsx b/web/src/hooks/barItems.tsx index 567ffd3..086bbf0 100644 --- a/web/src/hooks/barItems.tsx +++ b/web/src/hooks/barItems.tsx @@ -1,5 +1,5 @@ import { useAuth } from "@/store/auth"; -import { Blocks, Home, User, UserLock, Users } from "lucide-react"; +import { Blocks, EarthLock, Home, User, UserLock, Users } from "lucide-react"; import { useCallback, type ReactNode } from "react"; import { useLocation } from "react-router"; @@ -87,6 +87,12 @@ export const useBarItems = (): [Item[], (item: Item) => boolean] => { tab: "admin.user-sessions", pathname: "/admin/user-sessions", }, + { + icon: , + title: "Service Sessions", + tab: "admin.service-sessions", + pathname: "/admin/service-sessions", + }, ] : []), ], diff --git a/web/src/pages/Admin/ServiceSessions/index.tsx b/web/src/pages/Admin/ServiceSessions/index.tsx new file mode 100644 index 0000000..366d1a6 --- /dev/null +++ b/web/src/pages/Admin/ServiceSessions/index.tsx @@ -0,0 +1,196 @@ +import Breadcrumbs from "@/components/ui/breadcrumbs"; +import { Button } from "@/components/ui/button"; +import Avatar from "@/feature/Avatar"; +import { Ban } from "lucide-react"; +import { useCallback, useEffect, type FC } from "react"; +import { Link } from "react-router"; +import moment from "moment"; +import Pagination from "@/components/ui/pagination"; +import { useAuth } from "@/store/auth"; +import { useServiceSessions } from "@/store/admin/serviceSessions"; + +const AdminServiceSessionsPage: FC = () => { + const loading = useServiceSessions((s) => s.loading); + const sessions = useServiceSessions((s) => s.items); + + const page = useServiceSessions((s) => s.page); + const totalPages = useServiceSessions((s) => s.totalPages); + + const fetchSessions = useServiceSessions((s) => s.fetch); + const revokeSession = useServiceSessions((s) => s.revoke); + + const revokingId = useServiceSessions((s) => s.revokingId); + + const profile = useAuth((s) => s.profile); + + const handleRevokeSession = useCallback( + (id: string) => { + revokeSession(id); + }, + [revokeSession], + ); + + useEffect(() => { + fetchSessions(1); + }, [fetchSessions]); + + return ( +
+
+ +
+
+

Search...

+ {/* TODO: Filters */} +
+ +
+ + {loading && ( +
+
+ Loading... +
+
+ )} + + + + + + + + + + + + + + + {!loading && sessions.length === 0 ? ( + + + + ) : ( + sessions.map((session) => ( + + + + + + + + + + + )) + )} + +
+ Service + + Source + + Status + + Issued At + + Expires At + + Last Active + + Revoked At + + Actions +
+ No sessions found. +
+
+ {typeof session.user?.profile_picture === "string" && ( + + )} + + +

+ {session.user?.full_name ?? ""}{" "} + {session.user_id === profile?.id ? "(You)" : ""} +

+ +
+
+ {/* */} +

{session.client_id}

+
+ + {session.is_active ? "Active" : "Inactive"} + {moment(session.expires_at).isSameOrBefore( + moment(new Date()), + ) && " (Expired)"} + + + {moment(session.issued_at).format("LLLL")} + + {session.expires_at + ? moment(session.expires_at).format("LLLL") + : "never"} + + {session.last_active + ? moment(session.last_active).format("LLLL") + : "never"} + + {session.revoked_at + ? new Date(session.revoked_at).toLocaleString() + : "never"} + +
+ +
+
+
+ fetchSessions(newPage)} + totalPages={totalPages} + /> +
+ ); +}; + +export default AdminServiceSessionsPage; diff --git a/web/src/pages/Admin/UserSessions/index.tsx b/web/src/pages/Admin/UserSessions/index.tsx index 6da72fa..e495388 100644 --- a/web/src/pages/Admin/UserSessions/index.tsx +++ b/web/src/pages/Admin/UserSessions/index.tsx @@ -23,7 +23,7 @@ const SessionSource: FC<{ deviceInfo: string }> = ({ deviceInfo }) => { ); }; -const AdminSessionsPage: FC = () => { +const AdminUserSessionsPage: FC = () => { const loading = useUserSessions((s) => s.loading); const sessions = useUserSessions((s) => s.items); @@ -206,4 +206,4 @@ const AdminSessionsPage: FC = () => { ); }; -export default AdminSessionsPage; +export default AdminUserSessionsPage; diff --git a/web/src/store/admin/serviceSessions.ts b/web/src/store/admin/serviceSessions.ts new file mode 100644 index 0000000..e6e315b --- /dev/null +++ b/web/src/store/admin/serviceSessions.ts @@ -0,0 +1,59 @@ +import { + adminGetServiceSessionsApi, + adminRevokeServiceSessionApi, +} from "@/api/admin/sessions"; +import type { ServiceSession } from "@/types"; +import { create } from "zustand"; + +export const ADMIN_SERVICE_SESSIONS_PAGE_SIZE = 10; + +export interface IServiceSessionsState { + items: ServiceSession[]; + totalPages: number; + page: number; + + loading: boolean; + + revokingId: string | null; + + fetch: (page: number) => Promise; + revoke: (id: string) => Promise; +} + +export const useServiceSessions = create((set, get) => ({ + items: [], + totalPages: 0, + page: 1, + loading: false, + revokingId: null, + + fetch: async (page) => { + set({ loading: true, page }); + + try { + const response = await adminGetServiceSessionsApi({ + page, + size: ADMIN_SERVICE_SESSIONS_PAGE_SIZE, + }); + set({ items: response.items, totalPages: response.total_pages }); + } catch (err) { + console.log("ERR: Failed to fetch admin service sessions:", err); + } finally { + set({ loading: false }); + } + }, + + revoke: async (id) => { + set({ revokingId: id }); + + try { + await adminRevokeServiceSessionApi(id); + } catch (err) { + console.log("ERR: Failed to revoke service sessions:", err); + } finally { + set({ revokingId: null }); + const { fetch, page } = get(); + await fetch(page); + } + }, +}));