@progalaxyelabs/ngx-stonescriptphp-client 1.11.1 → 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 non-empty access token.
114
- * Token is treated as opaque — validity is determined by the auth server.
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
- /**
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
24
  /**
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 = {
@@ -234,667 +40,815 @@ class MyEnvironmentModel {
234
40
  csrfHeaderName: 'X-CSRF-Token'
235
41
  };
236
42
  /**
237
- * Custom OAuth provider configurations.
238
- * Register additional OAuth providers beyond the built-in ones
239
- * (google, linkedin, apple, microsoft, github, zoho).
43
+ * Multiple authentication servers configuration (for StoneScriptPHPAuth multi-server mode).
240
44
  * @example
241
45
  * ```typescript
242
- * customProviders: {
243
- * okta: { label: 'Sign in with Okta', cssClass: 'btn-okta', buttonStyle: { borderColor: '#007dc1' } },
244
- * keycloak: { label: 'Sign in with Keycloak', icon: '🔑' }
46
+ * authServers: {
47
+ * customer: { url: 'https://auth.progalaxyelabs.com', default: true },
48
+ * employee: { url: 'https://admin-auth.progalaxyelabs.com' }
245
49
  * }
246
50
  * ```
247
51
  */
248
- customProviders;
52
+ authServers;
249
53
  /**
250
- * Auth response field mapping.
251
- * Defaults to StoneScriptPHP format: { status: 'ok', data: { access_token, user, ... } }
252
- *
253
- * For external auth servers that return flat responses like
254
- * { access_token, identity, ... }, override with:
54
+ * Custom OAuth provider configurations.
55
+ * @example
255
56
  * ```typescript
256
- * authResponseMap: {
257
- * accessTokenPath: 'access_token',
258
- * refreshTokenPath: 'refresh_token',
259
- * userPath: 'identity',
260
- * errorMessagePath: 'message'
57
+ * customProviders: {
58
+ * okta: { label: 'Sign in with Okta', cssClass: 'btn-okta', buttonStyle: { borderColor: '#007dc1' } }
261
59
  * }
262
60
  * ```
263
61
  */
264
- authResponseMap;
62
+ customProviders;
265
63
  /**
266
- * Branding configuration for auth components
267
- * Allows platforms to customize login/register pages without creating wrappers
64
+ * Branding configuration for auth UI components.
268
65
  */
269
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;
270
83
  }
271
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
+ };
272
96
  /**
273
- * CSRF Token Service
97
+ * Built-in auth plugin for StoneScriptPHP backends.
274
98
  *
275
- * Manages CSRF tokens for cookie-based authentication.
276
- * 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
277
108
  */
278
- class CsrfService {
279
- /**
280
- * Get CSRF token from cookie
281
- */
282
- 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';
283
169
  const cookies = document.cookie.split(';');
284
170
  for (const cookie of cookies) {
285
171
  const [name, value] = cookie.trim().split('=');
286
- if (name === cookieName) {
172
+ if (name === cookieName)
287
173
  return decodeURIComponent(value);
288
- }
289
174
  }
290
175
  return null;
291
176
  }
292
- /**
293
- * Check if CSRF token exists
294
- */
295
- hasCsrfToken(cookieName = 'csrf_token') {
296
- 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;
297
189
  }
