@mesob/auth-react 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/auth/{auth-page-layout.d.ts → auth-layout.d.ts} +3 -3
- package/dist/components/auth/{auth-page-layout.js → auth-layout.js} +4 -4
- package/dist/components/auth/auth-layout.js.map +1 -0
- package/dist/components/auth/countdown.js +15 -6
- package/dist/components/auth/countdown.js.map +1 -1
- package/dist/components/auth/forgot-password.d.ts +1 -9
- package/dist/components/auth/forgot-password.js +267 -43
- package/dist/components/auth/forgot-password.js.map +1 -1
- package/dist/components/auth/reset-password-form.d.ts +2 -10
- package/dist/components/auth/reset-password-form.js +366 -118
- package/dist/components/auth/reset-password-form.js.map +1 -1
- package/dist/components/auth/sign-in.d.ts +2 -10
- package/dist/components/auth/sign-in.js +358 -130
- package/dist/components/auth/sign-in.js.map +1 -1
- package/dist/components/auth/sign-up.d.ts +2 -10
- package/dist/components/auth/sign-up.js +379 -131
- package/dist/components/auth/sign-up.js.map +1 -1
- package/dist/components/auth/verification-form.d.ts +2 -16
- package/dist/components/auth/verification-form.js +15 -6
- package/dist/components/auth/verification-form.js.map +1 -1
- package/dist/components/auth/verify-email.d.ts +10 -0
- package/dist/components/auth/{pages/verify-email-page.js → verify-email.js} +54 -34
- package/dist/components/auth/verify-email.js.map +1 -0
- package/dist/components/auth/verify-phone.d.ts +11 -0
- package/dist/components/auth/{pages/verify-phone-page.js → verify-phone.js} +53 -46
- package/dist/components/auth/verify-phone.js.map +1 -0
- package/dist/components/error-boundary.d.ts +2 -2
- package/dist/components/iam/permissions.d.ts +5 -0
- package/dist/components/iam/{permissions/permissions-page.js → permissions.js} +29 -13
- package/dist/components/iam/permissions.js.map +1 -0
- package/dist/components/iam/roles.d.ts +5 -0
- package/dist/components/iam/{roles/roles-page.js → roles.js} +29 -13
- package/dist/components/iam/roles.js.map +1 -0
- package/dist/components/iam/sessions.d.ts +5 -0
- package/dist/components/iam/{sessions/sessions-page.js → sessions.js} +13 -11
- package/dist/components/iam/sessions.js.map +1 -0
- package/dist/components/iam/tenants.d.ts +5 -0
- package/dist/components/iam/{tenants/tenants-page.js → tenants.js} +13 -11
- package/dist/components/iam/tenants.js.map +1 -0
- package/dist/components/iam/users.d.ts +5 -0
- package/dist/components/iam/{users/users-page.js → users.js} +13 -11
- package/dist/components/iam/users.js.map +1 -0
- package/dist/components/profile/account.d.ts +5 -0
- package/dist/components/profile/account.js +66 -0
- package/dist/components/profile/account.js.map +1 -0
- package/dist/components/profile/change-email-form.d.ts +5 -0
- package/dist/components/profile/change-email-form.js +721 -0
- package/dist/components/profile/change-email-form.js.map +1 -0
- package/dist/components/profile/change-password-form.d.ts +5 -0
- package/dist/components/profile/change-password-form.js +256 -0
- package/dist/components/profile/change-password-form.js.map +1 -0
- package/dist/components/profile/change-phone-form.d.ts +5 -0
- package/dist/components/profile/change-phone-form.js +758 -0
- package/dist/components/profile/change-phone-form.js.map +1 -0
- package/dist/components/profile/change-profile.d.ts +9 -0
- package/dist/components/profile/change-profile.js +37 -0
- package/dist/components/profile/change-profile.js.map +1 -0
- package/dist/components/profile/otp-verification-modal.d.ts +15 -0
- package/dist/components/profile/otp-verification-modal.js +261 -0
- package/dist/components/profile/otp-verification-modal.js.map +1 -0
- package/dist/components/profile/request-change-email-form.d.ts +10 -0
- package/dist/components/profile/request-change-email-form.js +293 -0
- package/dist/components/profile/request-change-email-form.js.map +1 -0
- package/dist/components/profile/request-change-phone-form.d.ts +10 -0
- package/dist/components/profile/request-change-phone-form.js +331 -0
- package/dist/components/profile/request-change-phone-form.js.map +1 -0
- package/dist/components/profile/security.d.ts +5 -0
- package/dist/components/profile/security.js +1439 -0
- package/dist/components/profile/security.js.map +1 -0
- package/dist/components/profile/verify-change-email-form.d.ts +11 -0
- package/dist/components/profile/verify-change-email-form.js +402 -0
- package/dist/components/profile/verify-change-email-form.js.map +1 -0
- package/dist/components/profile/verify-change-phone-form.d.ts +11 -0
- package/dist/components/profile/verify-change-phone-form.js +406 -0
- package/dist/components/profile/verify-change-phone-form.js.map +1 -0
- package/dist/components/shared/{data-table/data-table.js → data-table.js} +2 -2
- package/dist/components/shared/data-table.js.map +1 -0
- package/dist/index.d.ts +42 -93
- package/dist/index.js +2297 -1299
- package/dist/index.js.map +1 -1
- package/dist/types-D3s9oE-5.d.ts +75 -0
- package/dist/verification-form-ipSRTtQB.d.ts +22 -0
- package/package.json +3 -2
- package/dist/components/auth/auth-page-layout.js.map +0 -1
- package/dist/components/auth/pages/forgot-password-page.d.ts +0 -6
- package/dist/components/auth/pages/forgot-password-page.js +0 -362
- package/dist/components/auth/pages/forgot-password-page.js.map +0 -1
- package/dist/components/auth/pages/reset-password-page.d.ts +0 -9
- package/dist/components/auth/pages/reset-password-page.js +0 -514
- package/dist/components/auth/pages/reset-password-page.js.map +0 -1
- package/dist/components/auth/pages/sign-in-page.d.ts +0 -8
- package/dist/components/auth/pages/sign-in-page.js +0 -565
- package/dist/components/auth/pages/sign-in-page.js.map +0 -1
- package/dist/components/auth/pages/sign-up-page.d.ts +0 -9
- package/dist/components/auth/pages/sign-up-page.js +0 -518
- package/dist/components/auth/pages/sign-up-page.js.map +0 -1
- package/dist/components/auth/pages/verify-email-page.d.ts +0 -10
- package/dist/components/auth/pages/verify-email-page.js.map +0 -1
- package/dist/components/auth/pages/verify-phone-page.d.ts +0 -11
- package/dist/components/auth/pages/verify-phone-page.js.map +0 -1
- package/dist/components/iam/permissions/permissions-page.d.ts +0 -5
- package/dist/components/iam/permissions/permissions-page.js.map +0 -1
- package/dist/components/iam/roles/roles-page.d.ts +0 -5
- package/dist/components/iam/roles/roles-page.js.map +0 -1
- package/dist/components/iam/sessions/sessions-page.d.ts +0 -5
- package/dist/components/iam/sessions/sessions-page.js.map +0 -1
- package/dist/components/iam/tenants/tenants-page.d.ts +0 -5
- package/dist/components/iam/tenants/tenants-page.js.map +0 -1
- package/dist/components/iam/users/users-page.d.ts +0 -5
- package/dist/components/iam/users/users-page.js.map +0 -1
- package/dist/components/profile/profile-page.d.ts +0 -8
- package/dist/components/profile/profile-page.js +0 -163
- package/dist/components/profile/profile-page.js.map +0 -1
- package/dist/components/shared/data-table/data-table.js.map +0 -1
- package/dist/handle-error-BqDMxnQZ.d.ts +0 -8
- /package/dist/components/shared/{data-table/data-table.d.ts → data-table.d.ts} +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
// src/components/iam/roles
|
|
3
|
+
// src/components/iam/roles.tsx
|
|
4
4
|
import { Badge } from "@mesob/ui/components/badge";
|
|
5
5
|
import { Button } from "@mesob/ui/components/button";
|
|
6
|
+
import { useLocale } from "next-intl";
|
|
6
7
|
import { useState as useState2 } from "react";
|
|
7
8
|
|
|
8
9
|
// src/provider.tsx
|
|
@@ -10,7 +11,12 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
|
10
11
|
import { deepmerge } from "deepmerge-ts";
|
|
11
12
|
import createFetchClient from "openapi-fetch";
|
|
12
13
|
import createClient from "openapi-react-query";
|
|
13
|
-
import { createContext, useContext,
|
|
14
|
+
import { createContext, useContext, useMemo, useState } from "react";
|
|
15
|
+
|
|
16
|
+
// src/utils/cookie.ts
|
|
17
|
+
var isProduction = typeof process !== "undefined" && process.env.NODE_ENV === "production";
|
|
18
|
+
|
|
19
|
+
// src/provider.tsx
|
|
14
20
|
import { jsx } from "react/jsx-runtime";
|
|
15
21
|
var SessionContext = createContext(null);
|
|
16
22
|
var ApiContext = createContext(null);
|
|
@@ -18,9 +24,6 @@ var ConfigContext = createContext(null);
|
|
|
18
24
|
var queryClient = new QueryClient({
|
|
19
25
|
defaultOptions: {
|
|
20
26
|
queries: {
|
|
21
|
-
staleTime: 1e3 * 60 * 5,
|
|
22
|
-
gcTime: 1e3 * 60 * 10,
|
|
23
|
-
retry: 1,
|
|
24
27
|
refetchOnWindowFocus: false
|
|
25
28
|
}
|
|
26
29
|
}
|
|
@@ -33,7 +36,7 @@ function useApi() {
|
|
|
33
36
|
return context;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
|
-
// src/components/shared/data-table
|
|
39
|
+
// src/components/shared/data-table.tsx
|
|
37
40
|
import {
|
|
38
41
|
Table,
|
|
39
42
|
TableBody,
|
|
@@ -77,7 +80,7 @@ function TableSkeleton({ columns = 5, rows = 10 }) {
|
|
|
77
80
|
] });
|
|
78
81
|
}
|
|
79
82
|
|
|
80
|
-
// src/components/shared/data-table
|
|
83
|
+
// src/components/shared/data-table.tsx
|
|
81
84
|
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
82
85
|
function DataTable({
|
|
83
86
|
data,
|
|
@@ -113,10 +116,23 @@ function DataTable({
|
|
|
113
116
|
] });
|
|
114
117
|
}
|
|
115
118
|
|
|
116
|
-
// src/components/iam/roles
|
|
119
|
+
// src/components/iam/roles.tsx
|
|
117
120
|
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
118
|
-
function
|
|
121
|
+
function getTranslation(value, locale) {
|
|
122
|
+
if (!value) {
|
|
123
|
+
return "";
|
|
124
|
+
}
|
|
125
|
+
if (typeof value === "string") {
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
if (typeof value === "object") {
|
|
129
|
+
return value[locale] ?? value.en ?? value.am ?? "";
|
|
130
|
+
}
|
|
131
|
+
return "";
|
|
132
|
+
}
|
|
133
|
+
function Roles() {
|
|
119
134
|
const { hooks } = useApi();
|
|
135
|
+
const locale = useLocale();
|
|
120
136
|
const [page, setPage] = useState2(1);
|
|
121
137
|
const limit = 20;
|
|
122
138
|
const { data, isLoading, error } = hooks.useQuery("get", "/roles", {
|
|
@@ -132,14 +148,14 @@ function RolesPage() {
|
|
|
132
148
|
key: "name",
|
|
133
149
|
header: "Role",
|
|
134
150
|
cell: (role) => /* @__PURE__ */ jsxs3("div", { children: [
|
|
135
|
-
/* @__PURE__ */ jsx4("p", { className: "font-medium", children: role.name }),
|
|
151
|
+
/* @__PURE__ */ jsx4("p", { className: "font-medium", children: getTranslation(role.name, locale) }),
|
|
136
152
|
/* @__PURE__ */ jsx4(Badge, { variant: "outline", className: "mt-1", children: role.code })
|
|
137
153
|
] })
|
|
138
154
|
},
|
|
139
155
|
{
|
|
140
156
|
key: "description",
|
|
141
157
|
header: "Description",
|
|
142
|
-
cell: (role) => /* @__PURE__ */ jsx4("p", { className: "text-sm text-muted-foreground", children: role.description })
|
|
158
|
+
cell: (role) => /* @__PURE__ */ jsx4("p", { className: "text-sm text-muted-foreground", children: getTranslation(role.description, locale) })
|
|
143
159
|
},
|
|
144
160
|
{
|
|
145
161
|
key: "createdAt",
|
|
@@ -194,6 +210,6 @@ function RolesPage() {
|
|
|
194
210
|
] });
|
|
195
211
|
}
|
|
196
212
|
export {
|
|
197
|
-
|
|
213
|
+
Roles
|
|
198
214
|
};
|
|
199
|
-
//# sourceMappingURL=roles
|
|
215
|
+
//# sourceMappingURL=roles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/iam/roles.tsx","../../../src/provider.tsx","../../../src/utils/cookie.ts","../../../src/components/shared/data-table.tsx","../../../src/components/skeletons/table-skeleton.tsx"],"sourcesContent":["'use client';\n\nimport { Badge } from '@mesob/ui/components/badge';\nimport { Button } from '@mesob/ui/components/button';\nimport { useLocale } from 'next-intl';\nimport { useState } from 'react';\nimport { useApi } from '../../provider';\nimport { DataTable, type DataTableColumn } from '../shared/data-table';\n\n// Role type from OpenAPI schema\ntype Role = {\n id: string;\n code: string;\n name: string | { am?: string; en?: string };\n description: string | { am?: string; en?: string };\n createdAt: string;\n};\n\nfunction getTranslation(\n value: string | { am?: string; en?: string } | null | undefined,\n locale: string,\n): string {\n if (!value) {\n return '';\n }\n if (typeof value === 'string') {\n return value;\n }\n if (typeof value === 'object') {\n return value[locale as 'am' | 'en'] ?? value.en ?? value.am ?? '';\n }\n return '';\n}\n\nexport function Roles() {\n const { hooks } = useApi();\n const locale = useLocale();\n const [page, setPage] = useState(1);\n const limit = 20;\n\n // Use openapi-react-query hooks\n const { data, isLoading, error } = hooks.useQuery('get', '/roles', {\n params: {\n query: {\n page: String(page),\n limit: String(limit),\n },\n },\n });\n\n const columns: DataTableColumn<Role>[] = [\n {\n key: 'name',\n header: 'Role',\n cell: (role) => (\n <div>\n <p className=\"font-medium\">{getTranslation(role.name, locale)}</p>\n <Badge variant=\"outline\" className=\"mt-1\">\n {role.code}\n </Badge>\n </div>\n ),\n },\n {\n key: 'description',\n header: 'Description',\n cell: (role) => (\n <p className=\"text-sm text-muted-foreground\">\n {getTranslation(role.description, locale)}\n </p>\n ),\n },\n {\n key: 'createdAt',\n header: 'Created',\n cell: (role) => (\n <p className=\"text-sm\">\n {new Date(role.createdAt).toLocaleDateString()}\n </p>\n ),\n },\n {\n key: 'actions',\n header: 'Actions',\n cell: (_role) => (\n <div className=\"flex gap-2\">\n <Button variant=\"outline\" size=\"sm\">\n Permissions\n </Button>\n <Button variant=\"outline\" size=\"sm\">\n Edit\n </Button>\n </div>\n ),\n },\n ];\n\n if (error) {\n return (\n <div className=\"p-6 text-center\">\n <p className=\"text-destructive\">Error loading roles</p>\n </div>\n );\n }\n\n return (\n <div className=\"w-full p-6 space-y-4\">\n <div className=\"flex justify-between items-center\">\n <div>\n <h1 className=\"text-3xl font-bold\">Roles</h1>\n <p className=\"text-muted-foreground\">Manage user roles</p>\n </div>\n <Button>Create Role</Button>\n </div>\n\n <DataTable\n data={(data as { roles: Role[] })?.roles || []}\n columns={columns}\n isLoading={isLoading}\n emptyMessage=\"No roles found\"\n />\n\n {data &&\n 'roles' in data &&\n data.roles &&\n (data.roles as Role[]).length >= limit && (\n <div className=\"flex justify-between items-center\">\n <Button\n variant=\"outline\"\n disabled={page === 1}\n onClick={() => setPage(page - 1)}\n >\n Previous\n </Button>\n <span className=\"text-sm text-muted-foreground\">Page {page}</span>\n <Button variant=\"outline\" onClick={() => setPage(page + 1)}>\n Next\n </Button>\n </div>\n )}\n </div>\n );\n}\n","'use client';\n\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport { deepmerge } from 'deepmerge-ts';\nimport createFetchClient from 'openapi-fetch';\nimport createClient from 'openapi-react-query';\nimport type { ReactNode } from 'react';\nimport { createContext, useContext, useMemo, useState } from 'react';\nimport type { paths } from './data/openapi';\nimport { createTranslator } from './lib/translations';\nimport {\n type AuthClientConfig,\n type AuthResponse,\n defaultAuthClientConfig,\n type Session,\n type User,\n} from './types';\nimport { getSessionCookieName } from './utils/cookie';\nimport { createCustomFetch } from './utils/custom-fetch';\n\n// biome-ignore lint/suspicious/noExplicitAny: OpenAPI hooks type\ntype OpenApiHooks = any;\n\n// --- Utility: Check if running on server ---\nfunction isServer(): boolean {\n return typeof document === 'undefined';\n}\n\n/**\n * @deprecated Cookie is httpOnly and cannot be read client-side.\n * Use `useSession().isAuthenticated` instead.\n * This function always returns false on client.\n */\nexport function hasAuthCookie(_cookieName: string): boolean {\n // Cookie is httpOnly, can't check client-side\n // Always return false - use useSession() for auth status\n return false;\n}\n\n// --- Types ---\nexport type AuthStatus = 'loading' | 'authenticated' | 'unauthenticated';\n\ntype AuthState = {\n user: User | null;\n session: Session | null;\n status: AuthStatus;\n error: Error | null;\n};\n\ntype SessionContextValue = AuthState & {\n isLoading: boolean;\n isAuthenticated: boolean;\n refresh: () => Promise<void>;\n signOut: () => Promise<void>;\n};\n\ntype ApiContextValue = {\n hooks: OpenApiHooks;\n setAuth: (auth: AuthResponse) => void;\n clearAuth: () => void;\n refresh: () => Promise<void>;\n};\n\ntype ConfigContextValue = {\n config: AuthClientConfig;\n cookieName: string;\n t: (key: string, params?: Record<string, string | number>) => string;\n};\n\nconst SessionContext = createContext<SessionContextValue | null>(null);\nconst ApiContext = createContext<ApiContextValue | null>(null);\nconst ConfigContext = createContext<ConfigContextValue | null>(null);\n\nconst queryClient = new QueryClient({\n defaultOptions: {\n queries: {\n refetchOnWindowFocus: false,\n },\n },\n});\n\n// --- Hooks ---\n\n/**\n * Get session state including user, session, and auth status.\n * - `status`: 'loading' | 'authenticated' | 'unauthenticated'\n * - `isLoading`: true while fetching session\n * - `isAuthenticated`: true if user and session exist\n */\nexport function useSession(): SessionContextValue {\n const context = useContext(SessionContext);\n if (!context) {\n throw new Error('useSession must be used within MesobAuthProvider');\n }\n return context;\n}\n\nexport function useApi(): ApiContextValue {\n const context = useContext(ApiContext);\n if (!context) {\n throw new Error('useApi must be used within MesobAuthProvider');\n }\n return context;\n}\n\nexport function useConfig(): ConfigContextValue {\n const context = useContext(ConfigContext);\n if (!context) {\n throw new Error('useConfig must be used within MesobAuthProvider');\n }\n return context;\n}\n\n/**\n * @deprecated Cookie is httpOnly, can't be checked client-side.\n * Use `useSession().isAuthenticated` instead.\n */\nexport function useHasAuthCookie(): boolean {\n const { status } = useSession();\n return status === 'authenticated' || status === 'loading';\n}\n\n// --- Provider ---\n\ntype MesobAuthProviderProps = {\n config: AuthClientConfig;\n children: ReactNode;\n};\n\nexport function MesobAuthProvider({\n config,\n children,\n}: MesobAuthProviderProps) {\n const mergedConfig = useMemo(\n () =>\n deepmerge(\n { ...defaultAuthClientConfig } as Partial<AuthClientConfig>,\n config,\n ) as AuthClientConfig,\n [config],\n );\n\n const api = useMemo(\n () =>\n createFetchClient<paths>({\n baseUrl: mergedConfig.baseURL,\n fetch: createCustomFetch(mergedConfig),\n }),\n [mergedConfig],\n );\n\n const hooks = useMemo(() => createClient(api), [api]);\n const cookieName = useMemo(\n () => getSessionCookieName(mergedConfig),\n [mergedConfig],\n );\n\n return (\n <QueryClientProvider client={queryClient}>\n <AuthStateProvider\n config={mergedConfig}\n hooks={hooks}\n cookieName={cookieName}\n >\n {children}\n </AuthStateProvider>\n </QueryClientProvider>\n );\n}\n\ntype AuthStateProviderProps = {\n config: AuthClientConfig;\n hooks: OpenApiHooks;\n cookieName: string;\n children: ReactNode;\n};\n\nfunction AuthStateProvider({\n config,\n hooks,\n cookieName,\n children,\n}: AuthStateProviderProps) {\n // Manual override for sign-out / sign-in\n const [override, setOverride] = useState<AuthState | null>(null);\n\n // Always fetch session - cookie is httpOnly, can't check client-side\n // Server will read the cookie and return user/session if valid\n const {\n data: sessionData,\n isLoading,\n isFetched,\n error: sessionError,\n refetch,\n } = hooks.useQuery(\n 'get',\n '/session',\n {},\n {\n enabled: !(override || isServer()),\n refetchOnMount: false,\n refetchOnWindowFocus: false,\n refetchOnReconnect: false,\n retry: false,\n gcTime: 0,\n staleTime: 0,\n },\n );\n\n // Derive state directly - no useEffect\n const user = override?.user ?? sessionData?.user ?? null;\n const session = override?.session ?? sessionData?.session ?? null;\n const error = override?.error ?? (sessionError as Error | null);\n\n // Check error status code\n const errorStatus = (() => {\n if (!sessionError) {\n return null;\n }\n const err = sessionError as { status?: number };\n return err.status ?? null;\n })();\n\n // Check if error is a network/connection error\n const isNetworkError = (() => {\n if (!sessionError) {\n return false;\n }\n const error = sessionError as Error & { cause?: unknown; data?: unknown };\n const errorMessage =\n error.message || String(error) || JSON.stringify(error);\n // Network errors: TypeError, DOMException, or fetch failures\n if (\n error instanceof TypeError ||\n error instanceof DOMException ||\n error.name === 'TypeError' ||\n errorMessage.includes('Failed to fetch') ||\n errorMessage.includes('ERR_CONNECTION_REFUSED') ||\n errorMessage.includes('NetworkError') ||\n errorMessage.includes('Network request failed') ||\n errorMessage.includes('fetch failed')\n ) {\n return true;\n }\n // Check error cause\n if (error.cause) {\n const causeStr = String(error.cause);\n if (\n causeStr.includes('Failed to fetch') ||\n causeStr.includes('ERR_CONNECTION_REFUSED') ||\n causeStr.includes('NetworkError')\n ) {\n return true;\n }\n }\n return false;\n })();\n\n // Compute status\n // biome-ignore lint: Status determination requires multiple checks\n const status: AuthStatus = (() => {\n if (override) {\n return override.status;\n }\n if (isServer()) {\n return 'loading';\n }\n if (user && session) {\n return 'authenticated';\n }\n // Check for network errors or auth errors first - allow auth page to show\n if (isNetworkError || errorStatus === 401) {\n return 'unauthenticated';\n }\n // If we have an error but it's not a network error, still check loading state\n if (sessionError && !isNetworkError && errorStatus !== 401) {\n if (errorStatus && errorStatus >= 500) {\n return 'authenticated';\n }\n // Other errors mean unauthenticated\n if (isFetched) {\n return 'unauthenticated';\n }\n }\n if (isLoading || !isFetched) {\n return 'loading';\n }\n if (isFetched && !user && !session) {\n return 'unauthenticated';\n }\n return 'unauthenticated';\n })();\n\n const signOutMutation = hooks.useMutation('post', '/sign-out');\n const t = createTranslator(config.messages || {});\n\n const setAuth = (auth: AuthResponse) => {\n setOverride({\n user: auth.user,\n session: auth.session,\n status: 'authenticated',\n error: null,\n });\n };\n\n const clearAuth = () => {\n setOverride({\n user: null,\n session: null,\n status: 'unauthenticated',\n error: null,\n });\n };\n\n const refresh = async () => {\n setOverride(null);\n await refetch();\n };\n\n const signOut = async () => {\n try {\n await signOutMutation.mutateAsync({});\n } finally {\n clearAuth();\n }\n };\n\n return (\n <ConfigContext.Provider value={{ config, cookieName, t }}>\n <ApiContext.Provider value={{ hooks, setAuth, clearAuth, refresh }}>\n <SessionContext.Provider\n value={{\n user,\n session,\n status,\n error,\n isLoading: status === 'loading',\n isAuthenticated: status === 'authenticated',\n refresh,\n signOut,\n }}\n >\n {children}\n </SessionContext.Provider>\n </ApiContext.Provider>\n </ConfigContext.Provider>\n );\n}\n","import type { AuthClientConfig } from '../types';\n\nconst isProduction =\n typeof process !== 'undefined' && process.env.NODE_ENV === 'production';\n\nexport const getSessionCookieName = (config: AuthClientConfig): string => {\n const prefix = config.cookiePrefix || '';\n const baseName = 'session_token';\n if (prefix) {\n return `${prefix}_${baseName}`;\n }\n return isProduction ? '__Host-session_token' : baseName;\n};\n","'use client';\n\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '@mesob/ui/components/table';\nimport type { ReactNode } from 'react';\nimport { TableSkeleton } from '../skeletons/table-skeleton';\n\nexport type DataTableColumn<T> = {\n key: string;\n header: string;\n cell: (row: T) => ReactNode;\n sortable?: boolean;\n};\n\ntype DataTableProps<T> = {\n data: T[];\n columns: DataTableColumn<T>[];\n isLoading?: boolean;\n onRowClick?: (row: T) => void;\n emptyMessage?: string;\n actions?: ReactNode;\n};\n\nexport function DataTable<T extends { id: string }>({\n data,\n columns,\n isLoading,\n onRowClick,\n emptyMessage = 'No data available',\n actions,\n}: DataTableProps<T>) {\n if (isLoading) {\n return <TableSkeleton columns={columns.length} rows={5} />;\n }\n\n if (data.length === 0) {\n return (\n <div className=\"flex flex-col items-center justify-center min-h-[400px] p-6\">\n <p className=\"text-muted-foreground\">{emptyMessage}</p>\n {actions && <div className=\"mt-4\">{actions}</div>}\n </div>\n );\n }\n\n return (\n <div className=\"w-full space-y-4\">\n {actions && <div className=\"flex justify-end\">{actions}</div>}\n\n <div className=\"border rounded-lg overflow-hidden\">\n <Table>\n <TableHeader>\n <TableRow>\n {columns.map((column) => (\n <TableHead key={column.key}>{column.header}</TableHead>\n ))}\n </TableRow>\n </TableHeader>\n <TableBody>\n {data.map((row) => (\n <TableRow\n key={row.id}\n onClick={() => onRowClick?.(row)}\n className={onRowClick ? 'cursor-pointer hover:bg-muted/50' : ''}\n >\n {columns.map((column) => (\n <TableCell key={`${row.id}-${column.key}`}>\n {column.cell(row)}\n </TableCell>\n ))}\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n </div>\n );\n}\n","'use client';\n\nimport { Skeleton } from '@mesob/ui/components/skeleton';\n\ntype TableSkeletonProps = {\n columns?: number;\n rows?: number;\n};\n\nexport function TableSkeleton({ columns = 5, rows = 10 }: TableSkeletonProps) {\n const headerKeys = Array.from({ length: columns }, (_, i) => `header-${i}`);\n const rowKeys = Array.from({ length: rows }, (_, i) => `row-${i}`);\n const cellKeys = Array.from({ length: rows }, (_, rowIdx) =>\n Array.from({ length: columns }, (_, colIdx) => `cell-${rowIdx}-${colIdx}`),\n );\n\n return (\n <div className=\"w-full space-y-4\">\n {/* Header */}\n <div className=\"flex justify-between items-center\">\n <Skeleton className=\"h-8 w-48\" />\n <Skeleton className=\"h-10 w-32\" />\n </div>\n\n {/* Search/filters */}\n <div className=\"flex gap-4\">\n <Skeleton className=\"h-10 flex-1 max-w-sm\" />\n <Skeleton className=\"h-10 w-24\" />\n <Skeleton className=\"h-10 w-24\" />\n </div>\n\n {/* Table */}\n <div className=\"border rounded-lg overflow-hidden\">\n {/* Table header */}\n <div className=\"flex gap-4 p-4 bg-muted\">\n {headerKeys.map((key) => (\n <Skeleton key={key} className=\"h-4 flex-1\" />\n ))}\n </div>\n\n {/* Table rows */}\n {rowKeys.map((rowKey, rowIdx) => (\n <div key={rowKey} className=\"flex gap-4 p-4 border-t\">\n {cellKeys[rowIdx]?.map((cellKey) => (\n <Skeleton key={cellKey} className=\"h-4 flex-1\" />\n ))}\n </div>\n ))}\n </div>\n\n {/* Pagination */}\n <div className=\"flex justify-between items-center\">\n <Skeleton className=\"h-4 w-32\" />\n <div className=\"flex gap-2\">\n <Skeleton className=\"h-10 w-20\" />\n <Skeleton className=\"h-10 w-20\" />\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["useState","jsx","_","jsx","jsxs","jsx","jsxs","useState"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
// src/components/iam/sessions
|
|
3
|
+
// src/components/iam/sessions.tsx
|
|
4
4
|
import { Button } from "@mesob/ui/components/button";
|
|
5
5
|
import { useState as useState2 } from "react";
|
|
6
6
|
|
|
@@ -9,7 +9,12 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
|
9
9
|
import { deepmerge } from "deepmerge-ts";
|
|
10
10
|
import createFetchClient from "openapi-fetch";
|
|
11
11
|
import createClient from "openapi-react-query";
|
|
12
|
-
import { createContext, useContext,
|
|
12
|
+
import { createContext, useContext, useMemo, useState } from "react";
|
|
13
|
+
|
|
14
|
+
// src/utils/cookie.ts
|
|
15
|
+
var isProduction = typeof process !== "undefined" && process.env.NODE_ENV === "production";
|
|
16
|
+
|
|
17
|
+
// src/provider.tsx
|
|
13
18
|
import { jsx } from "react/jsx-runtime";
|
|
14
19
|
var SessionContext = createContext(null);
|
|
15
20
|
var ApiContext = createContext(null);
|
|
@@ -17,9 +22,6 @@ var ConfigContext = createContext(null);
|
|
|
17
22
|
var queryClient = new QueryClient({
|
|
18
23
|
defaultOptions: {
|
|
19
24
|
queries: {
|
|
20
|
-
staleTime: 1e3 * 60 * 5,
|
|
21
|
-
gcTime: 1e3 * 60 * 10,
|
|
22
|
-
retry: 1,
|
|
23
25
|
refetchOnWindowFocus: false
|
|
24
26
|
}
|
|
25
27
|
}
|
|
@@ -32,7 +34,7 @@ function useApi() {
|
|
|
32
34
|
return context;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
// src/components/shared/data-table
|
|
37
|
+
// src/components/shared/data-table.tsx
|
|
36
38
|
import {
|
|
37
39
|
Table,
|
|
38
40
|
TableBody,
|
|
@@ -76,7 +78,7 @@ function TableSkeleton({ columns = 5, rows = 10 }) {
|
|
|
76
78
|
] });
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
// src/components/shared/data-table
|
|
81
|
+
// src/components/shared/data-table.tsx
|
|
80
82
|
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
81
83
|
function DataTable({
|
|
82
84
|
data,
|
|
@@ -112,9 +114,9 @@ function DataTable({
|
|
|
112
114
|
] });
|
|
113
115
|
}
|
|
114
116
|
|
|
115
|
-
// src/components/iam/sessions
|
|
117
|
+
// src/components/iam/sessions.tsx
|
|
116
118
|
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
117
|
-
function
|
|
119
|
+
function Sessions() {
|
|
118
120
|
const { hooks } = useApi();
|
|
119
121
|
const [selectedSessionId] = useState2(null);
|
|
120
122
|
const { data, isLoading, error, refetch } = hooks.useQuery(
|
|
@@ -197,6 +199,6 @@ function SessionsPage() {
|
|
|
197
199
|
] });
|
|
198
200
|
}
|
|
199
201
|
export {
|
|
200
|
-
|
|
202
|
+
Sessions
|
|
201
203
|
};
|
|
202
|
-
//# sourceMappingURL=sessions
|
|
204
|
+
//# sourceMappingURL=sessions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/iam/sessions.tsx","../../../src/provider.tsx","../../../src/utils/cookie.ts","../../../src/components/shared/data-table.tsx","../../../src/components/skeletons/table-skeleton.tsx"],"sourcesContent":["'use client';\n\nimport { Button } from '@mesob/ui/components/button';\nimport { useState } from 'react';\nimport { useApi } from '../../provider';\nimport { DataTable, type DataTableColumn } from '../shared/data-table';\n\n// Session type from OpenAPI schema\ntype Session = {\n id: string;\n userId: string;\n expiresAt: string;\n createdAt: string;\n userAgent: string | null;\n ip: string | null;\n};\n\nexport function Sessions() {\n const { hooks } = useApi();\n const [selectedSessionId] = useState<string | null>(null);\n\n // Fetch sessions\n const { data, isLoading, error, refetch } = hooks.useQuery(\n 'get',\n '/sessions',\n );\n\n // Delete session mutation\n const deleteMutation = hooks.useMutation('delete', '/sessions/{id}', {\n onSuccess: () => {\n // Refetch sessions after deletion\n refetch();\n },\n });\n\n const handleDeleteSession = async (sessionId: string) => {\n if (confirm('Are you sure you want to revoke this session?')) {\n try {\n await deleteMutation.mutateAsync({\n params: {\n path: { id: sessionId },\n },\n });\n } catch (_err) {\n // Error handled by mutation error state\n }\n }\n };\n\n const columns: DataTableColumn<Session>[] = [\n {\n key: 'createdAt',\n header: 'Created',\n cell: (session) => (\n <div>\n <p className=\"text-sm font-medium\">\n {new Date(session.createdAt).toLocaleDateString()}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {new Date(session.createdAt).toLocaleTimeString()}\n </p>\n </div>\n ),\n },\n {\n key: 'expiresAt',\n header: 'Expires',\n cell: (session) => (\n <p className=\"text-sm\">\n {new Date(session.expiresAt).toLocaleDateString()}\n </p>\n ),\n },\n {\n key: 'userAgent',\n header: 'Device',\n cell: (session) => (\n <p className=\"text-sm truncate max-w-xs\">\n {session.userAgent || 'Unknown'}\n </p>\n ),\n },\n {\n key: 'ip',\n header: 'IP Address',\n cell: (session) => (\n <p className=\"text-sm font-mono\">{session.ip || 'Unknown'}</p>\n ),\n },\n {\n key: 'actions',\n header: 'Actions',\n cell: (session) => (\n <Button\n variant=\"destructive\"\n size=\"sm\"\n onClick={() => handleDeleteSession(session.id)}\n disabled={\n deleteMutation.isPending && selectedSessionId === session.id\n }\n >\n {deleteMutation.isPending && selectedSessionId === session.id\n ? 'Revoking...'\n : 'Revoke'}\n </Button>\n ),\n },\n ];\n\n if (error) {\n return (\n <div className=\"p-6 text-center\">\n <p className=\"text-destructive\">Error loading sessions</p>\n </div>\n );\n }\n\n return (\n <div className=\"w-full p-6 space-y-4\">\n <div>\n <h1 className=\"text-3xl font-bold\">Sessions</h1>\n <p className=\"text-muted-foreground\">View and manage active sessions</p>\n </div>\n\n <DataTable\n data={(data as { sessions: Session[] })?.sessions || []}\n columns={columns}\n isLoading={isLoading}\n emptyMessage=\"No active sessions\"\n />\n </div>\n );\n}\n","'use client';\n\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport { deepmerge } from 'deepmerge-ts';\nimport createFetchClient from 'openapi-fetch';\nimport createClient from 'openapi-react-query';\nimport type { ReactNode } from 'react';\nimport { createContext, useContext, useMemo, useState } from 'react';\nimport type { paths } from './data/openapi';\nimport { createTranslator } from './lib/translations';\nimport {\n type AuthClientConfig,\n type AuthResponse,\n defaultAuthClientConfig,\n type Session,\n type User,\n} from './types';\nimport { getSessionCookieName } from './utils/cookie';\nimport { createCustomFetch } from './utils/custom-fetch';\n\n// biome-ignore lint/suspicious/noExplicitAny: OpenAPI hooks type\ntype OpenApiHooks = any;\n\n// --- Utility: Check if running on server ---\nfunction isServer(): boolean {\n return typeof document === 'undefined';\n}\n\n/**\n * @deprecated Cookie is httpOnly and cannot be read client-side.\n * Use `useSession().isAuthenticated` instead.\n * This function always returns false on client.\n */\nexport function hasAuthCookie(_cookieName: string): boolean {\n // Cookie is httpOnly, can't check client-side\n // Always return false - use useSession() for auth status\n return false;\n}\n\n// --- Types ---\nexport type AuthStatus = 'loading' | 'authenticated' | 'unauthenticated';\n\ntype AuthState = {\n user: User | null;\n session: Session | null;\n status: AuthStatus;\n error: Error | null;\n};\n\ntype SessionContextValue = AuthState & {\n isLoading: boolean;\n isAuthenticated: boolean;\n refresh: () => Promise<void>;\n signOut: () => Promise<void>;\n};\n\ntype ApiContextValue = {\n hooks: OpenApiHooks;\n setAuth: (auth: AuthResponse) => void;\n clearAuth: () => void;\n refresh: () => Promise<void>;\n};\n\ntype ConfigContextValue = {\n config: AuthClientConfig;\n cookieName: string;\n t: (key: string, params?: Record<string, string | number>) => string;\n};\n\nconst SessionContext = createContext<SessionContextValue | null>(null);\nconst ApiContext = createContext<ApiContextValue | null>(null);\nconst ConfigContext = createContext<ConfigContextValue | null>(null);\n\nconst queryClient = new QueryClient({\n defaultOptions: {\n queries: {\n refetchOnWindowFocus: false,\n },\n },\n});\n\n// --- Hooks ---\n\n/**\n * Get session state including user, session, and auth status.\n * - `status`: 'loading' | 'authenticated' | 'unauthenticated'\n * - `isLoading`: true while fetching session\n * - `isAuthenticated`: true if user and session exist\n */\nexport function useSession(): SessionContextValue {\n const context = useContext(SessionContext);\n if (!context) {\n throw new Error('useSession must be used within MesobAuthProvider');\n }\n return context;\n}\n\nexport function useApi(): ApiContextValue {\n const context = useContext(ApiContext);\n if (!context) {\n throw new Error('useApi must be used within MesobAuthProvider');\n }\n return context;\n}\n\nexport function useConfig(): ConfigContextValue {\n const context = useContext(ConfigContext);\n if (!context) {\n throw new Error('useConfig must be used within MesobAuthProvider');\n }\n return context;\n}\n\n/**\n * @deprecated Cookie is httpOnly, can't be checked client-side.\n * Use `useSession().isAuthenticated` instead.\n */\nexport function useHasAuthCookie(): boolean {\n const { status } = useSession();\n return status === 'authenticated' || status === 'loading';\n}\n\n// --- Provider ---\n\ntype MesobAuthProviderProps = {\n config: AuthClientConfig;\n children: ReactNode;\n};\n\nexport function MesobAuthProvider({\n config,\n children,\n}: MesobAuthProviderProps) {\n const mergedConfig = useMemo(\n () =>\n deepmerge(\n { ...defaultAuthClientConfig } as Partial<AuthClientConfig>,\n config,\n ) as AuthClientConfig,\n [config],\n );\n\n const api = useMemo(\n () =>\n createFetchClient<paths>({\n baseUrl: mergedConfig.baseURL,\n fetch: createCustomFetch(mergedConfig),\n }),\n [mergedConfig],\n );\n\n const hooks = useMemo(() => createClient(api), [api]);\n const cookieName = useMemo(\n () => getSessionCookieName(mergedConfig),\n [mergedConfig],\n );\n\n return (\n <QueryClientProvider client={queryClient}>\n <AuthStateProvider\n config={mergedConfig}\n hooks={hooks}\n cookieName={cookieName}\n >\n {children}\n </AuthStateProvider>\n </QueryClientProvider>\n );\n}\n\ntype AuthStateProviderProps = {\n config: AuthClientConfig;\n hooks: OpenApiHooks;\n cookieName: string;\n children: ReactNode;\n};\n\nfunction AuthStateProvider({\n config,\n hooks,\n cookieName,\n children,\n}: AuthStateProviderProps) {\n // Manual override for sign-out / sign-in\n const [override, setOverride] = useState<AuthState | null>(null);\n\n // Always fetch session - cookie is httpOnly, can't check client-side\n // Server will read the cookie and return user/session if valid\n const {\n data: sessionData,\n isLoading,\n isFetched,\n error: sessionError,\n refetch,\n } = hooks.useQuery(\n 'get',\n '/session',\n {},\n {\n enabled: !(override || isServer()),\n refetchOnMount: false,\n refetchOnWindowFocus: false,\n refetchOnReconnect: false,\n retry: false,\n gcTime: 0,\n staleTime: 0,\n },\n );\n\n // Derive state directly - no useEffect\n const user = override?.user ?? sessionData?.user ?? null;\n const session = override?.session ?? sessionData?.session ?? null;\n const error = override?.error ?? (sessionError as Error | null);\n\n // Check error status code\n const errorStatus = (() => {\n if (!sessionError) {\n return null;\n }\n const err = sessionError as { status?: number };\n return err.status ?? null;\n })();\n\n // Check if error is a network/connection error\n const isNetworkError = (() => {\n if (!sessionError) {\n return false;\n }\n const error = sessionError as Error & { cause?: unknown; data?: unknown };\n const errorMessage =\n error.message || String(error) || JSON.stringify(error);\n // Network errors: TypeError, DOMException, or fetch failures\n if (\n error instanceof TypeError ||\n error instanceof DOMException ||\n error.name === 'TypeError' ||\n errorMessage.includes('Failed to fetch') ||\n errorMessage.includes('ERR_CONNECTION_REFUSED') ||\n errorMessage.includes('NetworkError') ||\n errorMessage.includes('Network request failed') ||\n errorMessage.includes('fetch failed')\n ) {\n return true;\n }\n // Check error cause\n if (error.cause) {\n const causeStr = String(error.cause);\n if (\n causeStr.includes('Failed to fetch') ||\n causeStr.includes('ERR_CONNECTION_REFUSED') ||\n causeStr.includes('NetworkError')\n ) {\n return true;\n }\n }\n return false;\n })();\n\n // Compute status\n // biome-ignore lint: Status determination requires multiple checks\n const status: AuthStatus = (() => {\n if (override) {\n return override.status;\n }\n if (isServer()) {\n return 'loading';\n }\n if (user && session) {\n return 'authenticated';\n }\n // Check for network errors or auth errors first - allow auth page to show\n if (isNetworkError || errorStatus === 401) {\n return 'unauthenticated';\n }\n // If we have an error but it's not a network error, still check loading state\n if (sessionError && !isNetworkError && errorStatus !== 401) {\n if (errorStatus && errorStatus >= 500) {\n return 'authenticated';\n }\n // Other errors mean unauthenticated\n if (isFetched) {\n return 'unauthenticated';\n }\n }\n if (isLoading || !isFetched) {\n return 'loading';\n }\n if (isFetched && !user && !session) {\n return 'unauthenticated';\n }\n return 'unauthenticated';\n })();\n\n const signOutMutation = hooks.useMutation('post', '/sign-out');\n const t = createTranslator(config.messages || {});\n\n const setAuth = (auth: AuthResponse) => {\n setOverride({\n user: auth.user,\n session: auth.session,\n status: 'authenticated',\n error: null,\n });\n };\n\n const clearAuth = () => {\n setOverride({\n user: null,\n session: null,\n status: 'unauthenticated',\n error: null,\n });\n };\n\n const refresh = async () => {\n setOverride(null);\n await refetch();\n };\n\n const signOut = async () => {\n try {\n await signOutMutation.mutateAsync({});\n } finally {\n clearAuth();\n }\n };\n\n return (\n <ConfigContext.Provider value={{ config, cookieName, t }}>\n <ApiContext.Provider value={{ hooks, setAuth, clearAuth, refresh }}>\n <SessionContext.Provider\n value={{\n user,\n session,\n status,\n error,\n isLoading: status === 'loading',\n isAuthenticated: status === 'authenticated',\n refresh,\n signOut,\n }}\n >\n {children}\n </SessionContext.Provider>\n </ApiContext.Provider>\n </ConfigContext.Provider>\n );\n}\n","import type { AuthClientConfig } from '../types';\n\nconst isProduction =\n typeof process !== 'undefined' && process.env.NODE_ENV === 'production';\n\nexport const getSessionCookieName = (config: AuthClientConfig): string => {\n const prefix = config.cookiePrefix || '';\n const baseName = 'session_token';\n if (prefix) {\n return `${prefix}_${baseName}`;\n }\n return isProduction ? '__Host-session_token' : baseName;\n};\n","'use client';\n\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '@mesob/ui/components/table';\nimport type { ReactNode } from 'react';\nimport { TableSkeleton } from '../skeletons/table-skeleton';\n\nexport type DataTableColumn<T> = {\n key: string;\n header: string;\n cell: (row: T) => ReactNode;\n sortable?: boolean;\n};\n\ntype DataTableProps<T> = {\n data: T[];\n columns: DataTableColumn<T>[];\n isLoading?: boolean;\n onRowClick?: (row: T) => void;\n emptyMessage?: string;\n actions?: ReactNode;\n};\n\nexport function DataTable<T extends { id: string }>({\n data,\n columns,\n isLoading,\n onRowClick,\n emptyMessage = 'No data available',\n actions,\n}: DataTableProps<T>) {\n if (isLoading) {\n return <TableSkeleton columns={columns.length} rows={5} />;\n }\n\n if (data.length === 0) {\n return (\n <div className=\"flex flex-col items-center justify-center min-h-[400px] p-6\">\n <p className=\"text-muted-foreground\">{emptyMessage}</p>\n {actions && <div className=\"mt-4\">{actions}</div>}\n </div>\n );\n }\n\n return (\n <div className=\"w-full space-y-4\">\n {actions && <div className=\"flex justify-end\">{actions}</div>}\n\n <div className=\"border rounded-lg overflow-hidden\">\n <Table>\n <TableHeader>\n <TableRow>\n {columns.map((column) => (\n <TableHead key={column.key}>{column.header}</TableHead>\n ))}\n </TableRow>\n </TableHeader>\n <TableBody>\n {data.map((row) => (\n <TableRow\n key={row.id}\n onClick={() => onRowClick?.(row)}\n className={onRowClick ? 'cursor-pointer hover:bg-muted/50' : ''}\n >\n {columns.map((column) => (\n <TableCell key={`${row.id}-${column.key}`}>\n {column.cell(row)}\n </TableCell>\n ))}\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n </div>\n );\n}\n","'use client';\n\nimport { Skeleton } from '@mesob/ui/components/skeleton';\n\ntype TableSkeletonProps = {\n columns?: number;\n rows?: number;\n};\n\nexport function TableSkeleton({ columns = 5, rows = 10 }: TableSkeletonProps) {\n const headerKeys = Array.from({ length: columns }, (_, i) => `header-${i}`);\n const rowKeys = Array.from({ length: rows }, (_, i) => `row-${i}`);\n const cellKeys = Array.from({ length: rows }, (_, rowIdx) =>\n Array.from({ length: columns }, (_, colIdx) => `cell-${rowIdx}-${colIdx}`),\n );\n\n return (\n <div className=\"w-full space-y-4\">\n {/* Header */}\n <div className=\"flex justify-between items-center\">\n <Skeleton className=\"h-8 w-48\" />\n <Skeleton className=\"h-10 w-32\" />\n </div>\n\n {/* Search/filters */}\n <div className=\"flex gap-4\">\n <Skeleton className=\"h-10 flex-1 max-w-sm\" />\n <Skeleton className=\"h-10 w-24\" />\n <Skeleton className=\"h-10 w-24\" />\n </div>\n\n {/* Table */}\n <div className=\"border rounded-lg overflow-hidden\">\n {/* Table header */}\n <div className=\"flex gap-4 p-4 bg-muted\">\n {headerKeys.map((key) => (\n <Skeleton key={key} className=\"h-4 flex-1\" />\n ))}\n </div>\n\n {/* Table rows */}\n {rowKeys.map((rowKey, rowIdx) => (\n <div key={rowKey} className=\"flex gap-4 p-4 border-t\">\n {cellKeys[rowIdx]?.map((cellKey) => (\n <Skeleton key={cellKey} className=\"h-4 flex-1\" />\n ))}\n </div>\n ))}\n </div>\n\n {/* Pagination */}\n <div className=\"flex justify-between items-center\">\n <Skeleton className=\"h-4 w-32\" />\n <div className=\"flex gap-2\">\n <Skeleton className=\"h-10 w-20\" />\n <Skeleton className=\"h-10 w-20\" />\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["useState","jsx","_","jsx","jsxs","jsx","jsxs","useState"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
// src/components/iam/tenants
|
|
3
|
+
// src/components/iam/tenants.tsx
|
|
4
4
|
import { Badge } from "@mesob/ui/components/badge";
|
|
5
5
|
import { Button } from "@mesob/ui/components/button";
|
|
6
6
|
import { useState as useState2 } from "react";
|
|
@@ -10,7 +10,12 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
|
10
10
|
import { deepmerge } from "deepmerge-ts";
|
|
11
11
|
import createFetchClient from "openapi-fetch";
|
|
12
12
|
import createClient from "openapi-react-query";
|
|
13
|
-
import { createContext, useContext,
|
|
13
|
+
import { createContext, useContext, useMemo, useState } from "react";
|
|
14
|
+
|
|
15
|
+
// src/utils/cookie.ts
|
|
16
|
+
var isProduction = typeof process !== "undefined" && process.env.NODE_ENV === "production";
|
|
17
|
+
|
|
18
|
+
// src/provider.tsx
|
|
14
19
|
import { jsx } from "react/jsx-runtime";
|
|
15
20
|
var SessionContext = createContext(null);
|
|
16
21
|
var ApiContext = createContext(null);
|
|
@@ -18,9 +23,6 @@ var ConfigContext = createContext(null);
|
|
|
18
23
|
var queryClient = new QueryClient({
|
|
19
24
|
defaultOptions: {
|
|
20
25
|
queries: {
|
|
21
|
-
staleTime: 1e3 * 60 * 5,
|
|
22
|
-
gcTime: 1e3 * 60 * 10,
|
|
23
|
-
retry: 1,
|
|
24
26
|
refetchOnWindowFocus: false
|
|
25
27
|
}
|
|
26
28
|
}
|
|
@@ -33,7 +35,7 @@ function useApi() {
|
|
|
33
35
|
return context;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
// src/components/shared/data-table
|
|
38
|
+
// src/components/shared/data-table.tsx
|
|
37
39
|
import {
|
|
38
40
|
Table,
|
|
39
41
|
TableBody,
|
|
@@ -77,7 +79,7 @@ function TableSkeleton({ columns = 5, rows = 10 }) {
|
|
|
77
79
|
] });
|
|
78
80
|
}
|
|
79
81
|
|
|
80
|
-
// src/components/shared/data-table
|
|
82
|
+
// src/components/shared/data-table.tsx
|
|
81
83
|
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
82
84
|
function DataTable({
|
|
83
85
|
data,
|
|
@@ -113,9 +115,9 @@ function DataTable({
|
|
|
113
115
|
] });
|
|
114
116
|
}
|
|
115
117
|
|
|
116
|
-
// src/components/iam/tenants
|
|
118
|
+
// src/components/iam/tenants.tsx
|
|
117
119
|
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
118
|
-
function
|
|
120
|
+
function Tenants() {
|
|
119
121
|
const { hooks } = useApi();
|
|
120
122
|
const [page, setPage] = useState2(1);
|
|
121
123
|
const limit = 20;
|
|
@@ -197,6 +199,6 @@ function TenantsPage() {
|
|
|
197
199
|
] });
|
|
198
200
|
}
|
|
199
201
|
export {
|
|
200
|
-
|
|
202
|
+
Tenants
|
|
201
203
|
};
|
|
202
|
-
//# sourceMappingURL=tenants
|
|
204
|
+
//# sourceMappingURL=tenants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/iam/tenants.tsx","../../../src/provider.tsx","../../../src/utils/cookie.ts","../../../src/components/shared/data-table.tsx","../../../src/components/skeletons/table-skeleton.tsx"],"sourcesContent":["'use client';\n\nimport { Badge } from '@mesob/ui/components/badge';\nimport { Button } from '@mesob/ui/components/button';\nimport { useState } from 'react';\nimport { useApi } from '../../provider';\nimport { DataTable, type DataTableColumn } from '../shared/data-table';\n\n// Tenant type from OpenAPI schema\ntype Tenant = {\n id: string;\n name: string;\n slug: string;\n status: string;\n createdAt: string;\n};\n\nexport function Tenants() {\n const { hooks } = useApi();\n const [page, setPage] = useState(1);\n const limit = 20;\n\n // Use openapi-react-query hooks\n const { data, isLoading, error } = hooks.useQuery('get', '/tenants', {\n params: {\n query: {\n page: String(page),\n limit: String(limit),\n },\n },\n });\n\n const columns: DataTableColumn<Tenant>[] = [\n {\n key: 'name',\n header: 'Tenant',\n cell: (tenant) => (\n <div>\n <p className=\"font-medium\">{tenant.name}</p>\n <p className=\"text-sm text-muted-foreground\">/{tenant.slug}</p>\n </div>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n cell: (tenant) => (\n <Badge variant={tenant.status === 'active' ? 'default' : 'secondary'}>\n {tenant.status}\n </Badge>\n ),\n },\n {\n key: 'createdAt',\n header: 'Created',\n cell: (tenant) => (\n <p className=\"text-sm\">\n {new Date(tenant.createdAt).toLocaleDateString()}\n </p>\n ),\n },\n {\n key: 'actions',\n header: 'Actions',\n cell: (_tenant) => (\n <div className=\"flex gap-2\">\n <Button variant=\"outline\" size=\"sm\">\n Domains\n </Button>\n <Button variant=\"outline\" size=\"sm\">\n Edit\n </Button>\n </div>\n ),\n },\n ];\n\n if (error) {\n return (\n <div className=\"p-6 text-center\">\n <p className=\"text-destructive\">Error loading tenants</p>\n </div>\n );\n }\n\n return (\n <div className=\"w-full p-6 space-y-4\">\n <div className=\"flex justify-between items-center\">\n <div>\n <h1 className=\"text-3xl font-bold\">Tenants</h1>\n <p className=\"text-muted-foreground\">Manage tenant organizations</p>\n </div>\n <Button>Create Tenant</Button>\n </div>\n\n <DataTable\n data={(data as { tenants: Tenant[] })?.tenants || []}\n columns={columns}\n isLoading={isLoading}\n emptyMessage=\"No tenants found\"\n />\n\n {data &&\n 'tenants' in data &&\n data.tenants &&\n (data.tenants as Tenant[]).length >= limit && (\n <div className=\"flex justify-between items-center\">\n <Button\n variant=\"outline\"\n disabled={page === 1}\n onClick={() => setPage(page - 1)}\n >\n Previous\n </Button>\n <span className=\"text-sm text-muted-foreground\">Page {page}</span>\n <Button variant=\"outline\" onClick={() => setPage(page + 1)}>\n Next\n </Button>\n </div>\n )}\n </div>\n );\n}\n","'use client';\n\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport { deepmerge } from 'deepmerge-ts';\nimport createFetchClient from 'openapi-fetch';\nimport createClient from 'openapi-react-query';\nimport type { ReactNode } from 'react';\nimport { createContext, useContext, useMemo, useState } from 'react';\nimport type { paths } from './data/openapi';\nimport { createTranslator } from './lib/translations';\nimport {\n type AuthClientConfig,\n type AuthResponse,\n defaultAuthClientConfig,\n type Session,\n type User,\n} from './types';\nimport { getSessionCookieName } from './utils/cookie';\nimport { createCustomFetch } from './utils/custom-fetch';\n\n// biome-ignore lint/suspicious/noExplicitAny: OpenAPI hooks type\ntype OpenApiHooks = any;\n\n// --- Utility: Check if running on server ---\nfunction isServer(): boolean {\n return typeof document === 'undefined';\n}\n\n/**\n * @deprecated Cookie is httpOnly and cannot be read client-side.\n * Use `useSession().isAuthenticated` instead.\n * This function always returns false on client.\n */\nexport function hasAuthCookie(_cookieName: string): boolean {\n // Cookie is httpOnly, can't check client-side\n // Always return false - use useSession() for auth status\n return false;\n}\n\n// --- Types ---\nexport type AuthStatus = 'loading' | 'authenticated' | 'unauthenticated';\n\ntype AuthState = {\n user: User | null;\n session: Session | null;\n status: AuthStatus;\n error: Error | null;\n};\n\ntype SessionContextValue = AuthState & {\n isLoading: boolean;\n isAuthenticated: boolean;\n refresh: () => Promise<void>;\n signOut: () => Promise<void>;\n};\n\ntype ApiContextValue = {\n hooks: OpenApiHooks;\n setAuth: (auth: AuthResponse) => void;\n clearAuth: () => void;\n refresh: () => Promise<void>;\n};\n\ntype ConfigContextValue = {\n config: AuthClientConfig;\n cookieName: string;\n t: (key: string, params?: Record<string, string | number>) => string;\n};\n\nconst SessionContext = createContext<SessionContextValue | null>(null);\nconst ApiContext = createContext<ApiContextValue | null>(null);\nconst ConfigContext = createContext<ConfigContextValue | null>(null);\n\nconst queryClient = new QueryClient({\n defaultOptions: {\n queries: {\n refetchOnWindowFocus: false,\n },\n },\n});\n\n// --- Hooks ---\n\n/**\n * Get session state including user, session, and auth status.\n * - `status`: 'loading' | 'authenticated' | 'unauthenticated'\n * - `isLoading`: true while fetching session\n * - `isAuthenticated`: true if user and session exist\n */\nexport function useSession(): SessionContextValue {\n const context = useContext(SessionContext);\n if (!context) {\n throw new Error('useSession must be used within MesobAuthProvider');\n }\n return context;\n}\n\nexport function useApi(): ApiContextValue {\n const context = useContext(ApiContext);\n if (!context) {\n throw new Error('useApi must be used within MesobAuthProvider');\n }\n return context;\n}\n\nexport function useConfig(): ConfigContextValue {\n const context = useContext(ConfigContext);\n if (!context) {\n throw new Error('useConfig must be used within MesobAuthProvider');\n }\n return context;\n}\n\n/**\n * @deprecated Cookie is httpOnly, can't be checked client-side.\n * Use `useSession().isAuthenticated` instead.\n */\nexport function useHasAuthCookie(): boolean {\n const { status } = useSession();\n return status === 'authenticated' || status === 'loading';\n}\n\n// --- Provider ---\n\ntype MesobAuthProviderProps = {\n config: AuthClientConfig;\n children: ReactNode;\n};\n\nexport function MesobAuthProvider({\n config,\n children,\n}: MesobAuthProviderProps) {\n const mergedConfig = useMemo(\n () =>\n deepmerge(\n { ...defaultAuthClientConfig } as Partial<AuthClientConfig>,\n config,\n ) as AuthClientConfig,\n [config],\n );\n\n const api = useMemo(\n () =>\n createFetchClient<paths>({\n baseUrl: mergedConfig.baseURL,\n fetch: createCustomFetch(mergedConfig),\n }),\n [mergedConfig],\n );\n\n const hooks = useMemo(() => createClient(api), [api]);\n const cookieName = useMemo(\n () => getSessionCookieName(mergedConfig),\n [mergedConfig],\n );\n\n return (\n <QueryClientProvider client={queryClient}>\n <AuthStateProvider\n config={mergedConfig}\n hooks={hooks}\n cookieName={cookieName}\n >\n {children}\n </AuthStateProvider>\n </QueryClientProvider>\n );\n}\n\ntype AuthStateProviderProps = {\n config: AuthClientConfig;\n hooks: OpenApiHooks;\n cookieName: string;\n children: ReactNode;\n};\n\nfunction AuthStateProvider({\n config,\n hooks,\n cookieName,\n children,\n}: AuthStateProviderProps) {\n // Manual override for sign-out / sign-in\n const [override, setOverride] = useState<AuthState | null>(null);\n\n // Always fetch session - cookie is httpOnly, can't check client-side\n // Server will read the cookie and return user/session if valid\n const {\n data: sessionData,\n isLoading,\n isFetched,\n error: sessionError,\n refetch,\n } = hooks.useQuery(\n 'get',\n '/session',\n {},\n {\n enabled: !(override || isServer()),\n refetchOnMount: false,\n refetchOnWindowFocus: false,\n refetchOnReconnect: false,\n retry: false,\n gcTime: 0,\n staleTime: 0,\n },\n );\n\n // Derive state directly - no useEffect\n const user = override?.user ?? sessionData?.user ?? null;\n const session = override?.session ?? sessionData?.session ?? null;\n const error = override?.error ?? (sessionError as Error | null);\n\n // Check error status code\n const errorStatus = (() => {\n if (!sessionError) {\n return null;\n }\n const err = sessionError as { status?: number };\n return err.status ?? null;\n })();\n\n // Check if error is a network/connection error\n const isNetworkError = (() => {\n if (!sessionError) {\n return false;\n }\n const error = sessionError as Error & { cause?: unknown; data?: unknown };\n const errorMessage =\n error.message || String(error) || JSON.stringify(error);\n // Network errors: TypeError, DOMException, or fetch failures\n if (\n error instanceof TypeError ||\n error instanceof DOMException ||\n error.name === 'TypeError' ||\n errorMessage.includes('Failed to fetch') ||\n errorMessage.includes('ERR_CONNECTION_REFUSED') ||\n errorMessage.includes('NetworkError') ||\n errorMessage.includes('Network request failed') ||\n errorMessage.includes('fetch failed')\n ) {\n return true;\n }\n // Check error cause\n if (error.cause) {\n const causeStr = String(error.cause);\n if (\n causeStr.includes('Failed to fetch') ||\n causeStr.includes('ERR_CONNECTION_REFUSED') ||\n causeStr.includes('NetworkError')\n ) {\n return true;\n }\n }\n return false;\n })();\n\n // Compute status\n // biome-ignore lint: Status determination requires multiple checks\n const status: AuthStatus = (() => {\n if (override) {\n return override.status;\n }\n if (isServer()) {\n return 'loading';\n }\n if (user && session) {\n return 'authenticated';\n }\n // Check for network errors or auth errors first - allow auth page to show\n if (isNetworkError || errorStatus === 401) {\n return 'unauthenticated';\n }\n // If we have an error but it's not a network error, still check loading state\n if (sessionError && !isNetworkError && errorStatus !== 401) {\n if (errorStatus && errorStatus >= 500) {\n return 'authenticated';\n }\n // Other errors mean unauthenticated\n if (isFetched) {\n return 'unauthenticated';\n }\n }\n if (isLoading || !isFetched) {\n return 'loading';\n }\n if (isFetched && !user && !session) {\n return 'unauthenticated';\n }\n return 'unauthenticated';\n })();\n\n const signOutMutation = hooks.useMutation('post', '/sign-out');\n const t = createTranslator(config.messages || {});\n\n const setAuth = (auth: AuthResponse) => {\n setOverride({\n user: auth.user,\n session: auth.session,\n status: 'authenticated',\n error: null,\n });\n };\n\n const clearAuth = () => {\n setOverride({\n user: null,\n session: null,\n status: 'unauthenticated',\n error: null,\n });\n };\n\n const refresh = async () => {\n setOverride(null);\n await refetch();\n };\n\n const signOut = async () => {\n try {\n await signOutMutation.mutateAsync({});\n } finally {\n clearAuth();\n }\n };\n\n return (\n <ConfigContext.Provider value={{ config, cookieName, t }}>\n <ApiContext.Provider value={{ hooks, setAuth, clearAuth, refresh }}>\n <SessionContext.Provider\n value={{\n user,\n session,\n status,\n error,\n isLoading: status === 'loading',\n isAuthenticated: status === 'authenticated',\n refresh,\n signOut,\n }}\n >\n {children}\n </SessionContext.Provider>\n </ApiContext.Provider>\n </ConfigContext.Provider>\n );\n}\n","import type { AuthClientConfig } from '../types';\n\nconst isProduction =\n typeof process !== 'undefined' && process.env.NODE_ENV === 'production';\n\nexport const getSessionCookieName = (config: AuthClientConfig): string => {\n const prefix = config.cookiePrefix || '';\n const baseName = 'session_token';\n if (prefix) {\n return `${prefix}_${baseName}`;\n }\n return isProduction ? '__Host-session_token' : baseName;\n};\n","'use client';\n\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '@mesob/ui/components/table';\nimport type { ReactNode } from 'react';\nimport { TableSkeleton } from '../skeletons/table-skeleton';\n\nexport type DataTableColumn<T> = {\n key: string;\n header: string;\n cell: (row: T) => ReactNode;\n sortable?: boolean;\n};\n\ntype DataTableProps<T> = {\n data: T[];\n columns: DataTableColumn<T>[];\n isLoading?: boolean;\n onRowClick?: (row: T) => void;\n emptyMessage?: string;\n actions?: ReactNode;\n};\n\nexport function DataTable<T extends { id: string }>({\n data,\n columns,\n isLoading,\n onRowClick,\n emptyMessage = 'No data available',\n actions,\n}: DataTableProps<T>) {\n if (isLoading) {\n return <TableSkeleton columns={columns.length} rows={5} />;\n }\n\n if (data.length === 0) {\n return (\n <div className=\"flex flex-col items-center justify-center min-h-[400px] p-6\">\n <p className=\"text-muted-foreground\">{emptyMessage}</p>\n {actions && <div className=\"mt-4\">{actions}</div>}\n </div>\n );\n }\n\n return (\n <div className=\"w-full space-y-4\">\n {actions && <div className=\"flex justify-end\">{actions}</div>}\n\n <div className=\"border rounded-lg overflow-hidden\">\n <Table>\n <TableHeader>\n <TableRow>\n {columns.map((column) => (\n <TableHead key={column.key}>{column.header}</TableHead>\n ))}\n </TableRow>\n </TableHeader>\n <TableBody>\n {data.map((row) => (\n <TableRow\n key={row.id}\n onClick={() => onRowClick?.(row)}\n className={onRowClick ? 'cursor-pointer hover:bg-muted/50' : ''}\n >\n {columns.map((column) => (\n <TableCell key={`${row.id}-${column.key}`}>\n {column.cell(row)}\n </TableCell>\n ))}\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n </div>\n );\n}\n","'use client';\n\nimport { Skeleton } from '@mesob/ui/components/skeleton';\n\ntype TableSkeletonProps = {\n columns?: number;\n rows?: number;\n};\n\nexport function TableSkeleton({ columns = 5, rows = 10 }: TableSkeletonProps) {\n const headerKeys = Array.from({ length: columns }, (_, i) => `header-${i}`);\n const rowKeys = Array.from({ length: rows }, (_, i) => `row-${i}`);\n const cellKeys = Array.from({ length: rows }, (_, rowIdx) =>\n Array.from({ length: columns }, (_, colIdx) => `cell-${rowIdx}-${colIdx}`),\n );\n\n return (\n <div className=\"w-full space-y-4\">\n {/* Header */}\n <div className=\"flex justify-between items-center\">\n <Skeleton className=\"h-8 w-48\" />\n <Skeleton className=\"h-10 w-32\" />\n </div>\n\n {/* Search/filters */}\n <div className=\"flex gap-4\">\n <Skeleton className=\"h-10 flex-1 max-w-sm\" />\n <Skeleton className=\"h-10 w-24\" />\n <Skeleton className=\"h-10 w-24\" />\n </div>\n\n {/* Table */}\n <div className=\"border rounded-lg overflow-hidden\">\n {/* Table header */}\n <div className=\"flex gap-4 p-4 bg-muted\">\n {headerKeys.map((key) => (\n <Skeleton key={key} className=\"h-4 flex-1\" />\n ))}\n </div>\n\n {/* Table rows */}\n {rowKeys.map((rowKey, rowIdx) => (\n <div key={rowKey} className=\"flex gap-4 p-4 border-t\">\n {cellKeys[rowIdx]?.map((cellKey) => (\n <Skeleton key={cellKey} className=\"h-4 flex-1\" />\n ))}\n </div>\n ))}\n </div>\n\n {/* Pagination */}\n <div className=\"flex justify-between items-center\">\n <Skeleton className=\"h-4 w-32\" />\n <div className=\"flex gap-2\">\n <Skeleton className=\"h-10 w-20\" />\n <Skeleton className=\"h-10 w-20\" />\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["useState","jsx","_","jsx","jsxs","jsx","jsxs","useState"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
// src/components/iam/users
|
|
3
|
+
// src/components/iam/users.tsx
|
|
4
4
|
import { Badge } from "@mesob/ui/components/badge";
|
|
5
5
|
import { Button } from "@mesob/ui/components/button";
|
|
6
6
|
import { useState as useState2 } from "react";
|
|
@@ -10,7 +10,12 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
|
10
10
|
import { deepmerge } from "deepmerge-ts";
|
|
11
11
|
import createFetchClient from "openapi-fetch";
|
|
12
12
|
import createClient from "openapi-react-query";
|
|
13
|
-
import { createContext, useContext,
|
|
13
|
+
import { createContext, useContext, useMemo, useState } from "react";
|
|
14
|
+
|
|
15
|
+
// src/utils/cookie.ts
|
|
16
|
+
var isProduction = typeof process !== "undefined" && process.env.NODE_ENV === "production";
|
|
17
|
+
|
|
18
|
+
// src/provider.tsx
|
|
14
19
|
import { jsx } from "react/jsx-runtime";
|
|
15
20
|
var SessionContext = createContext(null);
|
|
16
21
|
var ApiContext = createContext(null);
|
|
@@ -18,9 +23,6 @@ var ConfigContext = createContext(null);
|
|
|
18
23
|
var queryClient = new QueryClient({
|
|
19
24
|
defaultOptions: {
|
|
20
25
|
queries: {
|
|
21
|
-
staleTime: 1e3 * 60 * 5,
|
|
22
|
-
gcTime: 1e3 * 60 * 10,
|
|
23
|
-
retry: 1,
|
|
24
26
|
refetchOnWindowFocus: false
|
|
25
27
|
}
|
|
26
28
|
}
|
|
@@ -33,7 +35,7 @@ function useApi() {
|
|
|
33
35
|
return context;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
// src/components/shared/data-table
|
|
38
|
+
// src/components/shared/data-table.tsx
|
|
37
39
|
import {
|
|
38
40
|
Table,
|
|
39
41
|
TableBody,
|
|
@@ -77,7 +79,7 @@ function TableSkeleton({ columns = 5, rows = 10 }) {
|
|
|
77
79
|
] });
|
|
78
80
|
}
|
|
79
81
|
|
|
80
|
-
// src/components/shared/data-table
|
|
82
|
+
// src/components/shared/data-table.tsx
|
|
81
83
|
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
82
84
|
function DataTable({
|
|
83
85
|
data,
|
|
@@ -113,9 +115,9 @@ function DataTable({
|
|
|
113
115
|
] });
|
|
114
116
|
}
|
|
115
117
|
|
|
116
|
-
// src/components/iam/users
|
|
118
|
+
// src/components/iam/users.tsx
|
|
117
119
|
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
118
|
-
function
|
|
120
|
+
function Users() {
|
|
119
121
|
const { hooks } = useApi();
|
|
120
122
|
const [page, setPage] = useState2(1);
|
|
121
123
|
const limit = 20;
|
|
@@ -206,6 +208,6 @@ function UsersPage() {
|
|
|
206
208
|
] });
|
|
207
209
|
}
|
|
208
210
|
export {
|
|
209
|
-
|
|
211
|
+
Users
|
|
210
212
|
};
|
|
211
|
-
//# sourceMappingURL=users
|
|
213
|
+
//# sourceMappingURL=users.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/iam/users.tsx","../../../src/provider.tsx","../../../src/utils/cookie.ts","../../../src/components/shared/data-table.tsx","../../../src/components/skeletons/table-skeleton.tsx"],"sourcesContent":["'use client';\n\nimport { Badge } from '@mesob/ui/components/badge';\nimport { Button } from '@mesob/ui/components/button';\nimport { useState } from 'react';\nimport { useApi } from '../../provider';\nimport { DataTable, type DataTableColumn } from '../shared/data-table';\n\n// User type from OpenAPI schema\ntype User = {\n id: string;\n fullName: string;\n email: string | null;\n phone: string | null;\n handle: string;\n emailVerified: boolean;\n phoneVerified: boolean;\n lastSignInAt: string | null;\n};\n\nexport function Users() {\n const { hooks } = useApi();\n const [page, setPage] = useState(1);\n const limit = 20;\n\n // Use openapi-react-query hooks\n const { data, isLoading, error } = hooks.useQuery('get', '/users', {\n params: {\n query: {\n page: String(page),\n limit: String(limit),\n },\n },\n });\n\n const columns: DataTableColumn<User>[] = [\n {\n key: 'fullName',\n header: 'Name',\n cell: (user) => (\n <div>\n <p className=\"font-medium\">{user.fullName}</p>\n <p className=\"text-sm text-muted-foreground\">@{user.handle}</p>\n </div>\n ),\n },\n {\n key: 'contact',\n header: 'Contact',\n cell: (user) => (\n <div className=\"space-y-1\">\n {user.email && (\n <div className=\"flex items-center gap-2\">\n <p className=\"text-sm\">{user.email}</p>\n {user.emailVerified && (\n <Badge variant=\"outline\" className=\"text-xs\">\n Verified\n </Badge>\n )}\n </div>\n )}\n {user.phone && (\n <div className=\"flex items-center gap-2\">\n <p className=\"text-sm\">{user.phone}</p>\n {user.phoneVerified && (\n <Badge variant=\"outline\" className=\"text-xs\">\n Verified\n </Badge>\n )}\n </div>\n )}\n </div>\n ),\n },\n {\n key: 'lastSignIn',\n header: 'Last Sign In',\n cell: (user) => (\n <p className=\"text-sm\">\n {user.lastSignInAt\n ? new Date(user.lastSignInAt).toLocaleDateString()\n : 'Never'}\n </p>\n ),\n },\n {\n key: 'actions',\n header: 'Actions',\n cell: (_user) => (\n <div className=\"flex gap-2\">\n <Button variant=\"outline\" size=\"sm\">\n View\n </Button>\n <Button variant=\"outline\" size=\"sm\">\n Edit\n </Button>\n </div>\n ),\n },\n ];\n\n if (error) {\n return (\n <div className=\"p-6 text-center\">\n <p className=\"text-destructive\">Error loading users</p>\n </div>\n );\n }\n\n return (\n <div className=\"w-full p-6 space-y-4\">\n <div className=\"flex justify-between items-center\">\n <div>\n <h1 className=\"text-3xl font-bold\">Users</h1>\n <p className=\"text-muted-foreground\">Manage user accounts</p>\n </div>\n <Button>Create User</Button>\n </div>\n\n <DataTable\n data={(data as { users: User[] })?.users || []}\n columns={columns}\n isLoading={isLoading}\n emptyMessage=\"No users found\"\n />\n\n {data &&\n 'users' in data &&\n data.users &&\n (data.users as User[]).length >= limit && (\n <div className=\"flex justify-between items-center\">\n <Button\n variant=\"outline\"\n disabled={page === 1}\n onClick={() => setPage(page - 1)}\n >\n Previous\n </Button>\n <span className=\"text-sm text-muted-foreground\">Page {page}</span>\n <Button variant=\"outline\" onClick={() => setPage(page + 1)}>\n Next\n </Button>\n </div>\n )}\n </div>\n );\n}\n","'use client';\n\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport { deepmerge } from 'deepmerge-ts';\nimport createFetchClient from 'openapi-fetch';\nimport createClient from 'openapi-react-query';\nimport type { ReactNode } from 'react';\nimport { createContext, useContext, useMemo, useState } from 'react';\nimport type { paths } from './data/openapi';\nimport { createTranslator } from './lib/translations';\nimport {\n type AuthClientConfig,\n type AuthResponse,\n defaultAuthClientConfig,\n type Session,\n type User,\n} from './types';\nimport { getSessionCookieName } from './utils/cookie';\nimport { createCustomFetch } from './utils/custom-fetch';\n\n// biome-ignore lint/suspicious/noExplicitAny: OpenAPI hooks type\ntype OpenApiHooks = any;\n\n// --- Utility: Check if running on server ---\nfunction isServer(): boolean {\n return typeof document === 'undefined';\n}\n\n/**\n * @deprecated Cookie is httpOnly and cannot be read client-side.\n * Use `useSession().isAuthenticated` instead.\n * This function always returns false on client.\n */\nexport function hasAuthCookie(_cookieName: string): boolean {\n // Cookie is httpOnly, can't check client-side\n // Always return false - use useSession() for auth status\n return false;\n}\n\n// --- Types ---\nexport type AuthStatus = 'loading' | 'authenticated' | 'unauthenticated';\n\ntype AuthState = {\n user: User | null;\n session: Session | null;\n status: AuthStatus;\n error: Error | null;\n};\n\ntype SessionContextValue = AuthState & {\n isLoading: boolean;\n isAuthenticated: boolean;\n refresh: () => Promise<void>;\n signOut: () => Promise<void>;\n};\n\ntype ApiContextValue = {\n hooks: OpenApiHooks;\n setAuth: (auth: AuthResponse) => void;\n clearAuth: () => void;\n refresh: () => Promise<void>;\n};\n\ntype ConfigContextValue = {\n config: AuthClientConfig;\n cookieName: string;\n t: (key: string, params?: Record<string, string | number>) => string;\n};\n\nconst SessionContext = createContext<SessionContextValue | null>(null);\nconst ApiContext = createContext<ApiContextValue | null>(null);\nconst ConfigContext = createContext<ConfigContextValue | null>(null);\n\nconst queryClient = new QueryClient({\n defaultOptions: {\n queries: {\n refetchOnWindowFocus: false,\n },\n },\n});\n\n// --- Hooks ---\n\n/**\n * Get session state including user, session, and auth status.\n * - `status`: 'loading' | 'authenticated' | 'unauthenticated'\n * - `isLoading`: true while fetching session\n * - `isAuthenticated`: true if user and session exist\n */\nexport function useSession(): SessionContextValue {\n const context = useContext(SessionContext);\n if (!context) {\n throw new Error('useSession must be used within MesobAuthProvider');\n }\n return context;\n}\n\nexport function useApi(): ApiContextValue {\n const context = useContext(ApiContext);\n if (!context) {\n throw new Error('useApi must be used within MesobAuthProvider');\n }\n return context;\n}\n\nexport function useConfig(): ConfigContextValue {\n const context = useContext(ConfigContext);\n if (!context) {\n throw new Error('useConfig must be used within MesobAuthProvider');\n }\n return context;\n}\n\n/**\n * @deprecated Cookie is httpOnly, can't be checked client-side.\n * Use `useSession().isAuthenticated` instead.\n */\nexport function useHasAuthCookie(): boolean {\n const { status } = useSession();\n return status === 'authenticated' || status === 'loading';\n}\n\n// --- Provider ---\n\ntype MesobAuthProviderProps = {\n config: AuthClientConfig;\n children: ReactNode;\n};\n\nexport function MesobAuthProvider({\n config,\n children,\n}: MesobAuthProviderProps) {\n const mergedConfig = useMemo(\n () =>\n deepmerge(\n { ...defaultAuthClientConfig } as Partial<AuthClientConfig>,\n config,\n ) as AuthClientConfig,\n [config],\n );\n\n const api = useMemo(\n () =>\n createFetchClient<paths>({\n baseUrl: mergedConfig.baseURL,\n fetch: createCustomFetch(mergedConfig),\n }),\n [mergedConfig],\n );\n\n const hooks = useMemo(() => createClient(api), [api]);\n const cookieName = useMemo(\n () => getSessionCookieName(mergedConfig),\n [mergedConfig],\n );\n\n return (\n <QueryClientProvider client={queryClient}>\n <AuthStateProvider\n config={mergedConfig}\n hooks={hooks}\n cookieName={cookieName}\n >\n {children}\n </AuthStateProvider>\n </QueryClientProvider>\n );\n}\n\ntype AuthStateProviderProps = {\n config: AuthClientConfig;\n hooks: OpenApiHooks;\n cookieName: string;\n children: ReactNode;\n};\n\nfunction AuthStateProvider({\n config,\n hooks,\n cookieName,\n children,\n}: AuthStateProviderProps) {\n // Manual override for sign-out / sign-in\n const [override, setOverride] = useState<AuthState | null>(null);\n\n // Always fetch session - cookie is httpOnly, can't check client-side\n // Server will read the cookie and return user/session if valid\n const {\n data: sessionData,\n isLoading,\n isFetched,\n error: sessionError,\n refetch,\n } = hooks.useQuery(\n 'get',\n '/session',\n {},\n {\n enabled: !(override || isServer()),\n refetchOnMount: false,\n refetchOnWindowFocus: false,\n refetchOnReconnect: false,\n retry: false,\n gcTime: 0,\n staleTime: 0,\n },\n );\n\n // Derive state directly - no useEffect\n const user = override?.user ?? sessionData?.user ?? null;\n const session = override?.session ?? sessionData?.session ?? null;\n const error = override?.error ?? (sessionError as Error | null);\n\n // Check error status code\n const errorStatus = (() => {\n if (!sessionError) {\n return null;\n }\n const err = sessionError as { status?: number };\n return err.status ?? null;\n })();\n\n // Check if error is a network/connection error\n const isNetworkError = (() => {\n if (!sessionError) {\n return false;\n }\n const error = sessionError as Error & { cause?: unknown; data?: unknown };\n const errorMessage =\n error.message || String(error) || JSON.stringify(error);\n // Network errors: TypeError, DOMException, or fetch failures\n if (\n error instanceof TypeError ||\n error instanceof DOMException ||\n error.name === 'TypeError' ||\n errorMessage.includes('Failed to fetch') ||\n errorMessage.includes('ERR_CONNECTION_REFUSED') ||\n errorMessage.includes('NetworkError') ||\n errorMessage.includes('Network request failed') ||\n errorMessage.includes('fetch failed')\n ) {\n return true;\n }\n // Check error cause\n if (error.cause) {\n const causeStr = String(error.cause);\n if (\n causeStr.includes('Failed to fetch') ||\n causeStr.includes('ERR_CONNECTION_REFUSED') ||\n causeStr.includes('NetworkError')\n ) {\n return true;\n }\n }\n return false;\n })();\n\n // Compute status\n // biome-ignore lint: Status determination requires multiple checks\n const status: AuthStatus = (() => {\n if (override) {\n return override.status;\n }\n if (isServer()) {\n return 'loading';\n }\n if (user && session) {\n return 'authenticated';\n }\n // Check for network errors or auth errors first - allow auth page to show\n if (isNetworkError || errorStatus === 401) {\n return 'unauthenticated';\n }\n // If we have an error but it's not a network error, still check loading state\n if (sessionError && !isNetworkError && errorStatus !== 401) {\n if (errorStatus && errorStatus >= 500) {\n return 'authenticated';\n }\n // Other errors mean unauthenticated\n if (isFetched) {\n return 'unauthenticated';\n }\n }\n if (isLoading || !isFetched) {\n return 'loading';\n }\n if (isFetched && !user && !session) {\n return 'unauthenticated';\n }\n return 'unauthenticated';\n })();\n\n const signOutMutation = hooks.useMutation('post', '/sign-out');\n const t = createTranslator(config.messages || {});\n\n const setAuth = (auth: AuthResponse) => {\n setOverride({\n user: auth.user,\n session: auth.session,\n status: 'authenticated',\n error: null,\n });\n };\n\n const clearAuth = () => {\n setOverride({\n user: null,\n session: null,\n status: 'unauthenticated',\n error: null,\n });\n };\n\n const refresh = async () => {\n setOverride(null);\n await refetch();\n };\n\n const signOut = async () => {\n try {\n await signOutMutation.mutateAsync({});\n } finally {\n clearAuth();\n }\n };\n\n return (\n <ConfigContext.Provider value={{ config, cookieName, t }}>\n <ApiContext.Provider value={{ hooks, setAuth, clearAuth, refresh }}>\n <SessionContext.Provider\n value={{\n user,\n session,\n status,\n error,\n isLoading: status === 'loading',\n isAuthenticated: status === 'authenticated',\n refresh,\n signOut,\n }}\n >\n {children}\n </SessionContext.Provider>\n </ApiContext.Provider>\n </ConfigContext.Provider>\n );\n}\n","import type { AuthClientConfig } from '../types';\n\nconst isProduction =\n typeof process !== 'undefined' && process.env.NODE_ENV === 'production';\n\nexport const getSessionCookieName = (config: AuthClientConfig): string => {\n const prefix = config.cookiePrefix || '';\n const baseName = 'session_token';\n if (prefix) {\n return `${prefix}_${baseName}`;\n }\n return isProduction ? '__Host-session_token' : baseName;\n};\n","'use client';\n\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '@mesob/ui/components/table';\nimport type { ReactNode } from 'react';\nimport { TableSkeleton } from '../skeletons/table-skeleton';\n\nexport type DataTableColumn<T> = {\n key: string;\n header: string;\n cell: (row: T) => ReactNode;\n sortable?: boolean;\n};\n\ntype DataTableProps<T> = {\n data: T[];\n columns: DataTableColumn<T>[];\n isLoading?: boolean;\n onRowClick?: (row: T) => void;\n emptyMessage?: string;\n actions?: ReactNode;\n};\n\nexport function DataTable<T extends { id: string }>({\n data,\n columns,\n isLoading,\n onRowClick,\n emptyMessage = 'No data available',\n actions,\n}: DataTableProps<T>) {\n if (isLoading) {\n return <TableSkeleton columns={columns.length} rows={5} />;\n }\n\n if (data.length === 0) {\n return (\n <div className=\"flex flex-col items-center justify-center min-h-[400px] p-6\">\n <p className=\"text-muted-foreground\">{emptyMessage}</p>\n {actions && <div className=\"mt-4\">{actions}</div>}\n </div>\n );\n }\n\n return (\n <div className=\"w-full space-y-4\">\n {actions && <div className=\"flex justify-end\">{actions}</div>}\n\n <div className=\"border rounded-lg overflow-hidden\">\n <Table>\n <TableHeader>\n <TableRow>\n {columns.map((column) => (\n <TableHead key={column.key}>{column.header}</TableHead>\n ))}\n </TableRow>\n </TableHeader>\n <TableBody>\n {data.map((row) => (\n <TableRow\n key={row.id}\n onClick={() => onRowClick?.(row)}\n className={onRowClick ? 'cursor-pointer hover:bg-muted/50' : ''}\n >\n {columns.map((column) => (\n <TableCell key={`${row.id}-${column.key}`}>\n {column.cell(row)}\n </TableCell>\n ))}\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n </div>\n );\n}\n","'use client';\n\nimport { Skeleton } from '@mesob/ui/components/skeleton';\n\ntype TableSkeletonProps = {\n columns?: number;\n rows?: number;\n};\n\nexport function TableSkeleton({ columns = 5, rows = 10 }: TableSkeletonProps) {\n const headerKeys = Array.from({ length: columns }, (_, i) => `header-${i}`);\n const rowKeys = Array.from({ length: rows }, (_, i) => `row-${i}`);\n const cellKeys = Array.from({ length: rows }, (_, rowIdx) =>\n Array.from({ length: columns }, (_, colIdx) => `cell-${rowIdx}-${colIdx}`),\n );\n\n return (\n <div className=\"w-full space-y-4\">\n {/* Header */}\n <div className=\"flex justify-between items-center\">\n <Skeleton className=\"h-8 w-48\" />\n <Skeleton className=\"h-10 w-32\" />\n </div>\n\n {/* Search/filters */}\n <div className=\"flex gap-4\">\n <Skeleton className=\"h-10 flex-1 max-w-sm\" />\n <Skeleton className=\"h-10 w-24\" />\n <Skeleton className=\"h-10 w-24\" />\n </div>\n\n {/* Table */}\n <div className=\"border rounded-lg overflow-hidden\">\n {/* Table header */}\n <div className=\"flex gap-4 p-4 bg-muted\">\n {headerKeys.map((key) => (\n <Skeleton key={key} className=\"h-4 flex-1\" />\n ))}\n </div>\n\n {/* Table rows */}\n {rowKeys.map((rowKey, rowIdx) => (\n <div key={rowKey} className=\"flex gap-4 p-4 border-t\">\n {cellKeys[rowIdx]?.map((cellKey) => (\n <Skeleton key={cellKey} className=\"h-4 flex-1\" />\n ))}\n </div>\n ))}\n </div>\n\n {/* Pagination */}\n <div className=\"flex justify-between items-center\">\n <Skeleton className=\"h-4 w-32\" />\n <div className=\"flex gap-2\">\n <Skeleton className=\"h-10 w-20\" />\n <Skeleton className=\"h-10 w-20\" />\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["useState","jsx","_","jsx","jsxs","jsx","jsxs","useState"]}
|