@mesob/auth-react 0.3.3 → 0.3.5
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-layout.d.ts +1 -1
- package/dist/components/auth/auth-layout.js +10 -2
- package/dist/components/auth/auth-layout.js.map +1 -1
- package/dist/components/auth/countdown.js +8 -6
- package/dist/components/auth/countdown.js.map +1 -1
- package/dist/components/auth/forgot-password.js +16 -18
- package/dist/components/auth/forgot-password.js.map +1 -1
- package/dist/components/auth/reset-password-form.js +17 -20
- package/dist/components/auth/reset-password-form.js.map +1 -1
- package/dist/components/auth/sign-in.js +23 -25
- package/dist/components/auth/sign-in.js.map +1 -1
- package/dist/components/auth/sign-up.js +18 -24
- package/dist/components/auth/sign-up.js.map +1 -1
- package/dist/components/auth/verification-form.js +24 -27
- package/dist/components/auth/verification-form.js.map +1 -1
- package/dist/components/auth/verify-email.js +35 -30
- package/dist/components/auth/verify-email.js.map +1 -1
- package/dist/components/auth/verify-phone.js +35 -30
- package/dist/components/auth/verify-phone.js.map +1 -1
- package/dist/components/error-boundary.d.ts +2 -2
- package/dist/components/iam/permissions.js +9 -2
- package/dist/components/iam/permissions.js.map +1 -1
- package/dist/components/iam/roles.js +9 -2
- package/dist/components/iam/roles.js.map +1 -1
- package/dist/components/iam/tenants.js +9 -2
- package/dist/components/iam/tenants.js.map +1 -1
- package/dist/components/iam/users.js +9 -2
- package/dist/components/iam/users.js.map +1 -1
- package/dist/components/profile/change-email-form.js +26 -29
- package/dist/components/profile/change-email-form.js.map +1 -1
- package/dist/components/profile/change-phone-form.js +26 -29
- package/dist/components/profile/change-phone-form.js.map +1 -1
- package/dist/components/profile/otp-verification-modal.js +24 -27
- package/dist/components/profile/otp-verification-modal.js.map +1 -1
- package/dist/components/profile/security.js +38 -41
- package/dist/components/profile/security.js.map +1 -1
- package/dist/components/profile/verify-change-email-form.js +24 -27
- package/dist/components/profile/verify-change-email-form.js.map +1 -1
- package/dist/components/profile/verify-change-phone-form.js +24 -27
- package/dist/components/profile/verify-change-phone-form.js.map +1 -1
- package/dist/index.js +147 -159
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +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, Button } from '@mesob/ui/components';\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';\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';\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
|
+
{"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, Button } from '@mesob/ui/components';\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((prev) => prev - 1)}\n >\n Previous\n </Button>\n <span className=\"text-sm text-muted-foreground\">Page {page}</span>\n <Button\n variant=\"outline\"\n onClick={() => setPage((prev) => prev + 1)}\n >\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';\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';\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"]}
|
|
@@ -194,7 +194,7 @@ function Users() {
|
|
|
194
194
|
{
|
|
195
195
|
variant: "outline",
|
|
196
196
|
disabled: page === 1,
|
|
197
|
-
onClick: () => setPage(
|
|
197
|
+
onClick: () => setPage((prev) => prev - 1),
|
|
198
198
|
children: "Previous"
|
|
199
199
|
}
|
|
200
200
|
),
|
|
@@ -202,7 +202,14 @@ function Users() {
|
|
|
202
202
|
"Page ",
|
|
203
203
|
page
|
|
204
204
|
] }),
|
|
205
|
-
/* @__PURE__ */ jsx4(
|
|
205
|
+
/* @__PURE__ */ jsx4(
|
|
206
|
+
Button,
|
|
207
|
+
{
|
|
208
|
+
variant: "outline",
|
|
209
|
+
onClick: () => setPage((prev) => prev + 1),
|
|
210
|
+
children: "Next"
|
|
211
|
+
}
|
|
212
|
+
)
|
|
206
213
|
] })
|
|
207
214
|
] });
|
|
208
215
|
}
|
|
@@ -1 +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, Button } from '@mesob/ui/components';\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';\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';\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
|
+
{"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, Button } from '@mesob/ui/components';\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((prev) => prev - 1)}\n >\n Previous\n </Button>\n <span className=\"text-sm text-muted-foreground\">Page {page}</span>\n <Button\n variant=\"outline\"\n onClick={() => setPage((prev) => prev + 1)}\n >\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';\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';\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"]}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/components/profile/change-email-form.tsx
|
|
4
4
|
import {
|
|
5
|
-
Button as
|
|
5
|
+
Button as Button3,
|
|
6
6
|
Collapsible,
|
|
7
7
|
CollapsibleContent,
|
|
8
8
|
CollapsibleTrigger
|
|
@@ -342,7 +342,7 @@ import {
|
|
|
342
342
|
// src/components/auth/verification-form.tsx
|
|
343
343
|
import { zodResolver as zodResolver2 } from "@hookform/resolvers/zod";
|
|
344
344
|
import {
|
|
345
|
-
Button as
|
|
345
|
+
Button as Button2,
|
|
346
346
|
Form,
|
|
347
347
|
FormControl,
|
|
348
348
|
FormField,
|
|
@@ -351,8 +351,7 @@ import {
|
|
|
351
351
|
FormMessage,
|
|
352
352
|
InputOTP,
|
|
353
353
|
InputOTPGroup,
|
|
354
|
-
InputOTPSlot
|
|
355
|
-
Spinner as Spinner3
|
|
354
|
+
InputOTPSlot
|
|
356
355
|
} from "@mesob/ui/components";
|
|
357
356
|
import { useForm as useForm2 } from "react-hook-form";
|
|
358
357
|
import { z as z2 } from "zod";
|
|
@@ -372,7 +371,7 @@ function useTranslator(namespace) {
|
|
|
372
371
|
}
|
|
373
372
|
|
|
374
373
|
// src/components/auth/countdown.tsx
|
|
375
|
-
import {
|
|
374
|
+
import { Spinner as Spinner2 } from "@mesob/ui/components";
|
|
376
375
|
import { useEffect as useEffect2, useState as useState3 } from "react";
|
|
377
376
|
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
378
377
|
var Countdown = ({
|
|
@@ -408,17 +407,19 @@ var Countdown = ({
|
|
|
408
407
|
setIsResending(false);
|
|
409
408
|
}
|
|
410
409
|
};
|
|
410
|
+
const busy = isResending || resending;
|
|
411
411
|
if (seconds > 0) {
|
|
412
|
-
return /* @__PURE__ */ jsx3(
|
|
412
|
+
return /* @__PURE__ */ jsx3("p", { className: "text-sm text-muted-foreground", children: t("resendIn", { seconds }) });
|
|
413
413
|
}
|
|
414
414
|
return /* @__PURE__ */ jsxs2(
|
|
415
|
-
|
|
415
|
+
"button",
|
|
416
416
|
{
|
|
417
|
-
|
|
417
|
+
type: "button",
|
|
418
418
|
onClick: handleResend,
|
|
419
|
-
disabled:
|
|
419
|
+
disabled: busy,
|
|
420
|
+
className: "text-sm text-primary hover:underline disabled:opacity-50 flex items-center gap-1",
|
|
420
421
|
children: [
|
|
421
|
-
|
|
422
|
+
busy && /* @__PURE__ */ jsx3(Spinner2, { className: "h-3 w-3" }),
|
|
422
423
|
t("resend")
|
|
423
424
|
]
|
|
424
425
|
}
|
|
@@ -438,13 +439,12 @@ var VerificationForm = ({
|
|
|
438
439
|
const t = useTranslator("Auth.verification");
|
|
439
440
|
const form = useForm2({
|
|
440
441
|
resolver: zodResolver2(verificationSchema(t)),
|
|
441
|
-
defaultValues: {
|
|
442
|
-
code: ""
|
|
443
|
-
}
|
|
442
|
+
defaultValues: { code: "" }
|
|
444
443
|
});
|
|
445
444
|
const handleSubmit = form.handleSubmit(async (values) => {
|
|
446
445
|
await onSubmit(values);
|
|
447
446
|
});
|
|
447
|
+
const codeLength = form.watch("code").length;
|
|
448
448
|
return /* @__PURE__ */ jsx4(Form, { ...form, children: /* @__PURE__ */ jsxs3(
|
|
449
449
|
"form",
|
|
450
450
|
{
|
|
@@ -482,21 +482,18 @@ var VerificationForm = ({
|
|
|
482
482
|
] })
|
|
483
483
|
}
|
|
484
484
|
),
|
|
485
|
-
/* @__PURE__ */
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
}
|
|
498
|
-
)
|
|
499
|
-
] })
|
|
485
|
+
/* @__PURE__ */ jsx4(
|
|
486
|
+
Button2,
|
|
487
|
+
{
|
|
488
|
+
type: "submit",
|
|
489
|
+
form: "verification-form",
|
|
490
|
+
className: "w-full",
|
|
491
|
+
disabled: isLoading || codeLength !== 6,
|
|
492
|
+
loading: isLoading,
|
|
493
|
+
children: t("form.confirm")
|
|
494
|
+
}
|
|
495
|
+
),
|
|
496
|
+
/* @__PURE__ */ jsx4("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx4(Countdown, { onResend, resending: isLoading }) })
|
|
500
497
|
]
|
|
501
498
|
}
|
|
502
499
|
) });
|
|
@@ -700,7 +697,7 @@ function ChangeEmailForm() {
|
|
|
700
697
|
CollapsibleTrigger,
|
|
701
698
|
{
|
|
702
699
|
render: /* @__PURE__ */ jsx7(
|
|
703
|
-
|
|
700
|
+
Button3,
|
|
704
701
|
{
|
|
705
702
|
variant: "ghost",
|
|
706
703
|
className: "w-full justify-between p-4 h-auto"
|