@progalaxyelabs/ngx-stonescriptphp-client 1.10.0 → 1.12.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.
@@ -1,228 +1,34 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, Inject, NgModule, EventEmitter, Output, Input, Component, Optional } from '@angular/core';
2
+ import { InjectionToken, makeEnvironmentProviders, Injectable, Inject, EventEmitter, Output, Input, Component, Optional } from '@angular/core';
3
3
  import { BehaviorSubject } from 'rxjs';
4
4
  import * as i3 from '@angular/common';
5
5
  import { CommonModule } from '@angular/common';
6
6
  import * as i4 from '@angular/forms';
7
7
  import { FormsModule } from '@angular/forms';
8
8
 
9
- class ApiResponse {
10
- status;
11
- data;
12
- message;
13
- get success() {
14
- return this.status === 'ok';
15
- }
16
- get errors() {
17
- return this.message ? [this.message] : [];
18
- }
19
- constructor(status, data = null, message = '') {
20
- this.status = status;
21
- this.data = data || null;
22
- this.message = message;
23
- }
24
- onOk(callback) {
25
- if (this.status === 'ok') {
26
- callback(this.data);
27
- }
28
- return this;
29
- }
30
- onNotOk(callback) {
31
- if (this.status === 'not ok') {
32
- callback(this.message, this.data);
33
- }
34
- return this;
35
- }
36
- onError(callback) {
37
- if (this.status === 'error') {
38
- callback();
39
- }
40
- return this;
41
- }
42
- isSuccess() {
43
- return this.status === 'ok';
44
- }
45
- isError() {
46
- return this.status === 'error' || this.status === 'not ok';
47
- }
48
- getData() {
49
- return this.data || null;
50
- }
51
- getError() {
52
- return this.message || 'Unknown error';
53
- }
54
- getStatus() {
55
- return this.status;
56
- }
57
- getMessage() {
58
- return this.message;
59
- }
60
- }
61
-
62
- class TokenService {
63
- accessToken = '';
64
- refreshToken = '';
65
- lsAccessTokenKey = 'progalaxyapi_access_token';
66
- lsRefreshTokenKey = 'progalaxyapi_refresh_token';
67
- constructor() { }
68
- setTokens(accessToken, refreshToken) {
69
- this.accessToken = accessToken;
70
- this.refreshToken = refreshToken;
71
- localStorage.setItem(this.lsAccessTokenKey, accessToken);
72
- localStorage.setItem(this.lsRefreshTokenKey, refreshToken);
73
- }
74
- setAccessToken(accessToken) {
75
- this.accessToken = accessToken;
76
- localStorage.setItem(this.lsAccessTokenKey, accessToken);
77
- }
78
- setRefreshToken(refreshToken) {
79
- this.refreshToken = refreshToken;
80
- localStorage.setItem(this.lsRefreshTokenKey, refreshToken);
81
- }
82
- getAccessToken() {
83
- if (this.accessToken) {
84
- return this.accessToken;
85
- }
86
- const storedAccessToken = localStorage.getItem(this.lsAccessTokenKey);
87
- if (storedAccessToken) {
88
- return storedAccessToken;
89
- }
90
- else {
91
- return '';
92
- }
93
- }
94
- getRefreshToken() {
95
- if (this.refreshToken) {
96
- return this.refreshToken;
97
- }
98
- const storedRefreshToken = localStorage.getItem(this.lsRefreshTokenKey);
99
- if (storedRefreshToken) {
100
- return storedRefreshToken;
101
- }
102
- else {
103
- return '';
104
- }
105
- }
106
- clear() {
107
- this.accessToken = '';
108
- this.refreshToken = '';
109
- localStorage.removeItem(this.lsAccessTokenKey);
110
- localStorage.removeItem(this.lsRefreshTokenKey);
111
- }
112
- /**
113
- * Check if there is a valid (non-empty) access token
114
- * @returns True if access token exists and is not empty
115
- */
116
- hasValidAccessToken() {
117
- const token = this.getAccessToken();
118
- return token !== null && token !== '';
119
- }
120
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TokenService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
121
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TokenService, providedIn: 'root' });
122
- }
123
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TokenService, decorators: [{
124
- type: Injectable,
125
- args: [{
126
- providedIn: 'root'
127
- }]
128
- }], ctorParameters: () => [] });
129
-
130
- /**
131
- * @deprecated Use boolean directly. Kept for backward compatibility.
132
- */
133
- var VerifyStatus;
134
- (function (VerifyStatus) {
135
- VerifyStatus["initialized"] = "initialized";
136
- VerifyStatus["yes"] = "yes";
137
- VerifyStatus["no"] = "no";
138
- })(VerifyStatus || (VerifyStatus = {}));
139
- class SigninStatusService {
140
- status;
141
- constructor() {
142
- this.status = new BehaviorSubject(false);
143
- }
144
- signedOut() {
145
- this.status.next(false);
146
- }
147
- signedIn() {
148
- this.status.next(true);
149
- }
150
- /**
151
- * Set signin status
152
- * @param isSignedIn - True if user is signed in, false otherwise
153
- */
154
- setSigninStatus(isSignedIn) {
155
- this.status.next(isSignedIn);
156
- }
157
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SigninStatusService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
158
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SigninStatusService, providedIn: 'root' });
159
- }
160
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SigninStatusService, decorators: [{
161
- type: Injectable,
162
- args: [{
163
- providedIn: 'root'
164
- }]
165
- }], ctorParameters: () => [] });
166
-
167
9
  class MyEnvironmentModel {
168
10
  production = true;
169
11
  /**
170
- * Platform code identifier (e.g., 'progalaxy', 'hr', 'admin')
171
- * Used for multi-tenant authentication
12
+ * Platform code identifier (e.g., 'progalaxy', 'hr', 'admin').
13
+ * Used for multi-tenant authentication.
172
14
  */
173
15
  platformCode = '';
174
16
  /**
175
- * Accounts platform URL for centralized authentication (single-server mode)
176
- * @example 'https://accounts.progalaxyelabs.com'
177
- * @deprecated Use authServers for multi-server support
178
- */
179
- accountsUrl = '';
180
- /**
181
- * Multiple authentication servers configuration
182
- * Enables platforms to authenticate against different identity providers
183
- * @example
184
- * ```typescript
185
- * authServers: {
186
- * customer: { url: 'https://auth.progalaxyelabs.com', default: true },
187
- * employee: { url: 'https://admin-auth.progalaxyelabs.com' }
188
- * }
189
- * ```
190
- */
191
- authServers;
192
- firebase = {
193
- projectId: '',
194
- appId: '',
195
- databaseURL: '',
196
- storageBucket: '',
197
- locationId: '',
198
- apiKey: '',
199
- authDomain: '',
200
- messagingSenderId: '',
201
- measurementId: ''
202
- };
203
- /**
204
- * Platform's own API base URL (e.g., '//api.medstoreapp.in')
205
- * Used to route registration through the platform API proxy instead of auth directly
17
+ * Platform's own API base URL.
18
+ * Used for routes that go through the platform API proxy (e.g. register-tenant).
19
+ * Falls back to apiServer.host if not set.
206
20
  * @example '//api.medstoreapp.in'
207
21
  */
208
22
  apiUrl;
209
23
  apiServer = { host: '' };
210
- chatServer = { host: '' };
211
24
  /**
212
- * Accounts/Authentication service server configuration
213
- * Use this when auth is on a different server than the API
214
- * Replaces the deprecated accountsUrl string with structured config
215
- * @example { host: 'https://accounts.progalaxyelabs.com' }
216
- */
217
- accountsServer;
218
- /**
219
- * Files service server configuration
220
- * Used by FilesService for file upload/download operations
25
+ * Files service server configuration.
26
+ * Used by FilesService for file upload/download operations.
221
27
  * @example { host: 'https://files.progalaxyelabs.com/api/' }
222
28
  */
223
29
  filesServer;
224
30
  /**
225
- * Authentication configuration
31
+ * Authentication configuration.
226
32
  * @default { mode: 'cookie', refreshEndpoint: '/auth/refresh', useCsrf: true }
227
33
  */
228
34
  auth = {
@@ -233,599 +39,816 @@ class MyEnvironmentModel {
233
39
  csrfTokenCookieName: 'csrf_token',
234
40
  csrfHeaderName: 'X-CSRF-Token'
235
41
  };
42
+ /**
43
+ * Multiple authentication servers configuration (for StoneScriptPHPAuth multi-server mode).
44
+ * @example
45
+ * ```typescript
46
+ * authServers: {
47
+ * customer: { url: 'https://auth.progalaxyelabs.com', default: true },
48
+ * employee: { url: 'https://admin-auth.progalaxyelabs.com' }
49
+ * }
50
+ * ```
51
+ */
52
+ authServers;
236
53
  /**
237
54
  * Custom OAuth provider configurations.
238
- * Register additional OAuth providers beyond the built-in ones
239
- * (google, linkedin, apple, microsoft, github, zoho).
240
55
  * @example
241
56
  * ```typescript
242
57
  * customProviders: {
243
- * okta: { label: 'Sign in with Okta', cssClass: 'btn-okta', buttonStyle: { borderColor: '#007dc1' } },
244
- * keycloak: { label: 'Sign in with Keycloak', icon: '🔑' }
58
+ * okta: { label: 'Sign in with Okta', cssClass: 'btn-okta', buttonStyle: { borderColor: '#007dc1' } }
245
59
  * }
246
60
  * ```
247
61
  */
248
62
  customProviders;
249
63
  /**
250
- * Branding configuration for auth components
251
- * Allows platforms to customize login/register pages without creating wrappers
64
+ * Branding configuration for auth UI components.
252
65
  */
253
66
  branding;
67
+ // ── Deprecated fields (kept for backward compatibility) ──────────────────
68
+ /**
69
+ * @deprecated Use auth.host instead.
70
+ * Auth server URL for centralized authentication.
71
+ */
72
+ accountsUrl = '';
73
+ /**
74
+ * @deprecated Use auth.host instead.
75
+ * Accounts/Authentication service server configuration.
76
+ */
77
+ accountsServer;
78
+ /**
79
+ * @deprecated Use auth.responseMap instead.
80
+ * Auth response field mapping for external auth compatibility.
81
+ */
82
+ authResponseMap;
254
83
  }
255
84
 
85
+ const AUTH_PLUGIN = new InjectionToken('AUTH_PLUGIN');
86
+
87
+ const ACTIVE_SERVER_KEY = 'progalaxyapi_active_auth_server';
88
+ const DEFAULT_RESPONSE_MAP = {
89
+ successPath: 'status',
90
+ successValue: 'ok',
91
+ accessTokenPath: 'data.access_token',
92
+ refreshTokenPath: 'data.refresh_token',
93
+ userPath: 'data.user',
94
+ errorMessagePath: 'message'
95
+ };
256
96
  /**
257
- * CSRF Token Service
97
+ * Built-in auth plugin for StoneScriptPHP backends.
258
98
  *
259
- * Manages CSRF tokens for cookie-based authentication.
260
- * Reads CSRF token from cookies and provides it for request headers.
99
+ * Handles StoneScriptPHP's auth format ({ status:'ok', data:{ access_token } })
100
+ * with optional authResponseMap overrides for external backends.
101
+ *
102
+ * Supports:
103
+ * - Cookie mode (httpOnly refresh token + CSRF)
104
+ * - Body mode (tokens in localStorage)
105
+ * - Multi-server auth (named authServers config)
106
+ * - OAuth popup login
107
+ * - Multi-tenant operations
261
108
  */
262
- class CsrfService {
263
- /**
264
- * Get CSRF token from cookie
265
- */
266
- getCsrfToken(cookieName = 'csrf_token') {
109
+ class StoneScriptPHPAuth {
110
+ config;
111
+ activeServer = null;
112
+ constructor(config) {
113
+ this.config = config;
114
+ this.restoreActiveServer();
115
+ }
116
+ // ── Response mapping ────────────────────────────────────────────────────
117
+ get responseMap() {
118
+ const m = this.config.responseMap;
119
+ if (!m)
120
+ return DEFAULT_RESPONSE_MAP;
121
+ return { ...DEFAULT_RESPONSE_MAP, ...m };
122
+ }
123
+ resolvePath(obj, path) {
124
+ return path.split('.').reduce((o, key) => o?.[key], obj);
125
+ }
126
+ isAuthSuccess(data) {
127
+ const map = this.responseMap;
128
+ if (map.successPath) {
129
+ return this.resolvePath(data, map.successPath) === (map.successValue ?? 'ok');
130
+ }
131
+ return !!this.resolvePath(data, map.accessTokenPath);
132
+ }
133
+ resolveAccessToken(data) {
134
+ return this.resolvePath(data, this.responseMap.accessTokenPath);
135
+ }
136
+ resolveRefreshToken(data) {
137
+ return this.resolvePath(data, this.responseMap.refreshTokenPath);
138
+ }
139
+ resolveUser(data) {
140
+ const raw = this.resolvePath(data, this.responseMap.userPath);
141
+ return raw ? this.normalizeUser(raw) : undefined;
142
+ }
143
+ resolveErrorMessage(data, fallback) {
144
+ const path = this.responseMap.errorMessagePath ?? 'message';
145
+ return this.resolvePath(data, path) || fallback;
146
+ }
147
+ normalizeUser(raw) {
148
+ return {
149
+ user_id: raw.user_id ?? (raw.id ? this.hashUUID(raw.id) : 0),
150
+ id: raw.id ?? String(raw.user_id),
151
+ email: raw.email,
152
+ display_name: raw.display_name ?? raw.email?.split('@')[0] ?? '',
153
+ photo_url: raw.photo_url,
154
+ is_email_verified: raw.is_email_verified ?? false
155
+ };
156
+ }
157
+ hashUUID(uuid) {
158
+ let hash = 0;
159
+ for (let i = 0; i < uuid.length; i++) {
160
+ const char = uuid.charCodeAt(i);
161
+ hash = ((hash << 5) - hash) + char;
162
+ hash = hash & hash;
163
+ }
164
+ return Math.abs(hash);
165
+ }
166
+ // ── CSRF (inlined, no Angular DI needed) ────────────────────────────────
167
+ getCsrfToken() {
168
+ const cookieName = this.config.auth?.csrfTokenCookieName ?? 'csrf_token';
267
169
  const cookies = document.cookie.split(';');
268
170
  for (const cookie of cookies) {
269
171
  const [name, value] = cookie.trim().split('=');
270
- if (name === cookieName) {
172
+ if (name === cookieName)
271
173
  return decodeURIComponent(value);
272
- }
273
174
  }
274
175
  return null;
275
176
  }
276
- /**
277
- * Check if CSRF token exists
278
- */
279
- hasCsrfToken(cookieName = 'csrf_token') {
280
- return this.getCsrfToken(cookieName) !== null;
177
+ // ── Server resolution ────────────────────────────────────────────────────
178
+ getAccountsUrl(serverName) {
179
+ if (this.config.authServers && Object.keys(this.config.authServers).length > 0) {
180
+ const target = serverName || this.activeServer || this.getDefaultServer();
181
+ if (!target)
182
+ throw new Error('No auth server specified and no default server configured');
183
+ const serverConfig = this.config.authServers[target];
184
+ if (!serverConfig)
185
+ throw new Error(`Auth server '${target}' not found in configuration`);
186
+ return serverConfig.url;
187
+ }
188
+ return this.config.host;
281
189
  }
282
- /**
283
- * Clear CSRF token (for logout)
284
- * Note: Client-side deletion is limited for httpOnly cookies
285
- */
286
- clearCsrfToken(cookieName = 'csrf_token') {
287
- // Can only clear non-httpOnly cookies
288
- document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
190
+ getPlatformApiUrl() {
191
+ return this.config.apiUrl || this.config.host;
289
192
  }
290
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: CsrfService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
291
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: CsrfService, providedIn: 'root' });
292
- }
293
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: CsrfService, decorators: [{
294
- type: Injectable,
295
- args: [{
296
- providedIn: 'root'
297
- }]
298
- }] });
299
-
300
- class ApiConnectionService {
301
- tokens;
302
- signinStatus;
303
- environment;
304
- csrf;
305
- host = ''; // base URL without trailing slash
306
- accessToken = '';
307
- authConfig;
308
- constructor(tokens, signinStatus, environment, csrf) {
309
- this.tokens = tokens;
310
- this.signinStatus = signinStatus;
311
- this.environment = environment;
312
- this.csrf = csrf;
313
- this.host = environment.apiServer.host;
314
- // Set default auth config based on mode
315
- this.authConfig = {
316
- mode: environment.auth?.mode || 'cookie',
317
- refreshEndpoint: environment.auth?.refreshEndpoint,
318
- useCsrf: environment.auth?.useCsrf,
319
- refreshTokenCookieName: environment.auth?.refreshTokenCookieName || 'refresh_token',
320
- csrfTokenCookieName: environment.auth?.csrfTokenCookieName || 'csrf_token',
321
- csrfHeaderName: environment.auth?.csrfHeaderName || 'X-CSRF-Token'
322
- };
323
- // Set default refresh endpoint based on mode if not specified
324
- if (!this.authConfig.refreshEndpoint) {
325
- this.authConfig.refreshEndpoint = this.authConfig.mode === 'cookie'
326
- ? '/auth/refresh'
327
- : '/user/refresh_access';
328
- }
329
- // Set default CSRF setting based on mode if not specified
330
- if (this.authConfig.useCsrf === undefined) {
331
- this.authConfig.useCsrf = this.authConfig.mode === 'cookie';
193
+ getDefaultServer() {
194
+ if (!this.config.authServers)
195
+ return null;
196
+ for (const [name, cfg] of Object.entries(this.config.authServers)) {
197
+ if (cfg.default)
198
+ return name;
332
199
  }
200
+ return Object.keys(this.config.authServers)[0] || null;
333
201
  }
334
- async request(url, options, data) {
202
+ restoreActiveServer() {
335
203
  try {
336
- if (data !== null) {
337
- const body = JSON.stringify(data);
338
- if (body) {
339
- options.body = body;
340
- }
341
- else {
342
- options.body = {};
343
- }
344
- }
345
- const accessTokenIncluded = this.includeAccessToken(options);
346
- let response = await fetch(url, options);
347
- if ((response.status === 401) && accessTokenIncluded) {
348
- response = await this.refreshAccessTokenAndRetry(url, options, response);
349
- }
350
- if (response.ok) {
351
- const json = await (response.json());
352
- return (new ApiResponse(json.status, json.data, json.message));
353
- }
354
- if (response.status === 401) {
355
- this.signinStatus.signedOut();
356
- }
357
- return this.handleError(response);
204
+ const saved = localStorage.getItem(ACTIVE_SERVER_KEY);
205
+ this.activeServer = (saved && this.config.authServers?.[saved])
206
+ ? saved
207
+ : this.getDefaultServer();
358
208
  }
359
- catch (error) {
360
- return this.handleError(error);
209
+ catch {
210
+ this.activeServer = this.getDefaultServer();
361
211
  }
362
212
  }
363
- handleError(error) {
364
- console.error(`Backend returned code ${error.status}, ` +
365
- `full error: `, error);
366
- return new ApiResponse('error');
367
- }
368
- async get(endpoint, queryParamsObj) {
369
- const url = this.host + endpoint + this.buildQueryString(queryParamsObj);
370
- const fetchOptions = {
371
- mode: 'cors',
372
- redirect: 'error'
373
- };
374
- return this.request(url, fetchOptions, null);
375
- }
376
- async post(pathWithQueryParams, data) {
377
- const url = this.host + pathWithQueryParams;
378
- const fetchOptions = {
379
- method: 'POST',
380
- mode: 'cors',
381
- redirect: 'error',
382
- headers: {
383
- 'Content-Type': 'application/json'
384
- },
385
- body: JSON.stringify(data)
386
- };
387
- return this.request(url, fetchOptions, data);
213
+ // ── Multi-server public API ──────────────────────────────────────────────
214
+ switchServer(serverName) {
215
+ if (!this.config.authServers?.[serverName]) {
216
+ throw new Error(`Auth server '${serverName}' not found in configuration`);
217
+ }
218
+ try {
219
+ localStorage.setItem(ACTIVE_SERVER_KEY, serverName);
220
+ }
221
+ catch { /* ignore */ }
222
+ this.activeServer = serverName;
388
223
  }
389
- async put(pathWithQueryParams, data) {
390
- const url = this.host + pathWithQueryParams;
391
- const fetchOptions = {
392
- method: 'PUT',
393
- mode: 'cors',
394
- redirect: 'error',
395
- headers: {
396
- 'Content-Type': 'application/json'
397
- },
398
- body: JSON.stringify(data)
399
- };
400
- return this.request(url, fetchOptions, data);
224
+ getAvailableServers() {
225
+ return Object.keys(this.config.authServers ?? {});
401
226
  }
402
- async patch(pathWithQueryParams, data) {
403
- const url = this.host + pathWithQueryParams;
404
- const fetchOptions = {
405
- method: 'PATCH',
406
- mode: 'cors',
407
- redirect: 'error',
408
- headers: {
409
- 'Content-Type': 'application/json'
410
- },
411
- body: JSON.stringify(data)
412
- };
413
- return this.request(url, fetchOptions, data);
227
+ getActiveServer() {
228
+ return this.activeServer;
414
229
  }
415
- async delete(endpoint, queryParamsObj) {
416
- const url = this.host + endpoint + this.buildQueryString(queryParamsObj);
417
- const fetchOptions = {
418
- method: 'DELETE',
419
- mode: 'cors',
420
- redirect: 'error'
421
- };
422
- return this.request(url, fetchOptions, null);
230
+ getServerConfig(serverName) {
231
+ if (!this.config.authServers)
232
+ return null;
233
+ const target = serverName || this.activeServer || this.getDefaultServer();
234
+ return target ? (this.config.authServers[target] ?? null) : null;
423
235
  }
424
- // async postFormWithFiles(pathWithQueryParams: string, formData: FormData): Promise<ApiResponse | null> {
425
- // const url = this.host + pathWithQueryParams.replace(/^\/+/, '')
426
- // try {
427
- // const fetchOptions: RequestInit = {
428
- // method: 'POST',
429
- // mode: 'cors',
430
- // redirect: 'error',
431
- // body: formData
432
- // }
433
- // const accessTokenIncluded = this.includeAccessToken(fetchOptions)
434
- // let response: Response = await fetch(url, fetchOptions)
435
- // if ((response.status === 401) && accessTokenIncluded) {
436
- // response = await this.refreshAccessTokenAndRetry(url, fetchOptions, response)
437
- // }
438
- // if (response.ok) {
439
- // return ((await (response.json()) as ApiResponse))
440
- // }
441
- // return this.handleError(response)
442
- // } catch (error) {
443
- // return this.handleError(error)
444
- // }
445
- // }
446
- includeAccessToken(options) {
447
- this.accessToken = this.tokens.getAccessToken();
448
- if (!this.accessToken) {
449
- return false;
236
+ // ── Core auth operations ─────────────────────────────────────────────────
237
+ async login(email, password) {
238
+ try {
239
+ const accountsUrl = this.getAccountsUrl();
240
+ const response = await fetch(`${accountsUrl}/api/auth/login`, {
241
+ method: 'POST',
242
+ headers: { 'Content-Type': 'application/json' },
243
+ credentials: 'include',
244
+ body: JSON.stringify({ email, password, platform: this.config.platformCode })
245
+ });
246
+ const data = await response.json();
247
+ if (this.isAuthSuccess(data)) {
248
+ return {
249
+ success: true,
250
+ accessToken: this.resolveAccessToken(data),
251
+ refreshToken: this.resolveRefreshToken(data),
252
+ user: this.resolveUser(data)
253
+ };
254
+ }
255
+ return { success: false, message: this.resolveErrorMessage(data, 'Invalid credentials') };
450
256
  }
451
- if (!options.headers) {
452
- options.headers = {};
257
+ catch {
258
+ return { success: false, message: 'Network error. Please try again.' };
453
259
  }
454
- options.headers['Authorization'] = 'Bearer ' + this.accessToken;
455
- return true;
456
260
  }
457
- async refreshAccessTokenAndRetry(url, fetchOptions, response) {
458
- const refreshStatusOk = await this.refreshAccessToken();
459
- if (!refreshStatusOk) {
460
- return response;
261
+ async register(email, password, displayName) {
262
+ try {
263
+ const accountsUrl = this.getAccountsUrl();
264
+ const response = await fetch(`${accountsUrl}/api/auth/register`, {
265
+ method: 'POST',
266
+ headers: { 'Content-Type': 'application/json' },
267
+ credentials: 'include',
268
+ body: JSON.stringify({
269
+ email,
270
+ password,
271
+ display_name: displayName,
272
+ platform: this.config.platformCode
273
+ })
274
+ });
275
+ const data = await response.json();
276
+ if (this.isAuthSuccess(data)) {
277
+ return {
278
+ success: true,
279
+ accessToken: this.resolveAccessToken(data),
280
+ refreshToken: this.resolveRefreshToken(data),
281
+ user: this.resolveUser(data),
282
+ needsVerification: !!data.needs_verification,
283
+ message: data.needs_verification ? 'Please verify your email' : undefined
284
+ };
285
+ }
286
+ return { success: false, message: this.resolveErrorMessage(data, 'Registration failed') };
287
+ }
288
+ catch {
289
+ return { success: false, message: 'Network error. Please try again.' };
461
290
  }
462
- fetchOptions.headers['Authorization'] = 'Bearer ' + this.accessToken;
463
- response = await fetch(url, fetchOptions);
464
- return response;
465
291
  }
466
- async refreshAccessToken() {
467
- if (this.authConfig.mode === 'none') {
468
- return false;
292
+ async logout(refreshToken) {
293
+ try {
294
+ const accountsUrl = this.getAccountsUrl();
295
+ await fetch(`${accountsUrl}/api/auth/logout`, {
296
+ method: 'POST',
297
+ headers: { 'Content-Type': 'application/json' },
298
+ credentials: 'include',
299
+ body: JSON.stringify({ refresh_token: refreshToken })
300
+ });
469
301
  }
470
- if (this.authConfig.mode === 'cookie') {
471
- return await this.refreshAccessTokenCookieMode();
302
+ catch (error) {
303
+ console.error('Logout API call failed:', error);
472
304
  }
473
- else {
474
- return await this.refreshAccessTokenBodyMode();
305
+ }
306
+ async checkSession() {
307
+ try {
308
+ const accountsUrl = this.getAccountsUrl();
309
+ const response = await fetch(`${accountsUrl}/api/auth/refresh`, {
310
+ method: 'POST',
311
+ credentials: 'include'
312
+ });
313
+ if (!response.ok)
314
+ return { success: false };
315
+ const data = await response.json();
316
+ const accessToken = this.resolveAccessToken(data);
317
+ if (accessToken) {
318
+ return { success: true, accessToken, user: this.resolveUser(data) };
319
+ }
320
+ return { success: false };
321
+ }
322
+ catch {
323
+ return { success: false };
475
324
  }
476
325
  }
477
- /**
478
- * Refresh access token using cookie-based auth (StoneScriptPHP v2.1.x default)
479
- */
480
- async refreshAccessTokenCookieMode() {
326
+ async refresh(accessToken, refreshToken) {
327
+ const mode = this.config.auth?.mode ?? 'cookie';
328
+ if (mode === 'none')
329
+ return null;
330
+ return mode === 'cookie'
331
+ ? this.refreshCookieMode()
332
+ : this.refreshBodyMode(accessToken, refreshToken);
333
+ }
334
+ async refreshCookieMode() {
481
335
  try {
482
- // Determine auth server: accountsServer.host > accountsUrl > apiServer.host
483
- const authHost = this.environment.accountsServer?.host
484
- || this.environment.accountsUrl
485
- || this.host;
486
- const refreshTokenUrl = authHost + this.authConfig.refreshEndpoint;
487
- const headers = {
488
- 'Content-Type': 'application/json'
489
- };
490
- // Add CSRF token if enabled
491
- if (this.authConfig.useCsrf) {
492
- const csrfToken = this.csrf.getCsrfToken(this.authConfig.csrfTokenCookieName);
336
+ const authHost = this.getAccountsUrl();
337
+ const endpoint = this.config.auth?.refreshEndpoint ?? '/auth/refresh';
338
+ const headers = { 'Content-Type': 'application/json' };
339
+ if (this.config.auth?.useCsrf !== false) {
340
+ const csrfToken = this.getCsrfToken();
493
341
  if (!csrfToken) {
494
342
  console.error('CSRF token not found in cookie');
495
- return false;
343
+ return null;
496
344
  }
497
- headers[this.authConfig.csrfHeaderName] = csrfToken;
345
+ headers[this.config.auth?.csrfHeaderName ?? 'X-CSRF-Token'] = csrfToken;
498
346
  }
499
- let refreshTokenResponse = await fetch(refreshTokenUrl, {
347
+ const response = await fetch(`${authHost}${endpoint}`, {
500
348
  method: 'POST',
501
349
  mode: 'cors',
502
- credentials: 'include', // Important: send cookies
350
+ credentials: 'include',
503
351
  redirect: 'error',
504
- headers: headers
352
+ headers
505
353
  });
506
- if (!refreshTokenResponse.ok) {
507
- this.accessToken = '';
508
- this.tokens.clear();
509
- return false;
510
- }
511
- let refreshAccessData = await refreshTokenResponse.json();
512
- if (!refreshAccessData || refreshAccessData.status !== 'ok') {
513
- return false;
514
- }
515
- // Extract access token from response
516
- const newAccessToken = refreshAccessData.data?.access_token || refreshAccessData.access_token;
517
- if (!newAccessToken) {
518
- console.error('No access token in refresh response');
519
- return false;
520
- }
521
- // Store new access token (refresh token is in httpOnly cookie)
522
- this.tokens.setAccessToken(newAccessToken);
523
- this.accessToken = newAccessToken;
524
- return true;
354
+ if (!response.ok)
355
+ return null;
356
+ const data = await response.json();
357
+ if (!this.isAuthSuccess(data))
358
+ return null;
359
+ return this.resolveAccessToken(data) ?? null;
525
360
  }
526
361
  catch (error) {
527
362
  console.error('Token refresh failed (cookie mode):', error);
528
- this.accessToken = '';
529
- this.tokens.clear();
530
- return false;
363
+ return null;
531
364
  }
532
365
  }
533
- /**
534
- * Refresh access token using body-based auth (legacy mode)
535
- */
536
- async refreshAccessTokenBodyMode() {
366
+ async refreshBodyMode(accessToken, refreshToken) {
367
+ if (!refreshToken)
368
+ return null;
537
369
  try {
538
- const refreshToken = this.tokens.getRefreshToken();
539
- if (!refreshToken) {
540
- return false;
541
- }
542
- // Determine auth server: accountsServer.host > accountsUrl > apiServer.host
543
- const authHost = this.environment.accountsServer?.host
544
- || this.environment.accountsUrl
545
- || this.host;
546
- const refreshTokenUrl = authHost + this.authConfig.refreshEndpoint;
547
- let refreshTokenResponse = await fetch(refreshTokenUrl, {
370
+ const authHost = this.getAccountsUrl();
371
+ const endpoint = this.config.auth?.refreshEndpoint ?? '/user/refresh_access';
372
+ const response = await fetch(`${authHost}${endpoint}`, {
548
373
  method: 'POST',
549
374
  mode: 'cors',
550
375
  redirect: 'error',
551
- headers: {
552
- 'Content-Type': 'application/json'
553
- },
554
- body: JSON.stringify({
555
- access_token: this.accessToken,
556
- refresh_token: refreshToken
557
- })
376
+ headers: { 'Content-Type': 'application/json' },
377
+ body: JSON.stringify({ access_token: accessToken, refresh_token: refreshToken })
558
378
  });
559
- if (!refreshTokenResponse.ok) {
560
- this.accessToken = '';
561
- this.tokens.clear();
562
- return false;
563
- }
564
- let refreshAccessData = await refreshTokenResponse.json();
565
- if (!refreshAccessData) {
566
- return false;
567
- }
568
- const newAccessToken = refreshAccessData.data?.access_token || refreshAccessData.access_token;
569
- if (!newAccessToken) {
570
- console.error('No access token in refresh response');
571
- return false;
572
- }
573
- this.tokens.setTokens(newAccessToken, refreshToken);
574
- this.accessToken = newAccessToken;
575
- return true;
379
+ if (!response.ok)
380
+ return null;
381
+ const data = await response.json();
382
+ return this.resolveAccessToken(data) ?? null;
576
383
  }
577
384
  catch (error) {
578
385
  console.error('Token refresh failed (body mode):', error);
579
- this.accessToken = '';
580
- this.tokens.clear();
581
- return false;
386
+ return null;
582
387
  }
583
388
  }
584
- buildQueryString(options) {
585
- if (options === undefined) {
586
- return '';
587
- }
588
- const array = [];
589
- for (let key in options) {
590
- if (options.hasOwnProperty(key) && (options[key] !== null) && (options[key] !== undefined)) {
591
- array.push(encodeURIComponent(key) + "=" + encodeURIComponent(options[key]));
389
+ // ── OAuth ────────────────────────────────────────────────────────────────
390
+ async loginWithProvider(provider) {
391
+ return new Promise((resolve) => {
392
+ const width = 500, height = 600;
393
+ const left = (window.screen.width - width) / 2;
394
+ const top = (window.screen.height - height) / 2;
395
+ const accountsUrl = this.getAccountsUrl();
396
+ const oauthUrl = `${accountsUrl}/oauth/${provider}?platform=${this.config.platformCode}&mode=popup`;
397
+ const popup = window.open(oauthUrl, `${provider}_login`, `width=${width},height=${height},left=${left},top=${top}`);
398
+ if (!popup) {
399
+ resolve({ success: false, message: 'Popup blocked. Please allow popups for this site.' });
400
+ return;
592
401
  }
593
- }
594
- const str = array.join('&');
595
- if (str !== '') {
596
- return '?' + str;
597
- }
598
- return '';
402
+ const messageHandler = (event) => {
403
+ if (event.origin !== new URL(accountsUrl).origin)
404
+ return;
405
+ if (event.data.type === 'oauth_success') {
406
+ window.removeEventListener('message', messageHandler);
407
+ popup.close();
408
+ const rawUser = event.data.user || this.resolveUser(event.data);
409
+ resolve({
410
+ success: true,
411
+ accessToken: event.data.access_token,
412
+ user: rawUser ? this.normalizeUser(rawUser) : undefined
413
+ });
414
+ }
415
+ else if (event.data.type === 'oauth_error') {
416
+ window.removeEventListener('message', messageHandler);
417
+ popup.close();
418
+ resolve({ success: false, message: event.data.message || 'OAuth login failed' });
419
+ }
420
+ };
421
+ window.addEventListener('message', messageHandler);
422
+ const checkClosed = setInterval(() => {
423
+ if (popup.closed) {
424
+ clearInterval(checkClosed);
425
+ window.removeEventListener('message', messageHandler);
426
+ resolve({ success: false, message: 'Login cancelled' });
427
+ }
428
+ }, 500);
429
+ });
599
430
  }
600
- /**
601
- * Upload a drawing (uses upload server if configured, otherwise API server)
602
- * @deprecated Platform-specific method - consider moving to platform service
603
- */
604
- async uploadDrawing(formData) {
605
- const uploadHost = this.environment.uploadServer?.host || this.host;
606
- const url = uploadHost + '/upload/drawing';
607
- const fetchOptions = {
608
- method: 'POST',
609
- mode: 'cors',
610
- redirect: 'error',
611
- body: formData
612
- };
613
- return this.request(url, fetchOptions, null);
614
- }
615
- /**
616
- * Upload an image (uses upload server if configured, otherwise API server)
617
- * @deprecated Platform-specific method - consider moving to platform service
618
- */
619
- async uploadImage(formData) {
620
- const uploadHost = this.environment.uploadServer?.host || this.host;
621
- const url = uploadHost + '/upload/image';
622
- const fetchOptions = {
623
- method: 'POST',
624
- mode: 'cors',
625
- redirect: 'error',
626
- body: formData
627
- };
628
- return this.request(url, fetchOptions, null);
629
- }
630
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ApiConnectionService, deps: [{ token: TokenService }, { token: SigninStatusService }, { token: MyEnvironmentModel }, { token: CsrfService }], target: i0.ɵɵFactoryTarget.Injectable });
631
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ApiConnectionService, providedIn: 'root' });
632
- }
633
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ApiConnectionService, decorators: [{
634
- type: Injectable,
635
- args: [{
636
- providedIn: 'root'
637
- }]
638
- }], ctorParameters: () => [{ type: TokenService }, { type: SigninStatusService }, { type: MyEnvironmentModel }, { type: CsrfService }] });
639
-
640
- class AuthService {
641
- tokens;
642
- signinStatus;
643
- environment;
644
- USER_STORAGE_KEY = 'progalaxyapi_user';
645
- ACTIVE_AUTH_SERVER_KEY = 'progalaxyapi_active_auth_server';
646
- // Observable user state
647
- userSubject = new BehaviorSubject(null);
648
- user$ = this.userSubject.asObservable();
649
- // Current active auth server name (for multi-server mode)
650
- activeAuthServer = null;
651
- constructor(tokens, signinStatus, environment) {
652
- this.tokens = tokens;
653
- this.signinStatus = signinStatus;
654
- this.environment = environment;
655
- // Restore user from localStorage on initialization
656
- this.restoreUser();
657
- // Restore active auth server
658
- this.restoreActiveAuthServer();
659
- }
660
- // ===== Multi-Server Support Methods =====
661
- /**
662
- * Get the current accounts URL based on configuration
663
- * Supports both single-server (accountsUrl) and multi-server (authServers) modes
664
- * @param serverName - Optional server name for multi-server mode
665
- */
666
- getAccountsUrl(serverName) {
667
- // Multi-server mode
668
- if (this.environment.authServers && Object.keys(this.environment.authServers).length > 0) {
669
- const targetServer = serverName || this.activeAuthServer || this.getDefaultAuthServer();
670
- if (!targetServer) {
671
- throw new Error('No auth server specified and no default server configured');
672
- }
673
- const serverConfig = this.environment.authServers[targetServer];
674
- if (!serverConfig) {
675
- throw new Error(`Auth server '${targetServer}' not found in configuration`);
431
+ // ── Multi-tenant ─────────────────────────────────────────────────────────
432
+ async selectTenant(tenantId, accessToken) {
433
+ try {
434
+ const accountsUrl = this.getAccountsUrl();
435
+ const response = await fetch(`${accountsUrl}/api/auth/select-tenant`, {
436
+ method: 'POST',
437
+ headers: {
438
+ 'Authorization': `Bearer ${accessToken}`,
439
+ 'Content-Type': 'application/json'
440
+ },
441
+ credentials: 'include',
442
+ body: JSON.stringify({ tenant_id: tenantId })
443
+ });
444
+ const data = await response.json();
445
+ if (this.isAuthSuccess(data)) {
446
+ return { success: true, accessToken: this.resolveAccessToken(data) };
676
447
  }
677
- return serverConfig.url;
448
+ return { success: false, message: this.resolveErrorMessage(data, 'Failed to select tenant') };
678
449
  }
679
- // Single-server mode (backward compatibility)
680
- if (this.environment.accountsUrl) {
681
- return this.environment.accountsUrl;
450
+ catch {
451
+ return { success: false, message: 'Network error. Please try again.' };
682
452
  }
683
- throw new Error('No authentication server configured. Set either accountsUrl or authServers in environment config.');
684
453
  }
685
- /**
686
- * Get the default auth server name
687
- */
688
- getDefaultAuthServer() {
689
- if (!this.environment.authServers) {
690
- return null;
454
+ async getTenantMemberships(accessToken) {
455
+ try {
456
+ const accountsUrl = this.getAccountsUrl();
457
+ const response = await fetch(`${accountsUrl}/api/auth/memberships`, {
458
+ method: 'GET',
459
+ headers: {
460
+ 'Authorization': `Bearer ${accessToken}`,
461
+ 'Content-Type': 'application/json'
462
+ },
463
+ credentials: 'include'
464
+ });
465
+ const data = await response.json();
466
+ return data.memberships || [];
691
467
  }
692
- // Find server marked as default
693
- for (const [name, config] of Object.entries(this.environment.authServers)) {
694
- if (config.default) {
695
- return name;
696
- }
468
+ catch {
469
+ return [];
697
470
  }
698
- // If no default is marked, use the first server
699
- const firstServer = Object.keys(this.environment.authServers)[0];
700
- return firstServer || null;
701
471
  }
702
- /**
703
- * Restore active auth server from localStorage
704
- */
705
- restoreActiveAuthServer() {
472
+ async registerTenant(data, accessToken) {
473
+ if (data.provider !== 'emailPassword') {
474
+ return this.registerTenantWithOAuth(data.tenantName, data.provider);
475
+ }
706
476
  try {
707
- const savedServer = localStorage.getItem(this.ACTIVE_AUTH_SERVER_KEY);
708
- if (savedServer && this.environment.authServers?.[savedServer]) {
709
- this.activeAuthServer = savedServer;
710
- }
711
- else {
712
- // Set to default if saved server is invalid
713
- this.activeAuthServer = this.getDefaultAuthServer();
477
+ const apiUrl = this.getPlatformApiUrl();
478
+ const body = {
479
+ tenant_name: data.tenantName,
480
+ email: data.email ?? '',
481
+ password: data.password ?? '',
482
+ provider: 'emailPassword'
483
+ };
484
+ if (data.displayName)
485
+ body['display_name'] = data.displayName;
486
+ if (data.countryCode)
487
+ body['country_code'] = data.countryCode;
488
+ if (data.role)
489
+ body['role'] = data.role;
490
+ const response = await fetch(`${apiUrl}/auth/register-tenant`, {
491
+ method: 'POST',
492
+ headers: {
493
+ 'Content-Type': 'application/json',
494
+ ...(accessToken ? { 'Authorization': `Bearer ${accessToken}` } : {})
495
+ },
496
+ credentials: 'include',
497
+ body: JSON.stringify(body)
498
+ });
499
+ return response.json();
500
+ }
501
+ catch {
502
+ return { success: false, message: 'Network error. Please try again.' };
503
+ }
504
+ }
505
+ async registerTenantWithOAuth(tenantName, provider) {
506
+ return new Promise((resolve) => {
507
+ const width = 500, height = 600;
508
+ const left = (window.screen.width - width) / 2;
509
+ const top = (window.screen.height - height) / 2;
510
+ const accountsUrl = this.getAccountsUrl();
511
+ const oauthUrl = `${accountsUrl}/oauth/${provider}?` +
512
+ `platform=${this.config.platformCode}&mode=popup&action=register_tenant&` +
513
+ `tenant_name=${encodeURIComponent(tenantName)}`;
514
+ const popup = window.open(oauthUrl, `${provider}_register_tenant`, `width=${width},height=${height},left=${left},top=${top}`);
515
+ if (!popup) {
516
+ resolve({ success: false, message: 'Popup blocked. Please allow popups for this site.' });
517
+ return;
714
518
  }
519
+ const messageHandler = (event) => {
520
+ if (event.origin !== new URL(accountsUrl).origin)
521
+ return;
522
+ if (event.data.type === 'tenant_register_success') {
523
+ window.removeEventListener('message', messageHandler);
524
+ popup.close();
525
+ resolve({ success: true, tenant: event.data.tenant, user: event.data.user,
526
+ access_token: event.data.access_token });
527
+ }
528
+ else if (event.data.type === 'tenant_register_error') {
529
+ window.removeEventListener('message', messageHandler);
530
+ popup.close();
531
+ resolve({ success: false, message: event.data.message || 'Tenant registration failed' });
532
+ }
533
+ };
534
+ window.addEventListener('message', messageHandler);
535
+ const checkClosed = setInterval(() => {
536
+ if (popup.closed) {
537
+ clearInterval(checkClosed);
538
+ window.removeEventListener('message', messageHandler);
539
+ resolve({ success: false, message: 'Registration cancelled' });
540
+ }
541
+ }, 500);
542
+ });
543
+ }
544
+ async checkTenantSlugAvailable(slug) {
545
+ try {
546
+ const accountsUrl = this.getAccountsUrl();
547
+ const response = await fetch(`${accountsUrl}/api/auth/check-tenant-slug/${slug}`, {
548
+ method: 'GET',
549
+ headers: { 'Content-Type': 'application/json' }
550
+ });
551
+ const data = await response.json();
552
+ return { available: data.available || false, suggestion: data.suggestion };
715
553
  }
716
- catch (error) {
717
- console.error('Failed to restore active auth server:', error);
718
- this.activeAuthServer = this.getDefaultAuthServer();
554
+ catch {
555
+ return { available: true };
719
556
  }
720
557
  }
721
- /**
722
- * Save active auth server to localStorage
723
- */
724
- saveActiveAuthServer(serverName) {
558
+ async checkOnboardingStatus(identityId, platformCode) {
559
+ const accountsUrl = this.getAccountsUrl();
560
+ const platform = platformCode ?? this.config.platformCode ?? '';
561
+ const response = await fetch(`${accountsUrl}/api/auth/onboarding/status?platform_code=${platform}&identity_id=${identityId}`, { method: 'GET', headers: { 'Content-Type': 'application/json' }, credentials: 'include' });
562
+ if (!response.ok)
563
+ throw new Error('Failed to check onboarding status');
564
+ return response.json();
565
+ }
566
+ async completeTenantOnboarding(countryCode, tenantName, accessToken) {
567
+ const apiUrl = this.getPlatformApiUrl();
568
+ const response = await fetch(`${apiUrl}/auth/register-tenant`, {
569
+ method: 'POST',
570
+ headers: {
571
+ 'Content-Type': 'application/json',
572
+ 'Authorization': `Bearer ${accessToken}`
573
+ },
574
+ credentials: 'include',
575
+ body: JSON.stringify({
576
+ platform: this.config.platformCode,
577
+ tenant_name: tenantName,
578
+ country_code: countryCode,
579
+ provider: 'google',
580
+ oauth_token: accessToken
581
+ })
582
+ });
583
+ if (!response.ok) {
584
+ const errorData = await response.json();
585
+ throw new Error(errorData.message || 'Failed to create tenant');
586
+ }
587
+ return response.json();
588
+ }
589
+ async checkEmail(email) {
725
590
  try {
726
- localStorage.setItem(this.ACTIVE_AUTH_SERVER_KEY, serverName);
727
- this.activeAuthServer = serverName;
591
+ const accountsUrl = this.getAccountsUrl();
592
+ const response = await fetch(`${accountsUrl}/api/auth/check-email`, {
593
+ method: 'POST',
594
+ headers: { 'Content-Type': 'application/json' },
595
+ body: JSON.stringify({ email })
596
+ });
597
+ const data = await response.json();
598
+ return { exists: data.exists, user: data.user };
728
599
  }
729
- catch (error) {
730
- console.error('Failed to save active auth server:', error);
600
+ catch {
601
+ return { exists: false };
731
602
  }
732
603
  }
733
- /**
734
- * Get available auth servers
735
- * @returns Array of server names or empty array if using single-server mode
736
- */
737
- getAvailableAuthServers() {
738
- if (!this.environment.authServers) {
739
- return [];
604
+ }
605
+
606
+ /**
607
+ * Configure the ngx-stonescriptphp-client library.
608
+ *
609
+ * @param environment - Library configuration (API server, auth settings, etc.)
610
+ * @param plugin - Optional auth plugin override. Defaults to StoneScriptPHPAuth.
611
+ * Provide your own plugin to use Firebase, progalaxyelabs-auth, Okta, or any other auth backend.
612
+ *
613
+ * @example Default (StoneScriptPHP backend)
614
+ * ```typescript
615
+ * // app.config.ts
616
+ * export const appConfig: ApplicationConfig = {
617
+ * providers: [
618
+ * provideNgxStoneScriptPhpClient(environment)
619
+ * ]
620
+ * };
621
+ * ```
622
+ *
623
+ * @example External auth plugin
624
+ * ```typescript
625
+ * import { ProgalaxyElabsAuth } from './progalaxyelabs-auth.auth-plugin';
626
+ *
627
+ * providers: [
628
+ * provideNgxStoneScriptPhpClient(environment, new ProgalaxyElabsAuth({ host: '...' }))
629
+ * ]
630
+ * ```
631
+ *
632
+ * @example Firebase
633
+ * ```typescript
634
+ * import { FirebaseAuthPlugin } from './firebase-auth.auth-plugin';
635
+ *
636
+ * providers: [
637
+ * provideNgxStoneScriptPhpClient(environment, new FirebaseAuthPlugin(firebaseConfig))
638
+ * ]
639
+ * ```
640
+ */
641
+ function provideNgxStoneScriptPhpClient(environment, plugin) {
642
+ const resolvedPlugin = plugin ?? new StoneScriptPHPAuth({
643
+ // Resolve auth host: auth.host → accountsServer.host (compat) → accountsUrl (compat) → apiServer.host
644
+ host: environment.auth?.host
645
+ || environment.accountsServer?.host
646
+ || environment.accountsUrl
647
+ || environment.apiServer.host,
648
+ platformCode: environment.platformCode,
649
+ authServers: environment.authServers,
650
+ responseMap: environment.auth?.responseMap ?? environment.authResponseMap,
651
+ auth: environment.auth,
652
+ apiUrl: environment.apiUrl
653
+ });
654
+ return makeEnvironmentProviders([
655
+ { provide: MyEnvironmentModel, useValue: environment },
656
+ { provide: AUTH_PLUGIN, useValue: resolvedPlugin }
657
+ ]);
658
+ }
659
+
660
+ class ApiResponse {
661
+ status;
662
+ data;
663
+ message;
664
+ get success() {
665
+ return this.status === 'ok';
666
+ }
667
+ get errors() {
668
+ return this.message ? [this.message] : [];
669
+ }
670
+ constructor(status, data = null, message = '') {
671
+ this.status = status;
672
+ this.data = data || null;
673
+ this.message = message;
674
+ }
675
+ onOk(callback) {
676
+ if (this.status === 'ok') {
677
+ callback(this.data);
740
678
  }
741
- return Object.keys(this.environment.authServers);
679
+ return this;
742
680
  }
743
- /**
744
- * Get current active auth server name
745
- * @returns Server name or null if using single-server mode
746
- */
747
- getActiveAuthServer() {
748
- return this.activeAuthServer;
681
+ onNotOk(callback) {
682
+ if (this.status === 'not ok') {
683
+ callback(this.message, this.data);
684
+ }
685
+ return this;
749
686
  }
750
- /**
751
- * Switch to a different auth server
752
- * @param serverName - Name of the server to switch to
753
- * @throws Error if server not found in configuration
754
- */
755
- switchAuthServer(serverName) {
756
- if (!this.environment.authServers) {
757
- throw new Error('Multi-server mode not configured. Use authServers in environment config.');
687
+ onError(callback) {
688
+ if (this.status === 'error') {
689
+ callback();
758
690
  }
759
- if (!this.environment.authServers[serverName]) {
760
- throw new Error(`Auth server '${serverName}' not found in configuration`);
691
+ return this;
692
+ }
693
+ isSuccess() {
694
+ return this.status === 'ok';
695
+ }
696
+ isError() {
697
+ return this.status === 'error' || this.status === 'not ok';
698
+ }
699
+ getData() {
700
+ return this.data || null;
701
+ }
702
+ getError() {
703
+ return this.message || 'Unknown error';
704
+ }
705
+ getStatus() {
706
+ return this.status;
707
+ }
708
+ getMessage() {
709
+ return this.message;
710
+ }
711
+ }
712
+
713
+ class TokenService {
714
+ accessToken = '';
715
+ refreshToken = '';
716
+ lsAccessTokenKey = 'progalaxyapi_access_token';
717
+ lsRefreshTokenKey = 'progalaxyapi_refresh_token';
718
+ constructor() { }
719
+ setTokens(accessToken, refreshToken) {
720
+ this.accessToken = accessToken;
721
+ this.refreshToken = refreshToken;
722
+ localStorage.setItem(this.lsAccessTokenKey, accessToken);
723
+ localStorage.setItem(this.lsRefreshTokenKey, refreshToken);
724
+ }
725
+ setAccessToken(accessToken) {
726
+ this.accessToken = accessToken;
727
+ localStorage.setItem(this.lsAccessTokenKey, accessToken);
728
+ }
729
+ setRefreshToken(refreshToken) {
730
+ this.refreshToken = refreshToken;
731
+ localStorage.setItem(this.lsRefreshTokenKey, refreshToken);
732
+ }
733
+ getAccessToken() {
734
+ if (this.accessToken) {
735
+ return this.accessToken;
736
+ }
737
+ const storedAccessToken = localStorage.getItem(this.lsAccessTokenKey);
738
+ if (storedAccessToken) {
739
+ return storedAccessToken;
740
+ }
741
+ else {
742
+ return '';
761
743
  }
762
- this.saveActiveAuthServer(serverName);
763
744
  }
764
- /**
765
- * Get auth server configuration
766
- * @param serverName - Optional server name (uses active server if not specified)
767
- */
768
- getAuthServerConfig(serverName) {
769
- if (!this.environment.authServers) {
770
- return null;
745
+ getRefreshToken() {
746
+ if (this.refreshToken) {
747
+ return this.refreshToken;
771
748
  }
772
- const targetServer = serverName || this.activeAuthServer || this.getDefaultAuthServer();
773
- if (!targetServer) {
774
- return null;
749
+ const storedRefreshToken = localStorage.getItem(this.lsRefreshTokenKey);
750
+ if (storedRefreshToken) {
751
+ return storedRefreshToken;
752
+ }
753
+ else {
754
+ return '';
775
755
  }
776
- return this.environment.authServers[targetServer] || null;
777
756
  }
778
- /**
779
- * Check if multi-server mode is enabled
780
- */
781
- isMultiServerMode() {
782
- return !!(this.environment.authServers && Object.keys(this.environment.authServers).length > 0);
757
+ clear() {
758
+ this.accessToken = '';
759
+ this.refreshToken = '';
760
+ localStorage.removeItem(this.lsAccessTokenKey);
761
+ localStorage.removeItem(this.lsRefreshTokenKey);
783
762
  }
784
763
  /**
785
- * Get the platform's own API base URL
786
- * Used for routes that go through the platform API proxy (e.g. register-tenant)
787
- * @throws Error if no API URL is configured
764
+ * Check if there is a non-empty access token.
765
+ * Token is treated as opaque validity is determined by the auth server.
788
766
  */
789
- getPlatformApiUrl() {
790
- if (this.environment.apiUrl) {
791
- return this.environment.apiUrl;
792
- }
793
- if (this.environment.apiServer?.host) {
794
- return this.environment.apiServer.host;
795
- }
796
- throw new Error('No platform API URL configured. Set apiUrl in environment config.');
767
+ hasValidAccessToken() {
768
+ const token = this.getAccessToken();
769
+ return token !== null && token !== '';
770
+ }
771
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TokenService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
772
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TokenService, providedIn: 'root' });
773
+ }
774
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TokenService, decorators: [{
775
+ type: Injectable,
776
+ args: [{
777
+ providedIn: 'root'
778
+ }]
779
+ }], ctorParameters: () => [] });
780
+
781
+ /**
782
+ * @deprecated Use boolean directly. Kept for backward compatibility.
783
+ */
784
+ var VerifyStatus;
785
+ (function (VerifyStatus) {
786
+ VerifyStatus["initialized"] = "initialized";
787
+ VerifyStatus["yes"] = "yes";
788
+ VerifyStatus["no"] = "no";
789
+ })(VerifyStatus || (VerifyStatus = {}));
790
+ class SigninStatusService {
791
+ status;
792
+ constructor() {
793
+ this.status = new BehaviorSubject(false);
794
+ }
795
+ signedOut() {
796
+ this.status.next(false);
797
+ }
798
+ signedIn() {
799
+ this.status.next(true);
797
800
  }
798
801
  /**
799
- * Hash UUID to numeric ID for backward compatibility
800
- * Converts UUID string to a consistent numeric ID for legacy code
802
+ * Set signin status
803
+ * @param isSignedIn - True if user is signed in, false otherwise
801
804
  */
802
- hashUUID(uuid) {
803
- let hash = 0;
804
- for (let i = 0; i < uuid.length; i++) {
805
- const char = uuid.charCodeAt(i);
806
- hash = ((hash << 5) - hash) + char;
807
- hash = hash & hash; // Convert to 32bit integer
808
- }
809
- return Math.abs(hash);
805
+ setSigninStatus(isSignedIn) {
806
+ this.status.next(isSignedIn);
807
+ }
808
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: SigninStatusService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
809
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: SigninStatusService, providedIn: 'root' });
810
+ }
811
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: SigninStatusService, decorators: [{
812
+ type: Injectable,
813
+ args: [{
814
+ providedIn: 'root'
815
+ }]
816
+ }], ctorParameters: () => [] });
817
+
818
+ /**
819
+ * AuthService — manages auth state and delegates all auth operations to the AuthPlugin.
820
+ *
821
+ * This service holds user state (via BehaviorSubject) and tokens (via TokenService).
822
+ * It does not make any HTTP calls directly — all auth logic lives in the plugin.
823
+ *
824
+ * Provide a plugin via provideNgxStoneScriptPhpClient():
825
+ * - Default: StoneScriptPHPAuth (built-in, matches StoneScriptPHP backend)
826
+ * - External: any class implementing AuthPlugin (Firebase, progalaxyelabs-auth, Okta, etc.)
827
+ */
828
+ class AuthService {
829
+ plugin;
830
+ tokens;
831
+ signinStatus;
832
+ USER_STORAGE_KEY = 'progalaxyapi_user';
833
+ userSubject = new BehaviorSubject(null);
834
+ user$ = this.userSubject.asObservable();
835
+ constructor(plugin, tokens, signinStatus) {
836
+ this.plugin = plugin;
837
+ this.tokens = tokens;
838
+ this.signinStatus = signinStatus;
839
+ this.restoreUser();
810
840
  }
811
- /**
812
- * Restore user from localStorage
813
- */
841
+ // ── State management ──────────────────────────────────────────────────────
814
842
  restoreUser() {
815
843
  try {
816
844
  const userJson = localStorage.getItem(this.USER_STORAGE_KEY);
817
- if (userJson) {
818
- const user = JSON.parse(userJson);
819
- this.updateUser(user);
820
- }
845
+ if (userJson)
846
+ this.updateUser(JSON.parse(userJson));
821
847
  }
822
848
  catch (error) {
823
849
  console.error('Failed to restore user from localStorage:', error);
824
850
  }
825
851
  }
826
- /**
827
- * Save user to localStorage
828
- */
829
852
  saveUser(user) {
830
853
  try {
831
854
  if (user) {
@@ -839,720 +862,431 @@ class AuthService {
839
862
  console.error('Failed to save user to localStorage:', error);
840
863
  }
841
864
  }
842
- /**
843
- * Update user subject and persist to localStorage
844
- */
845
865
  updateUser(user) {
846
866
  this.userSubject.next(user);
847
867
  this.saveUser(user);
848
868
  }
849
- /**
850
- * Login with email and password
851
- * @param email - User email
852
- * @param password - User password
853
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
854
- */
855
- async loginWithEmail(email, password, serverName) {
856
- try {
857
- const accountsUrl = this.getAccountsUrl(serverName);
858
- const response = await fetch(`${accountsUrl}/api/auth/login`, {
859
- method: 'POST',
860
- headers: { 'Content-Type': 'application/json' },
861
- credentials: 'include', // Include cookies for refresh token
862
- body: JSON.stringify({
863
- email,
864
- password,
865
- platform: this.environment.platformCode
866
- })
867
- });
868
- const data = await response.json();
869
- if (data.success && data.access_token) {
870
- this.tokens.setAccessToken(data.access_token);
871
- this.signinStatus.setSigninStatus(true);
872
- // Normalize user object to handle both response formats
873
- const normalizedUser = {
874
- user_id: data.user.user_id ?? (data.user.id ? this.hashUUID(data.user.id) : 0),
875
- id: data.user.id ?? String(data.user.user_id),
876
- email: data.user.email,
877
- display_name: data.user.display_name ?? data.user.email.split('@')[0],
878
- photo_url: data.user.photo_url,
879
- is_email_verified: data.user.is_email_verified ?? false
880
- };
881
- this.updateUser(normalizedUser);
882
- return { success: true, user: normalizedUser };
883
- }
884
- return {
885
- success: false,
886
- message: data.message || 'Invalid credentials'
887
- };
888
- }
889
- catch (error) {
890
- return {
891
- success: false,
892
- message: 'Network error. Please try again.'
893
- };
894
- }
869
+ storeAuthResult(result) {
870
+ if (result.accessToken)
871
+ this.tokens.setAccessToken(result.accessToken);
872
+ if (result.refreshToken)
873
+ this.tokens.setRefreshToken(result.refreshToken);
874
+ if (result.user)
875
+ this.updateUser(result.user);
876
+ this.signinStatus.setSigninStatus(true);
877
+ }
878
+ // ── Core auth operations ──────────────────────────────────────────────────
879
+ async loginWithEmail(email, password) {
880
+ const result = await this.plugin.login(email, password);
881
+ if (result.success)
882
+ this.storeAuthResult(result);
883
+ return result;
895
884
  }
896
- /**
897
- * Login with Google OAuth (popup window)
898
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
899
- */
900
885
  async loginWithGoogle(serverName) {
901
- return this.loginWithOAuth('google', serverName);
886
+ return this.loginWithProvider('google');
902
887
  }
903
- /**
904
- * Login with GitHub OAuth (popup window)
905
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
906
- */
907
888
  async loginWithGitHub(serverName) {
908
- return this.loginWithOAuth('github', serverName);
889
+ return this.loginWithProvider('github');
909
890
  }
910
- /**
911
- * Login with LinkedIn OAuth (popup window)
912
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
913
- */
914
891
  async loginWithLinkedIn(serverName) {
915
- return this.loginWithOAuth('linkedin', serverName);
892
+ return this.loginWithProvider('linkedin');
916
893
  }
917
- /**
918
- * Login with Apple OAuth (popup window)
919
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
920
- */
921
894
  async loginWithApple(serverName) {
922
- return this.loginWithOAuth('apple', serverName);
895
+ return this.loginWithProvider('apple');
923
896
  }
924
- /**
925
- * Login with Microsoft OAuth (popup window)
926
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
927
- */
928
897
  async loginWithMicrosoft(serverName) {
929
- return this.loginWithOAuth('microsoft', serverName);
898
+ return this.loginWithProvider('microsoft');
930
899
  }
931
- /**
932
- * Login with Zoho OAuth (popup window)
933
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
934
- */
935
900
  async loginWithZoho(serverName) {
936
- return this.loginWithOAuth('zoho', serverName);
901
+ return this.loginWithProvider('zoho');
937
902
  }
938
- /**
939
- * Generic provider-based login (supports all OAuth providers)
940
- * @param provider - The provider identifier
941
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
942
- */
943
903
  async loginWithProvider(provider, serverName) {
944
904
  if (provider === 'emailPassword') {
945
905
  throw new Error('Use loginWithEmail() for email/password authentication');
946
906
  }
947
- return this.loginWithOAuth(provider, serverName);
948
- }
949
- /**
950
- * Generic OAuth login handler
951
- * Opens popup window and listens for postMessage
952
- * @param provider - OAuth provider name
953
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
954
- */
955
- async loginWithOAuth(provider, serverName) {
956
- return new Promise((resolve) => {
957
- const width = 500;
958
- const height = 600;
959
- const left = (window.screen.width - width) / 2;
960
- const top = (window.screen.height - height) / 2;
961
- const accountsUrl = this.getAccountsUrl(serverName);
962
- const oauthUrl = `${accountsUrl}/oauth/${provider}?` +
963
- `platform=${this.environment.platformCode}&` +
964
- `mode=popup`;
965
- const popup = window.open(oauthUrl, `${provider}_login`, `width=${width},height=${height},left=${left},top=${top}`);
966
- if (!popup) {
967
- resolve({
968
- success: false,
969
- message: 'Popup blocked. Please allow popups for this site.'
970
- });
971
- return;
972
- }
973
- // Listen for message from popup
974
- const messageHandler = (event) => {
975
- // Verify origin
976
- if (event.origin !== new URL(accountsUrl).origin) {
977
- return;
978
- }
979
- if (event.data.type === 'oauth_success') {
980
- this.tokens.setAccessToken(event.data.access_token);
981
- this.signinStatus.setSigninStatus(true);
982
- // Normalize user object to handle both response formats
983
- const normalizedUser = {
984
- user_id: event.data.user.user_id ?? (event.data.user.id ? this.hashUUID(event.data.user.id) : 0),
985
- id: event.data.user.id ?? String(event.data.user.user_id),
986
- email: event.data.user.email,
987
- display_name: event.data.user.display_name ?? event.data.user.email.split('@')[0],
988
- photo_url: event.data.user.photo_url,
989
- is_email_verified: event.data.user.is_email_verified ?? false
990
- };
991
- this.updateUser(normalizedUser);
992
- window.removeEventListener('message', messageHandler);
993
- popup.close();
994
- resolve({
995
- success: true,
996
- user: normalizedUser
997
- });
998
- }
999
- else if (event.data.type === 'oauth_error') {
1000
- window.removeEventListener('message', messageHandler);
1001
- popup.close();
1002
- resolve({
1003
- success: false,
1004
- message: event.data.message || 'OAuth login failed'
1005
- });
1006
- }
1007
- };
1008
- window.addEventListener('message', messageHandler);
1009
- // Check if popup was closed manually
1010
- const checkClosed = setInterval(() => {
1011
- if (popup.closed) {
1012
- clearInterval(checkClosed);
1013
- window.removeEventListener('message', messageHandler);
1014
- resolve({
1015
- success: false,
1016
- message: 'Login cancelled'
1017
- });
1018
- }
1019
- }, 500);
1020
- });
907
+ if (!this.plugin.loginWithProvider) {
908
+ return { success: false, message: 'OAuth not supported by the configured auth plugin' };
909
+ }
910
+ const result = await this.plugin.loginWithProvider(provider);
911
+ if (result.success)
912
+ this.storeAuthResult(result);
913
+ return result;
1021
914
  }
1022
- /**
1023
- * Register new user
1024
- * @param email - User email
1025
- * @param password - User password
1026
- * @param displayName - Display name
1027
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
1028
- */
1029
915
  async register(email, password, displayName, serverName) {
1030
- try {
1031
- const accountsUrl = this.getAccountsUrl(serverName);
1032
- const response = await fetch(`${accountsUrl}/api/auth/register`, {
1033
- method: 'POST',
1034
- headers: { 'Content-Type': 'application/json' },
1035
- credentials: 'include',
1036
- body: JSON.stringify({
1037
- email,
1038
- password,
1039
- display_name: displayName,
1040
- platform: this.environment.platformCode
1041
- })
1042
- });
1043
- const data = await response.json();
1044
- if (data.success && data.access_token) {
1045
- this.tokens.setAccessToken(data.access_token);
1046
- this.signinStatus.setSigninStatus(true);
1047
- // Normalize user object to handle both response formats
1048
- const normalizedUser = {
1049
- user_id: data.user.user_id ?? (data.user.id ? this.hashUUID(data.user.id) : 0),
1050
- id: data.user.id ?? String(data.user.user_id),
1051
- email: data.user.email,
1052
- display_name: data.user.display_name ?? data.user.email.split('@')[0],
1053
- photo_url: data.user.photo_url,
1054
- is_email_verified: data.user.is_email_verified ?? false
1055
- };
1056
- this.updateUser(normalizedUser);
1057
- return {
1058
- success: true,
1059
- user: normalizedUser,
1060
- message: data.needs_verification ? 'Please verify your email' : undefined
1061
- };
1062
- }
1063
- return {
1064
- success: false,
1065
- message: data.message || 'Registration failed'
1066
- };
1067
- }
1068
- catch (error) {
1069
- return {
1070
- success: false,
1071
- message: 'Network error. Please try again.'
1072
- };
1073
- }
916
+ const result = await this.plugin.register(email, password, displayName);
917
+ if (result.success)
918
+ this.storeAuthResult(result);
919
+ return result;
1074
920
  }
1075
- /**
1076
- * Sign out user
1077
- * @param serverName - Optional: Specify which auth server to logout from (for multi-server mode)
1078
- */
1079
921
  async signout(serverName) {
1080
- try {
1081
- const refreshToken = this.tokens.getRefreshToken();
1082
- if (refreshToken) {
1083
- const accountsUrl = this.getAccountsUrl(serverName);
1084
- await fetch(`${accountsUrl}/api/auth/logout`, {
1085
- method: 'POST',
1086
- headers: {
1087
- 'Content-Type': 'application/json'
1088
- },
1089
- credentials: 'include',
1090
- body: JSON.stringify({
1091
- refresh_token: refreshToken
1092
- })
1093
- });
1094
- }
1095
- }
1096
- catch (error) {
1097
- console.error('Logout API call failed:', error);
1098
- }
1099
- finally {
1100
- this.tokens.clear();
1101
- this.signinStatus.setSigninStatus(false);
1102
- this.updateUser(null);
1103
- }
922
+ const refreshToken = this.tokens.getRefreshToken() || undefined;
923
+ await this.plugin.logout(refreshToken);
924
+ this.tokens.clear();
925
+ this.signinStatus.setSigninStatus(false);
926
+ this.updateUser(null);
1104
927
  }
1105
- /**
1106
- * Check for active session (call on app init)
1107
- * @param serverName - Optional: Specify which auth server to check (for multi-server mode)
1108
- */
1109
928
  async checkSession(serverName) {
1110
929
  if (this.tokens.hasValidAccessToken()) {
1111
930
  this.signinStatus.setSigninStatus(true);
1112
931
  return true;
1113
932
  }
1114
- // Try to refresh using httpOnly cookie
1115
- try {
1116
- const accountsUrl = this.getAccountsUrl(serverName);
1117
- const response = await fetch(`${accountsUrl}/api/auth/refresh`, {
1118
- method: 'POST',
1119
- credentials: 'include'
1120
- });
1121
- if (!response.ok) {
1122
- this.signinStatus.setSigninStatus(false);
1123
- return false;
1124
- }
1125
- const data = await response.json();
1126
- if (data.access_token) {
1127
- this.tokens.setAccessToken(data.access_token);
1128
- // Normalize user object to handle both response formats
1129
- if (data.user) {
1130
- const normalizedUser = {
1131
- user_id: data.user.user_id ?? (data.user.id ? this.hashUUID(data.user.id) : 0),
1132
- id: data.user.id ?? String(data.user.user_id),
1133
- email: data.user.email,
1134
- display_name: data.user.display_name ?? data.user.email.split('@')[0],
1135
- photo_url: data.user.photo_url,
1136
- is_email_verified: data.user.is_email_verified ?? false
1137
- };
1138
- this.updateUser(normalizedUser);
1139
- }
1140
- this.signinStatus.setSigninStatus(true);
1141
- return true;
1142
- }
1143
- return false;
1144
- }
1145
- catch (error) {
1146
- this.signinStatus.setSigninStatus(false);
1147
- return false;
933
+ const result = await this.plugin.checkSession();
934
+ if (result.success && result.accessToken) {
935
+ this.tokens.setAccessToken(result.accessToken);
936
+ if (result.user)
937
+ this.updateUser(result.user);
938
+ this.signinStatus.setSigninStatus(true);
939
+ return true;
1148
940
  }
941
+ this.tokens.clear();
942
+ this.updateUser(null);
943
+ this.signinStatus.setSigninStatus(false);
944
+ return false;
1149
945
  }
1150
946
  /**
1151
- * Check if user is authenticated
1152
- */
1153
- isAuthenticated() {
1154
- return this.tokens.hasValidAccessToken();
1155
- }
1156
- /**
1157
- * Get current user (synchronous)
1158
- */
1159
- getCurrentUser() {
1160
- return this.userSubject.value;
1161
- }
1162
- // ===== Multi-Tenant Methods =====
1163
- /**
1164
- * Register a new user AND create a new tenant (organization)
1165
- * This is used when a user wants to create their own organization
947
+ * Refresh the access token. Called by ApiConnectionService on 401.
948
+ * @returns true if token was refreshed, false if refresh failed (user is signed out)
1166
949
  */
1167
- async registerTenant(data) {
1168
- try {
1169
- // If using OAuth, initiate OAuth flow first
1170
- if (data.provider !== 'emailPassword') {
1171
- return await this.registerTenantWithOAuth(data.tenantName, data.tenantSlug, data.provider);
1172
- }
1173
- // Email/password registration — route through platform API proxy
1174
- const apiUrl = this.getPlatformApiUrl();
1175
- const response = await fetch(`${apiUrl}/auth/register-tenant`, {
1176
- method: 'POST',
1177
- headers: { 'Content-Type': 'application/json' },
1178
- credentials: 'include',
1179
- body: JSON.stringify({
1180
- platform: this.environment.platformCode,
1181
- tenant_name: data.tenantName,
1182
- tenant_slug: data.tenantSlug,
1183
- display_name: data.displayName,
1184
- email: data.email,
1185
- password: data.password,
1186
- provider: 'emailPassword'
1187
- })
1188
- });
1189
- const result = await response.json();
1190
- if (result.success && result.access_token) {
1191
- this.tokens.setAccessToken(result.access_token);
1192
- this.signinStatus.setSigninStatus(true);
1193
- if (result.user) {
1194
- // Normalize user object to handle both response formats
1195
- const normalizedUser = {
1196
- user_id: result.user.user_id ?? (result.user.id ? this.hashUUID(result.user.id) : 0),
1197
- id: result.user.id ?? String(result.user.user_id),
1198
- email: result.user.email,
1199
- display_name: result.user.display_name ?? result.user.email.split('@')[0],
1200
- photo_url: result.user.photo_url,
1201
- is_email_verified: result.user.is_email_verified ?? false
1202
- };
1203
- this.updateUser(normalizedUser);
1204
- }
1205
- }
1206
- return result;
1207
- }
1208
- catch (error) {
1209
- return {
1210
- success: false,
1211
- message: 'Network error. Please try again.'
1212
- };
950
+ async refresh() {
951
+ const newToken = await this.plugin.refresh(this.tokens.getAccessToken(), this.tokens.getRefreshToken() || undefined);
952
+ if (newToken) {
953
+ this.tokens.setAccessToken(newToken);
954
+ return true;
1213
955
  }
956
+ this.tokens.clear();
957
+ this.updateUser(null);
958
+ this.signinStatus.signedOut();
959
+ return false;
1214
960
  }
1215
- /**
1216
- * Register tenant with OAuth provider
1217
- * Opens popup window for OAuth flow
1218
- */
1219
- async registerTenantWithOAuth(tenantName, tenantSlug, provider) {
1220
- return new Promise((resolve) => {
1221
- const width = 500;
1222
- const height = 600;
1223
- const left = (window.screen.width - width) / 2;
1224
- const top = (window.screen.height - height) / 2;
1225
- // Build OAuth URL with tenant registration params
1226
- const accountsUrl = this.getAccountsUrl();
1227
- const oauthUrl = `${accountsUrl}/oauth/${provider}?` +
1228
- `platform=${this.environment.platformCode}&` +
1229
- `mode=popup&` +
1230
- `action=register_tenant&` +
1231
- `tenant_name=${encodeURIComponent(tenantName)}&` +
1232
- `tenant_slug=${encodeURIComponent(tenantSlug)}`;
1233
- const popup = window.open(oauthUrl, `${provider}_register_tenant`, `width=${width},height=${height},left=${left},top=${top}`);
1234
- if (!popup) {
1235
- resolve({
1236
- success: false,
1237
- message: 'Popup blocked. Please allow popups for this site.'
1238
- });
1239
- return;
1240
- }
1241
- // Listen for message from popup
1242
- const messageHandler = (event) => {
1243
- // Verify origin
1244
- if (event.origin !== new URL(accountsUrl).origin) {
1245
- return;
1246
- }
1247
- if (event.data.type === 'tenant_register_success') {
1248
- // Set tokens and user
1249
- if (event.data.access_token) {
1250
- this.tokens.setAccessToken(event.data.access_token);
1251
- this.signinStatus.setSigninStatus(true);
1252
- }
1253
- if (event.data.user) {
1254
- // Normalize user object to handle both response formats
1255
- const normalizedUser = {
1256
- user_id: event.data.user.user_id ?? (event.data.user.id ? this.hashUUID(event.data.user.id) : 0),
1257
- id: event.data.user.id ?? String(event.data.user.user_id),
1258
- email: event.data.user.email,
1259
- display_name: event.data.user.display_name ?? event.data.user.email.split('@')[0],
1260
- photo_url: event.data.user.photo_url,
1261
- is_email_verified: event.data.user.is_email_verified ?? false
1262
- };
1263
- this.updateUser(normalizedUser);
1264
- }
1265
- window.removeEventListener('message', messageHandler);
1266
- popup.close();
1267
- resolve({
1268
- success: true,
1269
- tenant: event.data.tenant,
1270
- user: event.data.user
1271
- });
1272
- }
1273
- else if (event.data.type === 'tenant_register_error') {
1274
- window.removeEventListener('message', messageHandler);
1275
- popup.close();
1276
- resolve({
1277
- success: false,
1278
- message: event.data.message || 'Tenant registration failed'
1279
- });
1280
- }
1281
- };
1282
- window.addEventListener('message', messageHandler);
1283
- // Check if popup was closed manually
1284
- const checkClosed = setInterval(() => {
1285
- if (popup.closed) {
1286
- clearInterval(checkClosed);
1287
- window.removeEventListener('message', messageHandler);
1288
- resolve({
1289
- success: false,
1290
- message: 'Registration cancelled'
1291
- });
1292
- }
1293
- }, 500);
1294
- });
1295
- }
1296
- /**
1297
- * Get all tenant memberships for the authenticated user
1298
- * @param serverName - Optional: Specify which auth server to query (for multi-server mode)
1299
- */
1300
- async getTenantMemberships(serverName) {
1301
- try {
1302
- const accountsUrl = this.getAccountsUrl(serverName);
1303
- const response = await fetch(`${accountsUrl}/api/auth/memberships`, {
1304
- method: 'GET',
1305
- headers: {
1306
- 'Authorization': `Bearer ${this.tokens.getAccessToken()}`,
1307
- 'Content-Type': 'application/json'
1308
- },
1309
- credentials: 'include'
1310
- });
1311
- const data = await response.json();
1312
- return {
1313
- memberships: data.memberships || []
1314
- };
1315
- }
1316
- catch (error) {
1317
- return { memberships: [] };
1318
- }
961
+ isAuthenticated() {
962
+ return this.tokens.hasValidAccessToken();
1319
963
  }
1320
- /**
1321
- * Select a tenant for the current session
1322
- * Updates the JWT token with tenant context
1323
- * @param tenantId - Tenant ID to select
1324
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
1325
- */
1326
- async selectTenant(tenantId, serverName) {
1327
- try {
1328
- const accountsUrl = this.getAccountsUrl(serverName);
1329
- const response = await fetch(`${accountsUrl}/api/auth/select-tenant`, {
1330
- method: 'POST',
1331
- headers: {
1332
- 'Authorization': `Bearer ${this.tokens.getAccessToken()}`,
1333
- 'Content-Type': 'application/json'
1334
- },
1335
- credentials: 'include',
1336
- body: JSON.stringify({ tenant_id: tenantId })
1337
- });
1338
- const data = await response.json();
1339
- if (data.success && data.access_token) {
1340
- this.tokens.setAccessToken(data.access_token);
1341
- return {
1342
- success: true,
1343
- access_token: data.access_token
1344
- };
1345
- }
1346
- return {
1347
- success: false,
1348
- message: data.message || 'Failed to select tenant'
1349
- };
964
+ getCurrentUser() {
965
+ return this.userSubject.value;
966
+ }
967
+ // ── Multi-tenant operations ───────────────────────────────────────────────
968
+ async registerTenant(data) {
969
+ if (!this.plugin.registerTenant) {
970
+ return { success: false, message: 'registerTenant not supported by the configured auth plugin' };
1350
971
  }
1351
- catch (error) {
1352
- return {
1353
- success: false,
1354
- message: 'Network error. Please try again.'
1355
- };
972
+ const accessToken = this.tokens.getAccessToken() || undefined;
973
+ const result = await this.plugin.registerTenant(data, accessToken);
974
+ if (result?.access_token) {
975
+ this.tokens.setAccessToken(result.access_token);
976
+ this.signinStatus.setSigninStatus(true);
977
+ }
978
+ if (result?.refresh_token) {
979
+ this.tokens.setRefreshToken(result.refresh_token);
1356
980
  }
981
+ return result;
1357
982
  }
1358
- /**
1359
- * Check if a tenant slug is available
1360
- * @param slug - Tenant slug to check
1361
- * @param serverName - Optional: Specify which auth server to query (for multi-server mode)
1362
- */
1363
- async checkTenantSlugAvailable(slug, serverName) {
1364
- try {
1365
- const accountsUrl = this.getAccountsUrl(serverName);
1366
- const response = await fetch(`${accountsUrl}/api/auth/check-tenant-slug/${slug}`, {
1367
- method: 'GET',
1368
- headers: { 'Content-Type': 'application/json' }
1369
- });
1370
- const data = await response.json();
1371
- return {
1372
- available: data.available || false,
1373
- suggestion: data.suggestion
1374
- };
983
+ async getTenantMemberships(serverName) {
984
+ if (!this.plugin.getTenantMemberships)
985
+ return { memberships: [] };
986
+ const memberships = await this.plugin.getTenantMemberships(this.tokens.getAccessToken());
987
+ return { memberships };
988
+ }
989
+ async selectTenant(tenantId, serverName) {
990
+ if (!this.plugin.selectTenant) {
991
+ return { success: false, message: 'selectTenant not supported by the configured auth plugin' };
1375
992
  }
1376
- catch (error) {
1377
- // On error, assume available (don't block registration)
993
+ const result = await this.plugin.selectTenant(tenantId, this.tokens.getAccessToken());
994
+ if (result.success && result.accessToken) {
995
+ this.tokens.setAccessToken(result.accessToken);
996
+ }
997
+ return { success: result.success, message: result.message, access_token: result.accessToken };
998
+ }
999
+ async checkTenantSlugAvailable(slug, serverName) {
1000
+ if (!this.plugin.checkTenantSlugAvailable)
1378
1001
  return { available: true };
1002
+ return this.plugin.checkTenantSlugAvailable(slug);
1003
+ }
1004
+ async checkOnboardingStatus(identityId, serverName) {
1005
+ if (!this.plugin.checkOnboardingStatus)
1006
+ throw new Error('checkOnboardingStatus not supported');
1007
+ return this.plugin.checkOnboardingStatus(identityId);
1008
+ }
1009
+ async completeTenantOnboarding(countryCode, tenantName, serverName) {
1010
+ if (!this.plugin.completeTenantOnboarding)
1011
+ throw new Error('completeTenantOnboarding not supported');
1012
+ const result = await this.plugin.completeTenantOnboarding(countryCode, tenantName, this.tokens.getAccessToken());
1013
+ if (result?.access_token) {
1014
+ this.tokens.setAccessToken(result.access_token);
1015
+ this.signinStatus.setSigninStatus(true);
1379
1016
  }
1017
+ return result;
1380
1018
  }
1381
- // ===== Backward Compatibility Methods =====
1382
- // These methods are deprecated and maintained for backward compatibility
1383
- // with existing platform code. New code should use the methods above.
1384
- /**
1385
- * @deprecated Use getCurrentUser()?.user_id instead
1386
- */
1387
- getUserId() {
1388
- return this.userSubject.value?.user_id || 0;
1019
+ // ── Multi-server (delegated to plugin) ────────────────────────────────────
1020
+ getAvailableAuthServers() {
1021
+ return this.plugin.getAvailableServers?.() ?? [];
1389
1022
  }
1390
- /**
1391
- * @deprecated Use getCurrentUser()?.display_name instead
1392
- */
1393
- getUserName() {
1394
- return this.userSubject.value?.display_name || '';
1023
+ getActiveAuthServer() {
1024
+ return this.plugin.getActiveServer?.() ?? null;
1395
1025
  }
1396
- /**
1397
- * @deprecated Use getCurrentUser()?.photo_url instead
1398
- */
1399
- getPhotoUrl() {
1400
- return this.userSubject.value?.photo_url || '';
1026
+ switchAuthServer(serverName) {
1027
+ if (!this.plugin.switchServer) {
1028
+ throw new Error('Multi-server mode not supported by the configured auth plugin');
1029
+ }
1030
+ this.plugin.switchServer(serverName);
1401
1031
  }
1402
- /**
1403
- * @deprecated Use getCurrentUser()?.display_name instead
1404
- */
1405
- getDisplayName() {
1406
- return this.userSubject.value?.display_name || '';
1032
+ getAuthServerConfig(serverName) {
1033
+ return this.plugin.getServerConfig?.(serverName) ?? null;
1407
1034
  }
1408
- /**
1409
- * @deprecated Use `/profile/${getCurrentUser()?.user_id}` instead
1410
- */
1035
+ isMultiServerMode() {
1036
+ return (this.plugin.getAvailableServers?.() ?? []).length > 0;
1037
+ }
1038
+ // ── Backward compatibility ────────────────────────────────────────────────
1039
+ /** @deprecated Use getCurrentUser()?.user_id instead */
1040
+ getUserId() { return this.userSubject.value?.user_id || 0; }
1041
+ /** @deprecated Use getCurrentUser()?.display_name instead */
1042
+ getUserName() { return this.userSubject.value?.display_name || ''; }
1043
+ /** @deprecated Use getCurrentUser()?.photo_url instead */
1044
+ getPhotoUrl() { return this.userSubject.value?.photo_url || ''; }
1045
+ /** @deprecated Use getCurrentUser()?.display_name instead */
1046
+ getDisplayName() { return this.userSubject.value?.display_name || ''; }
1047
+ /** @deprecated Use `/profile/${getCurrentUser()?.user_id}` instead */
1411
1048
  getProfileUrl() {
1412
1049
  const userId = this.userSubject.value?.user_id;
1413
1050
  return userId ? `/profile/${userId}` : '';
1414
1051
  }
1415
- /**
1416
- * @deprecated Use isAuthenticated() instead
1417
- */
1418
- async signin() {
1419
- return this.isAuthenticated();
1420
- }
1421
- /**
1422
- * @deprecated Use loginWithEmail() instead
1423
- */
1052
+ /** @deprecated Use isAuthenticated() instead */
1053
+ async signin() { return this.isAuthenticated(); }
1054
+ /** @deprecated Use loginWithEmail() instead */
1424
1055
  async verifyCredentials(email, password) {
1425
1056
  const result = await this.loginWithEmail(email, password);
1426
1057
  return result.success;
1427
1058
  }
1428
- /**
1429
- * @deprecated Check user.is_email_verified from getCurrentUser() instead
1430
- */
1431
- isSigninEmailValid() {
1432
- return this.userSubject.value?.is_email_verified || false;
1433
- }
1434
- /**
1435
- * @deprecated No longer needed - dialog is managed by platform
1436
- */
1437
- onDialogClose() {
1438
- // No-op for backward compatibility
1059
+ /** @deprecated Check user.is_email_verified from getCurrentUser() instead */
1060
+ isSigninEmailValid() { return this.userSubject.value?.is_email_verified || false; }
1061
+ /** @deprecated No longer needed */
1062
+ onDialogClose() { }
1063
+ /** @deprecated No longer needed */
1064
+ closeSocialAuthDialog() { }
1065
+ /** @deprecated Use checkEmail() from the plugin directly */
1066
+ async getUserProfile(email, serverName) {
1067
+ if (!this.plugin.checkEmail)
1068
+ return null;
1069
+ const result = await this.plugin.checkEmail(email);
1070
+ return result.exists ? result.user : null;
1439
1071
  }
1440
- /**
1441
- * @deprecated No longer needed - dialog is managed by platform
1442
- */
1443
- closeSocialAuthDialog() {
1444
- // No-op for backward compatibility
1072
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: AuthService, deps: [{ token: AUTH_PLUGIN }, { token: TokenService }, { token: SigninStatusService }], target: i0.ɵɵFactoryTarget.Injectable });
1073
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: AuthService, providedIn: 'root' });
1074
+ }
1075
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: AuthService, decorators: [{
1076
+ type: Injectable,
1077
+ args: [{
1078
+ providedIn: 'root'
1079
+ }]
1080
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
1081
+ type: Inject,
1082
+ args: [AUTH_PLUGIN]
1083
+ }] }, { type: TokenService }, { type: SigninStatusService }] });
1084
+
1085
+ class ApiConnectionService {
1086
+ tokens;
1087
+ signinStatus;
1088
+ environment;
1089
+ authService;
1090
+ host = ''; // base URL without trailing slash
1091
+ constructor(tokens, signinStatus, environment, authService) {
1092
+ this.tokens = tokens;
1093
+ this.signinStatus = signinStatus;
1094
+ this.environment = environment;
1095
+ this.authService = authService;
1096
+ this.host = environment.apiServer.host;
1445
1097
  }
1446
- /**
1447
- * @deprecated Check if user exists by calling /api/auth/check-email endpoint
1448
- */
1449
- async getUserProfile(email, serverName) {
1098
+ async request(url, options, data) {
1450
1099
  try {
1451
- const accountsUrl = this.getAccountsUrl(serverName);
1452
- const response = await fetch(`${accountsUrl}/api/auth/check-email`, {
1453
- method: 'POST',
1454
- headers: { 'Content-Type': 'application/json' },
1455
- body: JSON.stringify({ email })
1456
- });
1457
- const data = await response.json();
1458
- return data.exists ? data.user : null;
1100
+ if (data !== null) {
1101
+ const body = JSON.stringify(data);
1102
+ options.body = body || {};
1103
+ }
1104
+ const accessTokenIncluded = this.includeAccessToken(options);
1105
+ let response = await fetch(url, options);
1106
+ if (response.status === 401 && accessTokenIncluded) {
1107
+ response = await this.refreshAndRetry(url, options, response);
1108
+ }
1109
+ if (response.ok) {
1110
+ const json = await response.json();
1111
+ return new ApiResponse(json.status, json.data, json.message);
1112
+ }
1113
+ if (response.status === 401) {
1114
+ this.signinStatus.signedOut();
1115
+ }
1116
+ return this.handleError(response);
1459
1117
  }
1460
1118
  catch (error) {
1461
- return null;
1119
+ return this.handleError(error);
1462
1120
  }
1463
1121
  }
1122
+ handleError(error) {
1123
+ console.error(`Backend returned code ${error.status}, full error: `, error);
1124
+ return new ApiResponse('error');
1125
+ }
1126
+ async get(endpoint, queryParamsObj) {
1127
+ const url = this.host + endpoint + this.buildQueryString(queryParamsObj);
1128
+ const fetchOptions = { mode: 'cors', redirect: 'error' };
1129
+ return this.request(url, fetchOptions, null);
1130
+ }
1131
+ async post(pathWithQueryParams, data) {
1132
+ const url = this.host + pathWithQueryParams;
1133
+ const fetchOptions = {
1134
+ method: 'POST',
1135
+ mode: 'cors',
1136
+ redirect: 'error',
1137
+ headers: { 'Content-Type': 'application/json' },
1138
+ body: JSON.stringify(data)
1139
+ };
1140
+ return this.request(url, fetchOptions, data);
1141
+ }
1142
+ async put(pathWithQueryParams, data) {
1143
+ const url = this.host + pathWithQueryParams;
1144
+ const fetchOptions = {
1145
+ method: 'PUT',
1146
+ mode: 'cors',
1147
+ redirect: 'error',
1148
+ headers: { 'Content-Type': 'application/json' },
1149
+ body: JSON.stringify(data)
1150
+ };
1151
+ return this.request(url, fetchOptions, data);
1152
+ }
1153
+ async patch(pathWithQueryParams, data) {
1154
+ const url = this.host + pathWithQueryParams;
1155
+ const fetchOptions = {
1156
+ method: 'PATCH',
1157
+ mode: 'cors',
1158
+ redirect: 'error',
1159
+ headers: { 'Content-Type': 'application/json' },
1160
+ body: JSON.stringify(data)
1161
+ };
1162
+ return this.request(url, fetchOptions, data);
1163
+ }
1164
+ async delete(endpoint, queryParamsObj) {
1165
+ const url = this.host + endpoint + this.buildQueryString(queryParamsObj);
1166
+ const fetchOptions = { method: 'DELETE', mode: 'cors', redirect: 'error' };
1167
+ return this.request(url, fetchOptions, null);
1168
+ }
1169
+ includeAccessToken(options) {
1170
+ const accessToken = this.tokens.getAccessToken();
1171
+ if (!accessToken)
1172
+ return false;
1173
+ if (!options.headers)
1174
+ options.headers = {};
1175
+ options.headers['Authorization'] = 'Bearer ' + accessToken;
1176
+ return true;
1177
+ }
1178
+ async refreshAndRetry(url, fetchOptions, response) {
1179
+ const refreshed = await this.authService.refresh();
1180
+ if (!refreshed)
1181
+ return response;
1182
+ fetchOptions.headers['Authorization'] = 'Bearer ' + this.tokens.getAccessToken();
1183
+ return fetch(url, fetchOptions);
1184
+ }
1464
1185
  /**
1465
- * Check if user has completed onboarding (has a tenant)
1466
- * @param identityId - User identity ID
1467
- * @param serverName - Optional: Specify which auth server to query (for multi-server mode)
1186
+ * Refresh the access token (delegates to AuthService AuthPlugin).
1187
+ * Kept public for backward compatibility.
1468
1188
  */
1469
- async checkOnboardingStatus(identityId, serverName) {
1470
- try {
1471
- const accountsUrl = this.getAccountsUrl(serverName);
1472
- const response = await fetch(`${accountsUrl}/api/auth/onboarding/status?platform_code=${this.environment.platformCode}&identity_id=${identityId}`, {
1473
- method: 'GET',
1474
- headers: { 'Content-Type': 'application/json' },
1475
- credentials: 'include'
1476
- });
1477
- if (!response.ok) {
1478
- throw new Error('Failed to check onboarding status');
1189
+ async refreshAccessToken() {
1190
+ return this.authService.refresh();
1191
+ }
1192
+ buildQueryString(options) {
1193
+ if (options === undefined)
1194
+ return '';
1195
+ const array = [];
1196
+ for (const key in options) {
1197
+ if (options.hasOwnProperty(key) && options[key] !== null && options[key] !== undefined) {
1198
+ array.push(encodeURIComponent(key) + '=' + encodeURIComponent(options[key]));
1479
1199
  }
1480
- return await response.json();
1481
- }
1482
- catch (error) {
1483
- throw error;
1484
1200
  }
1201
+ const str = array.join('&');
1202
+ return str ? '?' + str : '';
1485
1203
  }
1486
1204
  /**
1487
- * Complete tenant onboarding (create tenant with country + org name)
1488
- * @param countryCode - Country code
1489
- * @param tenantName - Tenant organization name
1490
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
1205
+ * Upload a drawing (uses upload server if configured, otherwise API server)
1206
+ * @deprecated Platform-specific method - consider moving to platform service
1491
1207
  */
1492
- async completeTenantOnboarding(countryCode, tenantName, serverName) {
1493
- try {
1494
- const accessToken = this.tokens.getAccessToken();
1495
- if (!accessToken) {
1496
- throw new Error('Not authenticated');
1497
- }
1498
- // Route through platform API proxy — PHP API adds platform_secret header
1499
- const apiUrl = this.getPlatformApiUrl();
1500
- const response = await fetch(`${apiUrl}/auth/register-tenant`, {
1501
- method: 'POST',
1502
- headers: {
1503
- 'Content-Type': 'application/json',
1504
- 'Authorization': `Bearer ${accessToken}`
1505
- },
1506
- credentials: 'include',
1507
- body: JSON.stringify({
1508
- platform: this.environment.platformCode,
1509
- tenant_name: tenantName,
1510
- country_code: countryCode,
1511
- provider: 'google', // Assuming OAuth
1512
- oauth_token: accessToken
1513
- })
1514
- });
1515
- if (!response.ok) {
1516
- const errorData = await response.json();
1517
- throw new Error(errorData.message || 'Failed to create tenant');
1518
- }
1519
- const data = await response.json();
1520
- // Update tokens with new tenant-scoped tokens
1521
- if (data.access_token) {
1522
- this.tokens.setAccessToken(data.access_token);
1523
- this.signinStatus.setSigninStatus(true);
1524
- }
1525
- return data;
1526
- }
1527
- catch (error) {
1528
- throw error;
1529
- }
1208
+ async uploadDrawing(formData) {
1209
+ const uploadHost = this.environment.uploadServer?.host || this.host;
1210
+ const url = uploadHost + '/upload/drawing';
1211
+ const fetchOptions = { method: 'POST', mode: 'cors', redirect: 'error', body: formData };
1212
+ return this.request(url, fetchOptions, null);
1213
+ }
1214
+ /**
1215
+ * Upload an image (uses upload server if configured, otherwise API server)
1216
+ * @deprecated Platform-specific method - consider moving to platform service
1217
+ */
1218
+ async uploadImage(formData) {
1219
+ const uploadHost = this.environment.uploadServer?.host || this.host;
1220
+ const url = uploadHost + '/upload/image';
1221
+ const fetchOptions = { method: 'POST', mode: 'cors', redirect: 'error', body: formData };
1222
+ return this.request(url, fetchOptions, null);
1530
1223
  }
1531
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AuthService, deps: [{ token: TokenService }, { token: SigninStatusService }, { token: MyEnvironmentModel }], target: i0.ɵɵFactoryTarget.Injectable });
1532
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AuthService, providedIn: 'root' });
1224
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ApiConnectionService, deps: [{ token: TokenService }, { token: SigninStatusService }, { token: MyEnvironmentModel }, { token: AuthService }], target: i0.ɵɵFactoryTarget.Injectable });
1225
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ApiConnectionService, providedIn: 'root' });
1533
1226
  }
1534
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AuthService, decorators: [{
1227
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ApiConnectionService, decorators: [{
1535
1228
  type: Injectable,
1536
1229
  args: [{
1537
1230
  providedIn: 'root'
1538
1231
  }]
1539
- }], ctorParameters: () => [{ type: TokenService }, { type: SigninStatusService }, { type: MyEnvironmentModel, decorators: [{
1540
- type: Inject,
1541
- args: [MyEnvironmentModel]
1542
- }] }] });
1232
+ }], ctorParameters: () => [{ type: TokenService }, { type: SigninStatusService }, { type: MyEnvironmentModel }, { type: AuthService }] });
1543
1233
 
1544
1234
  class DbService {
1545
1235
  constructor() { }
1546
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DbService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1547
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DbService, providedIn: 'root' });
1236
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: DbService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1237
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: DbService, providedIn: 'root' });
1548
1238
  }
1549
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DbService, decorators: [{
1239
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: DbService, decorators: [{
1550
1240
  type: Injectable,
1551
1241
  args: [{
1552
1242
  providedIn: 'root'
1553
1243
  }]
1554
1244
  }], ctorParameters: () => [] });
1555
1245
 
1246
+ /**
1247
+ * CSRF Token Service
1248
+ *
1249
+ * Manages CSRF tokens for cookie-based authentication.
1250
+ * Reads CSRF token from cookies and provides it for request headers.
1251
+ */
1252
+ class CsrfService {
1253
+ /**
1254
+ * Get CSRF token from cookie
1255
+ */
1256
+ getCsrfToken(cookieName = 'csrf_token') {
1257
+ const cookies = document.cookie.split(';');
1258
+ for (const cookie of cookies) {
1259
+ const [name, value] = cookie.trim().split('=');
1260
+ if (name === cookieName) {
1261
+ return decodeURIComponent(value);
1262
+ }
1263
+ }
1264
+ return null;
1265
+ }
1266
+ /**
1267
+ * Check if CSRF token exists
1268
+ */
1269
+ hasCsrfToken(cookieName = 'csrf_token') {
1270
+ return this.getCsrfToken(cookieName) !== null;
1271
+ }
1272
+ /**
1273
+ * Clear CSRF token (for logout)
1274
+ * Note: Client-side deletion is limited for httpOnly cookies
1275
+ */
1276
+ clearCsrfToken(cookieName = 'csrf_token') {
1277
+ // Can only clear non-httpOnly cookies
1278
+ document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
1279
+ }
1280
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: CsrfService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1281
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: CsrfService, providedIn: 'root' });
1282
+ }
1283
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: CsrfService, decorators: [{
1284
+ type: Injectable,
1285
+ args: [{
1286
+ providedIn: 'root'
1287
+ }]
1288
+ }] });
1289
+
1556
1290
  /**
1557
1291
  * Service for interacting with the stonescriptphp-files server.
1558
1292
  * Handles file upload, download, list, and delete operations
@@ -1807,10 +1541,10 @@ class FilesService {
1807
1541
  return false;
1808
1542
  }
1809
1543
  }
1810
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: FilesService, deps: [{ token: TokenService }, { token: SigninStatusService }, { token: MyEnvironmentModel }, { token: CsrfService }], target: i0.ɵɵFactoryTarget.Injectable });
1811
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: FilesService, providedIn: 'root' });
1544
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: FilesService, deps: [{ token: TokenService }, { token: SigninStatusService }, { token: MyEnvironmentModel }, { token: CsrfService }], target: i0.ɵɵFactoryTarget.Injectable });
1545
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: FilesService, providedIn: 'root' });
1812
1546
  }
1813
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: FilesService, decorators: [{
1547
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: FilesService, decorators: [{
1814
1548
  type: Injectable,
1815
1549
  args: [{
1816
1550
  providedIn: 'root'
@@ -1986,10 +1720,10 @@ class ProviderRegistryService {
1986
1720
  }
1987
1721
  return Object.keys(styles).length > 0 ? styles : null;
1988
1722
  }
1989
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ProviderRegistryService, deps: [{ token: MyEnvironmentModel }], target: i0.ɵɵFactoryTarget.Injectable });
1990
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ProviderRegistryService, providedIn: 'root' });
1723
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ProviderRegistryService, deps: [{ token: MyEnvironmentModel }], target: i0.ɵɵFactoryTarget.Injectable });
1724
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ProviderRegistryService, providedIn: 'root' });
1991
1725
  }
1992
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ProviderRegistryService, decorators: [{
1726
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ProviderRegistryService, decorators: [{
1993
1727
  type: Injectable,
1994
1728
  args: [{
1995
1729
  providedIn: 'root'
@@ -1999,29 +1733,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
1999
1733
  args: [MyEnvironmentModel]
2000
1734
  }] }] });
2001
1735
 
2002
- class NgxStoneScriptPhpClientModule {
2003
- static forRoot(environment) {
2004
- return {
2005
- ngModule: NgxStoneScriptPhpClientModule,
2006
- providers: [
2007
- { provide: MyEnvironmentModel, useValue: environment }
2008
- ]
2009
- };
2010
- }
2011
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: NgxStoneScriptPhpClientModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
2012
- static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.15", ngImport: i0, type: NgxStoneScriptPhpClientModule, imports: [CommonModule] });
2013
- static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: NgxStoneScriptPhpClientModule, imports: [CommonModule] });
2014
- }
2015
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: NgxStoneScriptPhpClientModule, decorators: [{
2016
- type: NgModule,
2017
- args: [{
2018
- declarations: [],
2019
- imports: [
2020
- CommonModule
2021
- ]
2022
- }]
2023
- }] });
2024
-
2025
1736
  class TenantLoginComponent {
2026
1737
  auth;
2027
1738
  providerRegistry;
@@ -2249,8 +1960,8 @@ class TenantLoginComponent {
2249
1960
  event.preventDefault();
2250
1961
  this.createTenant.emit();
2251
1962
  }
2252
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantLoginComponent, deps: [{ token: AuthService }, { token: ProviderRegistryService }], target: i0.ɵɵFactoryTarget.Component });
2253
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: TenantLoginComponent, isStandalone: true, selector: "lib-tenant-login", inputs: { title: "title", providers: "providers", showTenantSelector: "showTenantSelector", autoSelectSingleTenant: "autoSelectSingleTenant", prefillEmail: "prefillEmail", allowTenantCreation: "allowTenantCreation", tenantSelectorTitle: "tenantSelectorTitle", tenantSelectorDescription: "tenantSelectorDescription", continueButtonText: "continueButtonText", registerLinkText: "registerLinkText", registerLinkAction: "registerLinkAction", createTenantLinkText: "createTenantLinkText", createTenantLinkAction: "createTenantLinkAction" }, outputs: { tenantSelected: "tenantSelected", createTenant: "createTenant" }, ngImport: i0, template: `
1963
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TenantLoginComponent, deps: [{ token: AuthService }, { token: ProviderRegistryService }], target: i0.ɵɵFactoryTarget.Component });
1964
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: TenantLoginComponent, isStandalone: true, selector: "lib-tenant-login", inputs: { title: "title", providers: "providers", showTenantSelector: "showTenantSelector", autoSelectSingleTenant: "autoSelectSingleTenant", prefillEmail: "prefillEmail", allowTenantCreation: "allowTenantCreation", tenantSelectorTitle: "tenantSelectorTitle", tenantSelectorDescription: "tenantSelectorDescription", continueButtonText: "continueButtonText", registerLinkText: "registerLinkText", registerLinkAction: "registerLinkAction", createTenantLinkText: "createTenantLinkText", createTenantLinkAction: "createTenantLinkAction" }, outputs: { tenantSelected: "tenantSelected", createTenant: "createTenant" }, ngImport: i0, template: `
2254
1965
  <div class="tenant-login-dialog">
2255
1966
  @if (!showingTenantSelector) {
2256
1967
  <!-- Step 1: Authentication -->
@@ -2418,7 +2129,7 @@ class TenantLoginComponent {
2418
2129
  </div>
2419
2130
  `, isInline: true, styles: [".tenant-login-dialog{padding:24px;max-width:450px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.welcome-message{margin-bottom:16px;padding:12px;background:#e8f5e9;border-radius:4px;text-align:center;font-size:14px;color:#2e7d32}.selector-description{margin-bottom:20px;font-size:14px;color:#666;text-align:center}.email-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.btn-zoho{background-color:#f0483e;color:#fff;border:1px solid #d63b32}.btn-zoho:hover{background-color:#d63b32}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.tenant-list{margin-bottom:20px;display:flex;flex-direction:column;gap:12px}.tenant-item{display:flex;align-items:flex-start;gap:12px;padding:16px;border:2px solid #e0e0e0;border-radius:6px;cursor:pointer;transition:all .2s}.tenant-item:hover{border-color:#4285f4;background:#f8f9ff}.tenant-item.selected{border-color:#4285f4;background:#e8f0fe}.tenant-radio{flex-shrink:0;padding-top:2px}.tenant-radio input[type=radio]{width:18px;height:18px;cursor:pointer}.tenant-info{flex:1}.tenant-name{font-size:16px;font-weight:500;color:#333;margin-bottom:4px}.tenant-meta{font-size:13px;color:#666}.tenant-role{font-weight:500;color:#4285f4}.tenant-separator{margin:0 6px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}.create-tenant-link{margin-top:16px;padding-top:16px;border-top:1px solid #e0e0e0;text-align:center;font-size:14px;color:#666}.create-tenant-link a{color:#4285f4;text-decoration:none;font-weight:500}.create-tenant-link a:hover{text-decoration:underline}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i4.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i4.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i4.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i4.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
2420
2131
  }
2421
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantLoginComponent, decorators: [{
2132
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TenantLoginComponent, decorators: [{
2422
2133
  type: Component,
2423
2134
  args: [{ selector: 'lib-tenant-login', standalone: true, imports: [CommonModule, FormsModule], template: `
2424
2135
  <div class="tenant-login-dialog">
@@ -2723,8 +2434,8 @@ class RegisterComponent {
2723
2434
  this.confirmPassword = '';
2724
2435
  this.displayName = '';
2725
2436
  }
2726
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RegisterComponent, deps: [{ token: AuthService }, { token: MyEnvironmentModel }], target: i0.ɵɵFactoryTarget.Component });
2727
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: RegisterComponent, isStandalone: true, selector: "lib-register", outputs: { navigateToLogin: "navigateToLogin" }, ngImport: i0, template: `
2437
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: RegisterComponent, deps: [{ token: AuthService }, { token: MyEnvironmentModel }], target: i0.ɵɵFactoryTarget.Component });
2438
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: RegisterComponent, isStandalone: true, selector: "lib-register", outputs: { navigateToLogin: "navigateToLogin" }, ngImport: i0, template: `
2728
2439
  <div class="register-dialog">
2729
2440
  <h2 class="register-title">Create Account</h2>
2730
2441
 
@@ -2853,7 +2564,7 @@ class RegisterComponent {
2853
2564
  </div>
2854
2565
  `, isInline: true, styles: [".register-dialog{padding:24px;max-width:400px;position:relative}.register-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.register-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-group label{display:block;margin-bottom:6px;font-size:14px;font-weight:500;color:#333}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:38px;background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.form-hint{display:block;margin-top:4px;font-size:12px;color:#666}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.success-message{margin-top:16px;padding:12px;background:#efe;color:#3a3;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.login-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.login-link a{color:#4285f4;text-decoration:none}.login-link a:hover{text-decoration:underline}.account-link-prompt{background:#f8f9fa;border:2px solid #4285f4;border-radius:8px;padding:24px;margin-bottom:16px;text-align:center}.prompt-icon{font-size:48px;margin-bottom:12px}.account-link-prompt h3{margin:0 0 12px;color:#333;font-size:20px;font-weight:500}.account-link-prompt p{margin:8px 0;color:#555;font-size:14px;line-height:1.6}.prompt-actions{margin-top:20px;display:flex;flex-direction:column;gap:10px}.btn-secondary{background:#fff;color:#333;border:1px solid #ddd}.btn-secondary:hover:not(:disabled){background:#f8f9fa;border-color:#ccc}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i4.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i4.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i4.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i4.MinLengthValidator, selector: "[minlength][formControlName],[minlength][formControl],[minlength][ngModel]", inputs: ["minlength"] }, { kind: "directive", type: i4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i4.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
2855
2566
  }
2856
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RegisterComponent, decorators: [{
2567
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: RegisterComponent, decorators: [{
2857
2568
  type: Component,
2858
2569
  args: [{ selector: 'lib-register', standalone: true, imports: [CommonModule, FormsModule], template: `
2859
2570
  <div class="register-dialog">
@@ -3039,8 +2750,8 @@ class AuthPageComponent {
3039
2750
  (B < 255 ? B < 1 ? 0 : B : 255))
3040
2751
  .toString(16).slice(1);
3041
2752
  }
3042
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AuthPageComponent, deps: [{ token: MyEnvironmentModel }], target: i0.ɵɵFactoryTarget.Component });
3043
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: AuthPageComponent, isStandalone: true, selector: "lib-auth-page", inputs: { providers: "providers" }, outputs: { authenticated: "authenticated" }, ngImport: i0, template: `
2753
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: AuthPageComponent, deps: [{ token: MyEnvironmentModel }], target: i0.ɵɵFactoryTarget.Component });
2754
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: AuthPageComponent, isStandalone: true, selector: "lib-auth-page", inputs: { providers: "providers" }, outputs: { authenticated: "authenticated" }, ngImport: i0, template: `
3044
2755
  <div class="auth-container" [style.background]="gradientStyle">
3045
2756
  <div class="auth-card">
3046
2757
  @if (logo) {
@@ -3067,7 +2778,7 @@ class AuthPageComponent {
3067
2778
  </div>
3068
2779
  `, isInline: true, styles: [".auth-container{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px;background:linear-gradient(135deg,#667eea,#764ba2)}.auth-card{background:#fff;border-radius:12px;box-shadow:0 10px 40px #0000001a;padding:40px;width:100%;max-width:480px}.logo{display:block;max-width:200px;max-height:80px;margin:0 auto 24px}.app-name{margin:0 0 12px;font-size:28px;font-weight:600;text-align:center;color:#1a202c}.subtitle{margin:0 0 32px;font-size:16px;text-align:center;color:#718096}:host ::ng-deep .tenant-login-dialog,:host ::ng-deep .register-dialog{padding:0;max-width:none}:host ::ng-deep .login-title,:host ::ng-deep .register-title{display:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: TenantLoginComponent, selector: "lib-tenant-login", inputs: ["title", "providers", "showTenantSelector", "autoSelectSingleTenant", "prefillEmail", "allowTenantCreation", "tenantSelectorTitle", "tenantSelectorDescription", "continueButtonText", "registerLinkText", "registerLinkAction", "createTenantLinkText", "createTenantLinkAction"], outputs: ["tenantSelected", "createTenant"] }, { kind: "component", type: RegisterComponent, selector: "lib-register", outputs: ["navigateToLogin"] }] });
3069
2780
  }
3070
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AuthPageComponent, decorators: [{
2781
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: AuthPageComponent, decorators: [{
3071
2782
  type: Component,
3072
2783
  args: [{ selector: 'lib-auth-page', standalone: true, imports: [CommonModule, TenantLoginComponent, RegisterComponent], template: `
3073
2784
  <div class="auth-container" [style.background]="gradientStyle">
@@ -3190,8 +2901,8 @@ class LoginDialogComponent {
3190
2901
  // For now, just emit a console message
3191
2902
  console.log('Register clicked - platform should handle navigation');
3192
2903
  }
3193
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: LoginDialogComponent, deps: [{ token: AuthService }, { token: ProviderRegistryService }], target: i0.ɵɵFactoryTarget.Component });
3194
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: LoginDialogComponent, isStandalone: true, selector: "lib-login-dialog", inputs: { providers: "providers" }, ngImport: i0, template: `
2904
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: LoginDialogComponent, deps: [{ token: AuthService }, { token: ProviderRegistryService }], target: i0.ɵɵFactoryTarget.Component });
2905
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: LoginDialogComponent, isStandalone: true, selector: "lib-login-dialog", inputs: { providers: "providers" }, ngImport: i0, template: `
3195
2906
  <div class="login-dialog">
3196
2907
  <h2 class="login-title">Sign In</h2>
3197
2908
 
@@ -3281,7 +2992,7 @@ class LoginDialogComponent {
3281
2992
  </div>
3282
2993
  `, isInline: true, styles: [".login-dialog{padding:24px;max-width:400px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.email-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i4.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i4.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i4.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i4.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
3283
2994
  }
3284
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: LoginDialogComponent, decorators: [{
2995
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: LoginDialogComponent, decorators: [{
3285
2996
  type: Component,
3286
2997
  args: [{ selector: 'lib-login-dialog', standalone: true, imports: [CommonModule, FormsModule], template: `
3287
2998
  <div class="login-dialog">
@@ -3536,7 +3247,6 @@ class TenantRegisterComponent {
3536
3247
  try {
3537
3248
  const result = await this.auth.registerTenant({
3538
3249
  tenantName: this.tenantName,
3539
- tenantSlug: this.tenantSlug,
3540
3250
  provider: provider
3541
3251
  });
3542
3252
  if (result.success && result.tenant && result.user) {
@@ -3569,7 +3279,6 @@ class TenantRegisterComponent {
3569
3279
  try {
3570
3280
  const result = await this.auth.registerTenant({
3571
3281
  tenantName: this.tenantName,
3572
- tenantSlug: this.tenantSlug,
3573
3282
  displayName: this.displayName,
3574
3283
  email: this.email,
3575
3284
  password: this.password,
@@ -3597,8 +3306,8 @@ class TenantRegisterComponent {
3597
3306
  event.preventDefault();
3598
3307
  this.navigateToLogin.emit();
3599
3308
  }
3600
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantRegisterComponent, deps: [{ token: AuthService }, { token: ProviderRegistryService }], target: i0.ɵɵFactoryTarget.Component });
3601
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: TenantRegisterComponent, isStandalone: true, selector: "lib-tenant-register", inputs: { title: "title", providers: "providers", requireTenantName: "requireTenantName", tenantSectionTitle: "tenantSectionTitle", tenantNameLabel: "tenantNameLabel", tenantNamePlaceholder: "tenantNamePlaceholder", tenantSlugLabel: "tenantSlugLabel", tenantSlugPlaceholder: "tenantSlugPlaceholder", urlPreviewEnabled: "urlPreviewEnabled", urlPreviewPrefix: "urlPreviewPrefix", userSectionTitle: "userSectionTitle", oauthDescription: "oauthDescription", ownershipTitle: "ownershipTitle", ownershipMessage: "ownershipMessage", submitButtonText: "submitButtonText", loginLinkText: "loginLinkText", loginLinkAction: "loginLinkAction" }, outputs: { tenantCreated: "tenantCreated", navigateToLogin: "navigateToLogin" }, ngImport: i0, template: `
3309
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TenantRegisterComponent, deps: [{ token: AuthService }, { token: ProviderRegistryService }], target: i0.ɵɵFactoryTarget.Component });
3310
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: TenantRegisterComponent, isStandalone: true, selector: "lib-tenant-register", inputs: { title: "title", providers: "providers", requireTenantName: "requireTenantName", tenantSectionTitle: "tenantSectionTitle", tenantNameLabel: "tenantNameLabel", tenantNamePlaceholder: "tenantNamePlaceholder", tenantSlugLabel: "tenantSlugLabel", tenantSlugPlaceholder: "tenantSlugPlaceholder", urlPreviewEnabled: "urlPreviewEnabled", urlPreviewPrefix: "urlPreviewPrefix", userSectionTitle: "userSectionTitle", oauthDescription: "oauthDescription", ownershipTitle: "ownershipTitle", ownershipMessage: "ownershipMessage", submitButtonText: "submitButtonText", loginLinkText: "loginLinkText", loginLinkAction: "loginLinkAction" }, outputs: { tenantCreated: "tenantCreated", navigateToLogin: "navigateToLogin" }, ngImport: i0, template: `
3602
3311
  <div class="tenant-register-dialog">
3603
3312
  <h2 class="register-title">{{ title }}</h2>
3604
3313
 
@@ -3808,7 +3517,7 @@ class TenantRegisterComponent {
3808
3517
  </div>
3809
3518
  `, isInline: true, styles: [".tenant-register-dialog{padding:24px;max-width:500px;position:relative}.register-title{margin:0 0 20px;font-size:24px;font-weight:500;text-align:center}.warning-box{display:flex;gap:12px;padding:16px;background:#fff3cd;border:1px solid #ffc107;border-radius:6px;margin-bottom:24px}.warning-icon{font-size:24px;line-height:1}.warning-content{flex:1}.warning-content strong{display:block;margin-bottom:4px;color:#856404;font-size:14px}.warning-content p{margin:0;color:#856404;font-size:13px;line-height:1.5}.section-header{font-size:16px;font-weight:600;margin:20px 0 12px;padding-bottom:8px;border-bottom:2px solid #e0e0e0;color:#333}.register-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-group label{display:block;margin-bottom:6px;font-size:14px;font-weight:500;color:#333}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box;transition:border-color .2s}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:38px;background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.form-control.input-error{border-color:#dc3545}.form-control.input-success{border-color:#28a745}.form-hint{display:block;margin-top:4px;font-size:12px;color:#666}.form-hint .checking{color:#666}.form-hint .available{color:#28a745;font-weight:500}.form-hint .error-text{color:#dc3545}.oauth-section{margin:16px 0}.oauth-description{margin-bottom:12px;font-size:14px;color:#666;text-align:center}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s,opacity .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.success-message{margin-top:16px;padding:12px;background:#efe;color:#3a3;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffffff2;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-text{margin:0;font-size:14px;color:#666}.login-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.login-link a{color:#4285f4;text-decoration:none}.login-link a:hover{text-decoration:underline}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i4.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i4.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i4.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i4.MinLengthValidator, selector: "[minlength][formControlName],[minlength][formControl],[minlength][ngModel]", inputs: ["minlength"] }, { kind: "directive", type: i4.PatternValidator, selector: "[pattern][formControlName],[pattern][formControl],[pattern][ngModel]", inputs: ["pattern"] }, { kind: "directive", type: i4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i4.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
3810
3519
  }
3811
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantRegisterComponent, decorators: [{
3520
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TenantRegisterComponent, decorators: [{
3812
3521
  type: Component,
3813
3522
  args: [{ selector: 'lib-tenant-register', standalone: true, imports: [CommonModule, FormsModule], template: `
3814
3523
  <div class="tenant-register-dialog">
@@ -4108,8 +3817,8 @@ class TenantLoginDialogComponent {
4108
3817
  this.dialogRef.close({ action: 'create_tenant' });
4109
3818
  }
4110
3819
  }
4111
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantLoginDialogComponent, deps: [{ token: 'DIALOG_DATA', optional: true }, { token: 'DIALOG_REF', optional: true }], target: i0.ɵɵFactoryTarget.Component });
4112
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: TenantLoginDialogComponent, isStandalone: true, selector: "lib-tenant-login-dialog", ngImport: i0, template: `
3820
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TenantLoginDialogComponent, deps: [{ token: 'DIALOG_DATA', optional: true }, { token: 'DIALOG_REF', optional: true }], target: i0.ɵɵFactoryTarget.Component });
3821
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", type: TenantLoginDialogComponent, isStandalone: true, selector: "lib-tenant-login-dialog", ngImport: i0, template: `
4113
3822
  <div class="dialog-wrapper">
4114
3823
  <lib-tenant-login
4115
3824
  [title]="data?.title || 'Sign In'"
@@ -4130,7 +3839,7 @@ class TenantLoginDialogComponent {
4130
3839
  </div>
4131
3840
  `, isInline: true, styles: [".dialog-wrapper{padding:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: TenantLoginComponent, selector: "lib-tenant-login", inputs: ["title", "providers", "showTenantSelector", "autoSelectSingleTenant", "prefillEmail", "allowTenantCreation", "tenantSelectorTitle", "tenantSelectorDescription", "continueButtonText", "registerLinkText", "registerLinkAction", "createTenantLinkText", "createTenantLinkAction"], outputs: ["tenantSelected", "createTenant"] }] });
4132
3841
  }
4133
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantLoginDialogComponent, decorators: [{
3842
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TenantLoginDialogComponent, decorators: [{
4134
3843
  type: Component,
4135
3844
  args: [{ selector: 'lib-tenant-login-dialog', standalone: true, imports: [CommonModule, TenantLoginComponent], template: `
4136
3845
  <div class="dialog-wrapper">
@@ -4211,8 +3920,8 @@ class TenantRegisterDialogComponent {
4211
3920
  this.dialogRef.close({ action: 'navigate_to_login' });
4212
3921
  }
4213
3922
  }
4214
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantRegisterDialogComponent, deps: [{ token: 'DIALOG_DATA', optional: true }, { token: 'DIALOG_REF', optional: true }], target: i0.ɵɵFactoryTarget.Component });
4215
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: TenantRegisterDialogComponent, isStandalone: true, selector: "lib-tenant-register-dialog", ngImport: i0, template: `
3923
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TenantRegisterDialogComponent, deps: [{ token: 'DIALOG_DATA', optional: true }, { token: 'DIALOG_REF', optional: true }], target: i0.ɵɵFactoryTarget.Component });
3924
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", type: TenantRegisterDialogComponent, isStandalone: true, selector: "lib-tenant-register-dialog", ngImport: i0, template: `
4216
3925
  <div class="dialog-wrapper">
4217
3926
  <lib-tenant-register
4218
3927
  [title]="data?.title || 'Create New Organization'"
@@ -4238,7 +3947,7 @@ class TenantRegisterDialogComponent {
4238
3947
  </div>
4239
3948
  `, isInline: true, styles: [".dialog-wrapper{padding:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: TenantRegisterComponent, selector: "lib-tenant-register", inputs: ["title", "providers", "requireTenantName", "tenantSectionTitle", "tenantNameLabel", "tenantNamePlaceholder", "tenantSlugLabel", "tenantSlugPlaceholder", "urlPreviewEnabled", "urlPreviewPrefix", "userSectionTitle", "oauthDescription", "ownershipTitle", "ownershipMessage", "submitButtonText", "loginLinkText", "loginLinkAction"], outputs: ["tenantCreated", "navigateToLogin"] }] });
4240
3949
  }
4241
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantRegisterDialogComponent, decorators: [{
3950
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TenantRegisterDialogComponent, decorators: [{
4242
3951
  type: Component,
4243
3952
  args: [{ selector: 'lib-tenant-register-dialog', standalone: true, imports: [CommonModule, TenantRegisterComponent], template: `
4244
3953
  <div class="dialog-wrapper">
@@ -4280,10 +3989,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
4280
3989
  /*
4281
3990
  * Public API Surface of ngx-stonescriptphp-client
4282
3991
  */
3992
+ // ── Core setup ────────────────────────────────────────────────────────────────
4283
3993
 
4284
3994
  /**
4285
3995
  * Generated bundle index. Do not edit.
4286
3996
  */
4287
3997
 
4288
- export { ApiConnectionService, ApiResponse, AuthPageComponent, AuthService, CsrfService, DbService, FilesService, LoginDialogComponent, MyEnvironmentModel, NgxStoneScriptPhpClientModule, ProviderRegistryService, RegisterComponent, SigninStatusService, TenantLoginComponent, TenantLoginDialogComponent, TenantRegisterComponent, TenantRegisterDialogComponent, TokenService, VerifyStatus };
3998
+ export { AUTH_PLUGIN, ApiConnectionService, ApiResponse, AuthPageComponent, AuthService, CsrfService, DbService, FilesService, LoginDialogComponent, MyEnvironmentModel, ProviderRegistryService, RegisterComponent, SigninStatusService, StoneScriptPHPAuth, TenantLoginComponent, TenantLoginDialogComponent, TenantRegisterComponent, TenantRegisterDialogComponent, TokenService, VerifyStatus, provideNgxStoneScriptPhpClient };
4289
3999
  //# sourceMappingURL=progalaxyelabs-ngx-stonescriptphp-client.mjs.map