@goplusvn/core 0.1.27 → 0.1.28

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.28 — Auth Bridge: core hết khóa cứng NextAuth (nền cho Better Auth)
4
+
5
+ `@goerp/core/ui` xuất `AuthBridgeProvider` + `useAuthBridge`/`useAuthSession`/
6
+ `useSafeAuthSession` + `nextAuthBridgeClient` (mặc định). 4 component client
7
+ từng import thẳng next-auth/react (SignInForm, UserDropdown,
8
+ useRoleOperations, PermissionsVersionWatcher) nay đi qua bridge — KHÔNG mount
9
+ provider thì fallback NextAuth y như cũ (zero behavior change). App migrate
10
+ Better Auth chỉ cần mount provider với client map từ createAuthClient
11
+ (useSession/signIn.email/signOut). Server-side proxy-gate vốn đã DI qua
12
+ `options.getToken`. LƯU Ý: bridge client phải là hằng số suốt vòng đời app
13
+ (rules of hooks).
14
+
3
15
  ## 0.1.27 — Live permissions + MultiSelect hết tràn badge
4
16
 
5
17
  Cơ chế "phiên bản bộ quyền" (đúc từ vinhhoa): admin sửa vai trò/phân quyền →
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@goplusvn/core",
3
3
  "description": "GoPlusVN Platform Kit - ERP kernel: layout, RBAC, CRUD, multi-tenant, system pages",
4
- "version": "0.1.27",
4
+ "version": "0.1.28",
5
5
  "private": false,
6
6
  "publishConfig": {
7
7
  "registry": "https://registry.npmjs.org",
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useState } from "react";
2
2
  import { useRouter } from "next/navigation";
3
- import { useSession } from "next-auth/react";
3
+ import { useSafeAuthSession } from "../../ui/auth/auth-bridge";
4
4
  import { toast } from "sonner";
5
5
 
6
6
  import { useTabContentCache } from "../../ui/layout/tab-content-cache";
@@ -9,18 +9,11 @@ import type { Role } from "../types";
9
9
 
10
10
  // Safe wrapper to prevent crashing when useSession is called outside of SessionProvider
11
11
  // This often happens during Error Boundary or Hydration failures.
12
- function useSafeSession() {
13
- try {
14
- return useSession();
15
- } catch (error) {
16
- console.warn("[useSafeSession] Context error caught. Fallback to unauthenticated state.");
17
- return { data: null, status: "unauthenticated" as const, update: async () => null };
18
- }
19
- }
12
+
20
13
 
