@smess/networking 1.0.15 → 1.0.16

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.
@@ -4,6 +4,7 @@ import { BehaviorSubject, Observable } from "rxjs";
4
4
  export interface AuthLoginRequest {
5
5
  identifier: string;
6
6
  password: string;
7
+ rememberMe?: boolean;
7
8
  }
8
9
  export interface ForgotPasswordRequest {
9
10
  identifier: string;
@@ -45,9 +46,15 @@ export interface AuthServiceType {
45
46
  }
46
47
  export declare class AuthService implements AuthServiceType {
47
48
  private readonly config;
48
- readonly $authToken: BehaviorSubject<string | null>;
49
+ private readonly $_authToken;
49
50
  constructor(config: NetworkingConfig);
51
+ get $authToken(): BehaviorSubject<string | null>;
50
52
  get currentAuthToken(): string | null;
53
+ private getValidToken;
54
+ private loadFromStorage;
55
+ private loadAuthFromStorage;
56
+ private saveAuthToStorage;
57
+ private clearAuthFromStorage;
51
58
  provider(target: AuthTarget): SmessService<AuthProvider>;
52
59
  login(payload: AuthLoginRequest): Observable<NetworkingUIEventWrapped<AuthLoginResponse>>;
53
60
  logout(): Observable<NetworkingUIEventWrapped<void>>;
@@ -1,6 +1,9 @@
1
1
  import { Accept, SMESSHeader, SmessService } from "./smess.service";
2
2
  import { BehaviorSubject } from "rxjs";
3
3
  import { tap } from "rxjs/operators";
4
+ // LocalStorage keys for auth persistence
5
+ const AUTH_TOKEN_KEY = 'smess_auth_token';
6
+ const AUTH_RESPONSE_KEY = 'smess_auth_response';
4
7
  export class AuthProvider {
5
8
  constructor(config, target, $authToken) {
6
9
  this.config = config;
@@ -68,11 +71,88 @@ export class AuthProvider {
68
71
  export class AuthService {
69
72
  constructor(config) {
70
73
  this.config = config;
71
- this.$authToken = new BehaviorSubject(null);
74
+ this.$_authToken = new BehaviorSubject(null);
75
+ this.loadAuthFromStorage();
76
+ }
77
+ // Getter that returns BehaviorSubject with token validation
78
+ get $authToken() {
79
+ const currentToken = this.getValidToken();
80
+ if (currentToken !== this.$_authToken.value) {
81
+ this.$_authToken.next(currentToken);
82
+ }
83
+ return this.$_authToken;
72
84
  }
73
85
  // Get current auth token value
74
86
  get currentAuthToken() {
75
- return this.$authToken.value;
87
+ return this.getValidToken();
88
+ }
89
+ // Get valid token from memory or localStorage
90
+ getValidToken() {
91
+ // If we have a token in memory, return it (assume it's valid)
92
+ if (this.$_authToken.value) {
93
+ return this.$_authToken.value;
94
+ }
95
+ // Otherwise, try to load from localStorage
96
+ return this.loadFromStorage();
97
+ }
98
+ // Load token from localStorage and check expiration
99
+ loadFromStorage() {
100
+ try {
101
+ const storedResponse = localStorage.getItem(AUTH_RESPONSE_KEY);
102
+ const storedAt = localStorage.getItem(`${AUTH_RESPONSE_KEY}_stored_at`);
103
+ if (storedResponse && storedAt) {
104
+ const authResponse = JSON.parse(storedResponse);
105
+ const storedTime = parseInt(storedAt, 10);
106
+ // Check if token is expired
107
+ if (authResponse.expires_in && typeof authResponse.expires_in === 'number') {
108
+ const expirationTime = storedTime + (authResponse.expires_in * 1000);
109
+ if (Date.now() >= expirationTime) {
110
+ // Token expired, clear storage
111
+ this.clearAuthFromStorage();
112
+ return null;
113
+ }
114
+ }
115
+ // Token is valid, store in memory and return
116
+ const token = String(authResponse.token);
117
+ this.$_authToken.next(token);
118
+ return token;
119
+ }
120
+ }
121
+ catch (error) {
122
+ console.warn('Failed to load token from localStorage:', error);
123
+ this.clearAuthFromStorage();
124
+ }
125
+ return null;
126
+ }
127
+ // Load auth token from localStorage on initialization
128
+ loadAuthFromStorage() {
129
+ this.loadFromStorage();
130
+ }
131
+ // Save auth response to localStorage
132
+ saveAuthToStorage(authResponse) {
133
+ try {
134
+ localStorage.setItem(AUTH_TOKEN_KEY, String(authResponse.token));
135
+ localStorage.setItem(AUTH_RESPONSE_KEY, JSON.stringify(authResponse));
136
+ localStorage.setItem(`${AUTH_RESPONSE_KEY}_stored_at`, Date.now().toString());
137
+ // Update memory
138
+ this.$_authToken.next(String(authResponse.token));
139
+ }
140
+ catch (error) {
141
+ console.warn('Failed to save auth to localStorage:', error);
142
+ }
143
+ }
144
+ // Clear auth from localStorage
145
+ clearAuthFromStorage() {
146
+ try {
147
+ localStorage.removeItem(AUTH_TOKEN_KEY);
148
+ localStorage.removeItem(AUTH_RESPONSE_KEY);
149
+ localStorage.removeItem(`${AUTH_RESPONSE_KEY}_stored_at`);
150
+ // Clear memory
151
+ this.$_authToken.next(null);
152
+ }
153
+ catch (error) {
154
+ console.warn('Failed to clear auth from localStorage:', error);
155
+ }
76
156
  }
77
157
  provider(target) {
78
158
  const targetWrapper = new AuthProvider(this.config, target, this.$authToken);
@@ -84,7 +164,11 @@ export class AuthService {
84
164
  .pipe(tap((result) => {
85
165
  if (result.hasSuccess && result.response?.token) {
86
166
  // Set the auth token when login is successful
87
- this.$authToken.next(String(result.response.token));
167
+ this.$_authToken.next(String(result.response.token));
168
+ // Save to localStorage if rememberMe is true
169
+ if (payload.rememberMe && result.response) {
170
+ this.saveAuthToStorage(result.response);
171
+ }
88
172
  }
89
173
  }));
90
174
  }
@@ -93,8 +177,8 @@ export class AuthService {
93
177
  .request()
94
178
  .pipe(tap((result) => {
95
179
  if (result.hasSuccess) {
96
- // Clear the auth token when logout is successful
97
- this.$authToken.next(null);
180
+ // Clear from localStorage (this will also clear memory)
181
+ this.clearAuthFromStorage();
98
182
  }
99
183
  }));
100
184
  }
@@ -1,3 +1,6 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
1
4
  import { AuthService, } from "./auth.service";
2
5
  import { NetworkingUIEventWrapped, AlertModel, } from "../types/types";
3
6
  import { of } from "rxjs";
@@ -9,7 +12,18 @@ describe("AuthService", () => {
9
12
  apiKey: "test-api-key",
10
13
  };
11
14
  let service;
15
+ let mockLocalStorage;
12
16
  beforeEach(() => {
17
+ // Mock localStorage for all tests
18
+ mockLocalStorage = {
19
+ setItem: jest.fn(),
20
+ getItem: jest.fn(),
21
+ removeItem: jest.fn(),
22
+ };
23
+ Object.defineProperty(window, 'localStorage', {
24
+ value: mockLocalStorage,
25
+ writable: true,
26
+ });
13
27
  service = new AuthService(config);
14
28
  });
15
29
  it("login success", async () => {
@@ -39,6 +53,55 @@ describe("AuthService", () => {
39
53
  expect(result?.hasFailed).toBe(true);
40
54
  expect(result?.alertModel).toBeInstanceOf(AlertModel);
41
55
  });
56
+ it("login success with rememberMe", async () => {
57
+ const payload = {
58
+ identifier: "user",
59
+ password: "pass",
60
+ rememberMe: true
61
+ };
62
+ const response = {
63
+ token_type: "Bearer",
64
+ expires_in: 3600,
65
+ token: "token",
66
+ refresh_token: "refresh",
67
+ };
68
+ jest.spyOn(service, "provider").mockReturnValue({
69
+ request: () => of(NetworkingUIEventWrapped.succeeded(response)),
70
+ });
71
+ const result = await service.login(payload).toPromise();
72
+ expect(result?.hasSuccess).toBe(true);
73
+ expect(result?.response).toEqual(response);
74
+ // Verify that auth token is set after successful login
75
+ expect(service.currentAuthToken).toBe("token");
76
+ expect(service.$authToken.value).toBe("token");
77
+ // Verify that localStorage was called to save the auth data
78
+ expect(mockLocalStorage.setItem).toHaveBeenCalledWith('smess_auth_token', 'token');
79
+ expect(mockLocalStorage.setItem).toHaveBeenCalledWith('smess_auth_response', JSON.stringify(response));
80
+ });
81
+ it("login success without rememberMe", async () => {
82
+ const payload = {
83
+ identifier: "user",
84
+ password: "pass",
85
+ rememberMe: false
86
+ };
87
+ const response = {
88
+ token_type: "Bearer",
89
+ expires_in: 3600,
90
+ token: "token",
91
+ refresh_token: "refresh",
92
+ };
93
+ jest.spyOn(service, "provider").mockReturnValue({
94
+ request: () => of(NetworkingUIEventWrapped.succeeded(response)),
95
+ });
96
+ const result = await service.login(payload).toPromise();
97
+ expect(result?.hasSuccess).toBe(true);
98
+ expect(result?.response).toEqual(response);
99
+ // Verify that auth token is set after successful login
100
+ expect(service.currentAuthToken).toBe("token");
101
+ expect(service.$authToken.value).toBe("token");
102
+ // Verify that localStorage was NOT called to save the auth data
103
+ expect(mockLocalStorage.setItem).not.toHaveBeenCalled();
104
+ });
42
105
  it("logout success", async () => {
43
106
  // First set a token to test clearing it
44
107
  service.$authToken.next("test-token");
@@ -51,6 +114,132 @@ describe("AuthService", () => {
51
114
  // Verify that auth token is cleared after successful logout
52
115
  expect(service.currentAuthToken).toBe(null);
53
116
  expect(service.$authToken.value).toBe(null);
117
+ // Verify that localStorage was cleared
118
+ expect(mockLocalStorage.removeItem).toHaveBeenCalledWith('smess_auth_token');
119
+ expect(mockLocalStorage.removeItem).toHaveBeenCalledWith('smess_auth_response');
120
+ });
121
+ it("loads auth token from localStorage on initialization", () => {
122
+ // Mock localStorage to return a stored auth response
123
+ const storedResponse = {
124
+ token_type: "Bearer",
125
+ expires_in: 3600,
126
+ token: "stored-token",
127
+ refresh_token: "refresh",
128
+ };
129
+ const mockLocalStorage = {
130
+ setItem: jest.fn(),
131
+ getItem: jest.fn().mockImplementation((key) => {
132
+ if (key === 'smess_auth_response') {
133
+ return JSON.stringify(storedResponse);
134
+ }
135
+ if (key === 'smess_auth_response_stored_at') {
136
+ return Date.now().toString();
137
+ }
138
+ return null;
139
+ }),
140
+ removeItem: jest.fn(),
141
+ };
142
+ Object.defineProperty(window, 'localStorage', {
143
+ value: mockLocalStorage,
144
+ writable: true,
145
+ });
146
+ // Create a new service instance
147
+ const newService = new AuthService(config);
148
+ // Verify that the stored token was loaded
149
+ expect(newService.currentAuthToken).toBe('stored-token');
150
+ expect(newService.$authToken.value).toBe('stored-token');
151
+ expect(mockLocalStorage.getItem).toHaveBeenCalledWith('smess_auth_response');
152
+ });
153
+ it("handles localStorage errors gracefully", () => {
154
+ // Mock localStorage to throw an error
155
+ const mockLocalStorage = {
156
+ setItem: jest.fn().mockImplementation(() => {
157
+ throw new Error('localStorage error');
158
+ }),
159
+ getItem: jest.fn().mockImplementation(() => {
160
+ throw new Error('localStorage error');
161
+ }),
162
+ removeItem: jest.fn().mockImplementation(() => {
163
+ throw new Error('localStorage error');
164
+ }),
165
+ };
166
+ Object.defineProperty(window, 'localStorage', {
167
+ value: mockLocalStorage,
168
+ writable: true,
169
+ });
170
+ // Should not throw an error during initialization
171
+ expect(() => new AuthService(config)).not.toThrow();
172
+ // Should not throw an error during login with rememberMe
173
+ const service = new AuthService(config);
174
+ const payload = {
175
+ identifier: "user",
176
+ password: "pass",
177
+ rememberMe: true
178
+ };
179
+ const response = {
180
+ token_type: "Bearer",
181
+ expires_in: 3600,
182
+ token: "token",
183
+ refresh_token: "refresh",
184
+ };
185
+ jest.spyOn(service, "provider").mockReturnValue({
186
+ request: () => of(NetworkingUIEventWrapped.succeeded(response)),
187
+ });
188
+ expect(async () => {
189
+ await service.login(payload).toPromise();
190
+ }).not.toThrow();
191
+ });
192
+ it("handles expired token from localStorage", () => {
193
+ // Mock localStorage with expired token
194
+ const expiredResponse = {
195
+ token_type: "Bearer",
196
+ expires_in: 1, // 1 second
197
+ token: "expired-token",
198
+ refresh_token: "refresh",
199
+ };
200
+ mockLocalStorage.getItem.mockImplementation((key) => {
201
+ if (key === 'smess_auth_response') {
202
+ return JSON.stringify(expiredResponse);
203
+ }
204
+ if (key === 'smess_auth_response_stored_at') {
205
+ // Set stored time to 2 seconds ago (expired)
206
+ return (Date.now() - 2000).toString();
207
+ }
208
+ return null;
209
+ });
210
+ // Create a new service instance
211
+ const newService = new AuthService(config);
212
+ // Verify that expired token is not returned
213
+ expect(newService.currentAuthToken).toBe(null);
214
+ expect(newService.$authToken.value).toBe(null);
215
+ // Verify that localStorage was cleared
216
+ expect(mockLocalStorage.removeItem).toHaveBeenCalledWith('smess_auth_token');
217
+ expect(mockLocalStorage.removeItem).toHaveBeenCalledWith('smess_auth_response');
218
+ expect(mockLocalStorage.removeItem).toHaveBeenCalledWith('smess_auth_response_stored_at');
219
+ });
220
+ it("handles valid token from localStorage", () => {
221
+ // Mock localStorage with valid token
222
+ const validResponse = {
223
+ token_type: "Bearer",
224
+ expires_in: 3600, // 1 hour
225
+ token: "valid-token",
226
+ refresh_token: "refresh",
227
+ };
228
+ mockLocalStorage.getItem.mockImplementation((key) => {
229
+ if (key === 'smess_auth_response') {
230
+ return JSON.stringify(validResponse);
231
+ }
232
+ if (key === 'smess_auth_response_stored_at') {
233
+ // Set stored time to 30 minutes ago (still valid)
234
+ return (Date.now() - 1800000).toString();
235
+ }
236
+ return null;
237
+ });
238
+ // Create a new service instance
239
+ const newService = new AuthService(config);
240
+ // Verify that valid token is returned
241
+ expect(newService.currentAuthToken).toBe("valid-token");
242
+ expect(newService.$authToken.value).toBe("valid-token");
54
243
  });
55
244
  it("forgotPassword success", async () => {
56
245
  const payload = { identifier: "user" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smess/networking",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
4
4
  "description": "Networking utilities and services for SMESS platform.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -28,6 +28,7 @@
28
28
  "eslint": "^9.31.0",
29
29
  "globals": "^16.3.0",
30
30
  "jest": "^30.0.4",
31
+ "jest-environment-jsdom": "^30.1.2",
31
32
  "ts-jest": "^29.4.0",
32
33
  "typescript": "^5.0.0",
33
34
  "typescript-eslint": "^8.37.0"