@progalaxyelabs/ngx-stonescriptphp-client 1.11.1 → 1.14.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,819 @@ 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) {
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();
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: { 'Content-Type': 'application/json' },
493
+ credentials: 'include',
494
+ body: JSON.stringify(body)
495
+ });
496
+ const result = await response.json();
497
+ if (result?.status === 'ok' || result?.success === true) {
498
+ return { success: true };
730
499
  }
500
+ return { success: false, message: result?.data?.message || result?.message || 'Registration failed' };
731
501
  }
732
- catch (error) {
733
- console.error('Failed to restore active auth server:', error);
734
- this.activeAuthServer = this.getDefaultAuthServer();
502
+ catch {
503
+ return { success: false, message: 'Network error. Please try again.' };
735
504
  }
736
505
  }
737
- /**
738
- * Save active auth server to localStorage
739
- */
740
- saveActiveAuthServer(serverName) {
506
+ async registerTenantWithOAuth(tenantName, provider) {
507
+ return new Promise((resolve) => {
508
+ const width = 500, height = 600;
509
+ const left = (window.screen.width - width) / 2;
510
+ const top = (window.screen.height - height) / 2;
511
+ const accountsUrl = this.getAccountsUrl();
512
+ const oauthUrl = `${accountsUrl}/oauth/${provider}?` +
513
+ `platform=${this.config.platformCode}&mode=popup&action=register_tenant&` +
514
+ `tenant_name=${encodeURIComponent(tenantName)}`;
515
+ const popup = window.open(oauthUrl, `${provider}_register_tenant`, `width=${width},height=${height},left=${left},top=${top}`);
516
+ if (!popup) {
517
+ resolve({ success: false, message: 'Popup blocked. Please allow popups for this site.' });
518
+ return;
519
+ }
520
+ const messageHandler = (event) => {
521
+ if (event.origin !== new URL(accountsUrl).origin)
522
+ return;
523
+ if (event.data.type === 'tenant_register_success') {
524
+ window.removeEventListener('message', messageHandler);
525
+ popup.close();
526
+ resolve({
527
+ success: true,
528
+ accessToken: event.data.access_token,
529
+ user: event.data.user ? this.normalizeUser(event.data.user) : undefined
530
+ });
531
+ }
532
+ else if (event.data.type === 'tenant_register_error') {
533
+ window.removeEventListener('message', messageHandler);
534
+ popup.close();
535
+ resolve({ success: false, message: event.data.message || 'Tenant registration failed' });
536
+ }
537
+ };
538
+ window.addEventListener('message', messageHandler);
539
+ const checkClosed = setInterval(() => {
540
+ if (popup.closed) {
541
+ clearInterval(checkClosed);
542
+ window.removeEventListener('message', messageHandler);
543
+ resolve({ success: false, message: 'Registration cancelled' });
544
+ }
545
+ }, 500);
546
+ });
547
+ }
548
+ async checkTenantSlugAvailable(slug) {
741
549
  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);
550
+ const accountsUrl = this.getAccountsUrl();
551
+ const response = await fetch(`${accountsUrl}/api/auth/check-tenant-slug/${slug}`, {
552
+ method: 'GET',
553
+ headers: { 'Content-Type': 'application/json' }
554
+ });
555
+ const data = await response.json();
556
+ return { available: data.available || false, suggestion: data.suggestion };
747
557
  }
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 [];
558
+ catch {
559
+ return { available: true };
756
560
  }
