@tern-secure/nextjs 3.4.3 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/app-router/client/TernSecureProvider.js +17 -2
- package/dist/cjs/app-router/client/TernSecureProvider.js.map +1 -1
- package/dist/cjs/app-router/client/actions.js +55 -55
- package/dist/cjs/app-router/client/actions.js.map +1 -1
- package/dist/cjs/app-router/route-handler/internal-route.js +22 -3
- package/dist/cjs/app-router/route-handler/internal-route.js.map +1 -1
- package/dist/cjs/boundary/TernSecureClientProvider.js +167 -34
- package/dist/cjs/boundary/TernSecureClientProvider.js.map +1 -1
- package/dist/cjs/boundary/TernSecureCtx.js.map +1 -1
- package/dist/cjs/boundary/hooks/useAuth.js +15 -2
- package/dist/cjs/boundary/hooks/useAuth.js.map +1 -1
- package/dist/cjs/components/sign-in.js +158 -35
- package/dist/cjs/components/sign-in.js.map +1 -1
- package/dist/cjs/components/sign-out-button.js +84 -0
- package/dist/cjs/components/sign-out-button.js.map +1 -0
- package/dist/cjs/components/sign-out.js +39 -9
- package/dist/cjs/components/sign-out.js.map +1 -1
- package/dist/cjs/components/sign-up.js +10 -5
- package/dist/cjs/components/sign-up.js.map +1 -1
- package/dist/cjs/errors.js +233 -5
- package/dist/cjs/errors.js.map +1 -1
- package/dist/cjs/index.js +3 -3
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/types.js +14 -0
- package/dist/cjs/types.js.map +1 -1
- package/dist/cjs/utils/construct.js +50 -18
- package/dist/cjs/utils/construct.js.map +1 -1
- package/dist/cjs/utils/redirect.js +57 -0
- package/dist/cjs/utils/redirect.js.map +1 -0
- package/dist/esm/app-router/client/TernSecureProvider.js +17 -2
- package/dist/esm/app-router/client/TernSecureProvider.js.map +1 -1
- package/dist/esm/app-router/client/actions.js +64 -56
- package/dist/esm/app-router/client/actions.js.map +1 -1
- package/dist/esm/app-router/route-handler/internal-route.js +18 -2
- package/dist/esm/app-router/route-handler/internal-route.js.map +1 -1
- package/dist/esm/boundary/TernSecureClientProvider.js +168 -35
- package/dist/esm/boundary/TernSecureClientProvider.js.map +1 -1
- package/dist/esm/boundary/TernSecureCtx.js.map +1 -1
- package/dist/esm/boundary/hooks/useAuth.js +15 -2
- package/dist/esm/boundary/hooks/useAuth.js.map +1 -1
- package/dist/esm/components/sign-in.js +160 -37
- package/dist/esm/components/sign-in.js.map +1 -1
- package/dist/esm/components/sign-out-button.js +60 -0
- package/dist/esm/components/sign-out-button.js.map +1 -0
- package/dist/esm/components/sign-out.js +30 -10
- package/dist/esm/components/sign-out.js.map +1 -1
- package/dist/esm/components/sign-up.js +10 -5
- package/dist/esm/components/sign-up.js.map +1 -1
- package/dist/esm/errors.js +229 -4
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/types.js +6 -0
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/utils/construct.js +46 -17
- package/dist/esm/utils/construct.js.map +1 -1
- package/dist/esm/utils/redirect.js +32 -0
- package/dist/esm/utils/redirect.js.map +1 -0
- package/dist/types/app-router/client/TernSecureProvider.d.ts +14 -3
- package/dist/types/app-router/client/TernSecureProvider.d.ts.map +1 -1
- package/dist/types/app-router/client/actions.d.ts +24 -18
- package/dist/types/app-router/client/actions.d.ts.map +1 -1
- package/dist/types/app-router/route-handler/internal-route.d.ts +8 -1
- package/dist/types/app-router/route-handler/internal-route.d.ts.map +1 -1
- package/dist/types/boundary/TernSecureClientProvider.d.ts +17 -1
- package/dist/types/boundary/TernSecureClientProvider.d.ts.map +1 -1
- package/dist/types/boundary/TernSecureCtx.d.ts +3 -8
- package/dist/types/boundary/TernSecureCtx.d.ts.map +1 -1
- package/dist/types/boundary/hooks/useAuth.d.ts +6 -1
- package/dist/types/boundary/hooks/useAuth.d.ts.map +1 -1
- package/dist/types/components/sign-in.d.ts.map +1 -1
- package/dist/types/components/sign-out-button.d.ts +14 -0
- package/dist/types/components/sign-out-button.d.ts.map +1 -0
- package/dist/types/components/sign-out.d.ts +7 -5
- package/dist/types/components/sign-out.d.ts.map +1 -1
- package/dist/types/components/sign-up.d.ts +4 -0
- package/dist/types/components/sign-up.d.ts.map +1 -1
- package/dist/types/components/ui/alert.d.ts +1 -1
- package/dist/types/components/ui/button.d.ts +1 -1
- package/dist/types/errors.d.ts +37 -2
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/types.d.ts +45 -0
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/utils/construct.d.ts +20 -4
- package/dist/types/utils/construct.d.ts.map +1 -1
- package/dist/types/utils/redirect.d.ts +9 -0
- package/dist/types/utils/redirect.d.ts.map +1 -0
- package/package.json +6 -6
- package/dist/cjs/boundary/hooks/useUser.js +0 -44
- package/dist/cjs/boundary/hooks/useUser.js.map +0 -1
- package/dist/esm/boundary/hooks/useUser.js +0 -20
- package/dist/esm/boundary/hooks/useUser.js.map +0 -1
- package/dist/types/boundary/hooks/useUser.d.ts +0 -7
- package/dist/types/boundary/hooks/useUser.d.ts.map +0 -1
|
@@ -4,23 +4,80 @@ import { useState, useEffect, useMemo, useCallback } from "react";
|
|
|
4
4
|
import { ternSecureAuth } from "../utils/client-init";
|
|
5
5
|
import { onAuthStateChanged } from "firebase/auth";
|
|
6
6
|
import { TernSecureCtx } from "./TernSecureCtx";
|
|
7
|
-
import { useRouter } from "next/navigation";
|
|
7
|
+
import { useRouter, usePathname } from "next/navigation";
|
|
8
|
+
import { isBaseAuthRoute, isInternalRoute, isAuthRoute } from "../app-router/route-handler/internal-route";
|
|
9
|
+
import { hasRedirectLoop } from "../utils/construct";
|
|
8
10
|
function TernSecureClientProvider({
|
|
9
11
|
children,
|
|
10
|
-
loginPath = "/sign-in",
|
|
11
|
-
|
|
12
|
+
loginPath = process.env.NEXT_PUBLIC_SIGN_IN_PATH || "/sign-in",
|
|
13
|
+
signUpPath = process.env.NEXT_PUBLIC_SIGN_UP_PATH || "/sign-up",
|
|
14
|
+
loadingComponent,
|
|
15
|
+
requiresVerification
|
|
12
16
|
}) {
|
|
13
17
|
const auth = useMemo(() => ternSecureAuth, []);
|
|
14
18
|
const router = useRouter();
|
|
19
|
+
const pathname = usePathname();
|
|
20
|
+
const [isRedirecting, setIsRedirecting] = useState(false);
|
|
15
21
|
const [authState, setAuthState] = useState(() => ({
|
|
16
22
|
userId: null,
|
|
17
23
|
isLoaded: false,
|
|
18
24
|
error: null,
|
|
19
25
|
isValid: false,
|
|
26
|
+
isVerified: false,
|
|
27
|
+
isAuthenticated: false,
|
|
20
28
|
token: null,
|
|
21
|
-
email: null
|
|
29
|
+
email: null,
|
|
30
|
+
status: "loading",
|
|
31
|
+
requiresVerification
|
|
22
32
|
}));
|
|
33
|
+
const constructUrlWithRedirect = useCallback(
|
|
34
|
+
(loginPath2, currentPath, loginPathParam, signUpPathParam) => {
|
|
35
|
+
const baseUrl = window.location.origin;
|
|
36
|
+
const signInUrl = new URL(loginPath2, baseUrl);
|
|
37
|
+
if (!currentPath.includes(loginPathParam) && !currentPath.includes(signUpPathParam)) {
|
|
38
|
+
signInUrl.searchParams.set("redirect", currentPath);
|
|
39
|
+
}
|
|
40
|
+
return signInUrl.toString();
|
|
41
|
+
},
|
|
42
|
+
[]
|
|
43
|
+
);
|
|
44
|
+
const shouldRedirect = useCallback(
|
|
45
|
+
(pathname2, isVerified) => {
|
|
46
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
47
|
+
if (isBaseAuthRoute(pathname2) && !searchParams.has("redirect")) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if (isInternalRoute(pathname2)) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
if (isAuthRoute(pathname2) && (!requiresVerification || isVerified)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
},
|
|
58
|
+
[requiresVerification]
|
|
59
|
+
);
|
|
60
|
+
const redirectToLogin = useCallback(
|
|
61
|
+
(currentPath) => {
|
|
62
|
+
const path = currentPath || pathname || "/";
|
|
63
|
+
if (isInternalRoute(path)) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (hasRedirectLoop(path, loginPath)) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
setIsRedirecting(true);
|
|
70
|
+
const loginUrl = constructUrlWithRedirect(loginPath, path, loginPath, signUpPath);
|
|
71
|
+
if (process.env.NODE_ENV === "production") {
|
|
72
|
+
window.location.href = loginUrl;
|
|
73
|
+
} else {
|
|
74
|
+
router.push(loginUrl);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
[router, loginPath, signUpPath, pathname, constructUrlWithRedirect]
|
|
78
|
+
);
|
|
23
79
|
const handleSignOut = useCallback(async (error) => {
|
|
80
|
+
const currentPath = window.location.pathname;
|
|
24
81
|
await auth.signOut();
|
|
25
82
|
setAuthState({
|
|
26
83
|
isLoaded: true,
|
|
@@ -28,50 +85,126 @@ function TernSecureClientProvider({
|
|
|
28
85
|
error: error || null,
|
|
29
86
|
isValid: false,
|
|
30
87
|
token: null,
|
|
31
|
-
email: null
|
|
88
|
+
email: null,
|
|
89
|
+
isVerified: false,
|
|
90
|
+
isAuthenticated: false,
|
|
91
|
+
status: "unauthenticated",
|
|
92
|
+
requiresVerification
|
|
32
93
|
});
|
|
33
|
-
|
|
34
|
-
}, [auth,
|
|
94
|
+
redirectToLogin(currentPath);
|
|
95
|
+
}, [auth, redirectToLogin, requiresVerification]);
|
|
35
96
|
const setEmail = useCallback((email) => {
|
|
36
97
|
setAuthState((prev) => ({
|
|
37
98
|
...prev,
|
|
38
99
|
email
|
|
39
100
|
}));
|
|
40
101
|
}, []);
|
|
102
|
+
const getAuthError = useCallback(() => {
|
|
103
|
+
if (authState.error) {
|
|
104
|
+
const error = authState.error;
|
|
105
|
+
return {
|
|
106
|
+
success: false,
|
|
107
|
+
message: error.message,
|
|
108
|
+
error: error.code,
|
|
109
|
+
user: null
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (authState.requiresVerification && authState.isValid && !authState.isVerified) {
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
message: "Email verification required",
|
|
116
|
+
error: "EMAIL_NOT_VERIFIED",
|
|
117
|
+
user: null
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (!authState.isAuthenticated && authState.status !== "loading") {
|
|
121
|
+
return {
|
|
122
|
+
success: false,
|
|
123
|
+
message: "User is not authenticated",
|
|
124
|
+
error: "AUTHENTICATED",
|
|
125
|
+
user: null
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
success: true,
|
|
130
|
+
user: ternSecureAuth.currentUser
|
|
131
|
+
};
|
|
132
|
+
}, [
|
|
133
|
+
authState.error,
|
|
134
|
+
authState.isValid,
|
|
135
|
+
authState.isVerified,
|
|
136
|
+
authState.isAuthenticated,
|
|
137
|
+
authState.status,
|
|
138
|
+
authState.requiresVerification
|
|
139
|
+
]);
|
|
41
140
|
useEffect(() => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
141
|
+
let mounted = true;
|
|
142
|
+
let initialLoad = true;
|
|
143
|
+
const unsubscribe = onAuthStateChanged(
|
|
144
|
+
auth,
|
|
145
|
+
async (user) => {
|
|
146
|
+
if (!mounted) return;
|
|
147
|
+
try {
|
|
148
|
+
if (user) {
|
|
149
|
+
const isValid = !!user.uid;
|
|
150
|
+
const isVerified = user.emailVerified;
|
|
151
|
+
const isAuthenticated = isValid && (!requiresVerification || isVerified);
|
|
152
|
+
setAuthState({
|
|
153
|
+
isLoaded: true,
|
|
154
|
+
userId: user.uid,
|
|
155
|
+
isValid,
|
|
156
|
+
isVerified,
|
|
157
|
+
isAuthenticated: isValid && isVerified,
|
|
158
|
+
token: user.getIdToken(),
|
|
159
|
+
error: null,
|
|
160
|
+
email: user.email,
|
|
161
|
+
status: isAuthenticated ? "authenticated" : "unverified",
|
|
162
|
+
requiresVerification
|
|
163
|
+
});
|
|
164
|
+
if (requiresVerification && !isVerified && shouldRedirect(pathname || "", isVerified)) {
|
|
165
|
+
if (initialLoad || !isRedirecting) {
|
|
166
|
+
redirectToLogin(pathname);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
setAuthState({
|
|
171
|
+
isLoaded: true,
|
|
172
|
+
userId: null,
|
|
173
|
+
isValid: false,
|
|
174
|
+
isVerified: false,
|
|
175
|
+
isAuthenticated: false,
|
|
176
|
+
token: null,
|
|
177
|
+
error: null,
|
|
178
|
+
email: null,
|
|
179
|
+
status: "unauthenticated",
|
|
180
|
+
requiresVerification
|
|
181
|
+
});
|
|
182
|
+
if (shouldRedirect(pathname || "", false) && initialLoad) {
|
|
183
|
+
redirectToLogin();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error("Auth state change error:", error);
|
|
188
|
+
if (mounted) {
|
|
189
|
+
handleSignOut(error instanceof Error ? error : new Error("Authentication error occurred"));
|
|
190
|
+
}
|
|
191
|
+
} finally {
|
|
192
|
+
initialLoad = false;
|
|
63
193
|
}
|
|
64
194
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
195
|
+
);
|
|
196
|
+
return () => {
|
|
197
|
+
mounted = false;
|
|
198
|
+
unsubscribe();
|
|
199
|
+
};
|
|
200
|
+
}, [auth, handleSignOut, redirectToLogin, requiresVerification, pathname, isRedirecting, shouldRedirect]);
|
|
70
201
|
const contextValue = useMemo(() => ({
|
|
71
202
|
...authState,
|
|
72
203
|
signOut: handleSignOut,
|
|
73
|
-
setEmail
|
|
74
|
-
|
|
204
|
+
setEmail,
|
|
205
|
+
getAuthError,
|
|
206
|
+
redirectToLogin
|
|
207
|
+
}), [authState, handleSignOut, setEmail, getAuthError, redirectToLogin]);
|
|
75
208
|
if (!authState.isLoaded) {
|
|
76
209
|
return /* @__PURE__ */ jsx(TernSecureCtx.Provider, { value: contextValue, children: loadingComponent || /* @__PURE__ */ jsx("div", { "aria-live": "polite", "aria-busy": "true", children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Loading authentication state..." }) }) });
|
|
77
210
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/boundary/TernSecureClientProvider.tsx"],"sourcesContent":["\"use client\"\n\nimport React, { useState, useEffect, useMemo, useCallback } from 'react'\nimport { ternSecureAuth } from '../utils/client-init'\nimport { onAuthStateChanged, User } from \"firebase/auth\"\nimport { TernSecureCtx, TernSecureCtxValue, TernSecureState } from './TernSecureCtx'\nimport { useRouter } from 'next/navigation'\n\ninterface TernSecureClientProviderProps {\n children: React.ReactNode;\n onUserChanged?: (user: User | null) => Promise<void>;\n loginPath?: string;\n loadingComponent?: React.ReactNode;\n}\n\nexport function TernSecureClientProvider({ \n children, \n loginPath = '/sign-in',\n loadingComponent\n}: TernSecureClientProviderProps) {\n const auth = useMemo(() => ternSecureAuth, []);\n const router = useRouter();\n const [authState, setAuthState] = useState<TernSecureState>(() => ({\n userId: null,\n isLoaded: false,\n error: null,\n isValid: false,\n token: null,\n email: null\n }));\n\n const handleSignOut = useCallback(async (error?: Error) => {\n await auth.signOut();\n setAuthState({\n isLoaded: true,\n userId: null,\n error: error || null,\n isValid: false,\n token: null,\n email: null\n });\n router.push(loginPath);\n }, [auth, router, loginPath]);\n\n const setEmail = useCallback((email: string) => {\n setAuthState((prev) => ({\n ...prev,\n email,\n }))\n }, [])\n\nuseEffect(() => {\n const unsubscribe = onAuthStateChanged(auth, async (user: User | null) => {\n if (user) {\n setAuthState({\n isLoaded: true,\n userId: user.uid,\n isValid: true,\n token: user.getIdToken(),\n error: null,\n email: user.email,\n })\n } else {\n setAuthState({\n isLoaded: true,\n userId: null,\n isValid: false,\n token: null,\n error: new Error('User is not authenticated'),\n email: null\n })\n if (!window.location.pathname.includes(\"/sign-up\")) {\n router.push(loginPath)\n }\n }\n }, (error) => {\n handleSignOut(error instanceof Error ? error : new Error('Authentication error occurred'));\n })\n \n return () => unsubscribe()\n }, [auth, handleSignOut, router, loginPath])\n\n const contextValue: TernSecureCtxValue = useMemo(() => ({\n ...authState,\n signOut: handleSignOut,\n setEmail\n }), [authState, auth, handleSignOut, setEmail]);\n\n if (!authState.isLoaded) {\n return (\n <TernSecureCtx.Provider value={contextValue}>\n {loadingComponent || (\n <div aria-live=\"polite\" aria-busy=\"true\">\n <span className=\"sr-only\">Loading authentication state...</span>\n </div>\n )}\n </TernSecureCtx.Provider>\n );\n }\n\n return (\n <TernSecureCtx.Provider value={contextValue}>\n {children}\n </TernSecureCtx.Provider>\n )\n}"],"mappings":";AA6FY;AA3FZ,SAAgB,UAAU,WAAW,SAAS,mBAAmB;AACjE,SAAS,sBAAsB;AAC/B,SAAS,0BAAgC;AACzC,SAAS,qBAA0D;AACnE,SAAS,iBAAiB;AASnB,SAAS,yBAAyB;AAAA,EACvC;AAAA,EACA,YAAY;AAAA,EACZ;AACF,GAAkC;AAChC,QAAM,OAAO,QAAQ,MAAM,gBAAgB,CAAC,CAAC;AAC7C,QAAM,SAAS,UAAU;AACzB,QAAM,CAAC,WAAW,YAAY,IAAI,SAA0B,OAAO;AAAA,IACjE,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,OAAO;AAAA,IACP,SAAS;AAAA,IACT,OAAO;AAAA,IACP,OAAO;AAAA,EACT,EAAE;AAEF,QAAM,gBAAgB,YAAY,OAAO,UAAkB;AACzD,UAAM,KAAK,QAAQ;AACnB,iBAAa;AAAA,MACX,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,OAAO,SAAS;AAAA,MAChB,SAAS;AAAA,MACT,OAAO;AAAA,MACP,OAAO;AAAA,IACT,CAAC;AACD,WAAO,KAAK,SAAS;AAAA,EACvB,GAAG,CAAC,MAAM,QAAQ,SAAS,CAAC;AAE5B,QAAM,WAAW,YAAY,CAAC,UAAkB;AAC9C,iBAAa,CAAC,UAAU;AAAA,MACtB,GAAG;AAAA,MACH;AAAA,IACF,EAAE;AAAA,EACJ,GAAG,CAAC,CAAC;AAEP,YAAU,MAAM;AACZ,UAAM,cAAc,mBAAmB,MAAM,OAAO,SAAsB;AACxE,UAAI,MAAM;AACR,qBAAa;AAAA,UACX,UAAU;AAAA,UACV,QAAQ,KAAK;AAAA,UACb,SAAS;AAAA,UACT,OAAO,KAAK,WAAW;AAAA,UACvB,OAAO;AAAA,UACP,OAAO,KAAK;AAAA,QACd,CAAC;AAAA,MACH,OAAO;AACL,qBAAa;AAAA,UACX,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,OAAO;AAAA,UACP,OAAO,IAAI,MAAM,2BAA2B;AAAA,UAC5C,OAAO;AAAA,QACT,CAAC;AACD,YAAI,CAAC,OAAO,SAAS,SAAS,SAAS,UAAU,GAAG;AAClD,iBAAO,KAAK,SAAS;AAAA,QACvB;AAAA,MACF;AAAA,IACF,GAAG,CAAC,UAAU;AACZ,oBAAc,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,+BAA+B,CAAC;AAAA,IAC3F,CAAC;AAED,WAAO,MAAM,YAAY;AAAA,EAC3B,GAAG,CAAC,MAAM,eAAe,QAAQ,SAAS,CAAC;AAE3C,QAAM,eAAmC,QAAQ,OAAO;AAAA,IACtD,GAAG;AAAA,IACH,SAAS;AAAA,IACT;AAAA,EACF,IAAI,CAAC,WAAW,MAAM,eAAe,QAAQ,CAAC;AAE9C,MAAI,CAAC,UAAU,UAAU;AACvB,WACE,oBAAC,cAAc,UAAd,EAAuB,OAAO,cAC5B,8BACC,oBAAC,SAAI,aAAU,UAAS,aAAU,QAChC,8BAAC,UAAK,WAAU,WAAU,6CAA+B,GAC3D,GAEJ;AAAA,EAEJ;AAEA,SACI,oBAAC,cAAc,UAAd,EAAuB,OAAO,cAC7B,UACF;AAEN;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/boundary/TernSecureClientProvider.tsx"],"sourcesContent":["\"use client\"\n\nimport React, { useState, useEffect, useMemo, useCallback } from 'react'\nimport { ternSecureAuth } from '../utils/client-init'\nimport { onAuthStateChanged, User } from \"firebase/auth\"\nimport { TernSecureCtx, TernSecureCtxValue } from './TernSecureCtx'\nimport type { TernSecureState, AuthError, SignInResponse } from \"../types\"\nimport type { ERRORS } from '../errors'\nimport { useRouter, usePathname } from 'next/navigation'\nimport { isBaseAuthRoute, isInternalRoute, isAuthRoute} from '../app-router/route-handler/internal-route'\nimport { hasRedirectLoop } from '../utils/construct'\n\n\n\n/**\n * @internal\n * Internal provider props - not meant for direct usage\n */\ninterface TernSecureClientProviderProps {\n children: React.ReactNode\n /** Callback when user state changes */\n onUserChanged?: (user: User | null) => Promise<void>\n /** Login page path */\n loginPath?: string\n /** Signup page path */\n signUpPath?: string\n /** Custom loading component */\n loadingComponent?: React.ReactNode\n /** Whether email verification is required */\n requiresVerification: boolean\n}\n\n/**\n * @internal\n * Internal provider component that handles authentication state\n * This is wrapped by the public TernSecureProvider\n */\n\nexport function TernSecureClientProvider({ \n children, \n loginPath = process.env.NEXT_PUBLIC_SIGN_IN_PATH || '/sign-in',\n signUpPath = process.env.NEXT_PUBLIC_SIGN_UP_PATH || '/sign-up',\n loadingComponent,\n requiresVerification,\n}: TernSecureClientProviderProps) {\n const auth = useMemo(() => ternSecureAuth, []);\n const router = useRouter();\n const pathname = usePathname() // Get current pathname\n const [isRedirecting, setIsRedirecting] = useState(false)\n\n const [authState, setAuthState] = useState<TernSecureState>(() => ({\n userId: null,\n isLoaded: false,\n error: null,\n isValid: false,\n isVerified: false,\n isAuthenticated: false,\n token: null,\n email: null,\n status: \"loading\",\n requiresVerification,\n }));\n\n const constructUrlWithRedirect = useCallback(\n (loginPath: string, currentPath: string, loginPathParam: string, signUpPathParam: string): string => {\n const baseUrl = window.location.origin\n const signInUrl = new URL(loginPath, baseUrl)\n\n // Only add redirect if not already on login or signup page\n if (!currentPath.includes(loginPathParam) && !currentPath.includes(signUpPathParam)) {\n signInUrl.searchParams.set(\"redirect\", currentPath)\n }\n return signInUrl.toString()\n },\n [],\n )\n\n\n const shouldRedirect = useCallback(\n (pathname: string, isVerified: boolean) => {\n // Get current search params\n const searchParams = new URLSearchParams(window.location.search)\n\n // Don't redirect if we're on the base sign-in page with no redirect param\n if (isBaseAuthRoute(pathname) && !searchParams.has(\"redirect\")) {\n return false\n }\n\n // Don't redirect if we're on an internal route\n if (isInternalRoute(pathname)) {\n return false\n }\n\n // Don't redirect if we're in auth routes (except when handling verification)\n if (isAuthRoute(pathname) && (!requiresVerification || isVerified)) {\n return false\n }\n\n return true\n },\n [requiresVerification],\n )\n\n const redirectToLogin = useCallback(\n (currentPath?: string) => {\n const path = currentPath || pathname || \"/\"\n\n\n if (isInternalRoute(path)) { // Don't redirect if we're already on an internal route\n return\n }\n\n // Check for redirect loops\n if (hasRedirectLoop(path, loginPath)) {\n return\n }\n\n setIsRedirecting(true)\n\n const loginUrl = constructUrlWithRedirect(loginPath, path, loginPath, signUpPath)\n\n if (process.env.NODE_ENV === \"production\") {\n window.location.href = loginUrl\n } else {\n // Use router.push for development\n router.push(loginUrl)\n }\n }, \n [router, loginPath, signUpPath, pathname, constructUrlWithRedirect]\n)\n\n const handleSignOut = useCallback(async (error?: Error) => {\n const currentPath = window.location.pathname\n await auth.signOut();\n setAuthState({\n isLoaded: true,\n userId: null,\n error: error || null,\n isValid: false,\n token: null,\n email: null,\n isVerified: false,\n isAuthenticated: false,\n status: \"unauthenticated\",\n requiresVerification,\n })\n redirectToLogin(currentPath)\n }, [auth, redirectToLogin, requiresVerification])\n\n const setEmail = useCallback((email: string) => {\n setAuthState((prev) => ({\n ...prev,\n email,\n }))\n }, [])\n\n const getAuthError = useCallback((): SignInResponse => {\n if (authState.error) {\n const error = authState.error as AuthError;\n return {\n success: false,\n message: error.message,\n error: error.code as keyof typeof ERRORS,\n user: null,\n }\n }\n\n if (authState.requiresVerification && authState.isValid && !authState.isVerified) {\n return {\n success: false,\n message: 'Email verification required',\n error: 'EMAIL_NOT_VERIFIED',\n user: null,\n }\n }\n\n if (!authState.isAuthenticated && authState.status !== \"loading\") {\n return {\n success: false,\n message: 'User is not authenticated',\n error: 'AUTHENTICATED',\n user: null,\n }\n }\n\n return {\n success: true,\n user: ternSecureAuth.currentUser,\n }\n }, [\n authState.error,\n authState.isValid,\n authState.isVerified,\n authState.isAuthenticated,\n authState.status,\n authState.requiresVerification,\n ])\n\nuseEffect(() => {\n let mounted = true\n let initialLoad = true\n\n const unsubscribe = onAuthStateChanged(\n auth,\n async (user: User | null) => {\n if (!mounted) return\n try {\n if (user) {\n const isValid = !!user.uid;\n const isVerified = user.emailVerified;\n const isAuthenticated = isValid && (!requiresVerification || isVerified) // Consider user authenticated if verification is not required or if email is verified\n\n setAuthState({\n isLoaded: true,\n userId: user.uid,\n isValid,\n isVerified,\n isAuthenticated: isValid && isVerified,\n token: user.getIdToken(),\n error: null,\n email: user.email,\n status: isAuthenticated ? \"authenticated\" : \"unverified\",\n requiresVerification,\n })\n \n if (requiresVerification && !isVerified && shouldRedirect(pathname || \"\", isVerified)) {\n if(initialLoad || !isRedirecting) {\n redirectToLogin(pathname)\n }\n }\n } else {\n setAuthState({\n isLoaded: true,\n userId: null,\n isValid: false,\n isVerified: false,\n isAuthenticated: false,\n token: null,\n error: null,\n email: null,\n status: \"unauthenticated\",\n requiresVerification,\n })\n \n if (shouldRedirect(pathname || \"\", false) && initialLoad) {\n redirectToLogin()\n }\n }\n } catch (error){\n console.error(\"Auth state change error:\", error)\n if (mounted) {\n handleSignOut(error instanceof Error ? error : new Error(\"Authentication error occurred\"))\n }\n } finally {\n initialLoad = false\n }\n })\n \n return () => {\n mounted = false\n unsubscribe()\n }\n }, [auth, handleSignOut, redirectToLogin, requiresVerification, pathname, isRedirecting, shouldRedirect])\n\n const contextValue: TernSecureCtxValue = useMemo(() => ({\n ...authState,\n signOut: handleSignOut,\n setEmail,\n getAuthError,\n redirectToLogin,\n }), [authState, handleSignOut, setEmail, getAuthError, redirectToLogin]);\n\n if (!authState.isLoaded) {\n return (\n <TernSecureCtx.Provider value={contextValue}>\n {loadingComponent || (\n <div aria-live=\"polite\" aria-busy=\"true\">\n <span className=\"sr-only\">Loading authentication state...</span>\n </div>\n )}\n </TernSecureCtx.Provider>\n );\n }\n\n return (\n <TernSecureCtx.Provider value={contextValue}>\n {children}\n </TernSecureCtx.Provider>\n )\n}"],"mappings":"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","names":["loginPath","pathname"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/boundary/TernSecureCtx.tsx"],"sourcesContent":["\"use client\"\n\nimport { createContext, useContext } from 'react'\nimport { ternSecureAuth } from '../utils/client-init';\nimport { User } from 'firebase/auth';\n\nexport const TernSecureUser = (): User | null => {\n return ternSecureAuth.currentUser;\n}\n\nexport interface TernSecureState {\n
|
|
1
|
+
{"version":3,"sources":["../../../src/boundary/TernSecureCtx.tsx"],"sourcesContent":["\"use client\"\n\nimport { createContext, useContext } from 'react'\nimport { ternSecureAuth } from '../utils/client-init';\nimport { User } from 'firebase/auth';\nimport type { TernSecureState, SignInResponse } from '../types';\n\nexport const TernSecureUser = (): User | null => {\n return ternSecureAuth.currentUser;\n}\n\nexport interface TernSecureCtxValue extends TernSecureState {\n signOut: () => Promise<void>\n setEmail: (email: string) => void\n getAuthError: () => SignInResponse\n redirectToLogin: () => void\n}\n\nexport const TernSecureCtx = createContext<TernSecureCtxValue | null>(null)\n\nTernSecureCtx.displayName = 'TernSecureCtx'\n\nexport const useTernSecure = (hookName: string) => {\n const context = useContext(TernSecureCtx)\n \n if (!context) {\n throw new Error(\n `${hookName} must be used within TernSecureProvider`\n )\n }\n\n return context\n}\n\n"],"mappings":";AAEA,SAAS,eAAe,kBAAkB;AAC1C,SAAS,sBAAsB;AAIxB,MAAM,iBAAiB,MAAmB;AAC/C,SAAO,eAAe;AACxB;AASO,MAAM,gBAAgB,cAAyC,IAAI;AAE1E,cAAc,cAAc;AAErB,MAAM,gBAAgB,CAAC,aAAqB;AACjD,QAAM,UAAU,WAAW,aAAa;AAExC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR,GAAG,QAAQ;AAAA,IACb;AAAA,EACF;AAEA,SAAO;AACT;","names":[]}
|
|
@@ -7,17 +7,30 @@ function useAuth() {
|
|
|
7
7
|
isLoaded,
|
|
8
8
|
error,
|
|
9
9
|
isValid,
|
|
10
|
+
isVerified,
|
|
11
|
+
isAuthenticated,
|
|
10
12
|
token,
|
|
13
|
+
getAuthError,
|
|
14
|
+
status,
|
|
15
|
+
requiresVerification,
|
|
11
16
|
signOut
|
|
12
17
|
} = useTernSecure("useAuth");
|
|
13
18
|
const user = TernSecureUser();
|
|
19
|
+
const authResponse = getAuthError();
|
|
14
20
|
return {
|
|
15
21
|
user,
|
|
16
22
|
userId,
|
|
17
23
|
isLoaded,
|
|
18
|
-
error,
|
|
19
|
-
|
|
24
|
+
error: authResponse.success ? null : authResponse,
|
|
25
|
+
isValid,
|
|
26
|
+
// User is signed in
|
|
27
|
+
isVerified,
|
|
28
|
+
// Email is verified
|
|
29
|
+
isAuthenticated,
|
|
30
|
+
// User is both signed in and verified
|
|
20
31
|
token,
|
|
32
|
+
status,
|
|
33
|
+
requiresVerification,
|
|
21
34
|
signOut
|
|
22
35
|
};
|
|
23
36
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/boundary/hooks/useAuth.ts"],"sourcesContent":["\"use client\"\n\nimport { useTernSecure } from '../TernSecureCtx'\nimport { User } from 'firebase/auth'\nimport { TernSecureUser } from '../TernSecureCtx'\n\nexport function useAuth() {\n const {\n userId,\n isLoaded,\n error,\n isValid,\n token,\n signOut\n } = useTernSecure('useAuth')\n\n const user: User | null = TernSecureUser()\n\n return {\n user,\n userId,\n isLoaded,\n error,\n isAuthenticated
|
|
1
|
+
{"version":3,"sources":["../../../../src/boundary/hooks/useAuth.ts"],"sourcesContent":["\"use client\"\n\nimport { useTernSecure } from '../TernSecureCtx'\nimport { User } from 'firebase/auth'\nimport { TernSecureUser } from '../TernSecureCtx'\nimport type { SignInResponse } from '../../types'\n\n\nexport function useAuth() {\n const {\n userId,\n isLoaded,\n error,\n isValid,\n isVerified,\n isAuthenticated,\n token,\n getAuthError,\n status,\n requiresVerification,\n signOut\n } = useTernSecure('useAuth')\n\n const user: User | null = TernSecureUser()\n const authResponse: SignInResponse = getAuthError()\n\n\n return {\n user,\n userId,\n isLoaded,\n error: authResponse.success ? null : authResponse,\n isValid, // User is signed in\n isVerified, // Email is verified\n isAuthenticated, // User is both signed in and verified\n token,\n status,\n requiresVerification,\n signOut\n }\n}\n"],"mappings":";AAEA,SAAS,qBAAqB;AAE9B,SAAS,sBAAsB;AAIxB,SAAS,UAAU;AACxB,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI,cAAc,SAAS;AAE3B,QAAM,OAAoB,eAAe;AACzC,QAAM,eAA+B,aAAa;AAGlD,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO,aAAa,UAAU,OAAO;AAAA,IACrC;AAAA;AAAA,IACA;AAAA;AAAA,IACA;AAAA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useCallback, useEffect } from "react";
|
|
4
|
-
import { useSearchParams } from "next/navigation";
|
|
4
|
+
import { useSearchParams, useRouter, usePathname } from "next/navigation";
|
|
5
5
|
import { signInWithEmail, signInWithRedirectGoogle, signInWithMicrosoft } from "../app-router/client/actions";
|
|
6
6
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./ui/card";
|
|
7
7
|
import { Input } from "./ui/input";
|
|
@@ -10,12 +10,15 @@ import { Button } from "./ui/button";
|
|
|
10
10
|
import { Alert, AlertDescription } from "./ui/alert";
|
|
11
11
|
import { Separator } from "./ui/separator";
|
|
12
12
|
import { cn } from "../lib/utils";
|
|
13
|
-
import { Loader2 } from "lucide-react";
|
|
13
|
+
import { Loader2, Eye, EyeOff } from "lucide-react";
|
|
14
14
|
import { getRedirectResult } from "firebase/auth";
|
|
15
15
|
import { ternSecureAuth } from "../utils/client-init";
|
|
16
16
|
import { createSessionCookie } from "../app-router/server/sessionTernSecure";
|
|
17
17
|
import { AuthBackground } from "./background";
|
|
18
18
|
import { getValidRedirectUrl } from "../utils/construct";
|
|
19
|
+
import { handleInternalRoute } from "../app-router/route-handler/internal-route";
|
|
20
|
+
import { useAuth } from "../boundary/hooks/useAuth";
|
|
21
|
+
import { getErrorAlertVariant } from "../errors";
|
|
19
22
|
const authDomain = process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN;
|
|
20
23
|
const appName = process.env.NEXT_PUBLIC_FIREBASE_APP_NAME || "TernSecure";
|
|
21
24
|
function SignIn({
|
|
@@ -27,11 +30,65 @@ function SignIn({
|
|
|
27
30
|
}) {
|
|
28
31
|
const [loading, setLoading] = useState(false);
|
|
29
32
|
const [checkingRedirect, setCheckingRedirect] = useState(true);
|
|
33
|
+
const [formError, setFormError] = useState(null);
|
|
30
34
|
const [error, setError] = useState("");
|
|
31
35
|
const [email, setEmail] = useState("");
|
|
32
36
|
const [password, setPassword] = useState("");
|
|
37
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
38
|
+
const [passwordFocused, setPasswordFocused] = useState(false);
|
|
39
|
+
const [authResponse, setAuthResponse] = useState(null);
|
|
40
|
+
const [authErrorMessage, setAuthErrorMessage] = useState(null);
|
|
33
41
|
const searchParams = useSearchParams();
|
|
34
42
|
const isRedirectSignIn = searchParams.get("signInRedirect") === "true";
|
|
43
|
+
const router = useRouter();
|
|
44
|
+
const pathname = usePathname();
|
|
45
|
+
const InternalComponent = handleInternalRoute(pathname || "");
|
|
46
|
+
const { requiresVerification, error: authError, status } = useAuth();
|
|
47
|
+
const validRedirectUrl = getValidRedirectUrl(searchParams, redirectUrl);
|
|
48
|
+
if (InternalComponent) {
|
|
49
|
+
return /* @__PURE__ */ jsx(InternalComponent, {});
|
|
50
|
+
}
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (authError && status !== "loading" && status !== "unauthenticated") {
|
|
53
|
+
const message = authError.message || "Authentication failed";
|
|
54
|
+
setAuthErrorMessage(message);
|
|
55
|
+
if (!authResponse || authResponse.message !== message) {
|
|
56
|
+
setAuthResponse(authError);
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
setAuthErrorMessage(null);
|
|
60
|
+
}
|
|
61
|
+
}, [authError, status, authResponse]);
|
|
62
|
+
const handleSuccessfulAuth = useCallback(
|
|
63
|
+
async (user) => {
|
|
64
|
+
try {
|
|
65
|
+
const idToken = await user.getIdToken();
|
|
66
|
+
const sessionResult = await createSessionCookie(idToken);
|
|
67
|
+
if (!sessionResult.success) {
|
|
68
|
+
setFormError({
|
|
69
|
+
success: false,
|
|
70
|
+
message: sessionResult.message || "Failed to create session",
|
|
71
|
+
error: "INTERNAL_ERROR",
|
|
72
|
+
user: null
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
onSuccess == null ? void 0 : onSuccess();
|
|
76
|
+
if (process.env.NODE_ENV === "production") {
|
|
77
|
+
window.location.href = validRedirectUrl;
|
|
78
|
+
} else {
|
|
79
|
+
router.push(validRedirectUrl);
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
setFormError({
|
|
83
|
+
success: false,
|
|
84
|
+
message: "Failed to complete authentication",
|
|
85
|
+
error: "INTERNAL_ERROR",
|
|
86
|
+
user: null
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
[validRedirectUrl, router, onSuccess]
|
|
91
|
+
);
|
|
35
92
|
const handleRedirectResult = useCallback(async () => {
|
|
36
93
|
if (!isRedirectSignIn) return false;
|
|
37
94
|
setCheckingRedirect(true);
|
|
@@ -52,15 +109,16 @@ function SignIn({
|
|
|
52
109
|
const storedRedirectUrl = sessionStorage.getItem("auth_return_url");
|
|
53
110
|
sessionStorage.removeItem("auth_redirect_url");
|
|
54
111
|
onSuccess == null ? void 0 : onSuccess();
|
|
55
|
-
window.location.href = storedRedirectUrl || getValidRedirectUrl(
|
|
112
|
+
window.location.href = storedRedirectUrl || getValidRedirectUrl(searchParams, redirectUrl);
|
|
56
113
|
return true;
|
|
57
114
|
}
|
|
58
115
|
setCheckingRedirect(false);
|
|
59
116
|
} catch (err) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
117
|
+
const errorMessage = err;
|
|
118
|
+
setFormError(errorMessage);
|
|
119
|
+
if (onError && err instanceof Error) {
|
|
120
|
+
onError(err);
|
|
121
|
+
}
|
|
64
122
|
sessionStorage.removeItem("auth_redirect_url");
|
|
65
123
|
return false;
|
|
66
124
|
}
|
|
@@ -69,21 +127,42 @@ function SignIn({
|
|
|
69
127
|
if (isRedirectSignIn) {
|
|
70
128
|
handleRedirectResult();
|
|
71
129
|
}
|
|
72
|
-
;
|
|
73
130
|
}, [handleRedirectResult, isRedirectSignIn]);
|
|
74
131
|
const handleSubmit = async (e) => {
|
|
75
132
|
e.preventDefault();
|
|
76
133
|
setLoading(true);
|
|
134
|
+
setFormError(null);
|
|
135
|
+
setAuthResponse(null);
|
|
77
136
|
try {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
137
|
+
const response = await signInWithEmail(email, password);
|
|
138
|
+
setAuthResponse(response);
|
|
139
|
+
if (!response.success) {
|
|
140
|
+
setFormError({
|
|
141
|
+
success: false,
|
|
142
|
+
message: response.message,
|
|
143
|
+
error: response.error,
|
|
144
|
+
user: null
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (response.user) {
|
|
149
|
+
if (requiresVerification && !response.user.emailVerified) {
|
|
150
|
+
setFormError({
|
|
151
|
+
success: false,
|
|
152
|
+
message: "Email verification required",
|
|
153
|
+
error: "REQUIRES_VERIFICATION",
|
|
154
|
+
user: response.user
|
|
155
|
+
});
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
await handleSuccessfulAuth(response.user);
|
|
82
159
|
}
|
|
83
160
|
} catch (err) {
|
|
84
|
-
const errorMessage = err
|
|
85
|
-
|
|
86
|
-
onError
|
|
161
|
+
const errorMessage = err;
|
|
162
|
+
setFormError(errorMessage);
|
|
163
|
+
if (onError && err instanceof Error) {
|
|
164
|
+
onError(err);
|
|
165
|
+
}
|
|
87
166
|
} finally {
|
|
88
167
|
setLoading(false);
|
|
89
168
|
}
|
|
@@ -91,8 +170,8 @@ function SignIn({
|
|
|
91
170
|
const handleSocialSignIn = async (provider) => {
|
|
92
171
|
setLoading(true);
|
|
93
172
|
try {
|
|
94
|
-
const
|
|
95
|
-
sessionStorage.setItem("auth_redirect_url",
|
|
173
|
+
const validRedirectUrl2 = getValidRedirectUrl(searchParams, redirectUrl);
|
|
174
|
+
sessionStorage.setItem("auth_redirect_url", validRedirectUrl2);
|
|
96
175
|
const currentUrl = new URL(window.location.href);
|
|
97
176
|
currentUrl.searchParams.set("signInRedirect", "true");
|
|
98
177
|
window.history.replaceState({}, "", currentUrl.toString());
|
|
@@ -101,29 +180,50 @@ function SignIn({
|
|
|
101
180
|
throw new Error(result.error);
|
|
102
181
|
}
|
|
103
182
|
} catch (err) {
|
|
104
|
-
const errorMessage = err
|
|
105
|
-
|
|
106
|
-
onError
|
|
183
|
+
const errorMessage = err;
|
|
184
|
+
setFormError(errorMessage);
|
|
185
|
+
if (onError && err instanceof Error) {
|
|
186
|
+
onError(err);
|
|
187
|
+
}
|
|
107
188
|
setLoading(false);
|
|
108
189
|
sessionStorage.removeItem("auth_redirect_url");
|
|
109
190
|
}
|
|
110
191
|
};
|
|
192
|
+
const handleVerificationRedirect = (e) => {
|
|
193
|
+
e.preventDefault();
|
|
194
|
+
router.push("/sign-in/verify");
|
|
195
|
+
};
|
|
111
196
|
if (checkingRedirect && isRedirectSignIn) {
|
|
112
197
|
return /* @__PURE__ */ jsx("div", { className: "flex min-h-screen items-center justify-center", children: /* @__PURE__ */ jsx("div", { className: "text-center space-y-4", children: /* @__PURE__ */ jsx("div", { className: "animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto" }) }) });
|
|
113
198
|
}
|
|
199
|
+
const activeError = formError || authResponse;
|
|
200
|
+
const showEmailVerificationButton = (activeError == null ? void 0 : activeError.error) === "EMAIL_NOT_VERIFIED" || (activeError == null ? void 0 : activeError.error) === "REQUIRES_VERIFICATION";
|
|
114
201
|
return /* @__PURE__ */ jsxs("div", { className: "relative flex items-center justify-center", children: [
|
|
115
202
|
/* @__PURE__ */ jsx(AuthBackground, {}),
|
|
116
203
|
/* @__PURE__ */ jsxs(Card, { className: cn("w-full max-w-md mx-auto mt-8", className, customStyles.card), children: [
|
|
117
204
|
/* @__PURE__ */ jsxs(CardHeader, { className: "space-y-1 text-center", children: [
|
|
118
205
|
/* @__PURE__ */ jsxs(CardTitle, { className: cn("font-bold", customStyles.title), children: [
|
|
119
206
|
"Sign in to ",
|
|
120
|
-
`${appName}
|
|
207
|
+
`${appName}`,
|
|
208
|
+
" "
|
|
121
209
|
] }),
|
|
122
210
|
/* @__PURE__ */ jsx(CardDescription, { className: cn("text-muted-foreground", customStyles.description), children: "Please sign in to continue" })
|
|
123
211
|
] }),
|
|
124
212
|
/* @__PURE__ */ jsxs(CardContent, { className: "space-y-4", children: [
|
|
125
213
|
/* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [
|
|
126
|
-
|
|
214
|
+
activeError && /* @__PURE__ */ jsx(Alert, { variant: getErrorAlertVariant(activeError), className: "animate-in fade-in-50", children: /* @__PURE__ */ jsxs(AlertDescription, { children: [
|
|
215
|
+
/* @__PURE__ */ jsx("span", { children: activeError.message }),
|
|
216
|
+
showEmailVerificationButton && /* @__PURE__ */ jsx(
|
|
217
|
+
Button,
|
|
218
|
+
{
|
|
219
|
+
type: "button",
|
|
220
|
+
variant: "link",
|
|
221
|
+
className: "p-0 h-auto font-normal text-sm hover:underline",
|
|
222
|
+
onClick: handleVerificationRedirect,
|
|
223
|
+
children: "Request new verification email \u2192"
|
|
224
|
+
}
|
|
225
|
+
)
|
|
226
|
+
] }) }),
|
|
127
227
|
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
128
228
|
/* @__PURE__ */ jsx(Label, { htmlFor: "email", className: cn(customStyles.label), children: "Email" }),
|
|
129
229
|
/* @__PURE__ */ jsx(
|
|
@@ -136,24 +236,47 @@ function SignIn({
|
|
|
136
236
|
onChange: (e) => setEmail(e.target.value),
|
|
137
237
|
disabled: loading,
|
|
138
238
|
className: cn(customStyles.input),
|
|
139
|
-
required: true
|
|
239
|
+
required: true,
|
|
240
|
+
"aria-invalid": (activeError == null ? void 0 : activeError.error) === "INVALID_EMAIL",
|
|
241
|
+
"aria-describedby": activeError ? "error-message" : void 0
|
|
140
242
|
}
|
|
141
243
|
)
|
|
142
244
|
] }),
|
|
143
245
|
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
144
246
|
/* @__PURE__ */ jsx(Label, { htmlFor: "password", className: cn(customStyles.label), children: "Password" }),
|
|
145
|
-
/* @__PURE__ */
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
247
|
+
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
248
|
+
/* @__PURE__ */ jsx(
|
|
249
|
+
Input,
|
|
250
|
+
{
|
|
251
|
+
id: "password",
|
|
252
|
+
name: "password",
|
|
253
|
+
type: showPassword ? "text" : "password",
|
|
254
|
+
value: password,
|
|
255
|
+
onChange: (e) => setPassword(e.target.value),
|
|
256
|
+
onFocus: () => setPasswordFocused(true),
|
|
257
|
+
onBlur: () => setPasswordFocused(false),
|
|
258
|
+
disabled: loading,
|
|
259
|
+
className: cn(customStyles.input),
|
|
260
|
+
required: true,
|
|
261
|
+
"aria-invalid": (activeError == null ? void 0 : activeError.error) === "INVALID_CREDENTIALS",
|
|
262
|
+
"aria-describedby": activeError ? "error-message" : void 0
|
|
263
|
+
}
|
|
264
|
+
),
|
|
265
|
+
/* @__PURE__ */ jsxs(
|
|
266
|
+
Button,
|
|
267
|
+
{
|
|
268
|
+
type: "button",
|
|
269
|
+
variant: "ghost",
|
|
270
|
+
size: "icon",
|
|
271
|
+
className: "absolute right-2 top-1/2 -translate-y-1/2 h-8 w-8 hover:bg-transparent",
|
|
272
|
+
onClick: () => setShowPassword(!showPassword),
|
|
273
|
+
children: [
|
|
274
|
+
showPassword ? /* @__PURE__ */ jsx(EyeOff, { className: "h-4 w-4 text-muted-foreground hover:text-foreground" }) : /* @__PURE__ */ jsx(Eye, { className: "h-4 w-4 text-muted-foreground hover:text-foreground" }),
|
|
275
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: showPassword ? "Hide password" : "Show password" })
|
|
276
|
+
]
|
|
277
|
+
}
|
|
278
|
+
)
|
|
279
|
+
] })
|
|
157
280
|
] }),
|
|
158
281
|
/* @__PURE__ */ jsx(Button, { type: "submit", disabled: loading, className: cn("w-full", customStyles.button), children: loading ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
159
282
|
/* @__PURE__ */ jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }),
|
|
@@ -161,8 +284,8 @@ function SignIn({
|
|
|
161
284
|
] }) : "Sign in" })
|
|
162
285
|
] }),
|
|
163
286
|
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
164
|
-
/* @__PURE__ */ jsx(
|
|
165
|
-
/* @__PURE__ */ jsx("div", { className: "
|
|
287
|
+
/* @__PURE__ */ jsx(Separator, { className: cn(customStyles.separator) }),
|
|
288
|
+
/* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "bg-background px-2 text-muted-foreground text-sm", children: "Or continue with" }) })
|
|
166
289
|
] }),
|
|
167
290
|
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
|
|
168
291
|
/* @__PURE__ */ jsxs(
|