@serhii.olishevskyi/docs 1.0.6 → 1.0.7

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.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  UserPopoverComponent,
3
3
  UserPopoverComponent_default
4
- } from "./chunk-UYCEKHKQ.mjs";
4
+ } from "./chunk-VCRXKPJ4.mjs";
5
5
  export {
6
6
  UserPopoverComponent,
7
7
  UserPopoverComponent_default as default
@@ -189,7 +189,54 @@ var config_default = i18n;
189
189
  // src/context/AuthProvider.tsx
190
190
  import { createContext as createContext3, useContext as useContext3, useEffect, useState, useCallback, useRef } from "react";
191
191
 
192
+ // src/utils/deviceId.ts
193
+ var getDeviceId = () => {
194
+ let deviceId = localStorage.getItem("app_device_id");
195
+ if (!deviceId) {
196
+ deviceId = crypto.randomUUID();
197
+ localStorage.setItem("app_device_id", deviceId);
198
+ }
199
+ return deviceId;
200
+ };
201
+
192
202
  // src/services/session.service.ts
203
+ var trackUserSession = async (supabase, userId) => {
204
+ const deviceId = getDeviceId();
205
+ let ip = "";
206
+ try {
207
+ const ipRes = await fetch("https://api.ipify.org?format=json");
208
+ const ipData = await ipRes.json();
209
+ ip = ipData.ip;
210
+ } catch (e) {
211
+ console.warn("Failed to fetch IP address", e);
212
+ }
213
+ await supabase.from("user_sessions").upsert(
214
+ {
215
+ user_id: userId,
216
+ device_id: deviceId,
217
+ user_agent: navigator.userAgent,
218
+ ip_address: ip,
219
+ url: window.location.href,
220
+ last_seen_at: (/* @__PURE__ */ new Date()).toISOString()
221
+ },
222
+ { onConflict: "user_id, device_id" }
223
+ );
224
+ };
225
+ var isCurrentSessionValid = async (supabase, userId) => {
226
+ try {
227
+ const currentDeviceId = getDeviceId();
228
+ const { data, error } = await supabase.from("user_sessions").select("id").eq("user_id", userId).eq("device_id", currentDeviceId).maybeSingle();
229
+ if (error) {
230
+ console.warn("\u041F\u043E\u043C\u0438\u043B\u043A\u0430 \u043F\u0435\u0440\u0435\u0432\u0456\u0440\u043A\u0438 \u0441\u0435\u0441\u0456\u0457:", error.message);
231
+ return true;
232
+ }
233
+ if (!data) return false;
234
+ return true;
235
+ } catch (err) {
236
+ console.error("\u041D\u0435\u0441\u043F\u043E\u0434\u0456\u0432\u0430\u043D\u0430 \u043F\u043E\u043C\u0438\u043B\u043A\u0430 \u043F\u0440\u0438 \u043F\u0435\u0440\u0435\u0432\u0456\u0440\u0446\u0456 \u0441\u0435\u0441\u0456\u0457:", err);
237
+ return true;
238
+ }
239
+ };
193
240
  var getUserLastSeenAt = async (supabase, userId) => {
194
241
  const { data, error } = await supabase.from("user_sessions").select("last_seen_at").eq("user_id", userId).order("last_seen_at", { ascending: false }).limit(1).maybeSingle();
195
242
  if (error || !data) return null;
@@ -199,6 +246,179 @@ var getUserLastSeenAt = async (supabase, userId) => {
199
246
  // src/context/AuthProvider.tsx
200
247
  import { jsx as jsx3 } from "react/jsx-runtime";
201
248
  var AuthContext = createContext3(void 0);
249
+ var AuthProvider = ({ children }) => {
250
+ const supabase = useSupabase();
251
+ const [user, setUser] = useState(null);
252
+ const [profile, setProfile] = useState(null);
253
+ const [membership, setMembership] = useState(null);
254
+ const [session, setSession] = useState(null);
255
+ const [loading, setLoading] = useState(true);
256
+ const currentUserIdRef = useRef(null);
257
+ const fetchProfileAndMembership = useCallback(async (userId) => {
258
+ try {
259
+ const { data, error } = await supabase.rpc("get_user_avatar_and_profile", {
260
+ p_user_id: userId
261
+ });
262
+ if (error || !data) {
263
+ console.error("Error fetching user context via RPC:", error?.message);
264
+ setProfile(null);
265
+ setMembership(null);
266
+ return;
267
+ }
268
+ const response = data;
269
+ if (response?.profile) {
270
+ const profileData = response.profile;
271
+ if (profileData.avatar_path && !profileData.avatar_path.startsWith("http")) {
272
+ const cleanPath = profileData.avatar_path.replace(/^user-media\//, "");
273
+ const { data: avatarSignedData } = await supabase.storage.from("user-media").createSignedUrl(cleanPath, 3600);
274
+ if (avatarSignedData?.signedUrl) {
275
+ profileData.avatar_url = avatarSignedData.signedUrl;
276
+ }
277
+ }
278
+ if (profileData.cover_image_path && !profileData.cover_image_path.startsWith("http")) {
279
+ const cleanPath = profileData.cover_image_path.replace(/^user-media\//, "");
280
+ const { data: coverSignedData } = await supabase.storage.from("user-media").createSignedUrl(cleanPath, 3600);
281
+ if (coverSignedData?.signedUrl) {
282
+ profileData.cover_image_url = coverSignedData.signedUrl;
283
+ }
284
+ }
285
+ let updatedMembership = response.membership;
286
+ if (updatedMembership) {
287
+ const now = (/* @__PURE__ */ new Date()).toISOString();
288
+ const isFirstLogin = !updatedMembership.first_login_at;
289
+ const updateData = {
290
+ last_login_at: now,
291
+ ...isFirstLogin && { first_login_at: now }
292
+ };
293
+ const { error: updateError } = await supabase.from("memberships").update(updateData).eq("id", updatedMembership.id);
294
+ if (updateError) {
295
+ console.error("Error updating membership login timestamps:", updateError.message);
296
+ } else {
297
+ updatedMembership = {
298
+ ...updatedMembership,
299
+ ...updateData
300
+ };
301
+ }
302
+ }
303
+ setProfile(profileData);
304
+ setMembership(updatedMembership);
305
+ }
306
+ } catch (err) {
307
+ console.error("Unexpected error fetching user context:", err);
308
+ setProfile(null);
309
+ setMembership(null);
310
+ }
311
+ }, []);
312
+ const refreshProfile = async () => {
313
+ if (currentUserIdRef.current) {
314
+ await fetchProfileAndMembership(currentUserIdRef.current);
315
+ }
316
+ };
317
+ const signOut = useCallback(async () => {
318
+ await supabase.auth.signOut();
319
+ currentUserIdRef.current = null;
320
+ setUser(null);
321
+ setProfile(null);
322
+ setMembership(null);
323
+ setSession(null);
324
+ }, []);
325
+ const setActiveCompany = async (companyId) => {
326
+ if (!user) {
327
+ throw new Error("\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C \u043D\u0435 \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u043E\u0432\u0430\u043D");
328
+ }
329
+ const { error: profileError } = await supabase.from("profiles").update({ active_company_id: companyId }).eq("id", user.id);
330
+ if (profileError) {
331
+ console.error("Error updating active company:", profileError);
332
+ throw new Error(profileError.message);
333
+ }
334
+ await refreshProfile();
335
+ };
336
+ const updateProfile = (newProfileData) => {
337
+ setProfile((prev) => prev ? { ...prev, ...newProfileData } : null);
338
+ };
339
+ useEffect(() => {
340
+ const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, currentSession) => {
341
+ if (event === "TOKEN_REFRESHED" && currentUserIdRef.current === currentSession?.user?.id) {
342
+ setSession(currentSession);
343
+ return;
344
+ }
345
+ const currentUser = currentSession?.user ?? null;
346
+ const newUserId = currentUser?.id ?? null;
347
+ if (newUserId !== currentUserIdRef.current) {
348
+ currentUserIdRef.current = newUserId;
349
+ setUser(currentUser);
350
+ if (newUserId) {
351
+ await trackUserSession(supabase, newUserId).catch(console.error);
352
+ const isAuthLinkEvent = event === "SIGNED_IN" || event === "INITIAL_SESSION" || event === "PASSWORD_RECOVERY";
353
+ if (!isAuthLinkEvent) {
354
+ const isValid = await isCurrentSessionValid(supabase, newUserId);
355
+ if (!isValid) {
356
+ await supabase.auth.signOut();
357
+ currentUserIdRef.current = null;
358
+ setUser(null);
359
+ setProfile(null);
360
+ setMembership(null);
361
+ setLoading(false);
362
+ return;
363
+ }
364
+ }
365
+ await fetchProfileAndMembership(newUserId);
366
+ setSession(currentSession);
367
+ } else {
368
+ setProfile(null);
369
+ setMembership(null);
370
+ }
371
+ }
372
+ setLoading(false);
373
+ });
374
+ return () => {
375
+ subscription.unsubscribe();
376
+ };
377
+ }, [fetchProfileAndMembership]);
378
+ useEffect(() => {
379
+ if (!user) return;
380
+ const checkSession = async () => {
381
+ try {
382
+ const valid = await isCurrentSessionValid(supabase, user.id);
383
+ if (!valid) {
384
+ console.warn("\u0421\u0435\u0441\u0456\u044E \u0432\u0456\u0434\u043A\u043B\u0438\u043A\u0430\u043D\u043E \u0437 \u0456\u043D\u0448\u043E\u0433\u043E \u043F\u0440\u0438\u0441\u0442\u0440\u043E\u044E.");
385
+ await signOut();
386
+ }
387
+ } catch (err) {
388
+ console.error("\u041F\u043E\u043C\u0438\u043B\u043A\u0430 \u043F\u0440\u0438 \u043F\u0435\u0440\u0435\u0432\u0456\u0440\u0446\u0456 \u0441\u0435\u0441\u0456\u0457:", err);
389
+ }
390
+ };
391
+ const interval = setInterval(checkSession, 6e4);
392
+ const handleFocus = () => checkSession();
393
+ window.addEventListener("focus", handleFocus);
394
+ return () => {
395
+ clearInterval(interval);
396
+ window.removeEventListener("focus", handleFocus);
397
+ };
398
+ }, [user, signOut]);
399
+ const activeCompanyId = profile?.active_company_id ?? null;
400
+ const userRole = membership?.role ?? null;
401
+ return /* @__PURE__ */ jsx3(
402
+ AuthContext.Provider,
403
+ {
404
+ value: {
405
+ user,
406
+ profile,
407
+ membership,
408
+ userRole,
409
+ activeCompanyId,
410
+ session,
411
+ isAuthenticated: !!user,
412
+ loading,
413
+ signOut,
414
+ updateProfile,
415
+ refreshProfile,
416
+ setActiveCompany
417
+ },
418
+ children
419
+ }
420
+ );
421
+ };
202
422
  var useAuth = () => {
203
423
  const context = useContext3(AuthContext);
204
424
  if (!context) {
@@ -2015,6 +2235,8 @@ var UserPopoverComponent_default = UserPopoverComponent;
2015
2235
  export {
2016
2236
  SupabaseProvider,
2017
2237
  useSupabase,
2238
+ AuthProvider,
2239
+ useAuth,
2018
2240
  TranslationEditProvider,
2019
2241
  useTranslationEdit,
2020
2242
  translationService,
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as React$1 from 'react';
2
2
  import React__default, { ReactNode } from 'react';
3
3
  import { TextFieldProps, IconButtonProps, TooltipProps, SxProps, Theme } from '@mui/material';
4
- import { SupabaseClient } from '@supabase/supabase-js';
4
+ import { SupabaseClient, User, Session } from '@supabase/supabase-js';
5
5
 
6
6
  interface UrlValue {
7
7
  url: string;
@@ -112,7 +112,50 @@ interface UserPopoverComponentProps {
112
112
  onClose: () => void;
113
113
  }
114
114
 
115
+ interface UserProfile {
116
+ id: string;
117
+ email: string;
118
+ system_role: 'superadmin' | 'admin' | 'support' | null;
119
+ updated_at: string;
120
+ avatar_url: string | null;
121
+ avatar_path?: string | null;
122
+ first_name: string | null;
123
+ middle_name: string | null;
124
+ last_name: string | null;
125
+ display_name: string | null;
126
+ locked: boolean;
127
+ archived: boolean;
128
+ birthday: string | null;
129
+ phone: string | null;
130
+ description: string | null;
131
+ created_at: string;
132
+ cover_image_url: string | null;
133
+ cover_image_path?: string | null;
134
+ active_company_id: string | null;
135
+ name_order: 'first_last' | 'last_first' | null;
136
+ include_middle_name: boolean | null;
137
+ name_genitive: string | null;
138
+ name_dative: string | null;
139
+ name_instrumental: string | null;
140
+ name_vocative: string | null;
141
+ }
142
+
115
143
  type MembershipRole = 'superadmin' | 'admin' | 'user';
144
+ interface Membership {
145
+ id: string;
146
+ user_id: string;
147
+ company_id: string;
148
+ role: 'superadmin' | 'admin' | 'user';
149
+ created_at: string;
150
+ last_updated_at: string | null;
151
+ first_login_at?: string | null;
152
+ last_login_at?: string | null;
153
+ employee_post: string | null;
154
+ work_schedule_id: string | null;
155
+ locked: boolean;
156
+ locked_reason: string | null;
157
+ excluded: boolean;
158
+ }
116
159
 
117
160
  interface UserRoleChipComponentProps {
118
161
  overrideNamespace?: string;
@@ -233,6 +276,25 @@ declare const AppSettingsProvider: React__default.FC<{
233
276
  }>;
234
277
  declare const useAppSettings: () => AppSettingsContextType;
235
278
 
279
+ interface AuthContextType {
280
+ user: User | null;
281
+ profile: UserProfile | null;
282
+ membership: Membership | null;
283
+ userRole: MembershipRole | null;
284
+ activeCompanyId: string | null;
285
+ session: Session | null;
286
+ isAuthenticated: boolean;
287
+ loading: boolean;
288
+ signOut: () => Promise<void>;
289
+ updateProfile: (newProfileData: Partial<UserProfile>) => void;
290
+ refreshProfile: () => Promise<void>;
291
+ setActiveCompany: (companyId: string) => Promise<void>;
292
+ }
293
+ declare const AuthProvider: React__default.FC<{
294
+ children: React__default.ReactNode;
295
+ }>;
296
+ declare const useAuth: () => AuthContextType;
297
+
236
298
  interface TranslateTextProps {
237
299
  overrideNamespace?: string;
238
300
  children: string;
@@ -376,4 +438,4 @@ declare const Providers: {
376
438
  readonly AI: React$1.FC<AIProviderProps>;
377
439
  };
378
440
 
379
- export { type AIConfig, type AIGenerateOptions, AIProvider, type AIProviderProps, type AIProviderType, AppSettingsProvider, BreadcrumbProvider, type ConfirmOptions, type ConfirmResult, ConfirmationProvider, type ConfirmationProviderProps, type CopyClipboardButtonComponentProps as CopyClipboardButtonProps, type DialogSeverity, type DisplayState, type EmailInputComponentProps as EmailInputProps, type Language, LocalizationProvider, type NumberInputComponentProps as NumberInputProps, Providers, type SupabaseConfig, SupabaseProvider, type SupabaseProviderProps, type TextFieldComponentProps as TextFieldProps, type ThemeColors, type ThemeConfig, type ThemeMode, ThemeProvider, ToastProvider, type ToastType, type TranslatableTooltipComponentProps as TranslatableTooltipProps, TranslateProvider, TranslateText, TranslationEditProvider, type TypographyConfig, UI, type UrlComponentProps as UrlProps, type UserAvatarComponentProps as UserAvatarProps, type UserPopoverComponentProps as UserPopoverProps, type UserRoleChipComponentProps as UserRoleChipProps, type UserStatusChipComponentProps as UserStatusChipProps, useAI, useAppSettings, useAppTheme, useBreadcrumb, useConfirmation, useLocalization, useSupabase, useToast, useTranslationEdit, useTranslationNamespace };
441
+ export { type AIConfig, type AIGenerateOptions, AIProvider, type AIProviderProps, type AIProviderType, AppSettingsProvider, AuthProvider, BreadcrumbProvider, type ConfirmOptions, type ConfirmResult, ConfirmationProvider, type ConfirmationProviderProps, type CopyClipboardButtonComponentProps as CopyClipboardButtonProps, type DialogSeverity, type DisplayState, type EmailInputComponentProps as EmailInputProps, type Language, LocalizationProvider, type NumberInputComponentProps as NumberInputProps, Providers, type SupabaseConfig, SupabaseProvider, type SupabaseProviderProps, type TextFieldComponentProps as TextFieldProps, type ThemeColors, type ThemeConfig, type ThemeMode, ThemeProvider, ToastProvider, type ToastType, type TranslatableTooltipComponentProps as TranslatableTooltipProps, TranslateProvider, TranslateText, TranslationEditProvider, type TypographyConfig, UI, type UrlComponentProps as UrlProps, type UserAvatarComponentProps as UserAvatarProps, type UserPopoverComponentProps as UserPopoverProps, type UserRoleChipComponentProps as UserRoleChipProps, type UserStatusChipComponentProps as UserStatusChipProps, useAI, useAppSettings, useAppTheme, useAuth, useBreadcrumb, useConfirmation, useLocalization, useSupabase, useToast, useTranslationEdit, useTranslationNamespace };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as React$1 from 'react';
2
2
  import React__default, { ReactNode } from 'react';
3
3
  import { TextFieldProps, IconButtonProps, TooltipProps, SxProps, Theme } from '@mui/material';
4
- import { SupabaseClient } from '@supabase/supabase-js';
4
+ import { SupabaseClient, User, Session } from '@supabase/supabase-js';
5
5
 
6
6
  interface UrlValue {
7
7
  url: string;
@@ -112,7 +112,50 @@ interface UserPopoverComponentProps {
112
112
  onClose: () => void;
113
113
  }
114
114
 
115
+ interface UserProfile {
116
+ id: string;
117
+ email: string;
118
+ system_role: 'superadmin' | 'admin' | 'support' | null;
119
+ updated_at: string;
120
+ avatar_url: string | null;
121
+ avatar_path?: string | null;
122
+ first_name: string | null;
123
+ middle_name: string | null;
124
+ last_name: string | null;
125
+ display_name: string | null;
126
+ locked: boolean;
127
+ archived: boolean;
128
+ birthday: string | null;
129
+ phone: string | null;
130
+ description: string | null;
131
+ created_at: string;
132
+ cover_image_url: string | null;
133
+ cover_image_path?: string | null;
134
+ active_company_id: string | null;
135
+ name_order: 'first_last' | 'last_first' | null;
136
+ include_middle_name: boolean | null;
137
+ name_genitive: string | null;
138
+ name_dative: string | null;
139
+ name_instrumental: string | null;
140
+ name_vocative: string | null;
141
+ }
142
+
115
143
  type MembershipRole = 'superadmin' | 'admin' | 'user';
144
+ interface Membership {
145
+ id: string;
146
+ user_id: string;
147
+ company_id: string;
148
+ role: 'superadmin' | 'admin' | 'user';
149
+ created_at: string;
150
+ last_updated_at: string | null;
151
+ first_login_at?: string | null;
152
+ last_login_at?: string | null;
153
+ employee_post: string | null;
154
+ work_schedule_id: string | null;
155
+ locked: boolean;
156
+ locked_reason: string | null;
157
+ excluded: boolean;
158
+ }
116
159
 
117
160
  interface UserRoleChipComponentProps {
118
161
  overrideNamespace?: string;
@@ -233,6 +276,25 @@ declare const AppSettingsProvider: React__default.FC<{
233
276
  }>;
234
277
  declare const useAppSettings: () => AppSettingsContextType;
235
278
 
279
+ interface AuthContextType {
280
+ user: User | null;
281
+ profile: UserProfile | null;
282
+ membership: Membership | null;
283
+ userRole: MembershipRole | null;
284
+ activeCompanyId: string | null;
285
+ session: Session | null;
286
+ isAuthenticated: boolean;
287
+ loading: boolean;
288
+ signOut: () => Promise<void>;
289
+ updateProfile: (newProfileData: Partial<UserProfile>) => void;
290
+ refreshProfile: () => Promise<void>;
291
+ setActiveCompany: (companyId: string) => Promise<void>;
292
+ }
293
+ declare const AuthProvider: React__default.FC<{
294
+ children: React__default.ReactNode;
295
+ }>;
296
+ declare const useAuth: () => AuthContextType;
297
+
236
298
  interface TranslateTextProps {
237
299
  overrideNamespace?: string;
238
300
  children: string;
@@ -376,4 +438,4 @@ declare const Providers: {
376
438
  readonly AI: React$1.FC<AIProviderProps>;
377
439
  };
378
440
 
379
- export { type AIConfig, type AIGenerateOptions, AIProvider, type AIProviderProps, type AIProviderType, AppSettingsProvider, BreadcrumbProvider, type ConfirmOptions, type ConfirmResult, ConfirmationProvider, type ConfirmationProviderProps, type CopyClipboardButtonComponentProps as CopyClipboardButtonProps, type DialogSeverity, type DisplayState, type EmailInputComponentProps as EmailInputProps, type Language, LocalizationProvider, type NumberInputComponentProps as NumberInputProps, Providers, type SupabaseConfig, SupabaseProvider, type SupabaseProviderProps, type TextFieldComponentProps as TextFieldProps, type ThemeColors, type ThemeConfig, type ThemeMode, ThemeProvider, ToastProvider, type ToastType, type TranslatableTooltipComponentProps as TranslatableTooltipProps, TranslateProvider, TranslateText, TranslationEditProvider, type TypographyConfig, UI, type UrlComponentProps as UrlProps, type UserAvatarComponentProps as UserAvatarProps, type UserPopoverComponentProps as UserPopoverProps, type UserRoleChipComponentProps as UserRoleChipProps, type UserStatusChipComponentProps as UserStatusChipProps, useAI, useAppSettings, useAppTheme, useBreadcrumb, useConfirmation, useLocalization, useSupabase, useToast, useTranslationEdit, useTranslationNamespace };
441
+ export { type AIConfig, type AIGenerateOptions, AIProvider, type AIProviderProps, type AIProviderType, AppSettingsProvider, AuthProvider, BreadcrumbProvider, type ConfirmOptions, type ConfirmResult, ConfirmationProvider, type ConfirmationProviderProps, type CopyClipboardButtonComponentProps as CopyClipboardButtonProps, type DialogSeverity, type DisplayState, type EmailInputComponentProps as EmailInputProps, type Language, LocalizationProvider, type NumberInputComponentProps as NumberInputProps, Providers, type SupabaseConfig, SupabaseProvider, type SupabaseProviderProps, type TextFieldComponentProps as TextFieldProps, type ThemeColors, type ThemeConfig, type ThemeMode, ThemeProvider, ToastProvider, type ToastType, type TranslatableTooltipComponentProps as TranslatableTooltipProps, TranslateProvider, TranslateText, TranslationEditProvider, type TypographyConfig, UI, type UrlComponentProps as UrlProps, type UserAvatarComponentProps as UserAvatarProps, type UserPopoverComponentProps as UserPopoverProps, type UserRoleChipComponentProps as UserRoleChipProps, type UserStatusChipComponentProps as UserStatusChipProps, useAI, useAppSettings, useAppTheme, useAuth, useBreadcrumb, useConfirmation, useLocalization, useSupabase, useToast, useTranslationEdit, useTranslationNamespace };
package/dist/index.js CHANGED
@@ -153,18 +153,64 @@ var init_config = __esm({
153
153
  });
154
154
 
155
155
  // src/utils/deviceId.ts
156
+ var getDeviceId;
156
157
  var init_deviceId = __esm({
157
158
  "src/utils/deviceId.ts"() {
158
159
  "use strict";
160
+ getDeviceId = () => {
161
+ let deviceId = localStorage.getItem("app_device_id");
162
+ if (!deviceId) {
163
+ deviceId = crypto.randomUUID();
164
+ localStorage.setItem("app_device_id", deviceId);
165
+ }
166
+ return deviceId;
167
+ };
159
168
  }
160
169
  });
161
170
 
162
171
  // src/services/session.service.ts
163
- var getUserLastSeenAt;
172
+ var trackUserSession, isCurrentSessionValid, getUserLastSeenAt;
164
173
  var init_session_service = __esm({
165
174
  "src/services/session.service.ts"() {
166
175
  "use strict";
167
176
  init_deviceId();
177
+ trackUserSession = async (supabase, userId) => {
178
+ const deviceId = getDeviceId();
179
+ let ip = "";
180
+ try {
181
+ const ipRes = await fetch("https://api.ipify.org?format=json");
182
+ const ipData = await ipRes.json();
183
+ ip = ipData.ip;
184
+ } catch (e) {
185
+ console.warn("Failed to fetch IP address", e);
186
+ }
187
+ await supabase.from("user_sessions").upsert(
188
+ {
189
+ user_id: userId,
190
+ device_id: deviceId,
191
+ user_agent: navigator.userAgent,
192
+ ip_address: ip,
193
+ url: window.location.href,
194
+ last_seen_at: (/* @__PURE__ */ new Date()).toISOString()
195
+ },
196
+ { onConflict: "user_id, device_id" }
197
+ );
198
+ };
199
+ isCurrentSessionValid = async (supabase, userId) => {
200
+ try {
201
+ const currentDeviceId = getDeviceId();
202
+ const { data, error } = await supabase.from("user_sessions").select("id").eq("user_id", userId).eq("device_id", currentDeviceId).maybeSingle();
203
+ if (error) {
204
+ console.warn("\u041F\u043E\u043C\u0438\u043B\u043A\u0430 \u043F\u0435\u0440\u0435\u0432\u0456\u0440\u043A\u0438 \u0441\u0435\u0441\u0456\u0457:", error.message);
205
+ return true;
206
+ }
207
+ if (!data) return false;
208
+ return true;
209
+ } catch (err) {
210
+ console.error("\u041D\u0435\u0441\u043F\u043E\u0434\u0456\u0432\u0430\u043D\u0430 \u043F\u043E\u043C\u0438\u043B\u043A\u0430 \u043F\u0440\u0438 \u043F\u0435\u0440\u0435\u0432\u0456\u0440\u0446\u0456 \u0441\u0435\u0441\u0456\u0457:", err);
211
+ return true;
212
+ }
213
+ };
168
214
  getUserLastSeenAt = async (supabase, userId) => {
169
215
  const { data, error } = await supabase.from("user_sessions").select("last_seen_at").eq("user_id", userId).order("last_seen_at", { ascending: false }).limit(1).maybeSingle();
170
216
  if (error || !data) return null;
@@ -209,7 +255,7 @@ var init_SupabaseProvider = __esm({
209
255
  });
210
256
 
211
257
  // src/context/AuthProvider.tsx
212
- var import_react2, import_jsx_runtime2, AuthContext, useAuth;
258
+ var import_react2, import_jsx_runtime2, AuthContext, AuthProvider, useAuth;
213
259
  var init_AuthProvider = __esm({
214
260
  "src/context/AuthProvider.tsx"() {
215
261
  "use strict";
@@ -218,6 +264,179 @@ var init_AuthProvider = __esm({
218
264
  init_SupabaseProvider();
219
265
  import_jsx_runtime2 = require("react/jsx-runtime");
220
266
  AuthContext = (0, import_react2.createContext)(void 0);
267
+ AuthProvider = ({ children }) => {
268
+ const supabase = useSupabase();
269
+ const [user, setUser] = (0, import_react2.useState)(null);
270
+ const [profile, setProfile] = (0, import_react2.useState)(null);
271
+ const [membership, setMembership] = (0, import_react2.useState)(null);
272
+ const [session, setSession] = (0, import_react2.useState)(null);
273
+ const [loading, setLoading] = (0, import_react2.useState)(true);
274
+ const currentUserIdRef = (0, import_react2.useRef)(null);
275
+ const fetchProfileAndMembership = (0, import_react2.useCallback)(async (userId) => {
276
+ try {
277
+ const { data, error } = await supabase.rpc("get_user_avatar_and_profile", {
278
+ p_user_id: userId
279
+ });
280
+ if (error || !data) {
281
+ console.error("Error fetching user context via RPC:", error?.message);
282
+ setProfile(null);
283
+ setMembership(null);
284
+ return;
285
+ }
286
+ const response = data;
287
+ if (response?.profile) {
288
+ const profileData = response.profile;
289
+ if (profileData.avatar_path && !profileData.avatar_path.startsWith("http")) {
290
+ const cleanPath = profileData.avatar_path.replace(/^user-media\//, "");
291
+ const { data: avatarSignedData } = await supabase.storage.from("user-media").createSignedUrl(cleanPath, 3600);
292
+ if (avatarSignedData?.signedUrl) {
293
+ profileData.avatar_url = avatarSignedData.signedUrl;
294
+ }
295
+ }
296
+ if (profileData.cover_image_path && !profileData.cover_image_path.startsWith("http")) {
297
+ const cleanPath = profileData.cover_image_path.replace(/^user-media\//, "");
298
+ const { data: coverSignedData } = await supabase.storage.from("user-media").createSignedUrl(cleanPath, 3600);
299
+ if (coverSignedData?.signedUrl) {
300
+ profileData.cover_image_url = coverSignedData.signedUrl;
301
+ }
302
+ }
303
+ let updatedMembership = response.membership;
304
+ if (updatedMembership) {
305
+ const now = (/* @__PURE__ */ new Date()).toISOString();
306
+ const isFirstLogin = !updatedMembership.first_login_at;
307
+ const updateData = {
308
+ last_login_at: now,
309
+ ...isFirstLogin && { first_login_at: now }
310
+ };
311
+ const { error: updateError } = await supabase.from("memberships").update(updateData).eq("id", updatedMembership.id);
312
+ if (updateError) {
313
+ console.error("Error updating membership login timestamps:", updateError.message);
314
+ } else {
315
+ updatedMembership = {
316
+ ...updatedMembership,
317
+ ...updateData
318
+ };
319
+ }
320
+ }
321
+ setProfile(profileData);
322
+ setMembership(updatedMembership);
323
+ }
324
+ } catch (err) {
325
+ console.error("Unexpected error fetching user context:", err);
326
+ setProfile(null);
327
+ setMembership(null);
328
+ }
329
+ }, []);
330
+ const refreshProfile = async () => {
331
+ if (currentUserIdRef.current) {
332
+ await fetchProfileAndMembership(currentUserIdRef.current);
333
+ }
334
+ };
335
+ const signOut = (0, import_react2.useCallback)(async () => {
336
+ await supabase.auth.signOut();
337
+ currentUserIdRef.current = null;
338
+ setUser(null);
339
+ setProfile(null);
340
+ setMembership(null);
341
+ setSession(null);
342
+ }, []);
343
+ const setActiveCompany = async (companyId) => {
344
+ if (!user) {
345
+ throw new Error("\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C \u043D\u0435 \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u043E\u0432\u0430\u043D");
346
+ }
347
+ const { error: profileError } = await supabase.from("profiles").update({ active_company_id: companyId }).eq("id", user.id);
348
+ if (profileError) {
349
+ console.error("Error updating active company:", profileError);
350
+ throw new Error(profileError.message);
351
+ }
352
+ await refreshProfile();
353
+ };
354
+ const updateProfile = (newProfileData) => {
355
+ setProfile((prev) => prev ? { ...prev, ...newProfileData } : null);
356
+ };
357
+ (0, import_react2.useEffect)(() => {
358
+ const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, currentSession) => {
359
+ if (event === "TOKEN_REFRESHED" && currentUserIdRef.current === currentSession?.user?.id) {
360
+ setSession(currentSession);
361
+ return;
362
+ }
363
+ const currentUser = currentSession?.user ?? null;
364
+ const newUserId = currentUser?.id ?? null;
365
+ if (newUserId !== currentUserIdRef.current) {
366
+ currentUserIdRef.current = newUserId;
367
+ setUser(currentUser);
368
+ if (newUserId) {
369
+ await trackUserSession(supabase, newUserId).catch(console.error);
370
+ const isAuthLinkEvent = event === "SIGNED_IN" || event === "INITIAL_SESSION" || event === "PASSWORD_RECOVERY";
371
+ if (!isAuthLinkEvent) {
372
+ const isValid = await isCurrentSessionValid(supabase, newUserId);
373
+ if (!isValid) {
374
+ await supabase.auth.signOut();
375
+ currentUserIdRef.current = null;
376
+ setUser(null);
377
+ setProfile(null);
378
+ setMembership(null);
379
+ setLoading(false);
380
+ return;
381
+ }
382
+ }
383
+ await fetchProfileAndMembership(newUserId);
384
+ setSession(currentSession);
385
+ } else {
386
+ setProfile(null);
387
+ setMembership(null);
388
+ }
389
+ }
390
+ setLoading(false);
391
+ });
392
+ return () => {
393
+ subscription.unsubscribe();
394
+ };
395
+ }, [fetchProfileAndMembership]);
396
+ (0, import_react2.useEffect)(() => {
397
+ if (!user) return;
398
+ const checkSession = async () => {
399
+ try {
400
+ const valid = await isCurrentSessionValid(supabase, user.id);
401
+ if (!valid) {
402
+ console.warn("\u0421\u0435\u0441\u0456\u044E \u0432\u0456\u0434\u043A\u043B\u0438\u043A\u0430\u043D\u043E \u0437 \u0456\u043D\u0448\u043E\u0433\u043E \u043F\u0440\u0438\u0441\u0442\u0440\u043E\u044E.");
403
+ await signOut();
404
+ }
405
+ } catch (err) {
406
+ console.error("\u041F\u043E\u043C\u0438\u043B\u043A\u0430 \u043F\u0440\u0438 \u043F\u0435\u0440\u0435\u0432\u0456\u0440\u0446\u0456 \u0441\u0435\u0441\u0456\u0457:", err);
407
+ }
408
+ };
409
+ const interval = setInterval(checkSession, 6e4);
410
+ const handleFocus = () => checkSession();
411
+ window.addEventListener("focus", handleFocus);
412
+ return () => {
413
+ clearInterval(interval);
414
+ window.removeEventListener("focus", handleFocus);
415
+ };
416
+ }, [user, signOut]);
417
+ const activeCompanyId = profile?.active_company_id ?? null;
418
+ const userRole = membership?.role ?? null;
419
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
420
+ AuthContext.Provider,
421
+ {
422
+ value: {
423
+ user,
424
+ profile,
425
+ membership,
426
+ userRole,
427
+ activeCompanyId,
428
+ session,
429
+ isAuthenticated: !!user,
430
+ loading,
431
+ signOut,
432
+ updateProfile,
433
+ refreshProfile,
434
+ setActiveCompany
435
+ },
436
+ children
437
+ }
438
+ );
439
+ };
221
440
  useAuth = () => {
222
441
  const context = (0, import_react2.useContext)(AuthContext);
223
442
  if (!context) {
@@ -1602,6 +1821,7 @@ var init_context = __esm({
1602
1821
  init_AppSettingsProvider();
1603
1822
  init_SupabaseProvider();
1604
1823
  init_AppSettingsProvider();
1824
+ init_AuthProvider();
1605
1825
  init_TranslateProvider();
1606
1826
  init_TranslateText();
1607
1827
  init_TranslationEditProvider();
@@ -2204,6 +2424,7 @@ var index_exports = {};
2204
2424
  __export(index_exports, {
2205
2425
  AIProvider: () => AIProvider,
2206
2426
  AppSettingsProvider: () => AppSettingsProvider,
2427
+ AuthProvider: () => AuthProvider,
2207
2428
  BreadcrumbProvider: () => BreadcrumbProvider,
2208
2429
  ConfirmationProvider: () => ConfirmationProvider,
2209
2430
  LocalizationProvider: () => LocalizationProvider,
@@ -2218,6 +2439,7 @@ __export(index_exports, {
2218
2439
  useAI: () => useAI,
2219
2440
  useAppSettings: () => useAppSettings,
2220
2441
  useAppTheme: () => useAppTheme,
2442
+ useAuth: () => useAuth,
2221
2443
  useBreadcrumb: () => useBreadcrumb,
2222
2444
  useConfirmation: () => useConfirmation,
2223
2445
  useLocalization: () => useLocalization,
@@ -3069,6 +3291,7 @@ init_context();
3069
3291
  0 && (module.exports = {
3070
3292
  AIProvider,
3071
3293
  AppSettingsProvider,
3294
+ AuthProvider,
3072
3295
  BreadcrumbProvider,
3073
3296
  ConfirmationProvider,
3074
3297
  LocalizationProvider,
@@ -3083,6 +3306,7 @@ init_context();
3083
3306
  useAI,
3084
3307
  useAppSettings,
3085
3308
  useAppTheme,
3309
+ useAuth,
3086
3310
  useBreadcrumb,
3087
3311
  useConfirmation,
3088
3312
  useLocalization,
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  AIProvider,
3
3
  AppSettingsProvider,
4
+ AuthProvider,
4
5
  BreadcrumbProvider,
5
6
  ConfirmationProvider,
6
7
  CopyClipboardButtonComponent,
@@ -21,6 +22,7 @@ import {
21
22
  useAI,
22
23
  useAppSettings,
23
24
  useAppTheme,
25
+ useAuth,
24
26
  useBreadcrumb,
25
27
  useConfirmation,
26
28
  useLocalization,
@@ -28,7 +30,7 @@ import {
28
30
  useToast,
29
31
  useTranslationEdit,
30
32
  useTranslationNamespace
31
- } from "./chunk-UYCEKHKQ.mjs";
33
+ } from "./chunk-VCRXKPJ4.mjs";
32
34
 
33
35
  // src/UI/components/inputs/TextFieldComponent.tsx
34
36
  import { useState } from "react";
@@ -726,7 +728,7 @@ var EmailInputComponent = ({
726
728
  import { useState as useState5, useEffect as useEffect2, lazy, Suspense } from "react";
727
729
  import { Avatar, Tooltip as Tooltip3, Typography as Typography2 } from "@mui/material";
728
730
  import { Fragment as Fragment4, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
729
- var LazyUserPopover = lazy(() => import("./UserPopoverComponent-MG2V632M.mjs"));
731
+ var LazyUserPopover = lazy(() => import("./UserPopoverComponent-ZPPVLFBL.mjs"));
730
732
  var getInitials = (name) => {
731
733
  if (!name) return "?";
732
734
  const parts = name.trim().split(" ");
@@ -865,6 +867,7 @@ var UI = {
865
867
  export {
866
868
  AIProvider,
867
869
  AppSettingsProvider,
870
+ AuthProvider,
868
871
  BreadcrumbProvider,
869
872
  ConfirmationProvider,
870
873
  LocalizationProvider,
@@ -879,6 +882,7 @@ export {
879
882
  useAI,
880
883
  useAppSettings,
881
884
  useAppTheme,
885
+ useAuth,
882
886
  useBreadcrumb,
883
887
  useConfirmation,
884
888
  useLocalization,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serhii.olishevskyi/docs",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "DOCS.UA react packages",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",