@mmapp/react 0.1.0-alpha.1 → 0.1.0-alpha.3
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/README.md +112 -0
- package/dist/index.d.mts +27 -2
- package/dist/index.d.ts +27 -2
- package/dist/index.js +70 -3
- package/dist/index.mjs +74 -12
- package/package.json +4 -3
- package/package.json.backup +0 -41
- package/src/Blueprint.ts +0 -216
- package/src/__tests__/Blueprint.test.ts +0 -106
- package/src/__tests__/action-context.test.ts +0 -166
- package/src/__tests__/actionCreators.test.ts +0 -179
- package/src/__tests__/builders.test.ts +0 -336
- package/src/__tests__/defineBlueprint-composition.test.ts +0 -106
- package/src/__tests__/factories.test.ts +0 -229
- package/src/__tests__/loader.test.ts +0 -159
- package/src/__tests__/logger.test.ts +0 -70
- package/src/__tests__/type-inference.test.ts +0 -160
- package/src/__tests__/typed-transitions.test.ts +0 -126
- package/src/__tests__/useModuleConfig.test.ts +0 -61
- package/src/actionCreators.ts +0 -132
- package/src/actions.ts +0 -547
- package/src/atoms/index.ts +0 -600
- package/src/authoring.ts +0 -92
- package/src/browser-player.ts +0 -783
- package/src/builders.ts +0 -1342
- package/src/components/ExperienceWorkflowBridge.tsx +0 -123
- package/src/components/PlayerProvider.tsx +0 -43
- package/src/components/atoms/index.tsx +0 -269
- package/src/components/index.ts +0 -36
- package/src/conditions.ts +0 -692
- package/src/config/defineBlueprint.ts +0 -329
- package/src/config/defineModel.ts +0 -753
- package/src/config/defineWorkspace.ts +0 -24
- package/src/core/WorkflowRuntime.ts +0 -153
- package/src/factories.ts +0 -425
- package/src/grammar/index.ts +0 -173
- package/src/hooks/index.ts +0 -106
- package/src/hooks/useAuth.ts +0 -288
- package/src/hooks/useChannel.ts +0 -304
- package/src/hooks/useComputed.ts +0 -154
- package/src/hooks/useDomainSubscription.ts +0 -110
- package/src/hooks/useDuringAction.ts +0 -99
- package/src/hooks/useExperienceState.ts +0 -59
- package/src/hooks/useExpressionLibrary.ts +0 -129
- package/src/hooks/useForm.ts +0 -352
- package/src/hooks/useGeolocation.ts +0 -207
- package/src/hooks/useMapView.ts +0 -259
- package/src/hooks/useMiddleware.ts +0 -291
- package/src/hooks/useModel.ts +0 -363
- package/src/hooks/useModule.ts +0 -59
- package/src/hooks/useModuleConfig.ts +0 -61
- package/src/hooks/useMutation.ts +0 -237
- package/src/hooks/useNotification.ts +0 -151
- package/src/hooks/useOnChange.ts +0 -30
- package/src/hooks/useOnEnter.ts +0 -59
- package/src/hooks/useOnEvent.ts +0 -37
- package/src/hooks/useOnExit.ts +0 -27
- package/src/hooks/useOnTransition.ts +0 -30
- package/src/hooks/usePackage.ts +0 -128
- package/src/hooks/useParams.ts +0 -33
- package/src/hooks/usePlayer.ts +0 -308
- package/src/hooks/useQuery.ts +0 -184
- package/src/hooks/useRealtimeQuery.ts +0 -222
- package/src/hooks/useRole.ts +0 -191
- package/src/hooks/useRouteParams.ts +0 -100
- package/src/hooks/useRouter.ts +0 -347
- package/src/hooks/useServerAction.ts +0 -178
- package/src/hooks/useServerState.ts +0 -284
- package/src/hooks/useToast.ts +0 -164
- package/src/hooks/useTransition.ts +0 -39
- package/src/hooks/useView.ts +0 -102
- package/src/hooks/useWhileIn.ts +0 -48
- package/src/hooks/useWorkflow.ts +0 -63
- package/src/index.ts +0 -465
- package/src/loader/experience-workflow-loader.ts +0 -192
- package/src/loader/index.ts +0 -6
- package/src/local/LocalEngine.ts +0 -388
- package/src/local/LocalEngineAdapter.ts +0 -175
- package/src/local/LocalEngineContext.ts +0 -30
- package/src/logger.ts +0 -37
- package/src/mixins.ts +0 -1160
- package/src/providers/RuntimeContext.ts +0 -20
- package/src/providers/WorkflowProvider.tsx +0 -28
- package/src/routing/instance-key.ts +0 -107
- package/src/server/transition-context.ts +0 -172
- package/src/testing/index.ts +0 -9
- package/src/testing/useBlueprintTestRunner.ts +0 -91
- package/src/testing/useGraphAnalysis.ts +0 -18
- package/src/testing/useTestRunner.ts +0 -77
- package/src/testing.ts +0 -995
- package/src/types/workflow-inference.ts +0 -158
- package/src/types.ts +0 -114
- package/tsconfig.json +0 -27
- package/vitest.config.ts +0 -8
package/src/grammar/index.ts
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Grammar Islands — tagged template functions for domain-specific languages.
|
|
3
|
-
*
|
|
4
|
-
* These tagged templates allow embedding DSL snippets inside React workflow
|
|
5
|
-
* files and model files. The compiler extracts them as IRGrammarIsland entries,
|
|
6
|
-
* preserving the raw source for backend evaluation.
|
|
7
|
-
*
|
|
8
|
-
* Supported grammars:
|
|
9
|
-
* - cedar: Authorization policies (Cedar policy language)
|
|
10
|
-
* - sql: Computed fields and queries
|
|
11
|
-
* - cron: Scheduled task expressions
|
|
12
|
-
* - dmn: Decision tables (DMN-like markdown syntax)
|
|
13
|
-
* - graphql: GraphQL query fragments
|
|
14
|
-
* - jsonpath: JSONPath expressions
|
|
15
|
-
*
|
|
16
|
-
* Usage in model files:
|
|
17
|
-
* import { cedar, sql, cron } from '@mindmatrix/react';
|
|
18
|
-
*
|
|
19
|
-
* export const access = cedar`
|
|
20
|
-
* permit (principal, action == Action::"read", resource)
|
|
21
|
-
* when { principal.role == "member" };
|
|
22
|
-
* `;
|
|
23
|
-
*
|
|
24
|
-
* export const computed = {
|
|
25
|
-
* messageCount: sql`SELECT COUNT(*) FROM messages WHERE channel_id = $instance_id`,
|
|
26
|
-
* };
|
|
27
|
-
*
|
|
28
|
-
* export const schedule = {
|
|
29
|
-
* cleanup: cron`0 3 * * *`,
|
|
30
|
-
* };
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
// =============================================================================
|
|
34
|
-
// Types
|
|
35
|
-
// =============================================================================
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Runtime marker for a grammar island.
|
|
39
|
-
* The compiler extracts these at build time.
|
|
40
|
-
* At runtime, they're inert marker objects.
|
|
41
|
-
*/
|
|
42
|
-
export interface GrammarIsland {
|
|
43
|
-
__grammarIsland: true;
|
|
44
|
-
contextTag: string;
|
|
45
|
-
rawSource: string;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Cedar policy island with parsed policy structures.
|
|
50
|
-
*/
|
|
51
|
-
export interface CedarPolicy {
|
|
52
|
-
effect: 'permit' | 'forbid';
|
|
53
|
-
principal?: string;
|
|
54
|
-
action?: string;
|
|
55
|
-
resource?: string;
|
|
56
|
-
conditions: string[];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Cron expression fields.
|
|
61
|
-
*/
|
|
62
|
-
export interface CronFields {
|
|
63
|
-
minute: string;
|
|
64
|
-
hour: string;
|
|
65
|
-
dayOfMonth: string;
|
|
66
|
-
month: string;
|
|
67
|
-
dayOfWeek: string;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* DMN decision rule.
|
|
72
|
-
*/
|
|
73
|
-
export interface DmnRule {
|
|
74
|
-
inputs: Record<string, string>;
|
|
75
|
-
output: Record<string, string>;
|
|
76
|
-
annotation?: string;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// =============================================================================
|
|
80
|
-
// Tagged Template Functions
|
|
81
|
-
// =============================================================================
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Creates a grammar island from a tagged template.
|
|
85
|
-
*/
|
|
86
|
-
function createIsland(contextTag: string, strings: TemplateStringsArray, values: unknown[]): GrammarIsland {
|
|
87
|
-
// Reconstruct the template with interpolated values
|
|
88
|
-
let rawSource = '';
|
|
89
|
-
for (let i = 0; i < strings.length; i++) {
|
|
90
|
-
rawSource += strings[i];
|
|
91
|
-
if (i < values.length) {
|
|
92
|
-
rawSource += String(values[i]);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return {
|
|
96
|
-
__grammarIsland: true,
|
|
97
|
-
contextTag,
|
|
98
|
-
rawSource: rawSource.trim(),
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Cedar authorization policy.
|
|
104
|
-
*
|
|
105
|
-
* Usage:
|
|
106
|
-
* const policy = cedar`
|
|
107
|
-
* permit (principal, action == Action::"read", resource)
|
|
108
|
-
* when { principal.role == "member" };
|
|
109
|
-
* `;
|
|
110
|
-
*/
|
|
111
|
-
export function cedar(strings: TemplateStringsArray, ...values: unknown[]): GrammarIsland {
|
|
112
|
-
return createIsland('cedar', strings, values);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* SQL computed field or query.
|
|
117
|
-
*
|
|
118
|
-
* Usage:
|
|
119
|
-
* const query = sql`SELECT COUNT(*) FROM messages WHERE channel_id = $instance_id`;
|
|
120
|
-
*/
|
|
121
|
-
export function sql(strings: TemplateStringsArray, ...values: unknown[]): GrammarIsland {
|
|
122
|
-
return createIsland('sql', strings, values);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Cron schedule expression (5-field format).
|
|
127
|
-
*
|
|
128
|
-
* Usage:
|
|
129
|
-
* const schedule = cron`0 3 * * *`; // Every day at 3 AM
|
|
130
|
-
*/
|
|
131
|
-
export function cron(strings: TemplateStringsArray, ...values: unknown[]): GrammarIsland {
|
|
132
|
-
return createIsland('cron', strings, values);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* DMN decision table (markdown-like syntax).
|
|
137
|
-
*
|
|
138
|
-
* Usage:
|
|
139
|
-
* const rules = dmn`
|
|
140
|
-
* | age | income | risk |
|
|
141
|
-
* |-----|--------|--------|
|
|
142
|
-
* | <18 | * | reject |
|
|
143
|
-
* | >=18| >50000 | low |
|
|
144
|
-
* | >=18| <=50000| medium |
|
|
145
|
-
* `;
|
|
146
|
-
*/
|
|
147
|
-
export function dmn(strings: TemplateStringsArray, ...values: unknown[]): GrammarIsland {
|
|
148
|
-
return createIsland('dmn', strings, values);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* GraphQL query fragment.
|
|
153
|
-
*
|
|
154
|
-
* Usage:
|
|
155
|
-
* const query = graphql`
|
|
156
|
-
* query GetChannel($id: ID!) {
|
|
157
|
-
* channel(id: $id) { name members { id } }
|
|
158
|
-
* }
|
|
159
|
-
* `;
|
|
160
|
-
*/
|
|
161
|
-
export function graphql(strings: TemplateStringsArray, ...values: unknown[]): GrammarIsland {
|
|
162
|
-
return createIsland('graphql', strings, values);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* JSONPath expression.
|
|
167
|
-
*
|
|
168
|
-
* Usage:
|
|
169
|
-
* const path = jsonpath`$.store.book[?(@.price < 10)]`;
|
|
170
|
-
*/
|
|
171
|
-
export function jsonpath(strings: TemplateStringsArray, ...values: unknown[]): GrammarIsland {
|
|
172
|
-
return createIsland('jsonpath', strings, values);
|
|
173
|
-
}
|
package/src/hooks/index.ts
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
export { usePlayer } from './usePlayer';
|
|
2
|
-
export { useDomainSubscription } from './useDomainSubscription';
|
|
3
|
-
export type { DomainSubscriptionTransport } from './useDomainSubscription';
|
|
4
|
-
export { useExperienceState, useStateField } from './useExperienceState';
|
|
5
|
-
export { useRole, setRoleHierarchy } from './useRole';
|
|
6
|
-
export type { RoleResult, RoleOptions } from './useRole';
|
|
7
|
-
export { useView, setViewResolver } from './useView';
|
|
8
|
-
export type { ViewDefinitionResult, ViewRecord, ViewResolver } from './useView';
|
|
9
|
-
export { useParams } from './useParams';
|
|
10
|
-
export { useExpressionLibrary, setExpressionLibraryResolver } from './useExpressionLibrary';
|
|
11
|
-
export type { ExpressionLibraryResult, LibraryParam, ExpressionLibraryRecord, ExpressionLibraryResolver } from './useExpressionLibrary';
|
|
12
|
-
export { useDuringAction } from './useDuringAction';
|
|
13
|
-
export type { DuringActionConfig } from './useDuringAction';
|
|
14
|
-
export { usePackage } from './usePackage';
|
|
15
|
-
export type { PackageManifest, PackageChild, PackageResult } from './usePackage';
|
|
16
|
-
|
|
17
|
-
// === New Scale-App Hooks ===
|
|
18
|
-
|
|
19
|
-
// Routing
|
|
20
|
-
export { useRouter } from './useRouter';
|
|
21
|
-
export type {
|
|
22
|
-
RouterHandle,
|
|
23
|
-
RouterOptions,
|
|
24
|
-
RouteDefinition,
|
|
25
|
-
RouteGuard,
|
|
26
|
-
RouteLocation,
|
|
27
|
-
} from './useRouter';
|
|
28
|
-
export { useRouteParams } from './useRouteParams';
|
|
29
|
-
export type { RouteParamsOptions } from './useRouteParams';
|
|
30
|
-
|
|
31
|
-
// Real-Time
|
|
32
|
-
export { useRealtimeQuery, setRealtimeQueryResolver } from './useRealtimeQuery';
|
|
33
|
-
export type { RealtimeQueryParams, RealtimeQueryResult } from './useRealtimeQuery';
|
|
34
|
-
export { useChannel, setChannelTransport } from './useChannel';
|
|
35
|
-
export type {
|
|
36
|
-
ChannelMessage,
|
|
37
|
-
ChannelOptions,
|
|
38
|
-
ChannelHandle,
|
|
39
|
-
ChannelTransport,
|
|
40
|
-
} from './useChannel';
|
|
41
|
-
|
|
42
|
-
// Geolocation
|
|
43
|
-
export { useGeolocation } from './useGeolocation';
|
|
44
|
-
export type {
|
|
45
|
-
GeoPosition,
|
|
46
|
-
GeolocationOptions,
|
|
47
|
-
GeolocationResult,
|
|
48
|
-
GeolocationError,
|
|
49
|
-
} from './useGeolocation';
|
|
50
|
-
export { useMapView } from './useMapView';
|
|
51
|
-
export type {
|
|
52
|
-
LatLng,
|
|
53
|
-
LatLngBounds,
|
|
54
|
-
MapMarker,
|
|
55
|
-
MapViewOptions,
|
|
56
|
-
MapViewHandle,
|
|
57
|
-
} from './useMapView';
|
|
58
|
-
|
|
59
|
-
// Auth
|
|
60
|
-
export { useAuth, setAuthResolver } from './useAuth';
|
|
61
|
-
export type {
|
|
62
|
-
AuthUser,
|
|
63
|
-
LoginCredentials,
|
|
64
|
-
AuthOptions,
|
|
65
|
-
AuthResult,
|
|
66
|
-
AuthResolver,
|
|
67
|
-
} from './useAuth';
|
|
68
|
-
|
|
69
|
-
// Forms
|
|
70
|
-
export { useForm } from './useForm';
|
|
71
|
-
export type {
|
|
72
|
-
FormValidator,
|
|
73
|
-
FieldValidator,
|
|
74
|
-
FormConfig,
|
|
75
|
-
FieldRegistration,
|
|
76
|
-
FormHandle,
|
|
77
|
-
} from './useForm';
|
|
78
|
-
|
|
79
|
-
// Notifications
|
|
80
|
-
export { useNotification } from './useNotification';
|
|
81
|
-
export type { NotifyOptions, NotificationResult } from './useNotification';
|
|
82
|
-
export { useToast } from './useToast';
|
|
83
|
-
export type {
|
|
84
|
-
ToastVariant,
|
|
85
|
-
ToastConfig,
|
|
86
|
-
ToastInstance,
|
|
87
|
-
ToastHandle,
|
|
88
|
-
} from './useToast';
|
|
89
|
-
|
|
90
|
-
// Middleware
|
|
91
|
-
export { useMiddleware, requireAuth, requireRole, prefetchData } from './useMiddleware';
|
|
92
|
-
export type {
|
|
93
|
-
MiddlewareResult,
|
|
94
|
-
MiddlewareContext,
|
|
95
|
-
MiddlewareFn,
|
|
96
|
-
MiddlewareOptions,
|
|
97
|
-
MiddlewareHandle,
|
|
98
|
-
} from './useMiddleware';
|
|
99
|
-
|
|
100
|
-
// Computed Fields
|
|
101
|
-
export { useComputed, useComputedWithMeta } from './useComputed';
|
|
102
|
-
export type {
|
|
103
|
-
ComputedFieldMode,
|
|
104
|
-
UseComputedOptions,
|
|
105
|
-
ComputedFieldResult,
|
|
106
|
-
} from './useComputed';
|
package/src/hooks/useAuth.ts
DELETED
|
@@ -1,288 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useAuth — Authentication hook for workflow applications.
|
|
3
|
-
*
|
|
4
|
-
* Provides login/logout/currentUser functionality with JWT token management.
|
|
5
|
-
* Integrates with the MindMatrix auth API and stores tokens in localStorage.
|
|
6
|
-
*
|
|
7
|
-
* Usage in .workflow.tsx:
|
|
8
|
-
* const { user, isAuthenticated, login, logout, loading } = useAuth();
|
|
9
|
-
*
|
|
10
|
-
* if (!isAuthenticated) {
|
|
11
|
-
* return <LoginForm onSubmit={login} />;
|
|
12
|
-
* }
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
16
|
-
|
|
17
|
-
// =============================================================================
|
|
18
|
-
// Types
|
|
19
|
-
// =============================================================================
|
|
20
|
-
|
|
21
|
-
/** User profile from the auth system. */
|
|
22
|
-
export interface AuthUser {
|
|
23
|
-
/** User ID. */
|
|
24
|
-
id: string;
|
|
25
|
-
/** Email address. */
|
|
26
|
-
email: string;
|
|
27
|
-
/** Display name. */
|
|
28
|
-
name: string;
|
|
29
|
-
/** User roles. */
|
|
30
|
-
roles: string[];
|
|
31
|
-
/** Tenant/organization ID. */
|
|
32
|
-
tenantId?: string;
|
|
33
|
-
/** Additional user metadata. */
|
|
34
|
-
metadata?: Record<string, unknown>;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Login credentials. */
|
|
38
|
-
export interface LoginCredentials {
|
|
39
|
-
email: string;
|
|
40
|
-
password: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Options for configuring auth. */
|
|
44
|
-
export interface AuthOptions {
|
|
45
|
-
/** Auth API base URL (defaults to /api/v1/auth). */
|
|
46
|
-
apiUrl?: string;
|
|
47
|
-
/** Storage key for the JWT token (default: 'auth_token'). */
|
|
48
|
-
tokenKey?: string;
|
|
49
|
-
/** Auto-fetch user on mount (default: true). */
|
|
50
|
-
autoFetch?: boolean;
|
|
51
|
-
/** Called on successful login. */
|
|
52
|
-
onLogin?: (user: AuthUser) => void;
|
|
53
|
-
/** Called on logout. */
|
|
54
|
-
onLogout?: () => void;
|
|
55
|
-
/** Called on auth error. */
|
|
56
|
-
onError?: (error: Error) => void;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Auth result returned by useAuth. */
|
|
60
|
-
export interface AuthResult {
|
|
61
|
-
/** Current user (null if not authenticated). */
|
|
62
|
-
user: AuthUser | null;
|
|
63
|
-
/** Whether a user is authenticated. */
|
|
64
|
-
isAuthenticated: boolean;
|
|
65
|
-
/** Loading state during auth operations. */
|
|
66
|
-
loading: boolean;
|
|
67
|
-
/** Auth error. */
|
|
68
|
-
error: Error | null;
|
|
69
|
-
/** JWT token (null if not authenticated). */
|
|
70
|
-
token: string | null;
|
|
71
|
-
/** Log in with credentials. */
|
|
72
|
-
login: (credentials: LoginCredentials) => Promise<AuthUser>;
|
|
73
|
-
/** Log out and clear token. */
|
|
74
|
-
logout: () => Promise<void>;
|
|
75
|
-
/** Refresh the current user's profile. */
|
|
76
|
-
refreshUser: () => Promise<void>;
|
|
77
|
-
/** Check if the current user has a specific role. */
|
|
78
|
-
hasRole: (role: string) => boolean;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** Auth resolver — pluggable auth backend. */
|
|
82
|
-
export interface AuthResolver {
|
|
83
|
-
login: (credentials: LoginCredentials) => Promise<{ token: string; user: AuthUser }>;
|
|
84
|
-
logout: (token: string) => Promise<void>;
|
|
85
|
-
getUser: (token: string) => Promise<AuthUser>;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Global resolver (set by provider)
|
|
89
|
-
let _globalAuthResolver: AuthResolver | null = null;
|
|
90
|
-
|
|
91
|
-
export function setAuthResolver(resolver: AuthResolver | null): void {
|
|
92
|
-
_globalAuthResolver = resolver;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// =============================================================================
|
|
96
|
-
// Internal — default API-based resolver
|
|
97
|
-
// =============================================================================
|
|
98
|
-
|
|
99
|
-
function createDefaultResolver(apiUrl: string): AuthResolver {
|
|
100
|
-
return {
|
|
101
|
-
login: async (credentials) => {
|
|
102
|
-
const res = await fetch(`${apiUrl}/login`, {
|
|
103
|
-
method: 'POST',
|
|
104
|
-
headers: { 'Content-Type': 'application/json' },
|
|
105
|
-
body: JSON.stringify(credentials),
|
|
106
|
-
});
|
|
107
|
-
if (!res.ok) {
|
|
108
|
-
const body = await res.json().catch(() => ({}));
|
|
109
|
-
throw new Error(body.message || `Login failed: ${res.status}`);
|
|
110
|
-
}
|
|
111
|
-
return res.json();
|
|
112
|
-
},
|
|
113
|
-
logout: async (token) => {
|
|
114
|
-
await fetch(`${apiUrl}/logout`, {
|
|
115
|
-
method: 'POST',
|
|
116
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
117
|
-
}).catch(() => {
|
|
118
|
-
// Logout is best-effort
|
|
119
|
-
});
|
|
120
|
-
},
|
|
121
|
-
getUser: async (token) => {
|
|
122
|
-
const res = await fetch(`${apiUrl}/me`, {
|
|
123
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
124
|
-
});
|
|
125
|
-
if (!res.ok) {
|
|
126
|
-
throw new Error(`Failed to fetch user: ${res.status}`);
|
|
127
|
-
}
|
|
128
|
-
return res.json();
|
|
129
|
-
},
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// =============================================================================
|
|
134
|
-
// Hook
|
|
135
|
-
// =============================================================================
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Authentication hook with login/logout and user management.
|
|
139
|
-
*
|
|
140
|
-
* @param options - Auth configuration.
|
|
141
|
-
* @returns Auth result with user, token, and auth methods.
|
|
142
|
-
*/
|
|
143
|
-
export function useAuth(options: AuthOptions = {}): AuthResult {
|
|
144
|
-
const {
|
|
145
|
-
apiUrl = '/api/v1/auth',
|
|
146
|
-
tokenKey = 'auth_token',
|
|
147
|
-
autoFetch = true,
|
|
148
|
-
onLogin,
|
|
149
|
-
onLogout,
|
|
150
|
-
onError,
|
|
151
|
-
} = options;
|
|
152
|
-
|
|
153
|
-
const [user, setUser] = useState<AuthUser | null>(null);
|
|
154
|
-
const [loading, setLoading] = useState(true);
|
|
155
|
-
const [error, setError] = useState<Error | null>(null);
|
|
156
|
-
const [token, setToken] = useState<string | null>(() => {
|
|
157
|
-
return typeof localStorage !== 'undefined' ? localStorage.getItem(tokenKey) : null;
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
const onLoginRef = useRef(onLogin);
|
|
161
|
-
onLoginRef.current = onLogin;
|
|
162
|
-
const onLogoutRef = useRef(onLogout);
|
|
163
|
-
onLogoutRef.current = onLogout;
|
|
164
|
-
const onErrorRef = useRef(onError);
|
|
165
|
-
onErrorRef.current = onError;
|
|
166
|
-
|
|
167
|
-
const getResolver = useCallback(
|
|
168
|
-
() => _globalAuthResolver ?? createDefaultResolver(apiUrl),
|
|
169
|
-
[apiUrl],
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
// Auto-fetch user on mount if we have a token
|
|
173
|
-
useEffect(() => {
|
|
174
|
-
if (!autoFetch || !token) {
|
|
175
|
-
setLoading(false);
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
let cancelled = false;
|
|
180
|
-
const resolver = getResolver();
|
|
181
|
-
|
|
182
|
-
resolver
|
|
183
|
-
.getUser(token)
|
|
184
|
-
.then((userData) => {
|
|
185
|
-
if (!cancelled) {
|
|
186
|
-
setUser(userData);
|
|
187
|
-
setError(null);
|
|
188
|
-
setLoading(false);
|
|
189
|
-
}
|
|
190
|
-
})
|
|
191
|
-
.catch((err) => {
|
|
192
|
-
if (!cancelled) {
|
|
193
|
-
const authError = err instanceof Error ? err : new Error(String(err));
|
|
194
|
-
setError(authError);
|
|
195
|
-
// Token is invalid — clear it
|
|
196
|
-
setToken(null);
|
|
197
|
-
setUser(null);
|
|
198
|
-
localStorage.removeItem(tokenKey);
|
|
199
|
-
setLoading(false);
|
|
200
|
-
onErrorRef.current?.(authError);
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
return () => {
|
|
205
|
-
cancelled = true;
|
|
206
|
-
};
|
|
207
|
-
}, [token, autoFetch, getResolver, tokenKey]);
|
|
208
|
-
|
|
209
|
-
const login = useCallback(
|
|
210
|
-
async (credentials: LoginCredentials): Promise<AuthUser> => {
|
|
211
|
-
setLoading(true);
|
|
212
|
-
setError(null);
|
|
213
|
-
|
|
214
|
-
try {
|
|
215
|
-
const resolver = getResolver();
|
|
216
|
-
const result = await resolver.login(credentials);
|
|
217
|
-
|
|
218
|
-
localStorage.setItem(tokenKey, result.token);
|
|
219
|
-
setToken(result.token);
|
|
220
|
-
setUser(result.user);
|
|
221
|
-
setLoading(false);
|
|
222
|
-
onLoginRef.current?.(result.user);
|
|
223
|
-
return result.user;
|
|
224
|
-
} catch (err) {
|
|
225
|
-
const authError = err instanceof Error ? err : new Error(String(err));
|
|
226
|
-
setError(authError);
|
|
227
|
-
setLoading(false);
|
|
228
|
-
onErrorRef.current?.(authError);
|
|
229
|
-
throw authError;
|
|
230
|
-
}
|
|
231
|
-
},
|
|
232
|
-
[getResolver, tokenKey],
|
|
233
|
-
);
|
|
234
|
-
|
|
235
|
-
const logout = useCallback(async () => {
|
|
236
|
-
try {
|
|
237
|
-
if (token) {
|
|
238
|
-
const resolver = getResolver();
|
|
239
|
-
await resolver.logout(token);
|
|
240
|
-
}
|
|
241
|
-
} finally {
|
|
242
|
-
localStorage.removeItem(tokenKey);
|
|
243
|
-
setToken(null);
|
|
244
|
-
setUser(null);
|
|
245
|
-
setError(null);
|
|
246
|
-
onLogoutRef.current?.();
|
|
247
|
-
}
|
|
248
|
-
}, [token, getResolver, tokenKey]);
|
|
249
|
-
|
|
250
|
-
const refreshUser = useCallback(async () => {
|
|
251
|
-
if (!token) return;
|
|
252
|
-
|
|
253
|
-
try {
|
|
254
|
-
const resolver = getResolver();
|
|
255
|
-
const userData = await resolver.getUser(token);
|
|
256
|
-
setUser(userData);
|
|
257
|
-
setError(null);
|
|
258
|
-
} catch (err) {
|
|
259
|
-
const authError = err instanceof Error ? err : new Error(String(err));
|
|
260
|
-
setError(authError);
|
|
261
|
-
onErrorRef.current?.(authError);
|
|
262
|
-
}
|
|
263
|
-
}, [token, getResolver]);
|
|
264
|
-
|
|
265
|
-
const hasRole = useCallback(
|
|
266
|
-
(role: string): boolean => {
|
|
267
|
-
return user?.roles.includes(role) ?? false;
|
|
268
|
-
},
|
|
269
|
-
[user],
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
const isAuthenticated = token !== null && user !== null;
|
|
273
|
-
|
|
274
|
-
return useMemo(
|
|
275
|
-
(): AuthResult => ({
|
|
276
|
-
user,
|
|
277
|
-
isAuthenticated,
|
|
278
|
-
loading,
|
|
279
|
-
error,
|
|
280
|
-
token,
|
|
281
|
-
login,
|
|
282
|
-
logout,
|
|
283
|
-
refreshUser,
|
|
284
|
-
hasRole,
|
|
285
|
-
}),
|
|
286
|
-
[user, isAuthenticated, loading, error, token, login, logout, refreshUser, hasRole],
|
|
287
|
-
);
|
|
288
|
-
}
|