@sanvika/auth 2.7.0 → 2.9.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.
Files changed (2) hide show
  1. package/dist/index.js +408 -23
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -17,10 +17,9 @@ var STORAGE_KEYS = {
17
17
  };
18
18
  var DEFAULT_AVATAR_SVG = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Ccircle cx='20' cy='20' r='20' fill='%23e5e7eb'/%3E%3Ccircle cx='20' cy='15' r='7' fill='%23adb5bd'/%3E%3Cellipse cx='20' cy='35' rx='12' ry='8' fill='%23adb5bd'/%3E%3C/svg%3E`;
19
19
 
20
- // SanvikaAuthProvider.jsx
21
- import { jsx } from "react/jsx-runtime";
22
- var S_AUTH_URL = "https://auth.sanvikaproduction.com";
23
- var SanvikaAuthContext = createContext(null);
20
+ // authFlow.js
21
+ var DEFAULT_AUTH_URL = "https://auth.sanvikaproduction.com";
22
+ var DEVICE_ID_STORAGE_KEY = "sanvika_deviceId";
24
23
  function randomDeviceId() {
25
24
  try {
26
25
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
@@ -30,6 +29,121 @@ function randomDeviceId() {
30
29
  }
31
30
  return `device-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
32
31
  }
32
+ function getOrCreateWebDeviceId() {
33
+ var _a, _b;
34
+ if (typeof window === "undefined") return randomDeviceId();
35
+ let deviceId = localStorage.getItem(DEVICE_ID_STORAGE_KEY);
36
+ if (!deviceId) {
37
+ const raw = `${navigator.userAgent}|${((_a = window.screen) == null ? void 0 : _a.width) ?? 0}x${((_b = window.screen) == null ? void 0 : _b.height) ?? 0}|${navigator.language}`;
38
+ let hash = 0;
39
+ for (let i = 0; i < raw.length; i++) {
40
+ hash = Math.imul(31, hash) + raw.charCodeAt(i) | 0;
41
+ }
42
+ deviceId = `d_${Math.abs(hash).toString(36)}_${Date.now().toString(36)}`;
43
+ localStorage.setItem(DEVICE_ID_STORAGE_KEY, deviceId);
44
+ }
45
+ return deviceId;
46
+ }
47
+ async function checkMobile({
48
+ authBaseUrl = DEFAULT_AUTH_URL,
49
+ mobile,
50
+ deviceId,
51
+ clientId,
52
+ callbackUrl
53
+ }) {
54
+ const res = await fetch(`${authBaseUrl}/api/auth/check-mobile`, {
55
+ method: "POST",
56
+ headers: { "Content-Type": "application/json" },
57
+ body: JSON.stringify({
58
+ mobile: String(mobile).trim(),
59
+ deviceId,
60
+ clientId,
61
+ ...callbackUrl ? { callbackUrl } : {}
62
+ })
63
+ });
64
+ const data = await res.json();
65
+ if (!data.success) {
66
+ const err = new Error(data.message || data.error || "Check mobile failed");
67
+ err.code = data.error;
68
+ throw err;
69
+ }
70
+ return data;
71
+ }
72
+ async function postLogin({
73
+ authBaseUrl = DEFAULT_AUTH_URL,
74
+ mobile,
75
+ password,
76
+ deviceId,
77
+ userAgent,
78
+ clientId,
79
+ deviceName
80
+ }) {
81
+ const body = {
82
+ mobile: String(mobile).trim(),
83
+ deviceId,
84
+ clientId,
85
+ userAgent: userAgent || deviceName || "Sanvika App"
86
+ };
87
+ if (password) {
88
+ body.password = password;
89
+ }
90
+ const res = await fetch(`${authBaseUrl}/api/auth/login`, {
91
+ method: "POST",
92
+ headers: { "Content-Type": "application/json" },
93
+ body: JSON.stringify(body)
94
+ });
95
+ const data = await res.json();
96
+ if (!data.success) {
97
+ const err = new Error(data.error || data.message || "Login failed");
98
+ err.code = data.error;
99
+ throw err;
100
+ }
101
+ return data;
102
+ }
103
+ async function deviceAwareLogin({
104
+ authBaseUrl = DEFAULT_AUTH_URL,
105
+ mobile,
106
+ password,
107
+ deviceId,
108
+ userAgent,
109
+ deviceName,
110
+ clientId,
111
+ callbackUrl,
112
+ resolveDeviceId
113
+ }) {
114
+ const resolvedDeviceId = deviceId || (typeof resolveDeviceId === "function" ? await resolveDeviceId() : getOrCreateWebDeviceId());
115
+ const check = await checkMobile({
116
+ authBaseUrl,
117
+ mobile,
118
+ deviceId: resolvedDeviceId,
119
+ clientId,
120
+ callbackUrl
121
+ });
122
+ if (!check.exists) {
123
+ const err = new Error("Mobile number not registered.");
124
+ err.code = "USER_NOT_REGISTERED";
125
+ throw err;
126
+ }
127
+ if (!check.isKnownDevice && !password) {
128
+ const err = new Error("Password is required for this device.");
129
+ err.code = "MISSING_PASSWORD";
130
+ err.requiresPassword = true;
131
+ throw err;
132
+ }
133
+ return postLogin({
134
+ authBaseUrl,
135
+ mobile,
136
+ password: check.isKnownDevice ? void 0 : password,
137
+ deviceId: resolvedDeviceId,
138
+ userAgent,
139
+ deviceName,
140
+ clientId
141
+ });
142
+ }
143
+
144
+ // SanvikaAuthProvider.jsx
145
+ import { jsx } from "react/jsx-runtime";
146
+ var SanvikaAuthContext = createContext(null);
33
147
  function createDefaultWebPersistence() {
34
148
  return {
35
149
  getItem: async (key) => typeof localStorage !== "undefined" ? localStorage.getItem(key) : null,
@@ -50,6 +164,7 @@ function SanvikaAuthProvider({
50
164
  clientId,
51
165
  redirectUri,
52
166
  dashboardPath,
167
+ authBaseUrl = DEFAULT_AUTH_URL,
53
168
  persistence: persistenceProp
54
169
  }) {
55
170
  const persistence = useMemo(() => {
@@ -83,25 +198,62 @@ function SanvikaAuthProvider({
83
198
  cancelled = true;
84
199
  };
85
200
  }, [persistence]);
86
- const login = async ({ mobile, password, deviceId, deviceName }) => {
87
- const response = await fetch(`${S_AUTH_URL}/api/auth/login`, {
88
- method: "POST",
89
- headers: { "Content-Type": "application/json" },
90
- body: JSON.stringify({
201
+ const persistSession = useCallback(
202
+ async (token, userData) => {
203
+ await persistence.setItem(STORAGE_KEYS.ACCESS_TOKEN, token);
204
+ await persistence.setItem(STORAGE_KEYS.USER, JSON.stringify(userData));
205
+ setToken(token);
206
+ setUser(userData);
207
+ },
208
+ [persistence]
209
+ );
210
+ const checkMobile2 = useCallback(
211
+ async ({ mobile, deviceId }) => {
212
+ const resolvedDeviceId = deviceId || getOrCreateWebDeviceId();
213
+ return checkMobile({
214
+ authBaseUrl,
215
+ mobile,
216
+ deviceId: resolvedDeviceId,
217
+ clientId,
218
+ callbackUrl: redirectUri
219
+ });
220
+ },
221
+ [authBaseUrl, clientId, redirectUri]
222
+ );
223
+ const login = async ({
224
+ mobile,
225
+ password,
226
+ deviceId,
227
+ deviceName,
228
+ userAgent,
229
+ skipDeviceCheck = false
230
+ }) => {
231
+ const resolveDeviceId = async () => deviceId || getOrCreateWebDeviceId();
232
+ let data;
233
+ if (skipDeviceCheck) {
234
+ data = await postLogin({
235
+ authBaseUrl,
91
236
  mobile,
92
237
  password,
93
238
  deviceId: deviceId || randomDeviceId(),
94
- deviceName: deviceName || "Browser"
95
- })
96
- });
97
- const data = await response.json();
98
- if (!data.success) {
99
- throw new Error(data.error || data.message || "Login failed");
239
+ userAgent: userAgent || deviceName,
240
+ clientId,
241
+ deviceName
242
+ });
243
+ } else {
244
+ data = await deviceAwareLogin({
245
+ authBaseUrl,
246
+ mobile,
247
+ password,
248
+ deviceId,
249
+ userAgent: userAgent || (typeof navigator !== "undefined" ? navigator.userAgent : void 0),
250
+ deviceName,
251
+ clientId,
252
+ callbackUrl: redirectUri,
253
+ resolveDeviceId
254
+ });
100
255
  }
101
- await persistence.setItem(STORAGE_KEYS.ACCESS_TOKEN, data.accessToken);
102
- await persistence.setItem(STORAGE_KEYS.USER, JSON.stringify(data.user));
103
- setToken(data.accessToken);
104
- setUser(data.user);
256
+ await persistSession(data.accessToken, data.user);
105
257
  return data;
106
258
  };
107
259
  const setAuth = useCallback(
@@ -115,7 +267,7 @@ function SanvikaAuthProvider({
115
267
  );
116
268
  const logout = async () => {
117
269
  try {
118
- await fetch(`${S_AUTH_URL}/api/auth/logout`, {
270
+ await fetch(`${authBaseUrl}/api/auth/logout`, {
119
271
  method: "POST",
120
272
  headers: {
121
273
  "Content-Type": "application/json",
@@ -153,12 +305,14 @@ function SanvikaAuthProvider({
153
305
  isAuthenticated: !!user,
154
306
  isLoggedIn: !!user,
155
307
  login,
308
+ checkMobile: checkMobile2,
156
309
  logout,
157
310
  setAuth,
158
311
  authFetch,
159
312
  clientId,
160
313
  redirectUri,
161
- dashboardPath
314
+ dashboardPath,
315
+ authBaseUrl
162
316
  };
163
317
  return /* @__PURE__ */ jsx(SanvikaAuthContext.Provider, { value, children });
164
318
  }
@@ -196,7 +350,7 @@ styleInject("@keyframes snvk-shimmer {\n 0% {\n background-position: 200% 0;
196
350
 
197
351
  // SanvikaAccountButton.jsx
198
352
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
199
- var S_AUTH_URL2 = "https://auth.sanvikaproduction.com";
353
+ var S_AUTH_URL = "https://auth.sanvikaproduction.com";
200
354
  var SanvikaAccountButtonErrorBoundary = class extends Component {
201
355
  constructor(props) {
202
356
  super(props);
@@ -329,7 +483,7 @@ function SanvikaAccountButtonContent({
329
483
  if (!isAuthenticated || loading) {
330
484
  const { clientId } = auth;
331
485
  const redirectUri = auth.redirectUri || (typeof window !== "undefined" && window.location ? window.location.origin + "/auth/callback" : "");
332
- const authorizeUrl = clientId && redirectUri ? `${S_AUTH_URL2}/authorize?client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUri)}` : `${S_AUTH_URL2}/authorize`;
486
+ const authorizeUrl = clientId && redirectUri ? `${S_AUTH_URL}/authorize?client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUri)}` : `${S_AUTH_URL}/authorize`;
333
487
  return /* @__PURE__ */ jsxs(
334
488
  "button",
335
489
  {
@@ -463,11 +617,242 @@ function SanvikaAccountButtonContent({
463
617
  function SanvikaAccountButton(props) {
464
618
  return /* @__PURE__ */ jsx2(SanvikaAccountButtonErrorBoundary, { children: /* @__PURE__ */ jsx2(SanvikaAccountButtonContent, { ...props }) });
465
619
  }
620
+
621
+ // SanvikaAdminLogin.jsx
622
+ import { useState as useState3, useEffect as useEffect3 } from "react";
623
+ import { useRouter } from "next/navigation";
624
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
625
+ var AUTH_BASE_URL = "https://auth.sanvikaproduction.com";
626
+ var DEVICE_ID_KEY = "sanvika_admin_device_id";
627
+ function getDeviceId() {
628
+ var _a;
629
+ if (typeof window === "undefined") return "";
630
+ try {
631
+ let id = window.localStorage.getItem(DEVICE_ID_KEY);
632
+ if (!id) {
633
+ id = ((_a = crypto == null ? void 0 : crypto.randomUUID) == null ? void 0 : _a.call(crypto)) || `dev-${Date.now()}-${Math.random()}`;
634
+ window.localStorage.setItem(DEVICE_ID_KEY, id);
635
+ }
636
+ return id;
637
+ } catch {
638
+ return `dev-${Date.now()}`;
639
+ }
640
+ }
641
+ function friendlyError(code) {
642
+ switch (code) {
643
+ case "INVALID_MOBILE":
644
+ return "Mobile number is not valid.";
645
+ case "MISSING_DEVICE_ID":
646
+ return "Device could not be identified. Reload and retry.";
647
+ case "USER_NOT_FOUND":
648
+ return "No account found for this mobile.";
649
+ case "MISSING_PASSWORD":
650
+ return "Please enter your password.";
651
+ case "INVALID_PASSWORD":
652
+ return "Incorrect password.";
653
+ case "RATE_LIMIT_EXCEEDED":
654
+ return "Too many attempts. Please try again in 15 minutes.";
655
+ case "SERVER_ERROR":
656
+ return "Auth service error. Please try again.";
657
+ default:
658
+ return "Login failed. Please try again.";
659
+ }
660
+ }
661
+ function SanvikaAdminLogin({
662
+ serviceName = "Sanvika",
663
+ dashboardPath = "/dashboard/admin",
664
+ homePath = "/"
665
+ }) {
666
+ const router = useRouter();
667
+ const { isAuthenticated, loading, user, setAuth } = useSanvikaAuth();
668
+ const [mobile, setMobile] = useState3("");
669
+ const [password, setPassword] = useState3("");
670
+ const [error, setError] = useState3("");
671
+ const [submitting, setSubmitting] = useState3(false);
672
+ const [ready, setReady] = useState3(false);
673
+ useEffect3(() => {
674
+ if (loading) return;
675
+ if (isAuthenticated && (user == null ? void 0 : user.role) === "superadmin") {
676
+ router.replace(dashboardPath);
677
+ return;
678
+ }
679
+ setReady(true);
680
+ }, [loading, isAuthenticated, user, router, dashboardPath]);
681
+ async function handleLogin(e) {
682
+ var _a;
683
+ e.preventDefault();
684
+ setError("");
685
+ setSubmitting(true);
686
+ try {
687
+ const res = await fetch(`${AUTH_BASE_URL}/api/auth/login`, {
688
+ method: "POST",
689
+ headers: { "Content-Type": "application/json" },
690
+ body: JSON.stringify({
691
+ mobile,
692
+ password,
693
+ deviceId: getDeviceId(),
694
+ deviceName: "Browser"
695
+ })
696
+ });
697
+ const data = await res.json().catch(() => ({}));
698
+ if (!data.success) {
699
+ setError(friendlyError(data.error));
700
+ setPassword("");
701
+ return;
702
+ }
703
+ if (((_a = data.user) == null ? void 0 : _a.role) !== "superadmin") {
704
+ setError("This account does not have admin access.");
705
+ setPassword("");
706
+ return;
707
+ }
708
+ setAuth(data.accessToken, data.user);
709
+ router.replace(dashboardPath);
710
+ } catch {
711
+ setError("Could not reach the auth service. Please try again.");
712
+ } finally {
713
+ setSubmitting(false);
714
+ }
715
+ }
716
+ const S = {
717
+ page: {
718
+ minHeight: "100vh",
719
+ display: "flex",
720
+ alignItems: "center",
721
+ justifyContent: "center",
722
+ padding: "20px",
723
+ background: "linear-gradient(135deg,#0f172a 0%,#1e1b4b 100%)",
724
+ fontFamily: "system-ui,-apple-system,'Segoe UI',Roboto,sans-serif"
725
+ },
726
+ card: {
727
+ position: "relative",
728
+ width: "100%",
729
+ maxWidth: "380px",
730
+ background: "#1e293b",
731
+ borderRadius: "16px",
732
+ padding: "36px 32px",
733
+ boxShadow: "0 20px 50px rgba(0,0,0,.4)",
734
+ border: "1px solid #334155"
735
+ },
736
+ closeBtn: {
737
+ position: "absolute",
738
+ top: "14px",
739
+ right: "16px",
740
+ background: "none",
741
+ border: "none",
742
+ color: "#64748b",
743
+ fontSize: "20px",
744
+ cursor: "pointer",
745
+ lineHeight: 1
746
+ },
747
+ logo: { fontSize: "40px", textAlign: "center", marginBottom: "8px" },
748
+ title: {
749
+ margin: "0 0 4px",
750
+ textAlign: "center",
751
+ color: "#f1f5f9",
752
+ fontSize: "22px",
753
+ fontWeight: 700
754
+ },
755
+ subtitle: {
756
+ margin: "0 0 24px",
757
+ textAlign: "center",
758
+ color: "#94a3b8",
759
+ fontSize: "13px"
760
+ },
761
+ form: { display: "flex", flexDirection: "column", gap: "6px" },
762
+ label: { color: "#cbd5e1", fontSize: "13px", marginTop: "10px" },
763
+ input: {
764
+ padding: "11px 13px",
765
+ borderRadius: "9px",
766
+ border: "1px solid #475569",
767
+ background: "#0f172a",
768
+ color: "#f1f5f9",
769
+ fontSize: "15px",
770
+ outline: "none"
771
+ },
772
+ button: {
773
+ marginTop: "18px",
774
+ padding: "12px",
775
+ borderRadius: "9px",
776
+ border: "none",
777
+ background: submitting ? "#4338ca" : "#6366f1",
778
+ color: "#fff",
779
+ fontSize: "15px",
780
+ fontWeight: 600,
781
+ cursor: submitting ? "default" : "pointer"
782
+ },
783
+ error: {
784
+ marginTop: "14px",
785
+ textAlign: "center",
786
+ color: "#f87171",
787
+ fontSize: "13px"
788
+ }
789
+ };
790
+ if (loading || !ready) {
791
+ return /* @__PURE__ */ jsx3("div", { style: S.page, children: /* @__PURE__ */ jsx3("div", { style: S.card, children: /* @__PURE__ */ jsx3("p", { style: S.subtitle, children: "Loading\u2026" }) }) });
792
+ }
793
+ return /* @__PURE__ */ jsx3("div", { style: S.page, children: /* @__PURE__ */ jsxs2("div", { style: S.card, children: [
794
+ /* @__PURE__ */ jsx3("button", { style: S.closeBtn, onClick: () => router.push(homePath), "aria-label": "Close", children: "\u2715" }),
795
+ /* @__PURE__ */ jsx3("div", { style: S.logo, children: "\u{1F6E1}\uFE0F" }),
796
+ /* @__PURE__ */ jsxs2("h1", { style: S.title, children: [
797
+ serviceName,
798
+ " Admin"
799
+ ] }),
800
+ /* @__PURE__ */ jsx3("p", { style: S.subtitle, children: "SuperAdmin access \u2014 Sanvika Accounts SSO" }),
801
+ /* @__PURE__ */ jsxs2("form", { onSubmit: handleLogin, style: S.form, autoComplete: "off", children: [
802
+ /* @__PURE__ */ jsx3("label", { style: S.label, children: "Mobile Number" }),
803
+ /* @__PURE__ */ jsx3(
804
+ "input",
805
+ {
806
+ type: "tel",
807
+ value: mobile,
808
+ onChange: (e) => setMobile(e.target.value.replace(/\D/g, "").slice(0, 10)),
809
+ placeholder: "10-digit mobile",
810
+ style: S.input,
811
+ maxLength: 10,
812
+ required: true,
813
+ autoFocus: true,
814
+ autoComplete: "off"
815
+ }
816
+ ),
817
+ /* @__PURE__ */ jsx3("label", { style: S.label, children: "Password" }),
818
+ /* @__PURE__ */ jsx3(
819
+ "input",
820
+ {
821
+ type: "password",
822
+ value: password,
823
+ onChange: (e) => setPassword(e.target.value),
824
+ placeholder: "Password",
825
+ style: S.input,
826
+ required: true,
827
+ autoComplete: "new-password"
828
+ }
829
+ ),
830
+ /* @__PURE__ */ jsx3(
831
+ "button",
832
+ {
833
+ type: "submit",
834
+ style: S.button,
835
+ disabled: submitting || mobile.length !== 10 || !password,
836
+ children: submitting ? "Signing in\u2026" : "Sign in"
837
+ }
838
+ )
839
+ ] }),
840
+ error && /* @__PURE__ */ jsx3("p", { style: S.error, children: error })
841
+ ] }) });
842
+ }
466
843
  export {
844
+ DEFAULT_AUTH_URL,
467
845
  DEFAULT_AVATAR_SVG,
846
+ DEVICE_ID_STORAGE_KEY,
468
847
  STORAGE_KEYS,
469
848
  SanvikaAccountButton,
849
+ SanvikaAdminLogin,
470
850
  SanvikaAuthContext,
471
851
  SanvikaAuthProvider,
852
+ checkMobile,
853
+ deviceAwareLogin,
854
+ getOrCreateWebDeviceId,
855
+ postLogin,
856
+ randomDeviceId,
472
857
  useSanvikaAuth
473
858
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanvika/auth",
3
- "version": "2.7.0",
3
+ "version": "2.9.0",
4
4
  "description": "Sanvika Auth SDK — React components/hooks + server-side token verification and user proxy",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",