757
- return Object.keys(this.environment.authServers);
758
561
  }
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;
562
+ async checkOnboardingStatus(identityId, platformCode) {
563
+ const accountsUrl = this.getAccountsUrl();
564
+ const platform = platformCode ?? this.config.platformCode ?? '';
565
+ const response = await fetch(`${accountsUrl}/api/auth/onboarding/status?platform_code=${platform}&identity_id=${identityId}`, { method: 'GET', headers: { 'Content-Type': 'application/json' }, credentials: 'include' });
566
+ if (!response.ok)
567
+ throw new Error('Failed to check onboarding status');
568
+ return response.json();
765
569
  }
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`);
570
+ async completeTenantOnboarding(countryCode, tenantName, accessToken) {
571
+ const apiUrl = this.getPlatformApiUrl();
572
+ const response = await fetch(`${apiUrl}/auth/register-tenant`, {
573
+ method: 'POST',
574
+ headers: {
575
+ 'Content-Type': 'application/json',
576
+ 'Authorization': `Bearer ${accessToken}`
577
+ },
578
+ credentials: 'include',
579
+ body: JSON.stringify({
580
+ platform: this.config.platformCode,
581
+ tenant_name: tenantName,
582
+ country_code: countryCode,
583
+ provider: 'google',
584
+ oauth_token: accessToken
585
+ })
586
+ });
587
+ if (!response.ok) {
588
+ const errorData = await response.json();
589
+ throw new Error(errorData.message || 'Failed to create tenant');
777
590
  }
778
- this.saveActiveAuthServer(serverName);
591
+ return response.json();
779
592
  }
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;
593
+ async checkEmail(email) {
594
+ try {
595
+ const accountsUrl = this.getAccountsUrl();
596
+ const response = await fetch(`${accountsUrl}/api/auth/check-email`, {
597
+ method: 'POST',
598
+ headers: { 'Content-Type': 'application/json' },
599
+ body: JSON.stringify({ email })
600
+ });
601
+ const data = await response.json();
602
+ return { exists: data.exists, user: data.user };
787
603
  }
788
- const targetServer = serverName || this.activeAuthServer || this.getDefaultAuthServer();
789
- if (!targetServer) {
790
- return null;
604
+ catch {
605
+ return { exists: false };
791
606
  }
792
- return this.environment.authServers[targetServer] || null;
793
607
  }
794
- /**
795
- * Check if multi-server mode is enabled
796
- */
797
- isMultiServerMode() {
798
- return !!(this.environment.authServers && Object.keys(this.environment.authServers).length > 0);
608
+ }
609
+
610
+ /**
611
+ * Configure the ngx-stonescriptphp-client library.
612
+ *
613
+ * @param environment - Library configuration (API server, auth settings, etc.)
614
+ * @param plugin - Optional auth plugin override. Defaults to StoneScriptPHPAuth.
615
+ * Provide your own plugin to use Firebase, progalaxyelabs-auth, Okta, or any other auth backend.
616
+ *
617
+ * @example Default (StoneScriptPHP backend)
618
+ * ```typescript
619
+ * // app.config.ts
620
+ * export const appConfig: ApplicationConfig = {
621
+ * providers: [
622
+ * provideNgxStoneScriptPhpClient(environment)
623
+ * ]
624
+ * };
625
+ * ```
626
+ *
627
+ * @example External auth plugin
628
+ * ```typescript
629
+ * import { ProgalaxyElabsAuth } from './progalaxyelabs-auth.auth-plugin';
630
+ *
631
+ * providers: [
632
+ * provideNgxStoneScriptPhpClient(environment, new ProgalaxyElabsAuth({ host: '...' }))
633
+ * ]
634
+ * ```
635
+ *
636
+ * @example Firebase
637
+ * ```typescript
638
+ * import { FirebaseAuthPlugin } from './firebase-auth.auth-plugin';
639
+ *
640
+ * providers: [
641
+ * provideNgxStoneScriptPhpClient(environment, new FirebaseAuthPlugin(firebaseConfig))
642
+ * ]
643
+ * ```
644
+ */
645
+ function provideNgxStoneScriptPhpClient(environment, plugin) {
646
+ const resolvedPlugin = plugin ?? new StoneScriptPHPAuth({
647
+ // Resolve auth host: auth.host → accountsServer.host (compat) → accountsUrl (compat) → apiServer.host
648
+ host: environment.auth?.host
649
+ || environment.accountsServer?.host
650
+ || environment.accountsUrl
651
+ || environment.apiServer.host,
652
+ platformCode: environment.platformCode,
653
+ authServers: environment.authServers,
654
+ responseMap: environment.auth?.responseMap ?? environment.authResponseMap,
655
+ auth: environment.auth,
656
+ apiUrl: environment.apiUrl
657
+ });
658
+ return makeEnvironmentProviders([
659
+ { provide: MyEnvironmentModel, useValue: environment },
660
+ { provide: AUTH_PLUGIN, useValue: resolvedPlugin }
661
+ ]);
662
+ }
663
+
664
+ class ApiResponse {
665
+ status;
666
+ data;
667
+ message;
668
+ get success() {
669
+ return this.status === 'ok';
799
670
  }
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;
671
+ get errors() {
672
+ return this.message ? [this.message] : [];
673
+ }
674
+ constructor(status, data = null, message = '') {
675
+ this.status = status;
676
+ this.data = data || null;
677
+ this.message = message;
678
+ }
679
+ onOk(callback) {
680
+ if (this.status === 'ok') {
681
+ callback(this.data);
808
682
  }
809
- if (this.environment.apiServer?.host) {
810
- return this.environment.apiServer.host;
683
+ return this;
684
+ }
685
+ onNotOk(callback) {
686
+ if (this.status === 'not ok') {
687
+ callback(this.message, this.data);
811
688
  }
812
- throw new Error('No platform API URL configured. Set apiUrl in environment config.');
689
+ return this;
813
690
  }
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
- };
691
+ onError(callback) {
692
+ if (this.status === 'error') {
693
+ callback();
694
+ }
695
+ return this;
825
696
  }
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);
697
+ isSuccess() {
698
+ return this.status === 'ok';
829
699
  }
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');
700
+ isError() {
701
+ return this.status === 'error' || this.status === 'not ok';
702
+ }
703
+ getData() {
704
+ return this.data || null;
705
+ }
706
+ getError() {
707
+ return this.message || 'Unknown error';
708
+ }
709
+ getStatus() {
710
+ return this.status;
711
+ }
712
+ getMessage() {
713
+ return this.message;
714
+ }
715
+ }
716
+
717
+ class TokenService {
718
+ accessToken = '';
719
+ refreshToken = '';
720
+ lsAccessTokenKey = 'progalaxyapi_access_token';
721
+ lsRefreshTokenKey = 'progalaxyapi_refresh_token';
722
+ constructor() { }
723
+ setTokens(accessToken, refreshToken) {
724
+ this.accessToken = accessToken;
725
+ this.refreshToken = refreshToken;
726
+ localStorage.setItem(this.lsAccessTokenKey, accessToken);
727
+ localStorage.setItem(this.lsRefreshTokenKey, refreshToken);
728
+ }
729
+ setAccessToken(accessToken) {
730
+ this.accessToken = accessToken;
731
+ localStorage.setItem(this.lsAccessTokenKey, accessToken);
732
+ }
733
+ setRefreshToken(refreshToken) {
734
+ this.refreshToken = refreshToken;
735
+ localStorage.setItem(this.lsRefreshTokenKey, refreshToken);
736
+ }
737
+ getAccessToken() {
738
+ if (this.accessToken) {
739
+ return this.accessToken;
740
+ }
741
+ const storedAccessToken = localStorage.getItem(this.lsAccessTokenKey);
742
+ if (storedAccessToken) {
743
+ return storedAccessToken;
744
+ }
745
+ else {
746
+ return '';
835
747
  }
836
- // No successPath configured — success = access token present
837
- return !!this.resolvePath(data, map.accessTokenPath);
838
748
  }
839
- /** Extract access token from auth response */
840
- getAccessToken(data) {
841
- return this.resolvePath(data, this.responseMap.accessTokenPath);
749
+ getRefreshToken() {
750
+ if (this.refreshToken) {
751
+ return this.refreshToken;
752
+ }
753
+ const storedRefreshToken = localStorage.getItem(this.lsRefreshTokenKey);
754
+ if (storedRefreshToken) {
755
+ return storedRefreshToken;
756
+ }
757
+ else {
758
+ return '';
759
+ }
842
760
  }
843
- /** Extract refresh token from auth response */
844
- getRefreshToken(data) {
845
- return this.resolvePath(data, this.responseMap.refreshTokenPath);
761
+ clear() {
762
+ this.accessToken = '';
763
+ this.refreshToken = '';
764
+ localStorage.removeItem(this.lsAccessTokenKey);
765
+ localStorage.removeItem(this.lsRefreshTokenKey);
846
766
  }
847
- /** Extract user/identity object from auth response */
848
- getUserFromResponse(data) {
849
- return this.resolvePath(data, this.responseMap.userPath);
767
+ /**
768
+ * Check if there is a non-empty access token.
769
+ * Token is treated as opaque — validity is determined by the auth server.
770
+ */
771
+ hasValidAccessToken() {
772
+ const token = this.getAccessToken();
773
+ return token !== null && token !== '';
850
774
  }
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;
775
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TokenService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
776
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TokenService, providedIn: 'root' });
777
+ }
778
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TokenService, decorators: [{
779
+ type: Injectable,
780
+ args: [{
781
+ providedIn: 'root'
782
+ }]
783
+ }], ctorParameters: () => [] });
784
+
785
+ /**
786
+ * @deprecated Use boolean directly. Kept for backward compatibility.
787
+ */
788
+ var VerifyStatus;
789
+ (function (VerifyStatus) {
790
+ VerifyStatus["initialized"] = "initialized";
791
+ VerifyStatus["yes"] = "yes";
792
+ VerifyStatus["no"] = "no";
793
+ })(VerifyStatus || (VerifyStatus = {}));
794
+ class SigninStatusService {
795
+ status;
796
+ constructor() {
797
+ this.status = new BehaviorSubject(false);
855
798
  }
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
- };
799
+ signedOut() {
800
+ this.status.next(false);
801
+ }
802
+ signedIn() {
803
+ this.status.next(true);
866
804
  }
867
805
  /**
868
- * Hash UUID to numeric ID for backward compatibility
869
- * Converts UUID string to a consistent numeric ID for legacy code
806
+ * Set signin status
807
+ * @param isSignedIn - True if user is signed in, false otherwise
870
808
  */
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);
809
+ setSigninStatus(isSignedIn) {
810
+ this.status.next(isSignedIn);
811
+ }
812
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: SigninStatusService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
813
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: SigninStatusService, providedIn: 'root' });
814
+ }
815
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: SigninStatusService, decorators: [{
816
+ type: Injectable,
817
+ args: [{
818
+ providedIn: 'root'
819
+ }]
820
+ }], ctorParameters: () => [] });
821
+
822
+ /**
823
+ * AuthService — manages auth state and delegates all auth operations to the AuthPlugin.
824
+ *
825
+ * This service holds user state (via BehaviorSubject) and tokens (via TokenService).
826
+ * It does not make any HTTP calls directly — all auth logic lives in the plugin.
827
+ *
828
+ * Provide a plugin via provideNgxStoneScriptPhpClient():
829
+ * - Default: StoneScriptPHPAuth (built-in, matches StoneScriptPHP backend)
830
+ * - External: any class implementing AuthPlugin (Firebase, progalaxyelabs-auth, Okta, etc.)
831
+ */
832
+ class AuthService {
833
+ plugin;
834
+ tokens;
835
+ signinStatus;
836
+ USER_STORAGE_KEY = 'progalaxyapi_user';
837
+ userSubject = new BehaviorSubject(null);
838
+ user$ = this.userSubject.asObservable();
839
+ constructor(plugin, tokens, signinStatus) {
840
+ this.plugin = plugin;
841
+ this.tokens = tokens;
842
+ this.signinStatus = signinStatus;
843
+ this.restoreUser();
879
844
  }
880
- /**
881
- * Restore user from localStorage
882
- */
845
+ // ── State management ──────────────────────────────────────────────────────
883
846
  restoreUser() {
884
847
  try {
885
848
  const userJson = localStorage.getItem(this.USER_STORAGE_KEY);
886
- if (userJson) {
887
- const user = JSON.parse(userJson);
888
- this.updateUser(user);
889
- }
849
+ if (userJson)
850
+ this.updateUser(JSON.parse(userJson));
890
851
  }
891
852
  catch (error) {
892
853
  console.error('Failed to restore user from localStorage:', error);
893
854
  }
894
855
  }
895
- /**
896
- * Save user to localStorage
897
- */
898
856
  saveUser(user) {
899
857
  try {
900
858
  if (user) {
@@ -908,699 +866,435 @@ class AuthService {
908
866
  console.error('Failed to save user to localStorage:', error);
909
867
  }
910
868
  }
911
- /**
912
- * Update user subject and persist to localStorage
913
- */
914
869
  updateUser(user) {
915
870
  this.userSubject.next(user);
916
871
  this.saveUser(user);
917
872
  }
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
- }
873
+ storeAuthResult(result) {
874
+ if (result.accessToken)
875
+ this.tokens.setAccessToken(result.accessToken);
876
+ if (result.refreshToken)
877
+ this.tokens.setRefreshToken(result.refreshToken);
878
+ if (result.user)
879
+ this.updateUser(result.user);
880
+ this.signinStatus.setSigninStatus(true);
881
+ }
882
+ // ── Core auth operations ──────────────────────────────────────────────────
883
+ async loginWithEmail(email, password) {
884
+ const result = await this.plugin.login(email, password);
885
+ if (result.success)
886
+ this.storeAuthResult(result);
887
+ return result;
961
888
  }
962
- /**
963
- * Login with Google OAuth (popup window)
964
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
965
- */
966
889
  async loginWithGoogle(serverName) {
967
- return this.loginWithOAuth('google', serverName);
890
+ return this.loginWithProvider('google');
968
891
  }
969
- /**
970
- * Login with GitHub OAuth (popup window)
971
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
972
- */
973
892
  async loginWithGitHub(serverName) {
974
- return this.loginWithOAuth('github', serverName);
893
+ return this.loginWithProvider('github');
975
894
  }
976
- /**
977
- * Login with LinkedIn OAuth (popup window)
978
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
979
- */
980
895
  async loginWithLinkedIn(serverName) {
981
- return this.loginWithOAuth('linkedin', serverName);
896
+ return this.loginWithProvider('linkedin');
982
897
  }
983
- /**
984
- * Login with Apple OAuth (popup window)
985
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
986
- */
987
898
  async loginWithApple(serverName) {
988
- return this.loginWithOAuth('apple', serverName);
899
+ return this.loginWithProvider('apple');
989
900
  }
990
- /**
991
- * Login with Microsoft OAuth (popup window)
992
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
993
- */
994
901
  async loginWithMicrosoft(serverName) {
995
- return this.loginWithOAuth('microsoft', serverName);
902
+ return this.loginWithProvider('microsoft');
996
903
  }
997
- /**
998
- * Login with Zoho OAuth (popup window)
999
- * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
1000
- */
1001
904
  async loginWithZoho(serverName) {
1002
- return this.loginWithOAuth('zoho', serverName);
905
+ return this.loginWithProvider('zoho');
1003
906
  }
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
907
  async loginWithProvider(provider, serverName) {
1010
908
  if (provider === 'emailPassword') {
1011
909
  throw new Error('Use loginWithEmail() for email/password authentication');
1012
910
  }
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
- });
911
+ if (!this.plugin.loginWithProvider) {
912
+ return { success: false, message: 'OAuth not supported by the configured auth plugin' };
913
+ }
914
+ const result = await this.plugin.loginWithProvider(provider);
915
+ if (result.success)
916
+ this.storeAuthResult(result);
917
+ return result;
1082
918
  }
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
919
  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
- }
920
+ const result = await this.plugin.register(email, password, displayName);
921
+ if (result.success)
922
+ this.storeAuthResult(result);
923
+ return result;
1132
924
  }
1133
- /**
1134
- * Sign out user
1135
- * @param serverName - Optional: Specify which auth server to logout from (for multi-server mode)
1136
- */
1137
925
  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
- }
926
+ const refreshToken = this.tokens.getRefreshToken() || undefined;
927
+ await this.plugin.logout(refreshToken);
928
+ this.tokens.clear();
929
+ this.signinStatus.setSigninStatus(false);
930
+ this.updateUser(null);
1162
931
  }
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
932
  async checkSession(serverName) {
1168
933
  if (this.tokens.hasValidAccessToken()) {
1169
934
  this.signinStatus.setSigninStatus(true);
1170
935
  return true;
1171
936
  }
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;
1209
- }
1210
- }
1211
- /**
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
1227
- */
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
- };
937
+ const result = await this.plugin.checkSession();
938
+ if (result.success && result.accessToken) {
939
+ this.tokens.setAccessToken(result.accessToken);
940
+ if (result.user)
941
+ this.updateUser(result.user);
942
+ this.signinStatus.setSigninStatus(true);
943
+ return true;
1269
944
  }
945
+ this.tokens.clear();
946
+ this.updateUser(null);
947
+ this.signinStatus.setSigninStatus(false);
948
+ return false;
1270
949
  }
1271
950
  /**
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)
951
+ * Refresh the access token. Called by ApiConnectionService on 401.
952
+ * @returns true if token was refreshed, false if refresh failed (user is signed out)
1346
953
  */
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: [] };
954
+ async refresh() {
955
+ const newToken = await this.plugin.refresh(this.tokens.getAccessToken(), this.tokens.getRefreshToken() || undefined);
956
+ if (newToken) {
957
+ this.tokens.setAccessToken(newToken);
958
+ return true;
1365
959
  }
960
+ this.tokens.clear();
961
+ this.updateUser(null);
962
+ this.signinStatus.signedOut();
963
+ return false;
1366
964
  }
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
- };
965
+ isAuthenticated() {
966
+ return this.tokens.hasValidAccessToken();
967
+ }
968
+ getCurrentUser() {
969
+ return this.userSubject.value;
970
+ }
971
+ // ── Multi-tenant operations ───────────────────────────────────────────────
972
+ async registerTenant(data) {
973
+ if (!this.plugin.registerTenant) {
974
+ return { success: false, message: 'registerTenant not supported by the configured auth plugin' };
1398
975
  }
1399
- catch (error) {
1400
- return {
1401
- success: false,
1402
- message: 'Network error. Please try again.'
1403
- };
976
+ const registered = await this.plugin.registerTenant(data);
977
+ if (!registered.success)
978
+ return registered;
979
+ // OAuth: token comes directly from the popup — store it and we're done.
980
+ if (registered.accessToken) {
981
+ this.storeAuthResult(registered);
982
+ return registered;
1404
983
  }
984
+ // emailPassword: registration succeeded but no token yet.
985
+ // Login via auth (SSO) to obtain tokens.
986
+ if (data.provider === 'emailPassword' && data.email && data.password) {
987
+ return await this.loginWithEmail(data.email, data.password);
988
+ }
989
+ return registered;
1405
990
  }
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
- };
991
+ async getTenantMemberships(serverName) {
992
+ if (!this.plugin.getTenantMemberships)
993
+ return { memberships: [] };
994
+ const memberships = await this.plugin.getTenantMemberships(this.tokens.getAccessToken());
995
+ return { memberships };
996
+ }
997
+ async selectTenant(tenantId, serverName) {
998
+ if (!this.plugin.selectTenant) {
999
+ return { success: false, message: 'selectTenant not supported by the configured auth plugin' };
1423
1000
  }
1424
- catch (error) {
1425
- // On error, assume available (don't block registration)
1001
+ const result = await this.plugin.selectTenant(tenantId, this.tokens.getAccessToken());
1002
+ if (result.success && result.accessToken) {
1003
+ this.tokens.setAccessToken(result.accessToken);
1004
+ }
1005
+ return { success: result.success, message: result.message, access_token: result.accessToken };
1006
+ }
1007
+ async checkTenantSlugAvailable(slug, serverName) {
1008
+ if (!this.plugin.checkTenantSlugAvailable)
1426
1009
  return { available: true };
1010
+ return this.plugin.checkTenantSlugAvailable(slug);
1011
+ }
1012
+ async checkOnboardingStatus(identityId, serverName) {
1013
+ if (!this.plugin.checkOnboardingStatus)
1014
+ throw new Error('checkOnboardingStatus not supported');
1015
+ return this.plugin.checkOnboardingStatus(identityId);
1016
+ }
1017
+ async completeTenantOnboarding(countryCode, tenantName, serverName) {
1018
+ if (!this.plugin.completeTenantOnboarding)
1019
+ throw new Error('completeTenantOnboarding not supported');
1020
+ const result = await this.plugin.completeTenantOnboarding(countryCode, tenantName, this.tokens.getAccessToken());
1021
+ if (result?.access_token) {
1022
+ this.tokens.setAccessToken(result.access_token);
1023
+ this.signinStatus.setSigninStatus(true);
1427
1024
  }
1025
+ return result;
1428
1026
  }
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;
1027
+ // ── Multi-server (delegated to plugin) ────────────────────────────────────
1028
+ getAvailableAuthServers() {
1029
+ return this.plugin.getAvailableServers?.() ?? [];
1437
1030
  }
1438
- /**
1439
- * @deprecated Use getCurrentUser()?.display_name instead
1440
- */
1441
- getUserName() {
1442
- return this.userSubject.value?.display_name || '';
1031
+ getActiveAuthServer() {
1032
+ return this.plugin.getActiveServer?.() ?? null;
1443
1033
  }
1444
- /**
1445
- * @deprecated Use getCurrentUser()?.photo_url instead
1446
- */
1447
- getPhotoUrl() {
1448
- return this.userSubject.value?.photo_url || '';
1034
+ switchAuthServer(serverName) {
1035
+ if (!this.plugin.switchServer) {
1036
+ throw new Error('Multi-server mode not supported by the configured auth plugin');
1037
+ }
1038
+ this.plugin.switchServer(serverName);
1449
1039
  }
1450
- /**
1451
- * @deprecated Use getCurrentUser()?.display_name instead
1452
- */
1453
- getDisplayName() {
1454
- return this.userSubject.value?.display_name || '';
1040
+ getAuthServerConfig(serverName) {
1041
+ return this.plugin.getServerConfig?.(serverName) ?? null;
1455
1042
  }
1456
- /**
1457
- * @deprecated Use `/profile/${getCurrentUser()?.user_id}` instead
1458
- */
1043
+ isMultiServerMode() {
1044
+ return (this.plugin.getAvailableServers?.() ?? []).length > 0;
1045
+ }
1046
+ // ── Backward compatibility ────────────────────────────────────────────────
1047
+ /** @deprecated Use getCurrentUser()?.user_id instead */
1048
+ getUserId() { return this.userSubject.value?.user_id || 0; }
1049
+ /** @deprecated Use getCurrentUser()?.display_name instead */
1050
+ getUserName() { return this.userSubject.value?.display_name || ''; }
1051
+ /** @deprecated Use getCurrentUser()?.photo_url instead */
1052
+ getPhotoUrl() { return this.userSubject.value?.photo_url || ''; }
1053
+ /** @deprecated Use getCurrentUser()?.display_name instead */
1054
+ getDisplayName() { return this.userSubject.value?.display_name || ''; }
1055
+ /** @deprecated Use `/profile/${getCurrentUser()?.user_id}` instead */
1459
1056
  getProfileUrl() {
1460
1057
  const userId = this.userSubject.value?.user_id;
1461
1058
  return userId ? `/profile/${userId}` : '';
1462
1059
  }
1463
- /**
1464
- * @deprecated Use isAuthenticated() instead
1465
- */
1466
- async signin() {
1467
- return this.isAuthenticated();
1468
- }
1469
- /**
1470
- * @deprecated Use loginWithEmail() instead
1471
- */
1060
+ /** @deprecated Use isAuthenticated() instead */
1061
+ async signin() { return this.isAuthenticated(); }
1062
+ /** @deprecated Use loginWithEmail() instead */
1472
1063
  async verifyCredentials(email, password) {
1473
1064
  const result = await this.loginWithEmail(email, password);
1474
1065
  return result.success;
1475
1066
  }
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
1067
+ /** @deprecated Check user.is_email_verified from getCurrentUser() instead */
1068
+ isSigninEmailValid() { return this.userSubject.value?.is_email_verified || false; }
1069
+ /** @deprecated No longer needed */
1070
+ onDialogClose() { }
1071
+ /** @deprecated No longer needed */
1072
+ closeSocialAuthDialog() { }
1073
+ /** @deprecated Use checkEmail() from the plugin directly */
1074
+ async getUserProfile(email, serverName) {
1075
+ if (!this.plugin.checkEmail)
1076
+ return null;
1077
+ const result = await this.plugin.checkEmail(email);
1078
+ return result.exists ? result.user : null;
1487
1079
  }
1488
- /**
1489
- * @deprecated No longer needed - dialog is managed by platform
1490
- */
1491
- closeSocialAuthDialog() {
1492
- // No-op for backward compatibility
1080
+ 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 });
1081
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: AuthService, providedIn: 'root' });
1082
+ }
1083
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: AuthService, decorators: [{
1084
+ type: Injectable,
1085
+ args: [{
1086
+ providedIn: 'root'
1087
+ }]
1088
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
1089
+ type: Inject,
1090
+ args: [AUTH_PLUGIN]
1091
+ }] }, { type: TokenService }, { type: SigninStatusService }] });
1092
+
1093
+ class ApiConnectionService {
1094
+ tokens;
1095
+ signinStatus;
1096
+ environment;
1097
+ authService;
1098
+ host = ''; // base URL without trailing slash
1099
+ constructor(tokens, signinStatus, environment, authService) {
1100
+ this.tokens = tokens;
1101
+ this.signinStatus = signinStatus;
1102
+ this.environment = environment;
1103
+ this.authService = authService;
1104
+ this.host = environment.apiServer.host;
1493
1105
  }
1494
- /**
1495
- * @deprecated Check if user exists by calling /api/auth/check-email endpoint
1496
- */
1497
- async getUserProfile(email, serverName) {
1106
+ async request(url, options, data) {
1498
1107
  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;
1108
+ if (data !== null) {
1109
+ const body = JSON.stringify(data);
1110
+ options.body = body || {};
1111
+ }
1112
+ const accessTokenIncluded = this.includeAccessToken(options);
1113
+ let response = await fetch(url, options);
1114
+ if (response.status === 401 && accessTokenIncluded) {
1115
+ response = await this.refreshAndRetry(url, options, response);
1116
+ }
1117
+ if (response.ok) {
1118
+ const json = await response.json();
1119
+ return new ApiResponse(json.status, json.data, json.message);
1120
+ }
1121
+ if (response.status === 401) {
1122
+ this.signinStatus.signedOut();
1123
+ }
1124
+ return this.handleError(response);
1507
1125
  }
1508
1126
  catch (error) {
1509
- return null;
1127
+ return this.handleError(error);
1510
1128
  }
1511
1129
  }
1130
+ handleError(error) {
1131
+ console.error(`Backend returned code ${error.status}, full error: `, error);
1132
+ return new ApiResponse('error');
1133
+ }
1134
+ async get(endpoint, queryParamsObj) {
1135
+ const url = this.host + endpoint + this.buildQueryString(queryParamsObj);
1136
+ const fetchOptions = { mode: 'cors', redirect: 'error' };
1137
+ return this.request(url, fetchOptions, null);
1138
+ }
1139
+ async post(pathWithQueryParams, data) {
1140
+ const url = this.host + pathWithQueryParams;
1141
+ const fetchOptions = {
1142
+ method: 'POST',
1143
+ mode: 'cors',
1144
+ redirect: 'error',
1145
+ headers: { 'Content-Type': 'application/json' },
1146
+ body: JSON.stringify(data)
1147
+ };
1148
+ return this.request(url, fetchOptions, data);
1149
+ }
1150
+ async put(pathWithQueryParams, data) {
1151
+ const url = this.host + pathWithQueryParams;
1152
+ const fetchOptions = {
1153
+ method: 'PUT',
1154
+ mode: 'cors',
1155
+ redirect: 'error',
1156
+ headers: { 'Content-Type': 'application/json' },
1157
+ body: JSON.stringify(data)
1158
+ };
1159
+ return this.request(url, fetchOptions, data);
1160
+ }
1161
+ async patch(pathWithQueryParams, data) {
1162
+ const url = this.host + pathWithQueryParams;
1163
+ const fetchOptions = {
1164
+ method: 'PATCH',
1165
+ mode: 'cors',
1166
+ redirect: 'error',
1167
+ headers: { 'Content-Type': 'application/json' },
1168
+ body: JSON.stringify(data)
1169
+ };
1170
+ return this.request(url, fetchOptions, data);
1171
+ }
1172
+ async delete(endpoint, queryParamsObj) {
1173
+ const url = this.host + endpoint + this.buildQueryString(queryParamsObj);
1174
+ const fetchOptions = { method: 'DELETE', mode: 'cors', redirect: 'error' };
1175
+ return this.request(url, fetchOptions, null);
1176
+ }
1177
+ includeAccessToken(options) {
1178
+ const accessToken = this.tokens.getAccessToken();
1179
+ if (!accessToken)
1180
+ return false;
1181
+ if (!options.headers)
1182
+ options.headers = {};
1183
+ options.headers['Authorization'] = 'Bearer ' + accessToken;
1184
+ return true;
1185
+ }
1186
+ async refreshAndRetry(url, fetchOptions, response) {
1187
+ const refreshed = await this.authService.refresh();
1188
+ if (!refreshed)
1189
+ return response;
1190
+ fetchOptions.headers['Authorization'] = 'Bearer ' + this.tokens.getAccessToken();
1191
+ return fetch(url, fetchOptions);
1192
+ }
1512
1193
  /**
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)
1194
+ * Refresh the access token (delegates to AuthService AuthPlugin).
1195
+ * Kept public for backward compatibility.
1516
1196
  */
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');
1197
+ async refreshAccessToken() {
1198
+ return this.authService.refresh();
1199
+ }
1200
+ buildQueryString(options) {
1201
+ if (options === undefined)
1202
+ return '';
1203
+ const array = [];
1204
+ for (const key in options) {
1205
+ if (options.hasOwnProperty(key) && options[key] !== null && options[key] !== undefined) {
1206
+ array.push(encodeURIComponent(key) + '=' + encodeURIComponent(options[key]));
1527
1207
  }
1528
- return await response.json();
1529
- }
1530
- catch (error) {
1531
- throw error;
1532
1208
  }
1209
+ const str = array.join('&');
1210
+ return str ? '?' + str : '';
1533
1211
  }
1534
1212
  /**
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)
1213
+ * Upload a drawing (uses upload server if configured, otherwise API server)
1214
+ * @deprecated Platform-specific method - consider moving to platform service
1539
1215
  */
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
- }
1216
+ async uploadDrawing(formData) {
1217
+ const uploadHost = this.environment.uploadServer?.host || this.host;
1218
+ const url = uploadHost + '/upload/drawing';
1219
+ const fetchOptions = { method: 'POST', mode: 'cors', redirect: 'error', body: formData };
1220
+ return this.request(url, fetchOptions, null);
1221
+ }
1222
+ /**
1223
+ * Upload an image (uses upload server if configured, otherwise API server)
1224
+ * @deprecated Platform-specific method - consider moving to platform service
1225
+ */
1226
+ async uploadImage(formData) {
1227
+ const uploadHost = this.environment.uploadServer?.host || this.host;
1228
+ const url = uploadHost + '/upload/image';
1229
+ const fetchOptions = { method: 'POST', mode: 'cors', redirect: 'error', body: formData };
1230
+ return this.request(url, fetchOptions, null);
1578
1231
  }
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' });
1232
+ 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 });
1233
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ApiConnectionService, providedIn: 'root' });
1581
1234
  }
1582
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AuthService, decorators: [{
1235
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ApiConnectionService, decorators: [{
1583
1236
  type: Injectable,
1584
1237
  args: [{
1585
1238
  providedIn: 'root'
1586
1239
  }]
1587
- }], ctorParameters: () => [{ type: TokenService }, { type: SigninStatusService }, { type: MyEnvironmentModel, decorators: [{
1588
- type: Inject,
1589
- args: [MyEnvironmentModel]
1590
- }] }] });
1240
+ }], ctorParameters: () => [{ type: TokenService }, { type: SigninStatusService }, { type: MyEnvironmentModel }, { type: AuthService }] });
1591
1241
 
1592
1242
  class DbService {
1593
1243
  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' });
1244
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: DbService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1245
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: DbService, providedIn: 'root' });
1596
1246
  }
1597
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DbService, decorators: [{
1247
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: DbService, decorators: [{
1598
1248
  type: Injectable,
1599
1249
  args: [{
1600
1250
  providedIn: 'root'
1601
1251
  }]
1602
1252
  }], ctorParameters: () => [] });
1603
1253
 
1254
+ /**
1255
+ * CSRF Token Service
1256
+ *
1257
+ * Manages CSRF tokens for cookie-based authentication.
1258
+ * Reads CSRF token from cookies and provides it for request headers.
1259
+ */
1260
+ class CsrfService {
1261
+ /**
1262
+ * Get CSRF token from cookie
1263
+ */
1264
+ getCsrfToken(cookieName = 'csrf_token') {
1265
+ const cookies = document.cookie.split(';');
1266
+ for (const cookie of cookies) {
1267
+ const [name, value] = cookie.trim().split('=');
1268
+ if (name === cookieName) {
1269
+ return decodeURIComponent(value);
1270
+ }
1271
+ }
1272
+ return null;
1273
+ }
1274
+ /**
1275
+ * Check if CSRF token exists
1276
+ */
1277
+ hasCsrfToken(cookieName = 'csrf_token') {
1278
+ return this.getCsrfToken(cookieName) !== null;
1279
+ }
1280
+ /**
1281
+ * Clear CSRF token (for logout)
1282
+ * Note: Client-side deletion is limited for httpOnly cookies
1283
+ */
1284
+ clearCsrfToken(cookieName = 'csrf_token') {
1285
+ // Can only clear non-httpOnly cookies
1286
+ document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
1287
+ }
1288
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: CsrfService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1289
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: CsrfService, providedIn: 'root' });
1290
+ }
1291
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: CsrfService, decorators: [{
1292
+ type: Injectable,
1293
+ args: [{
1294
+ providedIn: 'root'
1295
+ }]
1296
+ }] });
1297
+
1604
1298
  /**
1605
1299
  * Service for interacting with the stonescriptphp-files server.
1606
1300
  * Handles file upload, download, list, and delete operations
@@ -1855,10 +1549,10 @@ class FilesService {
1855
1549
  return false;
1856
1550
  }
1857
1551
  }
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' });
1552
+ 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 });
1553
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: FilesService, providedIn: 'root' });
1860
1554
  }
1861
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: FilesService, decorators: [{
1555
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: FilesService, decorators: [{
1862
1556
  type: Injectable,
1863
1557
  args: [{
1864
1558
  providedIn: 'root'
@@ -2034,10 +1728,10 @@ class ProviderRegistryService {
2034
1728
  }
2035
1729
  return Object.keys(styles).length > 0 ? styles : null;
2036
1730
  }
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' });
1731
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ProviderRegistryService, deps: [{ token: MyEnvironmentModel }], target: i0.ɵɵFactoryTarget.Injectable });
1732
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ProviderRegistryService, providedIn: 'root' });
2039
1733
  }
2040
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ProviderRegistryService, decorators: [{
1734
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ProviderRegistryService, decorators: [{
2041
1735
  type: Injectable,
2042
1736
  args: [{
2043
1737
  providedIn: 'root'
@@ -2047,29 +1741,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
2047
1741
  args: [MyEnvironmentModel]
2048
1742
  }] }] });
2049
1743
 
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
1744
  class TenantLoginComponent {
2074
1745
  auth;
2075
1746
  providerRegistry;
@@ -2297,8 +1968,8 @@ class TenantLoginComponent {
2297
1968
  event.preventDefault();
2298
1969
  this.createTenant.emit();
2299
1970
  }
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: `
1971
+ 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 });
1972
+ 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
1973
  <div class="tenant-login-dialog">