298
- /**
299
- * Clear CSRF token (for logout)
300
- * Note: Client-side deletion is limited for httpOnly cookies
301
- */
302
- clearCsrfToken(cookieName = 'csrf_token') {
303
- // Can only clear non-httpOnly cookies
304
- document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
190
+ getPlatformApiUrl() {
191
+ return this.config.apiUrl || this.config.host;
305
192
  }
306
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: CsrfService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
307
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: CsrfService, providedIn: 'root' });
308
- }
309
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: CsrfService, decorators: [{
310
- type: Injectable,
311
- args: [{
312
- providedIn: 'root'
313
- }]
314
- }] });
315
-
316
- class ApiConnectionService {
317
- tokens;
318
- signinStatus;
319
- environment;
320
- csrf;
321
- host = ''; // base URL without trailing slash
322
- accessToken = '';
323
- authConfig;
324
- constructor(tokens, signinStatus, environment, csrf) {
325
- this.tokens = tokens;
326
- this.signinStatus = signinStatus;
327
- this.environment = environment;
328
- this.csrf = csrf;
329
- this.host = environment.apiServer.host;
330
- // Set default auth config based on mode
331
- this.authConfig = {
332
- mode: environment.auth?.mode || 'cookie',
333
- refreshEndpoint: environment.auth?.refreshEndpoint,
334
- useCsrf: environment.auth?.useCsrf,
335
- refreshTokenCookieName: environment.auth?.refreshTokenCookieName || 'refresh_token',
336
- csrfTokenCookieName: environment.auth?.csrfTokenCookieName || 'csrf_token',
337
- csrfHeaderName: environment.auth?.csrfHeaderName || 'X-CSRF-Token'
338
- };
339
- // Set default refresh endpoint based on mode if not specified
340
- if (!this.authConfig.refreshEndpoint) {
341
- this.authConfig.refreshEndpoint = this.authConfig.mode === 'cookie'
342
- ? '/auth/refresh'
343
- : '/user/refresh_access';
344
- }
345
- // Set default CSRF setting based on mode if not specified
346
- if (this.authConfig.useCsrf === undefined) {
347
- 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;
348
199
  }
200
+ return Object.keys(this.config.authServers)[0] || null;
349
201
  }
350
- async request(url, options, data) {
202
+ restoreActiveServer() {
351
203
  try {
352
- if (data !== null) {
353
- const body = JSON.stringify(data);
354
- if (body) {
355
- options.body = body;
356
- }
357
- else {
358
- options.body = {};
359
- }
360
- }
361
- const accessTokenIncluded = this.includeAccessToken(options);
362
- let response = await fetch(url, options);
363
- if ((response.status === 401) && accessTokenIncluded) {
364
- response = await this.refreshAccessTokenAndRetry(url, options, response);
365
- }
366
- if (response.ok) {
367
- const json = await (response.json());
368
- return (new ApiResponse(json.status, json.data, json.message));
369
- }
370
- if (response.status === 401) {
371
- this.signinStatus.signedOut();
372
- }
373
- 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();
374
208
  }
375
- catch (error) {
376
- return this.handleError(error);
209
+ catch {
210
+ this.activeServer = this.getDefaultServer();
377
211
  }
378
212
  }
379
- handleError(error) {
380
- console.error(`Backend returned code ${error.status}, ` +
381
- `full error: `, error);
382
- return new ApiResponse('error');
383
- }
384
- async get(endpoint, queryParamsObj) {
385
- const url = this.host + endpoint + this.buildQueryString(queryParamsObj);
386
- const fetchOptions = {
387
- mode: 'cors',
388
- redirect: 'error'
389
- };
390
- return this.request(url, fetchOptions, null);
391
- }
392
- async post(pathWithQueryParams, data) {
393
- const url = this.host + pathWithQueryParams;
394
- const fetchOptions = {
395
- method: 'POST',
396
- mode: 'cors',
397
- redirect: 'error',
398
- headers: {
399
- 'Content-Type': 'application/json'
400
- },
401
- body: JSON.stringify(data)
402
- };
403
- 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;
404
223
  }
405
- async put(pathWithQueryParams, data) {
406
- const url = this.host + pathWithQueryParams;
407
- const fetchOptions = {
408
- method: 'PUT',
409
- mode: 'cors',
410
- redirect: 'error',
411
- headers: {
412
- 'Content-Type': 'application/json'
413
- },
414
- body: JSON.stringify(data)
415
- };
416
- return this.request(url, fetchOptions, data);
224
+ getAvailableServers() {
225
+ return Object.keys(this.config.authServers ?? {});
417
226
  }
418
- async patch(pathWithQueryParams, data) {
419
- const url = this.host + pathWithQueryParams;
420
- const fetchOptions = {
421
- method: 'PATCH',
422
- mode: 'cors',
423
- redirect: 'error',
424
- headers: {
425
- 'Content-Type': 'application/json'
426
- },
427
- body: JSON.stringify(data)
428
- };
429
- return this.request(url, fetchOptions, data);
227
+ getActiveServer() {
228
+ return this.activeServer;
430
229
  }
431
- async delete(endpoint, queryParamsObj) {
432
- const url = this.host + endpoint + this.buildQueryString(queryParamsObj);
433
- const fetchOptions = {
434
- method: 'DELETE',
435
- mode: 'cors',
436
- redirect: 'error'
437
- };
438
- 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;
439
235
  }
440
- // async postFormWithFiles(pathWithQueryParams: string, formData: FormData): Promise<ApiResponse | null> {
441
- // const url = this.host + pathWithQueryParams.replace(/^\/+/, '')
442
- // try {
443
- // const fetchOptions: RequestInit = {
444
- // method: 'POST',
445
- // mode: 'cors',
446
- // redirect: 'error',
447
- // body: formData
448
- // }
449
- // const accessTokenIncluded = this.includeAccessToken(fetchOptions)
450
- // let response: Response = await fetch(url, fetchOptions)
451
- // if ((response.status === 401) && accessTokenIncluded) {
452
- // response = await this.refreshAccessTokenAndRetry(url, fetchOptions, response)
453
- // }
454
- // if (response.ok) {
455
- // return ((await (response.json()) as ApiResponse))
456
- // }
457
- // return this.handleError(response)
458
- // } catch (error) {
459
- // return this.handleError(error)
460
- // }
461
- // }
462
- includeAccessToken(options) {
463
- this.accessToken = this.tokens.getAccessToken();
464
- if (!this.accessToken) {
465
- 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') };
466
256
  }
467
- if (!options.headers) {
468
- options.headers = {};
257
+ catch {
258
+ return { success: false, message: 'Network error. Please try again.' };
469
259
  }
470
- options.headers['Authorization'] = 'Bearer ' + this.accessToken;
471
- return true;
472
260
  }
473
- async refreshAccessTokenAndRetry(url, fetchOptions, response) {
474
- const refreshStatusOk = await this.refreshAccessToken();
475
- if (!refreshStatusOk) {
476
- 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.' };
477
290
  }
478
- fetchOptions.headers['Authorization'] = 'Bearer ' + this.accessToken;
479
- response = await fetch(url, fetchOptions);
480
- return response;
481
291
  }
482
- async refreshAccessToken() {
483
- if (this.authConfig.mode === 'none') {
484
- 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
+ });
485
301
  }
486
- if (this.authConfig.mode === 'cookie') {
487
- return await this.refreshAccessTokenCookieMode();
302
+ catch (error) {
303
+ console.error('Logout API call failed:', error);
488
304
  }
489
- else {
490
- 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 };
491
324
  }
492
325
  }
493
- /**
494
- * Refresh access token using cookie-based auth (StoneScriptPHP v2.1.x default)
495
- */
496
- 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() {
497
335
  try {
498
- // Determine auth server: accountsServer.host > accountsUrl > apiServer.host
499
- const authHost = this.environment.accountsServer?.host
500
- || this.environment.accountsUrl
501
- || this.host;
502
- const refreshTokenUrl = authHost + this.authConfig.refreshEndpoint;
503
- const headers = {
504
- 'Content-Type': 'application/json'
505
- };
506
- // Add CSRF token if enabled
507
- if (this.authConfig.useCsrf) {
508
- 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();
509
341
  if (!csrfToken) {
510
342
  console.error('CSRF token not found in cookie');
511
- return false;
343
+ return null;
512
344
  }
513
- headers[this.authConfig.csrfHeaderName] = csrfToken;
345
+ headers[this.config.auth?.csrfHeaderName ?? 'X-CSRF-Token'] = csrfToken;
514
346
  }
515
- let refreshTokenResponse = await fetch(refreshTokenUrl, {
347
+ const response = await fetch(`${authHost}${endpoint}`, {
516
348
  method: 'POST',
517
349
  mode: 'cors',
518
- credentials: 'include', // Important: send cookies
350
+ credentials: 'include',
519
351
  redirect: 'error',
520
- headers: headers
352
+ headers
521
353
  });
522
- if (!refreshTokenResponse.ok) {
523
- this.accessToken = '';
524
- this.tokens.clear();
525
- return false;
526
- }
527
- let refreshAccessData = await refreshTokenResponse.json();
528
- if (!refreshAccessData || refreshAccessData.status !== 'ok') {
529
- return false;
530
- }
531
- // Extract access token from response
532
- const newAccessToken = refreshAccessData.data?.access_token || refreshAccessData.access_token;
533
- if (!newAccessToken) {
534
- console.error('No access token in refresh response');
535
- return false;
536
- }
537
- // Store new access token (refresh token is in httpOnly cookie)
538
- this.tokens.setAccessToken(newAccessToken);
539
- this.accessToken = newAccessToken;
540
- 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;
541
360
  }
542
361
  catch (error) {
543
362
  console.error('Token refresh failed (cookie mode):', error);
544
- this.accessToken = '';
545
- this.tokens.clear();
546
- return false;
363
+ return null;
547
364
  }
548
365
  }
549
- /**
550
- * Refresh access token using body-based auth (legacy mode)
551
- */
552
- async refreshAccessTokenBodyMode() {
366
+ async refreshBodyMode(accessToken, refreshToken) {
367
+ if (!refreshToken)
368
+ return null;
553
369
  try {
554
- const refreshToken = this.tokens.getRefreshToken();
555
- if (!refreshToken) {
556
- return false;
557
- }
558
- // Determine auth server: accountsServer.host > accountsUrl > apiServer.host
559
- const authHost = this.environment.accountsServer?.host
560
- || this.environment.accountsUrl
561
- || this.host;
562
- const refreshTokenUrl = authHost + this.authConfig.refreshEndpoint;
563
- 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}`, {
564
373
  method: 'POST',
565
374
  mode: 'cors',
566
375
  redirect: 'error',
567
- headers: {
568
- 'Content-Type': 'application/json'
569
- },
570
- body: JSON.stringify({
571
- access_token: this.accessToken,
572
- refresh_token: refreshToken
573
- })
376
+ headers: { 'Content-Type': 'application/json' },
377
+ body: JSON.stringify({ access_token: accessToken, refresh_token: refreshToken })
574
378
  });
575
- if (!refreshTokenResponse.ok) {
576
- this.accessToken = '';
577
- this.tokens.clear();
578
- return false;
579
- }
580
- let refreshAccessData = await refreshTokenResponse.json();
581
- if (!refreshAccessData) {
582
- return false;
583
- }
584
- const newAccessToken = refreshAccessData.data?.access_token || refreshAccessData.access_token;
585
- if (!newAccessToken) {
586
- console.error('No access token in refresh response');
587
- return false;
588
- }
589
- this.tokens.setTokens(newAccessToken, refreshToken);
590
- this.accessToken = newAccessToken;
591
- return true;
379
+ if (!response.ok)
380
+ return null;
381
+ const data = await response.json();
382
+ return this.resolveAccessToken(data) ?? null;
592
383
  }
593
384
  catch (error) {
594
385
  console.error('Token refresh failed (body mode):', error);
595
- this.accessToken = '';
596
- this.tokens.clear();
597
- return false;
386
+ return null;
598
387
  }
599
388
  }
600
- buildQueryString(options) {
601
- if (options === undefined) {
602
- return '';
603
- }
604
- const array = [];
605
- for (let key in options) {
606
- if (options.hasOwnProperty(key) && (options[key] !== null) && (options[key] !== undefined)) {
607
- 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;
608
401
  }
609
- }
610
- const str = array.join('&');
611
- if (str !== '') {
612
- return '?' + str;
613
- }
614
- return '';
615
- }
616
- /**
617
- * Upload a drawing (uses upload server if configured, otherwise API server)
618
- * @deprecated Platform-specific method - consider moving to platform service
619
- */
620
- async uploadDrawing(formData) {
621
- const uploadHost = this.environment.uploadServer?.host || this.host;
622
- const url = uploadHost + '/upload/drawing';
623
- const fetchOptions = {
624
- method: 'POST',
625
- mode: 'cors',
626
- redirect: 'error',
627
- body: formData
628
- };
629
- return this.request(url, fetchOptions, null);
630
- }
631
- /**
632
- * Upload an image (uses upload server if configured, otherwise API server)
633
- * @deprecated Platform-specific method - consider moving to platform service
634
- */
635
- async uploadImage(formData) {
636
- const uploadHost = this.environment.uploadServer?.host || this.host;
637
- const url = uploadHost + '/upload/image';
638
- const fetchOptions = {
639
- method: 'POST',
640
- mode: 'cors',
641
- redirect: 'error',
642
- body: formData
643
- };
644
- return this.request(url, fetchOptions, null);
645
- }
646
- 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 });
647
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ApiConnectionService, providedIn: 'root' });
648
- }
649
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ApiConnectionService, decorators: [{
650
- type: Injectable,
651
- args: [{
652
- providedIn: 'root'
653
- }]
654
- }], ctorParameters: () => [{ type: TokenService }, { type: SigninStatusService }, { type: MyEnvironmentModel }, { type: CsrfService }] });
655
-
656
- class AuthService {
657
- tokens;
658
- signinStatus;
659
- environment;
660
- USER_STORAGE_KEY = 'progalaxyapi_user';
661
- ACTIVE_AUTH_SERVER_KEY = 'progalaxyapi_active_auth_server';
662
- // Observable user state
663
- userSubject = new BehaviorSubject(null);
664
- user$ = this.userSubject.asObservable();
665
- // Current active auth server name (for multi-server mode)
666
- activeAuthServer = null;
667
- constructor(tokens, signinStatus, environment) {
668
- this.tokens = tokens;
669
- this.signinStatus = signinStatus;
670
- this.environment = environment;
671
- // Restore user from localStorage on initialization
672
- this.restoreUser();
673
- // Restore active auth server
674
- this.restoreActiveAuthServer();
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
+ });
675
430
  }
676
- // ===== Multi-Server Support Methods =====
677
- /**
678
- * Get the current accounts URL based on configuration
679
- * Supports both single-server (accountsUrl) and multi-server (authServers) modes
680
- * @param serverName - Optional server name for multi-server mode
681
- */
682
- getAccountsUrl(serverName) {
683
- // Multi-server mode
684
- if (this.environment.authServers && Object.keys(this.environment.authServers).length > 0) {
685
- const targetServer = serverName || this.activeAuthServer || this.getDefaultAuthServer();
686
- if (!targetServer) {
687
- throw new Error('No auth server specified and no default server configured');
688
- }
689
- const serverConfig = this.environment.authServers[targetServer];
690
- if (!serverConfig) {
691
- 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) };
692
447
  }
693
- return serverConfig.url;
448
+ return { success: false, message: this.resolveErrorMessage(data, 'Failed to select tenant') };
694
449
  }
695
- // Single-server mode (backward compatibility)
696
- if (this.environment.accountsUrl) {
697
- return this.environment.accountsUrl;
450
+ catch {
451
+ return { success: false, message: 'Network error. Please try again.' };
698
452
  }
699
- throw new Error('No authentication server configured. Set either accountsUrl or authServers in environment config.');
700
453
  }
701
- /**
702
- * Get the default auth server name
703
- */
704
- getDefaultAuthServer() {
705
- if (!this.environment.authServers) {
706
- 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 || [];
707
467
  }
708
- // Find server marked as default
709
- for (const [name, config] of Object.entries(this.environment.authServers)) {
710
- if (config.default) {
711
- return name;
712
- }
468
+ catch {
469
+ return [];
713
470
  }
714
- // If no default is marked, use the first server
715
- const firstServer = Object.keys(this.environment.authServers)[0];
716
- return firstServer || null;
717
471
  }
718
- /**
719
- * Restore active auth server from localStorage
720
- */
721
- restoreActiveAuthServer() {
472
+ async registerTenant(data, accessToken) {
473
+ if (data.provider !== 'emailPassword') {
474
+ return this.registerTenantWithOAuth(data.tenantName, data.provider);
475
+ }
722
476
  try {
723
- const savedServer = localStorage.getItem(this.ACTIVE_AUTH_SERVER_KEY);
724
- if (savedServer && this.environment.authServers?.[savedServer]) {
725
- this.activeAuthServer = savedServer;
726
- }
727
- else {
728
- // Set to default if saved server is invalid
729
- this.activeAuthServer = this.getDefaultAuthServer();
730
- }
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();
731
500
  }
732
- catch (error) {
733
- console.error('Failed to restore active auth server:', error);
734
- this.activeAuthServer = this.getDefaultAuthServer();
501
+ catch {
502
+ return { success: false, message: 'Network error. Please try again.' };
735
503
  }
736
504
  }
737
- /**
738
- * Save active auth server to localStorage
739
- */
740
- saveActiveAuthServer(serverName) {
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;
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) {
741
545
  try {
742
- localStorage.setItem(this.ACTIVE_AUTH_SERVER_KEY, serverName);
743
- this.activeAuthServer = serverName;
744
- }
745
- catch (error) {
746
- console.error('Failed to save active auth server:', error);
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 };
747
553
  }
748
- }
749
- /**
750
- * Get available auth servers
751
- * @returns Array of server names or empty array if using single-server mode
752
- */
753
- getAvailableAuthServers() {
754
- if (!this.environment.authServers) {
755
- return [];
554
+ catch {
555
+ return { available: true };
756
556
  }
757
- return Object.keys(this.environment.authServers);
758
557
  }
759
- /**
760
- * Get current active auth server name
761
- * @returns Server name or null if using single-server mode
762
- */
763
- getActiveAuthServer() {
764
- return this.activeAuthServer;
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();
765
565
  }
766
- /**
767
- * Switch to a different auth server
768
- * @param serverName - Name of the server to switch to
769
- * @throws Error if server not found in configuration
770
- */
771
- switchAuthServer(serverName) {
772
- if (!this.environment.authServers) {
773
- throw new Error('Multi-server mode not configured. Use authServers in environment config.');
774
- }
775
- if (!this.environment.authServers[serverName]) {
776
- throw new Error(`Auth server '${serverName}' not found in configuration`);
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');
777
586
  }
778
- this.saveActiveAuthServer(serverName);
587
+ return response.json();
779
588
  }
780
- /**
781
- * Get auth server configuration
782
- * @param serverName - Optional server name (uses active server if not specified)
783
- */
784
- getAuthServerConfig(serverName) {
785
- if (!this.environment.authServers) {
786
- return null;
589
+ async checkEmail(email) {
590
+ try {
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 };
787
599
  }
788
- const targetServer = serverName || this.activeAuthServer || this.getDefaultAuthServer();
789
- if (!targetServer) {
790
- return null;
600
+ catch {
601
+ return { exists: false };
791
602
  }
792
- return this.environment.authServers[targetServer] || null;
793
603
  }
794
- /**
795
- * Check if multi-server mode is enabled
796
- */
797
- isMultiServerMode() {
798
- return !!(this.environment.authServers && Object.keys(this.environment.authServers).length > 0);
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';
799
666
  }
800
- /**
801
- * Get the platform's own API base URL
802
- * Used for routes that go through the platform API proxy (e.g. register-tenant)
803
- * @throws Error if no API URL is configured
804
- */
805
- getPlatformApiUrl() {
806
- if (this.environment.apiUrl) {
807
- return this.environment.apiUrl;
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);
808
678
  }
809
- if (this.environment.apiServer?.host) {
810
- return this.environment.apiServer.host;
679
+ return this;
680
+ }
681
+ onNotOk(callback) {
682
+ if (this.status === 'not ok') {
683
+ callback(this.message, this.data);
811
684
  }
812
- throw new Error('No platform API URL configured. Set apiUrl in environment config.');
685
+ return this;
813
686
  }
814
- // ===== Auth Response Mapping Helpers =====
815
- /** Default response map: StoneScriptPHP format */
816
- get responseMap() {
817
- return this.environment.authResponseMap ?? {
818
- successPath: 'status',
819
- successValue: 'ok',
820
- accessTokenPath: 'data.access_token',
821
- refreshTokenPath: 'data.refresh_token',
822
- userPath: 'data.user',
823
- errorMessagePath: 'message'
824
- };
687
+ onError(callback) {
688
+ if (this.status === 'error') {
689
+ callback();
690
+ }
691
+ return this;
825
692
  }
826
- /** Resolve a dot-notation path on an object (e.g., 'data.access_token') */
827
- resolvePath(obj, path) {
828
- return path.split('.').reduce((o, key) => o?.[key], obj);
693
+ isSuccess() {
694
+ return this.status === 'ok';
829
695
  }
830
- /** Check if an auth response indicates success */
831
- isAuthSuccess(data) {
832
- const map = this.responseMap;
833
- if (map.successPath) {
834
- return this.resolvePath(data, map.successPath) === (map.successValue ?? 'ok');
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 '';
835
743
  }
836
- // No successPath configured — success = access token present
837
- return !!this.resolvePath(data, map.accessTokenPath);
838
744
  }
839
- /** Extract access token from auth response */
840
- getAccessToken(data) {
841
- return this.resolvePath(data, this.responseMap.accessTokenPath);
745
+ getRefreshToken() {
746
+ if (this.refreshToken) {
747
+ return this.refreshToken;
748
+ }
749
+ const storedRefreshToken = localStorage.getItem(this.lsRefreshTokenKey);
750
+ if (storedRefreshToken) {
751
+ return storedRefreshToken;
752
+ }
753
+ else {
754
+ return '';
755
+ }
842
756
  }
843
- /** Extract refresh token from auth response */
844
- getRefreshToken(data) {
845
- return this.resolvePath(data, this.responseMap.refreshTokenPath);
757
+ clear() {
758
+ this.accessToken = '';
759
+ this.refreshToken = '';
760
+ localStorage.removeItem(this.lsAccessTokenKey);
761
+ localStorage.removeItem(this.lsRefreshTokenKey);
846
762
  }
847
- /** Extract user/identity object from auth response */
848
- getUserFromResponse(data) {
849
- return this.resolvePath(data, this.responseMap.userPath);
763
+ /**
764
+ * Check if there is a non-empty access token.
765
+ * Token is treated as opaque — validity is determined by the auth server.
766
+ */
767
+ hasValidAccessToken() {
768
+ const token = this.getAccessToken();
769
+ return token !== null && token !== '';
850
770
  }
851
- /** Extract error message from auth response */
852
- getErrorMessage(data, fallback) {
853
- const path = this.responseMap.errorMessagePath ?? 'message';
854
- return this.resolvePath(data, path) || fallback;
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);
855
794
  }
856
- /** Normalize a user/identity object into the standard User shape */
857
- normalizeUser(raw) {
858
- return {
859
- user_id: raw.user_id ?? (raw.id ? this.hashUUID(raw.id) : 0),
860
- id: raw.id ?? String(raw.user_id),
861
- email: raw.email,
862
- display_name: raw.display_name ?? raw.email?.split('@')[0] ?? '',
863
- photo_url: raw.photo_url,
864
- is_email_verified: raw.is_email_verified ?? false
865
- };
795
+ signedOut() {
796
+ this.status.next(false);
797
+ }
798
+ signedIn() {
799
+ this.status.next(true);
866
800
  }
867
801
  /**
868
- * Hash UUID to numeric ID for backward compatibility
869
- * 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
870
804
  */
871
- hashUUID(uuid) {
872
- let hash = 0;
873
- for (let i = 0; i < uuid.length; i++) {
874
- const char = uuid.charCodeAt(i);
875
- hash = ((hash << 5) - hash) + char;
876
- hash = hash & hash; // Convert to 32bit integer
877
- }
878
- 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();
879
840
  }
880
- /**
881
- * Restore user from localStorage
882
- */
841
+ // ── State management ──────────────────────────────────────────────────────
883
842
  restoreUser() {
884
843
  try {
885
844
  const userJson = localStorage.getItem(this.USER_STORAGE_KEY);
886
- if (userJson) {
887
- const user = JSON.parse(userJson);
888
- this.updateUser(user);
889
- }
845
+ if (userJson)
846
+ this.updateUser(JSON.parse(userJson));
890
847
  }
891
848
  catch (error) {
892
849
  console.error('Failed to restore user from localStorage:', error);
893
850
  }
894
851
  }
895
- /**
896
- * Save user to localStorage
897
- */
898
852
  saveUser(user) {
899
853
  try {
900
854
  if (user) {
@@ -908,699 +862,431 @@ class AuthService {
908
862
  console.error('Failed to save user to localStorage:', error);
909
863
  }
910
864
  }
911
- /**
912
- * Update user subject and persist to localStorage
913
- */
914
865
  updateUser(user) {
915
866
  this.userSubject.next(user);
916
867
  this.saveUser(user);
917
868
  }
918
- /**
919
- * Login with email and password
920
- * @param email - User email
921
- * @param password - User password
922
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
923
- */
924
- async loginWithEmail(email, password, serverName) {
925
- try {
926
- const accountsUrl = this.getAccountsUrl(serverName);
927
- const response = await fetch(`${accountsUrl}/api/auth/login`, {
928
- method: 'POST',
929
- headers: { 'Content-Type': 'application/json' },
930
- credentials: 'include',
931
- body: JSON.stringify({
932
- email,
933
- password,
934
- platform: this.environment.platformCode
935
- })
936
- });
937
- const data = await response.json();
938
- if (this.isAuthSuccess(data)) {
939
- const accessToken = this.getAccessToken(data);
940
- this.tokens.setAccessToken(accessToken);
941
- this.signinStatus.setSigninStatus(true);
942
- const rawUser = this.getUserFromResponse(data);
943
- if (rawUser) {
944
- const normalizedUser = this.normalizeUser(rawUser);
945
- this.updateUser(normalizedUser);
946
- return { success: true, user: normalizedUser };
947
- }
948
- return { success: true };
949
- }
950
- return {
951
- success: false,
952
- message: this.getErrorMessage(data, 'Invalid credentials')
953
- };
954
- }
955
- catch (error) {
956
- return {
957
- success: false,
958
- message: 'Network error. Please try again.'
959
- };
960
- }
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;
961
884
  }
962
- /**
963
- * Login with Google OAuth (popup window)
964
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
965
- */
966
885
  async loginWithGoogle(serverName) {
967
- return this.loginWithOAuth('google', serverName);
886
+ return this.loginWithProvider('google');
968
887
  }
969
- /**
970
- * Login with GitHub OAuth (popup window)
971
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
972
- */
973
888
  async loginWithGitHub(serverName) {
974
- return this.loginWithOAuth('github', serverName);
889
+ return this.loginWithProvider('github');
975
890
  }
976
- /**
977
- * Login with LinkedIn OAuth (popup window)
978
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
979
- */
980
891
  async loginWithLinkedIn(serverName) {
981
- return this.loginWithOAuth('linkedin', serverName);
892
+ return this.loginWithProvider('linkedin');
982
893
  }
983
- /**
984
- * Login with Apple OAuth (popup window)
985
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
986
- */
987
894
  async loginWithApple(serverName) {
988
- return this.loginWithOAuth('apple', serverName);
895
+ return this.loginWithProvider('apple');
989
896
  }
990
- /**
991
- * Login with Microsoft OAuth (popup window)
992
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
993
- */
994
897
  async loginWithMicrosoft(serverName) {
995
- return this.loginWithOAuth('microsoft', serverName);
898
+ return this.loginWithProvider('microsoft');
996
899
  }
997
- /**
998
- * Login with Zoho OAuth (popup window)
999
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
1000
- */
1001
900
  async loginWithZoho(serverName) {
1002
- return this.loginWithOAuth('zoho', serverName);
901
+ return this.loginWithProvider('zoho');
1003
902
  }
1004
- /**
1005
- * Generic provider-based login (supports all OAuth providers)
1006
- * @param provider - The provider identifier
1007
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
1008
- */
1009
903
  async loginWithProvider(provider, serverName) {
1010
904
  if (provider === 'emailPassword') {
1011
905
  throw new Error('Use loginWithEmail() for email/password authentication');
1012
906
  }
1013
- return this.loginWithOAuth(provider, serverName);
1014
- }
1015
- /**
1016
- * Generic OAuth login handler
1017
- * Opens popup window and listens for postMessage
1018
- * @param provider - OAuth provider name
1019
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
1020
- */
1021
- async loginWithOAuth(provider, serverName) {
1022
- return new Promise((resolve) => {
1023
- const width = 500;
1024
- const height = 600;
1025
- const left = (window.screen.width - width) / 2;
1026
- const top = (window.screen.height - height) / 2;
1027
- const accountsUrl = this.getAccountsUrl(serverName);
1028
- const oauthUrl = `${accountsUrl}/oauth/${provider}?` +
1029
- `platform=${this.environment.platformCode}&` +
1030
- `mode=popup`;
1031
- const popup = window.open(oauthUrl, `${provider}_login`, `width=${width},height=${height},left=${left},top=${top}`);
1032
- if (!popup) {
1033
- resolve({
1034
- success: false,
1035
- message: 'Popup blocked. Please allow popups for this site.'
1036
- });
1037
- return;
1038
- }
1039
- // Listen for message from popup
1040
- const messageHandler = (event) => {
1041
- // Verify origin
1042
- if (event.origin !== new URL(accountsUrl).origin) {
1043
- return;
1044
- }
1045
- if (event.data.type === 'oauth_success') {
1046
- this.tokens.setAccessToken(event.data.access_token);
1047
- this.signinStatus.setSigninStatus(true);
1048
- const rawUser = event.data.user || this.getUserFromResponse(event.data);
1049
- const normalizedUser = rawUser ? this.normalizeUser(rawUser) : undefined;
1050
- if (normalizedUser) {
1051
- this.updateUser(normalizedUser);
1052
- }
1053
- window.removeEventListener('message', messageHandler);
1054
- popup.close();
1055
- resolve({
1056
- success: true,
1057
- user: normalizedUser
1058
- });
1059
- }
1060
- else if (event.data.type === 'oauth_error') {
1061
- window.removeEventListener('message', messageHandler);
1062
- popup.close();
1063
- resolve({
1064
- success: false,
1065
- message: event.data.message || 'OAuth login failed'
1066
- });
1067
- }
1068
- };
1069
- window.addEventListener('message', messageHandler);
1070
- // Check if popup was closed manually
1071
- const checkClosed = setInterval(() => {
1072
- if (popup.closed) {
1073
- clearInterval(checkClosed);
1074
- window.removeEventListener('message', messageHandler);
1075
- resolve({
1076
- success: false,
1077
- message: 'Login cancelled'
1078
- });
1079
- }
1080
- }, 500);
1081
- });
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;
1082
914
  }
1083
- /**
1084
- * Register new user
1085
- * @param email - User email
1086
- * @param password - User password
1087
- * @param displayName - Display name
1088
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
1089
- */
1090
915
  async register(email, password, displayName, serverName) {
1091
- try {
1092
- const accountsUrl = this.getAccountsUrl(serverName);
1093
- const response = await fetch(`${accountsUrl}/api/auth/register`, {
1094
- method: 'POST',
1095
- headers: { 'Content-Type': 'application/json' },
1096
- credentials: 'include',
1097
- body: JSON.stringify({
1098
- email,
1099
- password,
1100
- display_name: displayName,
1101
- platform: this.environment.platformCode
1102
- })
1103
- });
1104
- const data = await response.json();
1105
- if (this.isAuthSuccess(data)) {
1106
- const accessToken = this.getAccessToken(data);
1107
- this.tokens.setAccessToken(accessToken);
1108
- this.signinStatus.setSigninStatus(true);
1109
- const rawUser = this.getUserFromResponse(data);
1110
- if (rawUser) {
1111
- const normalizedUser = this.normalizeUser(rawUser);
1112
- this.updateUser(normalizedUser);
1113
- return {
1114
- success: true,
1115
- user: normalizedUser,
1116
- message: data.needs_verification ? 'Please verify your email' : undefined
1117
- };
1118
- }
1119
- return { success: true };
1120
- }
1121
- return {
1122
- success: false,
1123
- message: this.getErrorMessage(data, 'Registration failed')
1124
- };
1125
- }
1126
- catch (error) {
1127
- return {
1128
- success: false,
1129
- message: 'Network error. Please try again.'
1130
- };
1131
- }
916
+ const result = await this.plugin.register(email, password, displayName);
917
+ if (result.success)
918
+ this.storeAuthResult(result);
919
+ return result;
1132
920
  }
1133
- /**
1134
- * Sign out user
1135
- * @param serverName - Optional: Specify which auth server to logout from (for multi-server mode)
1136
- */
1137
921
  async signout(serverName) {
1138
- try {
1139
- const refreshToken = this.tokens.getRefreshToken();
1140
- if (refreshToken) {
1141
- const accountsUrl = this.getAccountsUrl(serverName);
1142
- await fetch(`${accountsUrl}/api/auth/logout`, {
1143
- method: 'POST',
1144
- headers: {
1145
- 'Content-Type': 'application/json'
1146
- },
1147
- credentials: 'include',
1148
- body: JSON.stringify({
1149
- refresh_token: refreshToken
1150
- })
1151
- });
1152
- }
1153
- }
1154
- catch (error) {
1155
- console.error('Logout API call failed:', error);
1156
- }
1157
- finally {
1158
- this.tokens.clear();
1159
- this.signinStatus.setSigninStatus(false);
1160
- this.updateUser(null);
1161
- }
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);
1162
927
  }
1163
- /**
1164
- * Check for active session (call on app init)
1165
- * @param serverName - Optional: Specify which auth server to check (for multi-server mode)
1166
- */
1167
928
  async checkSession(serverName) {
1168
929
  if (this.tokens.hasValidAccessToken()) {
1169
930
  this.signinStatus.setSigninStatus(true);
1170
931
  return true;
1171
932
  }
1172
- // Try to refresh using httpOnly cookie
1173
- try {
1174
- const accountsUrl = this.getAccountsUrl(serverName);
1175
- const response = await fetch(`${accountsUrl}/api/auth/refresh`, {
1176
- method: 'POST',
1177
- credentials: 'include'
1178
- });
1179
- if (!response.ok) {
1180
- // Refresh failed (expired, revoked, wrong keypair) — clear stale tokens
1181
- // so the user gets a clean login page, not a broken retry loop
1182
- this.tokens.clear();
1183
- this.updateUser(null);
1184
- this.signinStatus.setSigninStatus(false);
1185
- return false;
1186
- }
1187
- const data = await response.json();
1188
- const accessToken = this.getAccessToken(data) ?? data.access_token;
1189
- if (accessToken) {
1190
- this.tokens.setAccessToken(accessToken);
1191
- const rawUser = this.getUserFromResponse(data) ?? data.user;
1192
- if (rawUser) {
1193
- this.updateUser(this.normalizeUser(rawUser));
1194
- }
1195
- this.signinStatus.setSigninStatus(true);
1196
- return true;
1197
- }
1198
- // Response OK but no token — clear stale state
1199
- this.tokens.clear();
1200
- this.updateUser(null);
1201
- return false;
1202
- }
1203
- catch (error) {
1204
- // Network error — clear stale state to avoid retry loops
1205
- this.tokens.clear();
1206
- this.updateUser(null);
1207
- this.signinStatus.setSigninStatus(false);
1208
- 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;
1209
940
  }
941
+ this.tokens.clear();
942
+ this.updateUser(null);
943
+ this.signinStatus.setSigninStatus(false);
944
+ return false;
1210
945
  }
1211
946
  /**
1212
- * Check if user is authenticated
1213
- */
1214
- isAuthenticated() {
1215
- return this.tokens.hasValidAccessToken();
1216
- }
1217
- /**
1218
- * Get current user (synchronous)
1219
- */
1220
- getCurrentUser() {
1221
- return this.userSubject.value;
1222
- }
1223
- // ===== Multi-Tenant Methods =====
1224
- /**
1225
- * Register a new user AND create a new tenant (organization)
1226
- * 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)
1227
949
  */
1228
- async registerTenant(data) {
1229
- try {
1230
- // If using OAuth, initiate OAuth flow first
1231
- if (data.provider !== 'emailPassword') {
1232
- return await this.registerTenantWithOAuth(data.tenantName, data.tenantSlug, data.provider);
1233
- }
1234
- // Email/password registration — route through platform API proxy
1235
- const apiUrl = this.getPlatformApiUrl();
1236
- const response = await fetch(`${apiUrl}/auth/register-tenant`, {
1237
- method: 'POST',
1238
- headers: { 'Content-Type': 'application/json' },
1239
- credentials: 'include',
1240
- body: JSON.stringify({
1241
- platform: this.environment.platformCode,
1242
- tenant_name: data.tenantName,
1243
- tenant_slug: data.tenantSlug,
1244
- display_name: data.displayName,
1245
- email: data.email,
1246
- password: data.password,
1247
- provider: 'emailPassword'
1248
- })
1249
- });
1250
- const result = await response.json();
1251
- if (this.isAuthSuccess(result)) {
1252
- const accessToken = this.getAccessToken(result);
1253
- if (accessToken) {
1254
- this.tokens.setAccessToken(accessToken);
1255
- this.signinStatus.setSigninStatus(true);
1256
- }
1257
- const rawUser = this.getUserFromResponse(result);
1258
- if (rawUser) {
1259
- this.updateUser(this.normalizeUser(rawUser));
1260
- }
1261
- }
1262
- return result;
1263
- }
1264
- catch (error) {
1265
- return {
1266
- success: false,
1267
- message: 'Network error. Please try again.'
1268
- };
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;
1269
955
  }
956
+ this.tokens.clear();
957
+ this.updateUser(null);
958
+ this.signinStatus.signedOut();
959
+ return false;
1270
960
  }
1271
- /**
1272
- * Register tenant with OAuth provider
1273
- * Opens popup window for OAuth flow
1274
- */
1275
- async registerTenantWithOAuth(tenantName, tenantSlug, provider) {
1276
- return new Promise((resolve) => {
1277
- const width = 500;
1278
- const height = 600;
1279
- const left = (window.screen.width - width) / 2;
1280
- const top = (window.screen.height - height) / 2;
1281
- // Build OAuth URL with tenant registration params
1282
- const accountsUrl = this.getAccountsUrl();
1283
- const oauthUrl = `${accountsUrl}/oauth/${provider}?` +
1284
- `platform=${this.environment.platformCode}&` +
1285
- `mode=popup&` +
1286
- `action=register_tenant&` +
1287
- `tenant_name=${encodeURIComponent(tenantName)}&` +
1288
- `tenant_slug=${encodeURIComponent(tenantSlug)}`;
1289
- const popup = window.open(oauthUrl, `${provider}_register_tenant`, `width=${width},height=${height},left=${left},top=${top}`);
1290
- if (!popup) {
1291
- resolve({
1292
- success: false,
1293
- message: 'Popup blocked. Please allow popups for this site.'
1294
- });
1295
- return;
1296
- }
1297
- // Listen for message from popup
1298
- const messageHandler = (event) => {
1299
- // Verify origin
1300
- if (event.origin !== new URL(accountsUrl).origin) {
1301
- return;
1302
- }
1303
- if (event.data.type === 'tenant_register_success') {
1304
- if (event.data.access_token) {
1305
- this.tokens.setAccessToken(event.data.access_token);
1306
- this.signinStatus.setSigninStatus(true);
1307
- }
1308
- const rawUser = event.data.user || this.getUserFromResponse(event.data);
1309
- if (rawUser) {
1310
- this.updateUser(this.normalizeUser(rawUser));
1311
- }
1312
- window.removeEventListener('message', messageHandler);
1313
- popup.close();
1314
- resolve({
1315
- success: true,
1316
- tenant: event.data.tenant,
1317
- user: event.data.user
1318
- });
1319
- }
1320
- else if (event.data.type === 'tenant_register_error') {
1321
- window.removeEventListener('message', messageHandler);
1322
- popup.close();
1323
- resolve({
1324
- success: false,
1325
- message: event.data.message || 'Tenant registration failed'
1326
- });
1327
- }
1328
- };
1329
- window.addEventListener('message', messageHandler);
1330
- // Check if popup was closed manually
1331
- const checkClosed = setInterval(() => {
1332
- if (popup.closed) {
1333
- clearInterval(checkClosed);
1334
- window.removeEventListener('message', messageHandler);
1335
- resolve({
1336
- success: false,
1337
- message: 'Registration cancelled'
1338
- });
1339
- }
1340
- }, 500);
1341
- });
1342
- }
1343
- /**
1344
- * Get all tenant memberships for the authenticated user
1345
- * @param serverName - Optional: Specify which auth server to query (for multi-server mode)
1346
- */
1347
- async getTenantMemberships(serverName) {
1348
- try {
1349
- const accountsUrl = this.getAccountsUrl(serverName);
1350
- const response = await fetch(`${accountsUrl}/api/auth/memberships`, {
1351
- method: 'GET',
1352
- headers: {
1353
- 'Authorization': `Bearer ${this.tokens.getAccessToken()}`,
1354
- 'Content-Type': 'application/json'
1355
- },
1356
- credentials: 'include'
1357
- });
1358
- const data = await response.json();
1359
- return {
1360
- memberships: data.memberships || []
1361
- };
1362
- }
1363
- catch (error) {
1364
- return { memberships: [] };
1365
- }
961
+ isAuthenticated() {
962
+ return this.tokens.hasValidAccessToken();
1366
963
  }
1367
- /**
1368
- * Select a tenant for the current session
1369
- * Updates the JWT token with tenant context
1370
- * @param tenantId - Tenant ID to select
1371
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
1372
- */
1373
- async selectTenant(tenantId, serverName) {
1374
- try {
1375
- const accountsUrl = this.getAccountsUrl(serverName);
1376
- const response = await fetch(`${accountsUrl}/api/auth/select-tenant`, {
1377
- method: 'POST',
1378
- headers: {
1379
- 'Authorization': `Bearer ${this.tokens.getAccessToken()}`,
1380
- 'Content-Type': 'application/json'
1381
- },
1382
- credentials: 'include',
1383
- body: JSON.stringify({ tenant_id: tenantId })
1384
- });
1385
- const data = await response.json();
1386
- if (this.isAuthSuccess(data)) {
1387
- const accessToken = this.getAccessToken(data);
1388
- this.tokens.setAccessToken(accessToken);
1389
- return {
1390
- success: true,
1391
- access_token: accessToken
1392
- };
1393
- }
1394
- return {
1395
- success: false,
1396
- message: this.getErrorMessage(data, 'Failed to select tenant')
1397
- };
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' };
1398
971
  }
1399
- catch (error) {
1400
- return {
1401
- success: false,
1402
- message: 'Network error. Please try again.'
1403
- };
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);
1404
980
  }
981
+ return result;
1405
982
  }
1406
- /**
1407
- * Check if a tenant slug is available
1408
- * @param slug - Tenant slug to check
1409
- * @param serverName - Optional: Specify which auth server to query (for multi-server mode)
1410
- */
1411
- async checkTenantSlugAvailable(slug, serverName) {
1412
- try {
1413
- const accountsUrl = this.getAccountsUrl(serverName);
1414
- const response = await fetch(`${accountsUrl}/api/auth/check-tenant-slug/${slug}`, {
1415
- method: 'GET',
1416
- headers: { 'Content-Type': 'application/json' }
1417
- });
1418
- const data = await response.json();
1419
- return {
1420
- available: data.available || false,
1421
- suggestion: data.suggestion
1422
- };
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' };
1423
992
  }
1424
- catch (error) {
1425
- // 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)
1426
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);
1427
1016
  }
1017
+ return result;
1428
1018
  }
1429
- // ===== Backward Compatibility Methods =====
1430
- // These methods are deprecated and maintained for backward compatibility
1431
- // with existing platform code. New code should use the methods above.
1432
- /**
1433
- * @deprecated Use getCurrentUser()?.user_id instead
1434
- */
1435
- getUserId() {
1436
- return this.userSubject.value?.user_id || 0;
1019
+ // ── Multi-server (delegated to plugin) ────────────────────────────────────
1020
+ getAvailableAuthServers() {
1021
+ return this.plugin.getAvailableServers?.() ?? [];
1437
1022
  }
1438
- /**
1439
- * @deprecated Use getCurrentUser()?.display_name instead
1440
- */
1441
- getUserName() {
1442
- return this.userSubject.value?.display_name || '';
1023
+ getActiveAuthServer() {
1024
+ return this.plugin.getActiveServer?.() ?? null;
1443
1025
  }
1444
- /**
1445
- * @deprecated Use getCurrentUser()?.photo_url instead
1446
- */
1447
- getPhotoUrl() {
1448
- 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);
1449
1031
  }
1450
- /**
1451
- * @deprecated Use getCurrentUser()?.display_name instead
1452
- */
1453
- getDisplayName() {
1454
- return this.userSubject.value?.display_name || '';
1032
+ getAuthServerConfig(serverName) {
1033
+ return this.plugin.getServerConfig?.(serverName) ?? null;
1455
1034
  }
1456
- /**
1457
- * @deprecated Use `/profile/${getCurrentUser()?.user_id}` instead
1458
- */
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 */
1459
1048
  getProfileUrl() {
1460
1049
  const userId = this.userSubject.value?.user_id;
1461
1050
  return userId ? `/profile/${userId}` : '';
1462
1051
  }
1463
- /**
1464
- * @deprecated Use isAuthenticated() instead
1465
- */
1466
- async signin() {
1467
- return this.isAuthenticated();
1468
- }
1469
- /**
1470
- * @deprecated Use loginWithEmail() instead
1471
- */
1052
+ /** @deprecated Use isAuthenticated() instead */
1053
+ async signin() { return this.isAuthenticated(); }
1054
+ /** @deprecated Use loginWithEmail() instead */
1472
1055
  async verifyCredentials(email, password) {
1473
1056
  const result = await this.loginWithEmail(email, password);
1474
1057
  return result.success;
1475
1058
  }
1476
- /**
1477
- * @deprecated Check user.is_email_verified from getCurrentUser() instead
1478
- */
1479
- isSigninEmailValid() {
1480
- return this.userSubject.value?.is_email_verified || false;
1481
- }
1482
- /**
1483
- * @deprecated No longer needed - dialog is managed by platform
1484
- */
1485
- onDialogClose() {
1486
- // 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;
1487
1071
  }
1488
- /**
1489
- * @deprecated No longer needed - dialog is managed by platform
1490
- */
1491
- closeSocialAuthDialog() {
1492
- // 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;
1493
1097
  }
1494
- /**
1495
- * @deprecated Check if user exists by calling /api/auth/check-email endpoint
1496
- */
1497
- async getUserProfile(email, serverName) {
1098
+ async request(url, options, data) {
1498
1099
  try {
1499
- const accountsUrl = this.getAccountsUrl(serverName);
1500
- const response = await fetch(`${accountsUrl}/api/auth/check-email`, {
1501
- method: 'POST',
1502
- headers: { 'Content-Type': 'application/json' },
1503
- body: JSON.stringify({ email })
1504
- });
1505
- const data = await response.json();
1506
- 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);
1507
1117
  }
1508
1118
  catch (error) {
1509
- return null;
1119
+ return this.handleError(error);
1510
1120
  }
1511
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
+ }
1512
1185
  /**
1513
- * Check if user has completed onboarding (has a tenant)
1514
- * @param identityId - User identity ID
1515
- * @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.
1516
1188
  */
1517
- async checkOnboardingStatus(identityId, serverName) {
1518
- try {
1519
- const accountsUrl = this.getAccountsUrl(serverName);
1520
- const response = await fetch(`${accountsUrl}/api/auth/onboarding/status?platform_code=${this.environment.platformCode}&identity_id=${identityId}`, {
1521
- method: 'GET',
1522
- headers: { 'Content-Type': 'application/json' },
1523
- credentials: 'include'
1524
- });
1525
- if (!response.ok) {
1526
- 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]));
1527
1199
  }
1528
- return await response.json();
1529
- }
1530
- catch (error) {
1531
- throw error;
1532
1200
  }
1201
+ const str = array.join('&');
1202
+ return str ? '?' + str : '';
1533
1203
  }
1534
1204
  /**
1535
- * Complete tenant onboarding (create tenant with country + org name)
1536
- * @param countryCode - Country code
1537
- * @param tenantName - Tenant organization name
1538
- * @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
1539
1207
  */
1540
- async completeTenantOnboarding(countryCode, tenantName, serverName) {
1541
- try {
1542
- const accessToken = this.tokens.getAccessToken();
1543
- if (!accessToken) {
1544
- throw new Error('Not authenticated');
1545
- }
1546
- // Route through platform API proxy — PHP API adds platform_secret header
1547
- const apiUrl = this.getPlatformApiUrl();
1548
- const response = await fetch(`${apiUrl}/auth/register-tenant`, {
1549
- method: 'POST',
1550
- headers: {
1551
- 'Content-Type': 'application/json',
1552
- 'Authorization': `Bearer ${accessToken}`
1553
- },
1554
- credentials: 'include',
1555
- body: JSON.stringify({
1556
- platform: this.environment.platformCode,
1557
- tenant_name: tenantName,
1558
- country_code: countryCode,
1559
- provider: 'google', // Assuming OAuth
1560
- oauth_token: accessToken
1561
- })
1562
- });
1563
- if (!response.ok) {
1564
- const errorData = await response.json();
1565
- throw new Error(errorData.message || 'Failed to create tenant');
1566
- }
1567
- const data = await response.json();
1568
- // Update tokens with new tenant-scoped tokens
1569
- if (data.access_token) {
1570
- this.tokens.setAccessToken(data.access_token);
1571
- this.signinStatus.setSigninStatus(true);
1572
- }
1573
- return data;
1574
- }
1575
- catch (error) {
1576
- throw error;
1577
- }
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);
1578
1223
  }
1579
- 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 });
1580
- 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' });
1581
1226
  }
1582
- 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: [{
1583
1228
  type: Injectable,
1584
1229
  args: [{
1585
1230
  providedIn: 'root'
1586
1231
  }]
1587
- }], ctorParameters: () => [{ type: TokenService }, { type: SigninStatusService }, { type: MyEnvironmentModel, decorators: [{
1588
- type: Inject,
1589
- args: [MyEnvironmentModel]
1590
- }] }] });
1232
+ }], ctorParameters: () => [{ type: TokenService }, { type: SigninStatusService }, { type: MyEnvironmentModel }, { type: AuthService }] });
1591
1233
 
1592
1234
  class DbService {
1593
1235
  constructor() { }
1594
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DbService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1595
- 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' });
1596
1238
  }
1597
- 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: [{
1598
1240
  type: Injectable,
1599
1241
  args: [{
1600
1242
  providedIn: 'root'
1601
1243
  }]
1602
1244
  }], ctorParameters: () => [] });
1603
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
+
1604
1290
  /**
1605
1291
  * Service for interacting with the stonescriptphp-files server.
1606
1292
  * Handles file upload, download, list, and delete operations
@@ -1855,10 +1541,10 @@ class FilesService {
1855
1541
  return false;
1856
1542
  }
1857
1543
  }
1858
- 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 });
1859
- 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' });
1860
1546
  }
1861
- 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: [{
1862
1548
  type: Injectable,
1863
1549
  args: [{
1864
1550
  providedIn: 'root'
@@ -2034,10 +1720,10 @@ class ProviderRegistryService {
2034
1720
  }
2035
1721
  return Object.keys(styles).length > 0 ? styles : null;
2036
1722
  }
2037
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ProviderRegistryService, deps: [{ token: MyEnvironmentModel }], target: i0.ɵɵFactoryTarget.Injectable });
2038
- 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' });
2039
1725
  }
2040
- 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: [{
2041
1727
  type: Injectable,
2042
1728
  args: [{
2043
1729
  providedIn: 'root'
@@ -2047,29 +1733,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
2047
1733
  args: [MyEnvironmentModel]
2048
1734
  }] }] });
2049
1735
 
2050
- class NgxStoneScriptPhpClientModule {
2051
- static forRoot(environment) {
2052
- return {
2053
- ngModule: NgxStoneScriptPhpClientModule,
2054
- providers: [
2055
- { provide: MyEnvironmentModel, useValue: environment }
2056
- ]
2057
- };
2058
- }
2059
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: NgxStoneScriptPhpClientModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
2060
- static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.15", ngImport: i0, type: NgxStoneScriptPhpClientModule, imports: [CommonModule] });
2061
- static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: NgxStoneScriptPhpClientModule, imports: [CommonModule] });
2062
- }
2063
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: NgxStoneScriptPhpClientModule, decorators: [{
2064
- type: NgModule,
2065
- args: [{
2066
- declarations: [],
2067
- imports: [
2068
- CommonModule
2069
- ]
2070
- }]
2071
- }] });
2072
-
2073
1736
  class TenantLoginComponent {
2074
1737
  auth;
2075
1738
  providerRegistry;
@@ -2297,8 +1960,8 @@ class TenantLoginComponent {
2297
1960
  event.preventDefault();
2298
1961
  this.createTenant.emit();
2299
1962
  }
2300
- 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 });
2301
- 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: `
2302
1965
  <div class="tenant-login-dialog">
2303
1966
  @if (!showingTenantSelector) {
2304
1967
  <!-- Step 1: Authentication -->
@@ -2466,7 +2129,7 @@ class TenantLoginComponent {
2466
2129
  </div>
2467
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"] }] });
2468
2131
  }
2469
- 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: [{
2470
2133
  type: Component,
2471
2134
  args: [{ selector: 'lib-tenant-login', standalone: true, imports: [CommonModule, FormsModule], template: `
2472
2135
  <div class="tenant-login-dialog">
@@ -2771,8 +2434,8 @@ class RegisterComponent {
2771
2434
  this.confirmPassword = '';
2772
2435
  this.displayName = '';
2773
2436
  }
2774
- 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 });
2775
- 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: `
2776
2439
  <div class="register-dialog">
2777
2440
  <h2 class="register-title">Create Account</h2>
2778
2441
 
@@ -2901,7 +2564,7 @@ class RegisterComponent {
2901
2564
  </div>
2902
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"] }] });
2903
2566
  }
2904
- 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: [{
2905
2568
  type: Component,
2906
2569
  args: [{ selector: 'lib-register', standalone: true, imports: [CommonModule, FormsModule], template: `
2907
2570
  <div class="register-dialog">
@@ -3087,8 +2750,8 @@ class AuthPageComponent {
3087
2750
  (B < 255 ? B < 1 ? 0 : B : 255))
3088
2751
  .toString(16).slice(1);
3089
2752
  }
3090
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AuthPageComponent, deps: [{ token: MyEnvironmentModel }], target: i0.ɵɵFactoryTarget.Component });
3091
- 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: `
3092
2755
  <div class="auth-container" [style.background]="gradientStyle">
3093
2756
  <div class="auth-card">
3094
2757
  @if (logo) {
@@ -3115,7 +2778,7 @@ class AuthPageComponent {
3115
2778
  </div>
3116
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"] }] });
3117
2780
  }
3118
- 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: [{
3119
2782
  type: Component,
3120
2783
  args: [{ selector: 'lib-auth-page', standalone: true, imports: [CommonModule, TenantLoginComponent, RegisterComponent], template: `
3121
2784
  <div class="auth-container" [style.background]="gradientStyle">
@@ -3238,8 +2901,8 @@ class LoginDialogComponent {
3238
2901
  // For now, just emit a console message
3239
2902
  console.log('Register clicked - platform should handle navigation');
3240
2903
  }
3241
- 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 });
3242
- 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: `
3243
2906
  <div class="login-dialog">
3244
2907
  <h2 class="login-title">Sign In</h2>
3245
2908
 
@@ -3329,7 +2992,7 @@ class LoginDialogComponent {
3329
2992
  </div>
3330
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"] }] });
3331
2994
  }
3332
- 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: [{
3333
2996
  type: Component,
3334
2997
  args: [{ selector: 'lib-login-dialog', standalone: true, imports: [CommonModule, FormsModule], template: `
3335
2998
  <div class="login-dialog">
@@ -3584,7 +3247,6 @@ class TenantRegisterComponent {
3584
3247
  try {
3585
3248
  const result = await this.auth.registerTenant({
3586
3249
  tenantName: this.tenantName,
3587
- tenantSlug: this.tenantSlug,
3588
3250
  provider: provider
3589
3251
  });
3590
3252
  if (result.success && result.tenant && result.user) {
@@ -3617,7 +3279,6 @@ class TenantRegisterComponent {
3617
3279
  try {
3618
3280
  const result = await this.auth.registerTenant({
3619
3281
  tenantName: this.tenantName,
3620
- tenantSlug: this.tenantSlug,
3621
3282
  displayName: this.displayName,
3622
3283
  email: this.email,
3623
3284
  password: this.password,
@@ -3645,8 +3306,8 @@ class TenantRegisterComponent {
3645
3306
  event.preventDefault();
3646
3307
  this.navigateToLogin.emit();
3647
3308
  }
3648
- 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 });
3649
- 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: `
3650
3311
  <div class="tenant-register-dialog">
3651
3312
  <h2 class="register-title">{{ title }}</h2>
3652
3313
 
@@ -3856,7 +3517,7 @@ class TenantRegisterComponent {
3856
3517
  </div>
3857
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"] }] });
3858
3519
  }
3859
- 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: [{
3860
3521
  type: Component,
3861
3522
  args: [{ selector: 'lib-tenant-register', standalone: true, imports: [CommonModule, FormsModule], template: `
3862
3523
  <div class="tenant-register-dialog">
@@ -4156,8 +3817,8 @@ class TenantLoginDialogComponent {
4156
3817
  this.dialogRef.close({ action: 'create_tenant' });
4157
3818
  }
4158
3819
  }
4159
- 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 });
4160
- 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: `
4161
3822
  <div class="dialog-wrapper">
4162
3823
  <lib-tenant-login
4163
3824
  [title]="data?.title || 'Sign In'"
@@ -4178,7 +3839,7 @@ class TenantLoginDialogComponent {
4178
3839
  </div>
4179
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"] }] });
4180
3841
  }
4181
- 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: [{
4182
3843
  type: Component,
4183
3844
  args: [{ selector: 'lib-tenant-login-dialog', standalone: true, imports: [CommonModule, TenantLoginComponent], template: `
4184
3845
  <div class="dialog-wrapper">
@@ -4259,8 +3920,8 @@ class TenantRegisterDialogComponent {
4259
3920
  this.dialogRef.close({ action: 'navigate_to_login' });
4260
3921
  }
4261
3922
  }
4262
- 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 });
4263
- 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: `
4264
3925
  <div class="dialog-wrapper">
4265
3926
  <lib-tenant-register
4266
3927
  [title]="data?.title || 'Create New Organization'"
@@ -4286,7 +3947,7 @@ class TenantRegisterDialogComponent {
4286
3947
  </div>
4287
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"] }] });
4288
3949
  }
4289
- 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: [{
4290
3951
  type: Component,
4291
3952
  args: [{ selector: 'lib-tenant-register-dialog', standalone: true, imports: [CommonModule, TenantRegisterComponent], template: `
4292
3953
  <div class="dialog-wrapper">
@@ -4328,10 +3989,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
4328
3989
  /*
4329
3990
  * Public API Surface of ngx-stonescriptphp-client
4330
3991
  */
3992
+ // ── Core setup ────────────────────────────────────────────────────────────────
4331
3993
 
4332
3994
  /**
4333
3995
  * Generated bundle index. Do not edit.
4334
3996
  */
4335
3997
 
4336
- 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 };
4337
3999
  //# sourceMappingURL=progalaxyelabs-ngx-stonescriptphp-client.mjs.map