@goplusvn/core 0.1.27 → 0.1.29

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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.29 — Footer: link "Goplus.vn" trỏ về https://goplus.vn
4
+
5
+ Trước trỏ nhầm github.com/Qualiora (template gốc).
6
+
7
+ ## 0.1.28 — Auth Bridge: core hết khóa cứng NextAuth (nền cho Better Auth)
8
+
9
+ `@goerp/core/ui` xuất `AuthBridgeProvider` + `useAuthBridge`/`useAuthSession`/
10
+ `useSafeAuthSession` + `nextAuthBridgeClient` (mặc định). 4 component client
11
+ từng import thẳng next-auth/react (SignInForm, UserDropdown,
12
+ useRoleOperations, PermissionsVersionWatcher) nay đi qua bridge — KHÔNG mount
13
+ provider thì fallback NextAuth y như cũ (zero behavior change). App migrate
14
+ Better Auth chỉ cần mount provider với client map từ createAuthClient
15
+ (useSession/signIn.email/signOut). Server-side proxy-gate vốn đã DI qua
16
+ `options.getToken`. LƯU Ý: bridge client phải là hằng số suốt vòng đời app
17
+ (rules of hooks).
18
+
3
19
  ## 0.1.27 — Live permissions + MultiSelect hết tràn badge
4
20
 
5
21
  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.29",
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);
@@ -25,7 +25,7 @@ export function Footer() {
25
25
  <p className="text-xs leading-tight text-muted-foreground">
26
26
  Make with ❤ by{" "}
27
27
  <a
28
- href="https://github.com/Qualiora"
28
+ href="https://goplus.vn"
29
29
  target="_blank"
30
30
  rel="noopener noreferrer"
31
31
  className={cn(
@@ -1,8 +1,9 @@
1
1
  "use client";
2
2
 
3
3
  import Link from "next/link";
4
- import { signOut, useSession } from "next-auth/react";
5
- import { LogOut, User, UserCog } from "lucide-react";
4
+ import { useAuthBridge, useSafeAuthSession } from "../auth/auth-bridge";
5
+ import { LogOut, User, UserCog, MonitorSmartphone,
6
+ } from "lucide-react";
6
7
 
7
8
  import type { DictionaryType } from "../../hooks";
8
9
  import type { LocaleType } from "../../types";
@@ -15,15 +16,6 @@ import { Button } from "../primitives/button";
15
16
  import { useTabContentCache } from "./tab-content-cache";
16
17
  import { useTabNavigation } from "./tab-navigation-provider";
17
18
 
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
19
 
28
20
  import {
29
21
  DropdownMenu,
@@ -62,7 +54,8 @@ export function UserDropdown({
62
54
  user: userProp,
63
55
  onSignOut,
64
56
  }: UserDropdownProps) {
65
- const { data: session } = useSafeSession();
57
+ const { data: session } = useSafeAuthSession();
58
+ const { signOut } = useAuthBridge();
66
59
  const { clearAllCache } = useTabContentCache();
67
60
  const { clearTabs } = useTabNavigation();
68
61
 
@@ -159,6 +152,18 @@ export function UserDropdown({
159
152
  <span>{dictionary.navigation.userNav.profile}</span>
160
153
  </Link>
161
154
  </DropdownMenuItem>
155
+ <DropdownMenuItem
156
+ asChild
157
+ className="h-8 px-2 rounded-md cursor-pointer focus:bg-primary/10 focus:text-primary transition-colors duration-200 group text-sm"
158
+ >
159
+ <Link
160
+ href={ensureLocalizedPathname("/security/sessions", locale)}
161
+ className="flex items-center w-full"
162
+ >
163
+ <MonitorSmartphone className="me-2 size-4 text-muted-foreground group-hover:text-primary group-focus:text-primary transition-colors" />
164
+ <span>Thiết bị & phiên đăng nhập</span>
165
+ </Link>
166
+ </DropdownMenuItem>
162
167
  {/* <DropdownMenuItem
163
168
  asChild
164
169
  className="h-8 px-2 rounded-md cursor-pointer focus:bg-primary/10 focus:text-primary transition-colors duration-200 group text-sm"
@@ -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);