2303
1974
  @if (!showingTenantSelector) {
2304
1975
  <!-- Step 1: Authentication -->
@@ -2466,7 +2137,7 @@ class TenantLoginComponent {
2466
2137
  </div>
2467
2138
  `, 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
2139
  }
2469
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantLoginComponent, decorators: [{
2140
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TenantLoginComponent, decorators: [{
2470
2141
  type: Component,
2471
2142
  args: [{ selector: 'lib-tenant-login', standalone: true, imports: [CommonModule, FormsModule], template: `
2472
2143
  <div class="tenant-login-dialog">
@@ -2771,8 +2442,8 @@ class RegisterComponent {
2771
2442
  this.confirmPassword = '';
2772
2443
  this.displayName = '';
2773
2444
  }
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: `
2445
+ 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 });
2446
+ 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
2447
  <div class="register-dialog">
2777
2448
  <h2 class="register-title">Create Account</h2>
2778
2449
 
@@ -2901,7 +2572,7 @@ class RegisterComponent {
2901
2572
  </div>
2902
2573
  `, 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
2574
  }
2904
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RegisterComponent, decorators: [{
2575
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: RegisterComponent, decorators: [{
2905
2576
  type: Component,
2906
2577
  args: [{ selector: 'lib-register', standalone: true, imports: [CommonModule, FormsModule], template: `
2907
2578
  <div class="register-dialog">
@@ -3087,8 +2758,8 @@ class AuthPageComponent {
3087
2758
  (B < 255 ? B < 1 ? 0 : B : 255))
3088
2759
  .toString(16).slice(1);
3089
2760
  }
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: `
2761
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: AuthPageComponent, deps: [{ token: MyEnvironmentModel }], target: i0.ɵɵFactoryTarget.Component });
2762
+ 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
2763
  <div class="auth-container" [style.background]="gradientStyle">
3093
2764
  <div class="auth-card">
3094
2765
  @if (logo) {
@@ -3115,7 +2786,7 @@ class AuthPageComponent {
3115
2786
  </div>
3116
2787
  `, 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
2788
  }
3118
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AuthPageComponent, decorators: [{
2789
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: AuthPageComponent, decorators: [{
3119
2790
  type: Component,
3120
2791
  args: [{ selector: 'lib-auth-page', standalone: true, imports: [CommonModule, TenantLoginComponent, RegisterComponent], template: `
3121
2792
  <div class="auth-container" [style.background]="gradientStyle">
@@ -3238,8 +2909,8 @@ class LoginDialogComponent {
3238
2909
  // For now, just emit a console message
3239
2910
  console.log('Register clicked - platform should handle navigation');
3240
2911
  }
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: `
2912
+ 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 });
2913
+ 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
2914
  <div class="login-dialog">
3244
2915
  <h2 class="login-title">Sign In</h2>
3245
2916
 
@@ -3329,7 +3000,7 @@ class LoginDialogComponent {
3329
3000
  </div>
3330
3001
  `, 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
3002
  }
3332
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: LoginDialogComponent, decorators: [{
3003
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: LoginDialogComponent, decorators: [{
3333
3004
  type: Component,
3334
3005
  args: [{ selector: 'lib-login-dialog', standalone: true, imports: [CommonModule, FormsModule], template: `
3335
3006
  <div class="login-dialog">
@@ -3584,15 +3255,11 @@ class TenantRegisterComponent {
3584
3255
  try {
3585
3256
  const result = await this.auth.registerTenant({
3586
3257
  tenantName: this.tenantName,
3587
- tenantSlug: this.tenantSlug,
3588
3258
  provider: provider
3589
3259
  });
3590
- if (result.success && result.tenant && result.user) {
3260
+ if (result.success && result.user) {
3591
3261
  this.success = 'Organization created successfully!';
3592
- this.tenantCreated.emit({
3593
- tenant: result.tenant,
3594
- user: result.user
3595
- });
3262
+ this.tenantCreated.emit({ user: result.user });
3596
3263
  }
3597
3264
  else {
3598
3265
  this.error = result.message || 'Registration failed';
@@ -3617,18 +3284,14 @@ class TenantRegisterComponent {
3617
3284
  try {
3618
3285
  const result = await this.auth.registerTenant({
3619
3286
  tenantName: this.tenantName,
3620
- tenantSlug: this.tenantSlug,
3621
3287
  displayName: this.displayName,
3622
3288
  email: this.email,
3623
3289
  password: this.password,
3624
3290
  provider: 'emailPassword'
3625
3291
  });
3626
- if (result.success && result.tenant && result.user) {
3292
+ if (result.success && result.user) {
3627
3293
  this.success = 'Organization created successfully!';
3628
- this.tenantCreated.emit({
3629
- tenant: result.tenant,
3630
- user: result.user
3631
- });
3294
+ this.tenantCreated.emit({ user: result.user });
3632
3295
  }
3633
3296
  else {
3634
3297
  this.error = result.message || 'Registration failed';
@@ -3645,8 +3308,8 @@ class TenantRegisterComponent {
3645
3308
  event.preventDefault();
3646
3309
  this.navigateToLogin.emit();
3647
3310
  }
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: `
3311
+ 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 });
3312
+ 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
3313
  <div class="tenant-register-dialog">
3651
3314
  <h2 class="register-title">{{ title }}</h2>
3652
3315
 
@@ -3856,7 +3519,7 @@ class TenantRegisterComponent {
3856
3519
  </div>
3857
3520
  `, 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
3521
  }
3859
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantRegisterComponent, decorators: [{
3522
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TenantRegisterComponent, decorators: [{
3860
3523
  type: Component,
3861
3524
  args: [{ selector: 'lib-tenant-register', standalone: true, imports: [CommonModule, FormsModule], template: `
3862
3525
  <div class="tenant-register-dialog">
@@ -4156,8 +3819,8 @@ class TenantLoginDialogComponent {
4156
3819
  this.dialogRef.close({ action: 'create_tenant' });
4157
3820
  }
4158
3821
  }
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: `
3822
+ 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 });
3823
+ 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
3824
  <div class="dialog-wrapper">
4162
3825
  <lib-tenant-login
4163
3826
  [title]="data?.title || 'Sign In'"
@@ -4178,7 +3841,7 @@ class TenantLoginDialogComponent {
4178
3841
  </div>
4179
3842
  `, 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
3843
  }
4181
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantLoginDialogComponent, decorators: [{
3844
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TenantLoginDialogComponent, decorators: [{
4182
3845
  type: Component,
4183
3846
  args: [{ selector: 'lib-tenant-login-dialog', standalone: true, imports: [CommonModule, TenantLoginComponent], template: `
4184
3847
  <div class="dialog-wrapper">
@@ -4259,8 +3922,8 @@ class TenantRegisterDialogComponent {
4259
3922
  this.dialogRef.close({ action: 'navigate_to_login' });
4260
3923
  }
4261
3924
  }
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: `
3925
+ 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 });
3926
+ 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
3927
  <div class="dialog-wrapper">
4265
3928
  <lib-tenant-register
4266
3929
  [title]="data?.title || 'Create New Organization'"
@@ -4286,7 +3949,7 @@ class TenantRegisterDialogComponent {
4286
3949
  </div>
4287
3950
  `, 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
3951
  }
4289
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantRegisterDialogComponent, decorators: [{
3952
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TenantRegisterDialogComponent, decorators: [{
4290
3953
  type: Component,
4291
3954
  args: [{ selector: 'lib-tenant-register-dialog', standalone: true, imports: [CommonModule, TenantRegisterComponent], template: `
4292
3955
  <div class="dialog-wrapper">
@@ -4328,10 +3991,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
4328
3991
  /*
4329
3992
  * Public API Surface of ngx-stonescriptphp-client
4330
3993
  */
3994
+ // ── Core setup ────────────────────────────────────────────────────────────────
4331
3995
 
4332
3996
  /**
4333
3997
  * Generated bundle index. Do not edit.
4334
3998
  */
4335
3999
 
4336
- export { ApiConnectionService, ApiResponse, AuthPageComponent, AuthService, CsrfService, DbService, FilesService, LoginDialogComponent, MyEnvironmentModel, NgxStoneScriptPhpClientModule, ProviderRegistryService, RegisterComponent, SigninStatusService, TenantLoginComponent, TenantLoginDialogComponent, TenantRegisterComponent, TenantRegisterDialogComponent, TokenService, VerifyStatus };
4000
+ export { AUTH_PLUGIN, ApiConnectionService, ApiResponse, AuthPageComponent, AuthService, CsrfService, DbService, FilesService, LoginDialogComponent, MyEnvironmentModel, ProviderRegistryService, RegisterComponent, SigninStatusService, StoneScriptPHPAuth, TenantLoginComponent, TenantLoginDialogComponent, TenantRegisterComponent, TenantRegisterDialogComponent, TokenService, VerifyStatus, provideNgxStoneScriptPhpClient };
4337
4001
  //# sourceMappingURL=progalaxyelabs-ngx-stonescriptphp-client.mjs.map