21
14
  export function useRoleOperations(mutate: () => void) {
22
15
  const router = useRouter();
23
- const { update } = useSafeSession();
16
+ const { update } = useSafeAuthSession();
24
17
  const { clearAllCache } = useTabContentCache();
25
18
  const { clearTabs } = useTabNavigation();
26
19
 
@@ -0,0 +1,104 @@
1
+ "use client";
2
+
3
+ // AUTH BRIDGE — lớp trừu tượng client-auth để core KHÔNG khóa cứng vào
4
+ // NextAuth. Mặc định (không mount provider) = NextAuth y như cũ → mọi app
5
+ // hiện tại không đổi hành vi. App migrate sang Better Auth chỉ cần mount:
6
+ //
7
+ // <AuthBridgeProvider client={betterAuthBridgeClient}>
8
+ //
9
+ // với client map từ createAuthClient() của Better Auth (useSession /
10
+ // signIn.email / signOut). LƯU Ý: client phải là hằng số suốt vòng đời app
11
+ // (rules of hooks — useSession của bridge được gọi như một hook).
12
+
13
+ import { createContext, useContext } from "react";
14
+ import type { ReactNode } from "react";
15
+ import {
16
+ signIn as nextAuthSignIn,
17
+ signOut as nextAuthSignOut,
18
+ useSession as nextAuthUseSession,
19
+ } from "next-auth/react";
20
+
21
+ export interface AuthSessionState {
22
+ /** Session object (shape do app quyết — core chỉ đọc user.id/permissions...) */
23
+ data: any;
24
+ status: "authenticated" | "unauthenticated" | "loading";
25
+ /** Ép làm mới session (NextAuth: jwt trigger="update"; Better Auth: refetch) */
26
+ update: () => Promise<unknown>;
27
+ }
28
+
29
+ export interface AuthBridgeClient {
30
+ /** Hook — phải gọi đúng luật hooks; bridge client không đổi lúc runtime. */
31
+ useSession: () => AuthSessionState;
32
+ /** Đăng nhập credentials. Trả { error } thay vì throw để form xử lý. */
33
+ signInWithCredentials: (credentials: {
34
+ email: string;
35
+ password: string;
36
+ }) => Promise<{ error?: string | null }>;
37
+ signOut: (options?: { callbackUrl?: string }) => Promise<void>;
38
+ }
39
+
40
+ /** Mặc định: NextAuth (v4) — các app chưa migrate dùng nguyên như cũ. */
41
+ export const nextAuthBridgeClient: AuthBridgeClient = {
42
+ useSession: () => {
43
+ // eslint-disable-next-line react-hooks/rules-of-hooks -- được gọi trong hook useAuthSession
44
+ const s = nextAuthUseSession();
45
+ return {
46
+ data: s.data,
47
+ status: s.status,
48
+ update: () => s.update(),
49
+ };
50
+ },
51
+ signInWithCredentials: async ({ email, password }) => {
52
+ const result = await nextAuthSignIn("credentials", {
53
+ redirect: false,
54
+ email,
55
+ password,
56
+ });
57
+ return { error: result?.error ?? null };
58
+ },
59
+ signOut: async (options) => {
60
+ await nextAuthSignOut(options);
61
+ },
62
+ };
63
+
64
+ const AuthBridgeContext = createContext<AuthBridgeClient>(nextAuthBridgeClient);
65
+
66
+ export function AuthBridgeProvider({
67
+ client,
68
+ children,
69
+ }: {
70
+ client: AuthBridgeClient;
71
+ children: ReactNode;
72
+ }) {
73
+ return (
74
+ <AuthBridgeContext.Provider value={client}>
75
+ {children}
76
+ </AuthBridgeContext.Provider>
77
+ );
78
+ }
79
+
80
+ export function useAuthBridge(): AuthBridgeClient {
81
+ return useContext(AuthBridgeContext);
82
+ }
83
+
84
+ /** Hook session qua bridge — thay cho useSession() của next-auth trong core. */
85
+ export function useAuthSession(): AuthSessionState {
86
+ return useAuthBridge().useSession();
87
+ }
88
+
89
+ /**
90
+ * Bản "an toàn" — không crash khi thiếu SessionProvider (error boundary /
91
+ * hydration fail). Fallback unauthenticated.
92
+ */
93
+ export function useSafeAuthSession(): AuthSessionState {
94
+ const bridge = useAuthBridge();
95
+ try {
96
+ return bridge.useSession();
97
+ } catch {
98
+ return {
99
+ data: null,
100
+ status: "unauthenticated",
101
+ update: async () => null,
102
+ };
103
+ }
104
+ }
@@ -5,3 +5,4 @@ export * from "./oauth-links";
5
5
  export * from "./forgot-password-form";
6
6
  export * from "./new-password-form";
7
7
  export * from "./verify-email-form";
8
+ export * from "./auth-bridge";
@@ -5,7 +5,7 @@ import * as React from "react";
5
5
  import Link from "next/link";
6
6
  import { useParams, useRouter, useSearchParams } from "next/navigation";
7
7
  import { zodResolver } from "@hookform/resolvers/zod";
8
- import { signIn } from "next-auth/react";
8
+ import { useAuthBridge } from "./auth-bridge";
9
9
  import { useForm } from "react-hook-form";
10
10
  import { Eye, EyeOff } from "lucide-react";
11
11
  import type { z } from "zod"; // Add this import
@@ -37,6 +37,7 @@ import { OAuthLinks } from "./oauth-links";
37
37
  type SignInFormType = z.infer<typeof SignInSchema>;
38
38
 
39
39
  export function SignInForm() {
40
+ const { signInWithCredentials } = useAuthBridge();
40
41
  const params = useParams();
41
42
  const searchParams = useSearchParams();
42
43
  const router = useRouter();
@@ -67,11 +68,7 @@ export function SignInForm() {
67
68
  const { email, password } = data;
68
69
 
69
70
  try {
70
- const result = await signIn("credentials", {
71
- redirect: false,
72
- email,
73
- password,
74
- });
71
+ const result = await signInWithCredentials({ email, password });
75
72
 
76
73
  if (result && result.error) {
77
74
  throw new Error(result.error);
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import Link from "next/link";
4
- import { signOut, useSession } from "next-auth/react";
4
+ import { useAuthBridge, useSafeAuthSession } from "../auth/auth-bridge";
5
5
  import { LogOut, User, UserCog } from "lucide-react";
6
6
 
7
7
  import type { DictionaryType } from "../../hooks";
@@ -15,15 +15,6 @@ import { Button } from "../primitives/button";
15
15
  import { useTabContentCache } from "./tab-content-cache";
16
16
  import { useTabNavigation } from "./tab-navigation-provider";
17
17
 
18
- // Safe wrapper to prevent crashing when useSession is called outside of SessionProvider
19
- // This often happens during Error Boundary or Hydration failures.
20
- function useSafeSession() {
21
- try {
22
- return useSession();
23
- } catch (error) {
24
- return { data: null, status: "unauthenticated" as const };
25
- }
26
- }
27
18
 
28
19
  import {
29
20
  DropdownMenu,
@@ -62,7 +53,8 @@ export function UserDropdown({
62
53
  user: userProp,
63
54
  onSignOut,
64
55
  }: UserDropdownProps) {
65
- const { data: session } = useSafeSession();
56
+ const { data: session } = useSafeAuthSession();
57
+ const { signOut } = useAuthBridge();
66
58
  const { clearAllCache } = useTabContentCache();
67
59
  const { clearTabs } = useTabNavigation();
68
60
 
@@ -16,9 +16,9 @@
16
16
 
17
17
  import { useCallback, useEffect, useRef } from "react";
18
18
  import { useRouter } from "next/navigation";
19
- import { useSession } from "next-auth/react";
20
19
  import { toast } from "sonner";
21
20
 
21
+ import { useAuthSession } from "../auth/auth-bridge";
22
22
  import { useTabContentCache } from "../layout/tab-content-cache";
23
23
 
24
24
  export interface PermissionsVersionWatcherProps {
@@ -35,7 +35,7 @@ export function PermissionsVersionWatcher({
35
35
  pollMs = 60_000,
36
36
  silent = false,
37
37
  }: PermissionsVersionWatcherProps = {}) {
38
- const { status, update } = useSession();
38
+ const { status, update } = useAuthSession();
39
39
  const router = useRouter();
40
40
  const { clearAllCache } = useTabContentCache();
41
41
  const lastSeenRef = useRef<string | null>(null);