@progalaxyelabs/ngx-stonescriptphp-client 1.2.0 → 1.3.1

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,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, Inject, NgModule, Input, Component } from '@angular/core';
2
+ import { Injectable, Inject, NgModule, Input, Component, EventEmitter, Output, Optional } from '@angular/core';
3
3
  import { BehaviorSubject } from 'rxjs';
4
4
  import { CommonModule } from '@angular/common';
5
5
  import * as i2 from '@angular/forms';
@@ -576,6 +576,7 @@ class AuthService {
576
576
  tokens;
577
577
  signinStatus;
578
578
  environment;
579
+ USER_STORAGE_KEY = 'progalaxyapi_user';
579
580
  // Observable user state
580
581
  userSubject = new BehaviorSubject(null);
581
582
  user$ = this.userSubject.asObservable();
@@ -583,6 +584,46 @@ class AuthService {
583
584
  this.tokens = tokens;
584
585
  this.signinStatus = signinStatus;
585
586
  this.environment = environment;
587
+ // Restore user from localStorage on initialization
588
+ this.restoreUser();
589
+ }
590
+ /**
591
+ * Restore user from localStorage
592
+ */
593
+ restoreUser() {
594
+ try {
595
+ const userJson = localStorage.getItem(this.USER_STORAGE_KEY);
596
+ if (userJson) {
597
+ const user = JSON.parse(userJson);
598
+ this.updateUser(user);
599
+ }
600
+ }
601
+ catch (error) {
602
+ console.error('Failed to restore user from localStorage:', error);
603
+ }
604
+ }
605
+ /**
606
+ * Save user to localStorage
607
+ */
608
+ saveUser(user) {
609
+ try {
610
+ if (user) {
611
+ localStorage.setItem(this.USER_STORAGE_KEY, JSON.stringify(user));
612
+ }
613
+ else {
614
+ localStorage.removeItem(this.USER_STORAGE_KEY);
615
+ }
616
+ }
617
+ catch (error) {
618
+ console.error('Failed to save user to localStorage:', error);
619
+ }
620
+ }
621
+ /**
622
+ * Update user subject and persist to localStorage
623
+ */
624
+ updateUser(user) {
625
+ this.updateUser(user);
626
+ this.saveUser(user);
586
627
  }
587
628
  /**
588
629
  * Login with email and password
@@ -603,7 +644,7 @@ class AuthService {
603
644
  if (data.success && data.access_token) {
604
645
  this.tokens.setAccessToken(data.access_token);
605
646
  this.signinStatus.setSigninStatus(true);
606
- this.userSubject.next(data.user);
647
+ this.updateUser(data.user);
607
648
  return { success: true, user: data.user };
608
649
  }
609
650
  return {
@@ -688,7 +729,7 @@ class AuthService {
688
729
  if (event.data.type === 'oauth_success') {
689
730
  this.tokens.setAccessToken(event.data.access_token);
690
731
  this.signinStatus.setSigninStatus(true);
691
- this.userSubject.next(event.data.user);
732
+ this.updateUser(event.data.user);
692
733
  window.removeEventListener('message', messageHandler);
693
734
  popup.close();
694
735
  resolve({
@@ -739,7 +780,7 @@ class AuthService {
739
780
  if (data.success && data.access_token) {
740
781
  this.tokens.setAccessToken(data.access_token);
741
782
  this.signinStatus.setSigninStatus(true);
742
- this.userSubject.next(data.user);
783
+ this.updateUser(data.user);
743
784
  return {
744
785
  success: true,
745
786
  user: data.user,
@@ -763,10 +804,19 @@ class AuthService {
763
804
  */
764
805
  async signout() {
765
806
  try {
766
- await fetch(`${this.environment.accountsUrl}/api/auth/logout`, {
767
- method: 'POST',
768
- credentials: 'include'
769
- });
807
+ const refreshToken = this.tokens.getRefreshToken();
808
+ if (refreshToken) {
809
+ await fetch(`${this.environment.accountsUrl}/api/auth/logout`, {
810
+ method: 'POST',
811
+ headers: {
812
+ 'Content-Type': 'application/json'
813
+ },
814
+ credentials: 'include',
815
+ body: JSON.stringify({
816
+ refresh_token: refreshToken
817
+ })
818
+ });
819
+ }
770
820
  }
771
821
  catch (error) {
772
822
  console.error('Logout API call failed:', error);
@@ -774,7 +824,7 @@ class AuthService {
774
824
  finally {
775
825
  this.tokens.clear();
776
826
  this.signinStatus.setSigninStatus(false);
777
- this.userSubject.next(null);
827
+ this.updateUser(null);
778
828
  }
779
829
  }
780
830
  /**
@@ -798,7 +848,7 @@ class AuthService {
798
848
  const data = await response.json();
799
849
  if (data.access_token) {
800
850
  this.tokens.setAccessToken(data.access_token);
801
- this.userSubject.next(data.user);
851
+ this.updateUser(data.user);
802
852
  this.signinStatus.setSigninStatus(true);
803
853
  return true;
804
854
  }
@@ -821,6 +871,197 @@ class AuthService {
821
871
  getCurrentUser() {
822
872
  return this.userSubject.value;
823
873
  }
874
+ // ===== Multi-Tenant Methods =====
875
+ /**
876
+ * Register a new user AND create a new tenant (organization)
877
+ * This is used when a user wants to create their own organization
878
+ */
879
+ async registerTenant(data) {
880
+ try {
881
+ // If using OAuth, initiate OAuth flow first
882
+ if (data.provider !== 'emailPassword') {
883
+ return await this.registerTenantWithOAuth(data.tenantName, data.tenantSlug, data.provider);
884
+ }
885
+ // Email/password registration
886
+ const response = await fetch(`${this.environment.accountsUrl}/api/auth/register-tenant`, {
887
+ method: 'POST',
888
+ headers: { 'Content-Type': 'application/json' },
889
+ credentials: 'include',
890
+ body: JSON.stringify({
891
+ platform: this.environment.platformCode,
892
+ tenant_name: data.tenantName,
893
+ tenant_slug: data.tenantSlug,
894
+ display_name: data.displayName,
895
+ email: data.email,
896
+ password: data.password,
897
+ provider: 'emailPassword'
898
+ })
899
+ });
900
+ const result = await response.json();
901
+ if (result.success && result.access_token) {
902
+ this.tokens.setAccessToken(result.access_token);
903
+ this.signinStatus.setSigninStatus(true);
904
+ if (result.user) {
905
+ this.updateUser(result.user);
906
+ }
907
+ }
908
+ return result;
909
+ }
910
+ catch (error) {
911
+ return {
912
+ success: false,
913
+ message: 'Network error. Please try again.'
914
+ };
915
+ }
916
+ }
917
+ /**
918
+ * Register tenant with OAuth provider
919
+ * Opens popup window for OAuth flow
920
+ */
921
+ async registerTenantWithOAuth(tenantName, tenantSlug, provider) {
922
+ return new Promise((resolve) => {
923
+ const width = 500;
924
+ const height = 600;
925
+ const left = (window.screen.width - width) / 2;
926
+ const top = (window.screen.height - height) / 2;
927
+ // Build OAuth URL with tenant registration params
928
+ const oauthUrl = `${this.environment.accountsUrl}/oauth/${provider}?` +
929
+ `platform=${this.environment.platformCode}&` +
930
+ `mode=popup&` +
931
+ `action=register_tenant&` +
932
+ `tenant_name=${encodeURIComponent(tenantName)}&` +
933
+ `tenant_slug=${encodeURIComponent(tenantSlug)}`;
934
+ const popup = window.open(oauthUrl, `${provider}_register_tenant`, `width=${width},height=${height},left=${left},top=${top}`);
935
+ if (!popup) {
936
+ resolve({
937
+ success: false,
938
+ message: 'Popup blocked. Please allow popups for this site.'
939
+ });
940
+ return;
941
+ }
942
+ // Listen for message from popup
943
+ const messageHandler = (event) => {
944
+ // Verify origin
945
+ if (event.origin !== new URL(this.environment.accountsUrl).origin) {
946
+ return;
947
+ }
948
+ if (event.data.type === 'tenant_register_success') {
949
+ // Set tokens and user
950
+ if (event.data.access_token) {
951
+ this.tokens.setAccessToken(event.data.access_token);
952
+ this.signinStatus.setSigninStatus(true);
953
+ }
954
+ if (event.data.user) {
955
+ this.updateUser(event.data.user);
956
+ }
957
+ window.removeEventListener('message', messageHandler);
958
+ popup.close();
959
+ resolve({
960
+ success: true,
961
+ tenant: event.data.tenant,
962
+ user: event.data.user
963
+ });
964
+ }
965
+ else if (event.data.type === 'tenant_register_error') {
966
+ window.removeEventListener('message', messageHandler);
967
+ popup.close();
968
+ resolve({
969
+ success: false,
970
+ message: event.data.message || 'Tenant registration failed'
971
+ });
972
+ }
973
+ };
974
+ window.addEventListener('message', messageHandler);
975
+ // Check if popup was closed manually
976
+ const checkClosed = setInterval(() => {
977
+ if (popup.closed) {
978
+ clearInterval(checkClosed);
979
+ window.removeEventListener('message', messageHandler);
980
+ resolve({
981
+ success: false,
982
+ message: 'Registration cancelled'
983
+ });
984
+ }
985
+ }, 500);
986
+ });
987
+ }
988
+ /**
989
+ * Get all tenant memberships for the authenticated user
990
+ */
991
+ async getTenantMemberships() {
992
+ try {
993
+ const response = await fetch(`${this.environment.accountsUrl}/api/auth/memberships`, {
994
+ method: 'GET',
995
+ headers: {
996
+ 'Authorization': `Bearer ${this.tokens.getAccessToken()}`,
997
+ 'Content-Type': 'application/json'
998
+ },
999
+ credentials: 'include'
1000
+ });
1001
+ const data = await response.json();
1002
+ return {
1003
+ memberships: data.memberships || []
1004
+ };
1005
+ }
1006
+ catch (error) {
1007
+ return { memberships: [] };
1008
+ }
1009
+ }
1010
+ /**
1011
+ * Select a tenant for the current session
1012
+ * Updates the JWT token with tenant context
1013
+ */
1014
+ async selectTenant(tenantId) {
1015
+ try {
1016
+ const response = await fetch(`${this.environment.accountsUrl}/api/auth/select-tenant`, {
1017
+ method: 'POST',
1018
+ headers: {
1019
+ 'Authorization': `Bearer ${this.tokens.getAccessToken()}`,
1020
+ 'Content-Type': 'application/json'
1021
+ },
1022
+ credentials: 'include',
1023
+ body: JSON.stringify({ tenant_id: tenantId })
1024
+ });
1025
+ const data = await response.json();
1026
+ if (data.success && data.access_token) {
1027
+ this.tokens.setAccessToken(data.access_token);
1028
+ return {
1029
+ success: true,
1030
+ access_token: data.access_token
1031
+ };
1032
+ }
1033
+ return {
1034
+ success: false,
1035
+ message: data.message || 'Failed to select tenant'
1036
+ };
1037
+ }
1038
+ catch (error) {
1039
+ return {
1040
+ success: false,
1041
+ message: 'Network error. Please try again.'
1042
+ };
1043
+ }
1044
+ }
1045
+ /**
1046
+ * Check if a tenant slug is available
1047
+ */
1048
+ async checkTenantSlugAvailable(slug) {
1049
+ try {
1050
+ const response = await fetch(`${this.environment.accountsUrl}/api/auth/check-tenant-slug/${slug}`, {
1051
+ method: 'GET',
1052
+ headers: { 'Content-Type': 'application/json' }
1053
+ });
1054
+ const data = await response.json();
1055
+ return {
1056
+ available: data.available || false,
1057
+ suggestion: data.suggestion
1058
+ };
1059
+ }
1060
+ catch (error) {
1061
+ // On error, assume available (don't block registration)
1062
+ return { available: true };
1063
+ }
1064
+ }
824
1065
  // ===== Backward Compatibility Methods =====
825
1066
  // These methods are deprecated and maintained for backward compatibility
826
1067
  // with existing platform code. New code should use the methods above.
@@ -903,6 +1144,65 @@ class AuthService {
903
1144
  return null;
904
1145
  }
905
1146
  }
1147
+ /**
1148
+ * Check if user has completed onboarding (has a tenant)
1149
+ */
1150
+ async checkOnboardingStatus(identityId) {
1151
+ try {
1152
+ const response = await fetch(`${this.environment.accountsUrl}/api/auth/onboarding/status?platform_code=${this.environment.platformCode}&identity_id=${identityId}`, {
1153
+ method: 'GET',
1154
+ headers: { 'Content-Type': 'application/json' },
1155
+ credentials: 'include'
1156
+ });
1157
+ if (!response.ok) {
1158
+ throw new Error('Failed to check onboarding status');
1159
+ }
1160
+ return await response.json();
1161
+ }
1162
+ catch (error) {
1163
+ throw error;
1164
+ }
1165
+ }
1166
+ /**
1167
+ * Complete tenant onboarding (create tenant with country + org name)
1168
+ */
1169
+ async completeTenantOnboarding(countryCode, tenantName) {
1170
+ try {
1171
+ const accessToken = this.tokens.getAccessToken();
1172
+ if (!accessToken) {
1173
+ throw new Error('Not authenticated');
1174
+ }
1175
+ const response = await fetch(`${this.environment.accountsUrl}/api/auth/register-tenant`, {
1176
+ method: 'POST',
1177
+ headers: {
1178
+ 'Content-Type': 'application/json',
1179
+ 'Authorization': `Bearer ${accessToken}`
1180
+ },
1181
+ credentials: 'include',
1182
+ body: JSON.stringify({
1183
+ platform: this.environment.platformCode,
1184
+ tenant_name: tenantName,
1185
+ country_code: countryCode,
1186
+ provider: 'google', // Assuming OAuth
1187
+ oauth_token: accessToken
1188
+ })
1189
+ });
1190
+ if (!response.ok) {
1191
+ const errorData = await response.json();
1192
+ throw new Error(errorData.message || 'Failed to create tenant');
1193
+ }
1194
+ const data = await response.json();
1195
+ // Update tokens with new tenant-scoped tokens
1196
+ if (data.access_token) {
1197
+ this.tokens.setAccessToken(data.access_token);
1198
+ this.signinStatus.setSigninStatus(true);
1199
+ }
1200
+ return data;
1201
+ }
1202
+ catch (error) {
1203
+ throw error;
1204
+ }
1205
+ }
906
1206
  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 });
907
1207
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AuthService, providedIn: 'root' });
908
1208
  }
@@ -1455,6 +1755,1447 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
1455
1755
  `, 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}.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}.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}\n"] }]
1456
1756
  }], ctorParameters: () => [{ type: AuthService }] });
1457
1757
 
1758
+ class TenantLoginComponent {
1759
+ auth;
1760
+ // Component Configuration
1761
+ title = 'Sign In';
1762
+ providers = ['google'];
1763
+ showTenantSelector = true;
1764
+ autoSelectSingleTenant = true;
1765
+ allowTenantCreation = true;
1766
+ // Tenant Selector Labels
1767
+ tenantSelectorTitle = 'Select Organization';
1768
+ tenantSelectorDescription = 'Choose which organization you want to access:';
1769
+ continueButtonText = 'Continue';
1770
+ // Link Labels
1771
+ registerLinkText = "Don't have an account?";
1772
+ registerLinkAction = 'Sign up';
1773
+ createTenantLinkText = "Don't see your organization?";
1774
+ createTenantLinkAction = 'Create New Organization';
1775
+ // Outputs
1776
+ tenantSelected = new EventEmitter();
1777
+ createTenant = new EventEmitter();
1778
+ // Form Fields
1779
+ email = '';
1780
+ password = '';
1781
+ // State
1782
+ error = '';
1783
+ loading = false;
1784
+ useOAuth = true;
1785
+ oauthProviders = [];
1786
+ // Tenant Selection State
1787
+ showingTenantSelector = false;
1788
+ memberships = [];
1789
+ selectedTenantId = null;
1790
+ userName = '';
1791
+ constructor(auth) {
1792
+ this.auth = auth;
1793
+ }
1794
+ ngOnInit() {
1795
+ if (!this.providers || this.providers.length === 0) {
1796
+ this.error = 'Configuration Error: No authentication providers specified.';
1797
+ throw new Error('TenantLoginComponent requires providers input.');
1798
+ }
1799
+ this.oauthProviders = this.providers.filter(p => p !== 'emailPassword');
1800
+ // If only emailPassword is available, use it by default
1801
+ if (this.oauthProviders.length === 0 && this.isProviderEnabled('emailPassword')) {
1802
+ this.useOAuth = false;
1803
+ }
1804
+ }
1805
+ isProviderEnabled(provider) {
1806
+ return this.providers.includes(provider);
1807
+ }
1808
+ getProviderLabel(provider) {
1809
+ const labels = {
1810
+ google: 'Sign in with Google',
1811
+ linkedin: 'Sign in with LinkedIn',
1812
+ apple: 'Sign in with Apple',
1813
+ microsoft: 'Sign in with Microsoft',
1814
+ github: 'Sign in with GitHub',
1815
+ emailPassword: 'Sign in with Email'
1816
+ };
1817
+ return labels[provider];
1818
+ }
1819
+ getProviderIcon(provider) {
1820
+ return undefined;
1821
+ }
1822
+ toggleAuthMethod(event) {
1823
+ event.preventDefault();
1824
+ this.useOAuth = !this.useOAuth;
1825
+ this.error = '';
1826
+ }
1827
+ async onEmailLogin() {
1828
+ if (!this.email || !this.password) {
1829
+ this.error = 'Please enter email and password';
1830
+ return;
1831
+ }
1832
+ this.loading = true;
1833
+ this.error = '';
1834
+ try {
1835
+ const result = await this.auth.loginWithEmail(this.email, this.password);
1836
+ if (!result.success) {
1837
+ this.error = result.message || 'Login failed';
1838
+ return;
1839
+ }
1840
+ // Authentication successful, now handle tenant selection
1841
+ await this.handlePostAuthFlow();
1842
+ }
1843
+ catch (err) {
1844
+ this.error = err.message || 'An unexpected error occurred';
1845
+ }
1846
+ finally {
1847
+ this.loading = false;
1848
+ }
1849
+ }
1850
+ async onOAuthLogin(provider) {
1851
+ this.loading = true;
1852
+ this.error = '';
1853
+ try {
1854
+ const result = await this.auth.loginWithProvider(provider);
1855
+ if (!result.success) {
1856
+ this.error = result.message || 'OAuth login failed';
1857
+ return;
1858
+ }
1859
+ // Authentication successful, now handle tenant selection
1860
+ await this.handlePostAuthFlow();
1861
+ }
1862
+ catch (err) {
1863
+ this.error = err.message || 'An unexpected error occurred';
1864
+ }
1865
+ finally {
1866
+ this.loading = false;
1867
+ }
1868
+ }
1869
+ async handlePostAuthFlow() {
1870
+ if (!this.showTenantSelector) {
1871
+ // Tenant selection is disabled, emit event immediately
1872
+ this.tenantSelected.emit({
1873
+ tenantId: '',
1874
+ tenantSlug: '',
1875
+ role: ''
1876
+ });
1877
+ return;
1878
+ }
1879
+ // Fetch user's tenant memberships
1880
+ this.loading = true;
1881
+ try {
1882
+ const result = await this.auth.getTenantMemberships();
1883
+ if (!result.memberships || result.memberships.length === 0) {
1884
+ // User has no tenants, prompt to create one
1885
+ this.error = 'You are not a member of any organization. Please create one.';
1886
+ if (this.allowTenantCreation) {
1887
+ setTimeout(() => this.createTenant.emit(), 2000);
1888
+ }
1889
+ return;
1890
+ }
1891
+ this.memberships = result.memberships;
1892
+ // Get user name if available
1893
+ const currentUser = this.auth.getCurrentUser();
1894
+ if (currentUser) {
1895
+ this.userName = currentUser.display_name || currentUser.email;
1896
+ }
1897
+ // Auto-select if user has only one tenant
1898
+ if (this.memberships.length === 1 && this.autoSelectSingleTenant) {
1899
+ await this.selectAndContinue(this.memberships[0]);
1900
+ }
1901
+ else {
1902
+ // Show tenant selector
1903
+ this.showingTenantSelector = true;
1904
+ }
1905
+ }
1906
+ catch (err) {
1907
+ this.error = err.message || 'Failed to load organizations';
1908
+ }
1909
+ finally {
1910
+ this.loading = false;
1911
+ }
1912
+ }
1913
+ selectTenantItem(tenantId) {
1914
+ this.selectedTenantId = tenantId;
1915
+ }
1916
+ async onContinueWithTenant() {
1917
+ if (!this.selectedTenantId) {
1918
+ this.error = 'Please select an organization';
1919
+ return;
1920
+ }
1921
+ const membership = this.memberships.find(m => m.tenant_id === this.selectedTenantId);
1922
+ if (!membership) {
1923
+ this.error = 'Selected organization not found';
1924
+ return;
1925
+ }
1926
+ await this.selectAndContinue(membership);
1927
+ }
1928
+ async selectAndContinue(membership) {
1929
+ this.loading = true;
1930
+ this.error = '';
1931
+ try {
1932
+ const result = await this.auth.selectTenant(membership.tenant_id);
1933
+ if (!result.success) {
1934
+ this.error = result.message || 'Failed to select organization';
1935
+ return;
1936
+ }
1937
+ // Emit tenant selected event
1938
+ this.tenantSelected.emit({
1939
+ tenantId: membership.tenant_id,
1940
+ tenantSlug: membership.slug,
1941
+ role: membership.role
1942
+ });
1943
+ }
1944
+ catch (err) {
1945
+ this.error = err.message || 'An unexpected error occurred';
1946
+ }
1947
+ finally {
1948
+ this.loading = false;
1949
+ }
1950
+ }
1951
+ formatRole(role) {
1952
+ return role.charAt(0).toUpperCase() + role.slice(1);
1953
+ }
1954
+ formatLastAccessed(dateStr) {
1955
+ try {
1956
+ const date = new Date(dateStr);
1957
+ const now = new Date();
1958
+ const diffMs = now.getTime() - date.getTime();
1959
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
1960
+ if (diffDays === 0)
1961
+ return 'today';
1962
+ if (diffDays === 1)
1963
+ return 'yesterday';
1964
+ if (diffDays < 7)
1965
+ return `${diffDays} days ago`;
1966
+ if (diffDays < 30)
1967
+ return `${Math.floor(diffDays / 7)} weeks ago`;
1968
+ return `${Math.floor(diffDays / 30)} months ago`;
1969
+ }
1970
+ catch {
1971
+ return dateStr;
1972
+ }
1973
+ }
1974
+ onCreateTenantClick(event) {
1975
+ event.preventDefault();
1976
+ this.createTenant.emit();
1977
+ }
1978
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantLoginComponent, deps: [{ token: AuthService }], target: i0.ɵɵFactoryTarget.Component });
1979
+ 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", allowTenantCreation: "allowTenantCreation", tenantSelectorTitle: "tenantSelectorTitle", tenantSelectorDescription: "tenantSelectorDescription", continueButtonText: "continueButtonText", registerLinkText: "registerLinkText", registerLinkAction: "registerLinkAction", createTenantLinkText: "createTenantLinkText", createTenantLinkAction: "createTenantLinkAction" }, outputs: { tenantSelected: "tenantSelected", createTenant: "createTenant" }, ngImport: i0, template: `
1980
+ <div class="tenant-login-dialog">
1981
+ @if (!showingTenantSelector) {
1982
+ <!-- Step 1: Authentication -->
1983
+ <h2 class="login-title">{{ title }}</h2>
1984
+
1985
+ <!-- Email/Password Form (if enabled) -->
1986
+ @if (isProviderEnabled('emailPassword') && !useOAuth) {
1987
+ <form (ngSubmit)="onEmailLogin()" class="email-form">
1988
+ <div class="form-group">
1989
+ <input
1990
+ [(ngModel)]="email"
1991
+ name="email"
1992
+ placeholder="Email"
1993
+ type="email"
1994
+ required
1995
+ class="form-control">
1996
+ </div>
1997
+ <div class="form-group">
1998
+ <input
1999
+ [(ngModel)]="password"
2000
+ name="password"
2001
+ placeholder="Password"
2002
+ type="password"
2003
+ required
2004
+ class="form-control">
2005
+ </div>
2006
+ <button
2007
+ type="submit"
2008
+ [disabled]="loading"
2009
+ class="btn btn-primary btn-block">
2010
+ {{ loading ? 'Signing in...' : 'Sign in with Email' }}
2011
+ </button>
2012
+ </form>
2013
+
2014
+ <!-- Divider -->
2015
+ @if (oauthProviders.length > 0) {
2016
+ <div class="divider">
2017
+ <span>OR</span>
2018
+ </div>
2019
+ }
2020
+ }
2021
+
2022
+ <!-- OAuth Providers -->
2023
+ @if (oauthProviders.length > 0 && (useOAuth || !isProviderEnabled('emailPassword'))) {
2024
+ <div class="oauth-buttons">
2025
+ @for (provider of oauthProviders; track provider) {
2026
+ <button
2027
+ type="button"
2028
+ (click)="onOAuthLogin(provider)"
2029
+ [disabled]="loading"
2030
+ class="btn btn-oauth btn-{{ provider }}">
2031
+ @if (getProviderIcon(provider)) {
2032
+ <span class="oauth-icon">
2033
+ {{ getProviderIcon(provider) }}
2034
+ </span>
2035
+ }
2036
+ {{ getProviderLabel(provider) }}
2037
+ </button>
2038
+ }
2039
+ </div>
2040
+
2041
+ <!-- Switch to Email/Password -->
2042
+ @if (isProviderEnabled('emailPassword') && oauthProviders.length > 0) {
2043
+ <div class="switch-method">
2044
+ <a href="#" (click)="toggleAuthMethod($event)">
2045
+ {{ useOAuth ? 'Use email/password instead' : 'Use OAuth instead' }}
2046
+ </a>
2047
+ </div>
2048
+ }
2049
+ }
2050
+
2051
+ <!-- Error Message -->
2052
+ @if (error) {
2053
+ <div class="error-message">
2054
+ {{ error }}
2055
+ </div>
2056
+ }
2057
+
2058
+ <!-- Register Link -->
2059
+ @if (allowTenantCreation) {
2060
+ <div class="register-link">
2061
+ {{ registerLinkText }}
2062
+ <a href="#" (click)="onCreateTenantClick($event)">{{ registerLinkAction }}</a>
2063
+ </div>
2064
+ }
2065
+ } @else {
2066
+ <!-- Step 2: Tenant Selection -->
2067
+ <h2 class="login-title">{{ tenantSelectorTitle }}</h2>
2068
+
2069
+ @if (userName) {
2070
+ <div class="welcome-message">
2071
+ Welcome back, <strong>{{ userName }}</strong>!
2072
+ </div>
2073
+ }
2074
+
2075
+ <p class="selector-description">{{ tenantSelectorDescription }}</p>
2076
+
2077
+ <div class="tenant-list">
2078
+ @for (membership of memberships; track membership.tenant_id) {
2079
+ <div
2080
+ class="tenant-item"
2081
+ [class.selected]="selectedTenantId === membership.tenant_id"
2082
+ (click)="selectTenantItem(membership.tenant_id)">
2083
+ <div class="tenant-radio">
2084
+ <input
2085
+ type="radio"
2086
+ [checked]="selectedTenantId === membership.tenant_id"
2087
+ [name]="'tenant-' + membership.tenant_id"
2088
+ [id]="'tenant-' + membership.tenant_id">
2089
+ </div>
2090
+ <div class="tenant-info">
2091
+ <div class="tenant-name">{{ membership.name }}</div>
2092
+ <div class="tenant-meta">
2093
+ <span class="tenant-role">{{ formatRole(membership.role) }}</span>
2094
+ @if (membership.last_accessed) {
2095
+ <span class="tenant-separator">·</span>
2096
+ <span class="tenant-last-accessed">
2097
+ Last accessed {{ formatLastAccessed(membership.last_accessed) }}
2098
+ </span>
2099
+ }
2100
+ </div>
2101
+ </div>
2102
+ </div>
2103
+ }
2104
+ </div>
2105
+
2106
+ <button
2107
+ type="button"
2108
+ (click)="onContinueWithTenant()"
2109
+ [disabled]="!selectedTenantId || loading"
2110
+ class="btn btn-primary btn-block">
2111
+ {{ loading ? 'Loading...' : continueButtonText }}
2112
+ </button>
2113
+
2114
+ <!-- Error Message -->
2115
+ @if (error) {
2116
+ <div class="error-message">
2117
+ {{ error }}
2118
+ </div>
2119
+ }
2120
+
2121
+ <!-- Create New Tenant Link -->
2122
+ @if (allowTenantCreation) {
2123
+ <div class="create-tenant-link">
2124
+ {{ createTenantLinkText }}
2125
+ <a href="#" (click)="onCreateTenantClick($event)">{{ createTenantLinkAction }}</a>
2126
+ </div>
2127
+ }
2128
+ }
2129
+
2130
+ <!-- Loading Overlay -->
2131
+ @if (loading) {
2132
+ <div class="loading-overlay">
2133
+ <div class="spinner"></div>
2134
+ </div>
2135
+ }
2136
+ </div>
2137
+ `, 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}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.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}.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: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.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: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
2138
+ }
2139
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantLoginComponent, decorators: [{
2140
+ type: Component,
2141
+ args: [{ selector: 'lib-tenant-login', standalone: true, imports: [CommonModule, FormsModule], template: `
2142
+ <div class="tenant-login-dialog">
2143
+ @if (!showingTenantSelector) {
2144
+ <!-- Step 1: Authentication -->
2145
+ <h2 class="login-title">{{ title }}</h2>
2146
+
2147
+ <!-- Email/Password Form (if enabled) -->
2148
+ @if (isProviderEnabled('emailPassword') && !useOAuth) {
2149
+ <form (ngSubmit)="onEmailLogin()" class="email-form">
2150
+ <div class="form-group">
2151
+ <input
2152
+ [(ngModel)]="email"
2153
+ name="email"
2154
+ placeholder="Email"
2155
+ type="email"
2156
+ required
2157
+ class="form-control">
2158
+ </div>
2159
+ <div class="form-group">
2160
+ <input
2161
+ [(ngModel)]="password"
2162
+ name="password"
2163
+ placeholder="Password"
2164
+ type="password"
2165
+ required
2166
+ class="form-control">
2167
+ </div>
2168
+ <button
2169
+ type="submit"
2170
+ [disabled]="loading"
2171
+ class="btn btn-primary btn-block">
2172
+ {{ loading ? 'Signing in...' : 'Sign in with Email' }}
2173
+ </button>
2174
+ </form>
2175
+
2176
+ <!-- Divider -->
2177
+ @if (oauthProviders.length > 0) {
2178
+ <div class="divider">
2179
+ <span>OR</span>
2180
+ </div>
2181
+ }
2182
+ }
2183
+
2184
+ <!-- OAuth Providers -->
2185
+ @if (oauthProviders.length > 0 && (useOAuth || !isProviderEnabled('emailPassword'))) {
2186
+ <div class="oauth-buttons">
2187
+ @for (provider of oauthProviders; track provider) {
2188
+ <button
2189
+ type="button"
2190
+ (click)="onOAuthLogin(provider)"
2191
+ [disabled]="loading"
2192
+ class="btn btn-oauth btn-{{ provider }}">
2193
+ @if (getProviderIcon(provider)) {
2194
+ <span class="oauth-icon">
2195
+ {{ getProviderIcon(provider) }}
2196
+ </span>
2197
+ }
2198
+ {{ getProviderLabel(provider) }}
2199
+ </button>
2200
+ }
2201
+ </div>
2202
+
2203
+ <!-- Switch to Email/Password -->
2204
+ @if (isProviderEnabled('emailPassword') && oauthProviders.length > 0) {
2205
+ <div class="switch-method">
2206
+ <a href="#" (click)="toggleAuthMethod($event)">
2207
+ {{ useOAuth ? 'Use email/password instead' : 'Use OAuth instead' }}
2208
+ </a>
2209
+ </div>
2210
+ }
2211
+ }
2212
+
2213
+ <!-- Error Message -->
2214
+ @if (error) {
2215
+ <div class="error-message">
2216
+ {{ error }}
2217
+ </div>
2218
+ }
2219
+
2220
+ <!-- Register Link -->
2221
+ @if (allowTenantCreation) {
2222
+ <div class="register-link">
2223
+ {{ registerLinkText }}
2224
+ <a href="#" (click)="onCreateTenantClick($event)">{{ registerLinkAction }}</a>
2225
+ </div>
2226
+ }
2227
+ } @else {
2228
+ <!-- Step 2: Tenant Selection -->
2229
+ <h2 class="login-title">{{ tenantSelectorTitle }}</h2>
2230
+
2231
+ @if (userName) {
2232
+ <div class="welcome-message">
2233
+ Welcome back, <strong>{{ userName }}</strong>!
2234
+ </div>
2235
+ }
2236
+
2237
+ <p class="selector-description">{{ tenantSelectorDescription }}</p>
2238
+
2239
+ <div class="tenant-list">
2240
+ @for (membership of memberships; track membership.tenant_id) {
2241
+ <div
2242
+ class="tenant-item"
2243
+ [class.selected]="selectedTenantId === membership.tenant_id"
2244
+ (click)="selectTenantItem(membership.tenant_id)">
2245
+ <div class="tenant-radio">
2246
+ <input
2247
+ type="radio"
2248
+ [checked]="selectedTenantId === membership.tenant_id"
2249
+ [name]="'tenant-' + membership.tenant_id"
2250
+ [id]="'tenant-' + membership.tenant_id">
2251
+ </div>
2252
+ <div class="tenant-info">
2253
+ <div class="tenant-name">{{ membership.name }}</div>
2254
+ <div class="tenant-meta">
2255
+ <span class="tenant-role">{{ formatRole(membership.role) }}</span>
2256
+ @if (membership.last_accessed) {
2257
+ <span class="tenant-separator">·</span>
2258
+ <span class="tenant-last-accessed">
2259
+ Last accessed {{ formatLastAccessed(membership.last_accessed) }}
2260
+ </span>
2261
+ }
2262
+ </div>
2263
+ </div>
2264
+ </div>
2265
+ }
2266
+ </div>
2267
+
2268
+ <button
2269
+ type="button"
2270
+ (click)="onContinueWithTenant()"
2271
+ [disabled]="!selectedTenantId || loading"
2272
+ class="btn btn-primary btn-block">
2273
+ {{ loading ? 'Loading...' : continueButtonText }}
2274
+ </button>
2275
+
2276
+ <!-- Error Message -->
2277
+ @if (error) {
2278
+ <div class="error-message">
2279
+ {{ error }}
2280
+ </div>
2281
+ }
2282
+
2283
+ <!-- Create New Tenant Link -->
2284
+ @if (allowTenantCreation) {
2285
+ <div class="create-tenant-link">
2286
+ {{ createTenantLinkText }}
2287
+ <a href="#" (click)="onCreateTenantClick($event)">{{ createTenantLinkAction }}</a>
2288
+ </div>
2289
+ }
2290
+ }
2291
+
2292
+ <!-- Loading Overlay -->
2293
+ @if (loading) {
2294
+ <div class="loading-overlay">
2295
+ <div class="spinner"></div>
2296
+ </div>
2297
+ }
2298
+ </div>
2299
+ `, 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}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.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}.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"] }]
2300
+ }], ctorParameters: () => [{ type: AuthService }], propDecorators: { title: [{
2301
+ type: Input
2302
+ }], providers: [{
2303
+ type: Input
2304
+ }], showTenantSelector: [{
2305
+ type: Input
2306
+ }], autoSelectSingleTenant: [{
2307
+ type: Input
2308
+ }], allowTenantCreation: [{
2309
+ type: Input
2310
+ }], tenantSelectorTitle: [{
2311
+ type: Input
2312
+ }], tenantSelectorDescription: [{
2313
+ type: Input
2314
+ }], continueButtonText: [{
2315
+ type: Input
2316
+ }], registerLinkText: [{
2317
+ type: Input
2318
+ }], registerLinkAction: [{
2319
+ type: Input
2320
+ }], createTenantLinkText: [{
2321
+ type: Input
2322
+ }], createTenantLinkAction: [{
2323
+ type: Input
2324
+ }], tenantSelected: [{
2325
+ type: Output
2326
+ }], createTenant: [{
2327
+ type: Output
2328
+ }] } });
2329
+
2330
+ class TenantRegisterComponent {
2331
+ auth;
2332
+ // Component Configuration
2333
+ title = 'Create New Organization';
2334
+ providers = ['google'];
2335
+ requireTenantName = true;
2336
+ // Tenant Labels
2337
+ tenantSectionTitle = 'Organization Information';
2338
+ tenantNameLabel = 'Organization Name';
2339
+ tenantNamePlaceholder = 'Enter your organization name';
2340
+ tenantSlugLabel = 'Organization URL';
2341
+ tenantSlugPlaceholder = 'organization-name';
2342
+ urlPreviewEnabled = true;
2343
+ urlPreviewPrefix = 'medstoreapp.in/';
2344
+ // User Labels
2345
+ userSectionTitle = 'Your Information';
2346
+ oauthDescription = 'Recommended: Sign up with your Google account';
2347
+ // Warning Message
2348
+ ownershipTitle = 'CREATING A NEW ORGANIZATION';
2349
+ ownershipMessage = 'You are registering as an organization owner. This will create a new organization that you will manage. If you are an employee, DO NOT use this form. Ask your organization owner to invite you, then use the Login page.';
2350
+ // Buttons and Links
2351
+ submitButtonText = 'Create Organization';
2352
+ loginLinkText = 'Already have an account?';
2353
+ loginLinkAction = 'Sign in';
2354
+ // Outputs
2355
+ tenantCreated = new EventEmitter();
2356
+ navigateToLogin = new EventEmitter();
2357
+ // Form Fields
2358
+ tenantName = '';
2359
+ tenantSlug = '';
2360
+ displayName = '';
2361
+ email = '';
2362
+ password = '';
2363
+ confirmPassword = '';
2364
+ // State
2365
+ error = '';
2366
+ success = '';
2367
+ loading = false;
2368
+ loadingText = 'Creating your organization...';
2369
+ checkingSlug = false;
2370
+ slugAvailable = false;
2371
+ slugError = '';
2372
+ useEmailPassword = false;
2373
+ oauthProviders = [];
2374
+ constructor(auth) {
2375
+ this.auth = auth;
2376
+ }
2377
+ ngOnInit() {
2378
+ if (!this.providers || this.providers.length === 0) {
2379
+ this.error = 'Configuration Error: No authentication providers specified.';
2380
+ throw new Error('TenantRegisterComponent requires providers input.');
2381
+ }
2382
+ this.oauthProviders = this.providers.filter(p => p !== 'emailPassword');
2383
+ // If only emailPassword is available, show it by default
2384
+ if (this.oauthProviders.length === 0 && this.isProviderEnabled('emailPassword')) {
2385
+ this.useEmailPassword = true;
2386
+ }
2387
+ }
2388
+ isProviderEnabled(provider) {
2389
+ return this.providers.includes(provider);
2390
+ }
2391
+ getProviderLabel(provider) {
2392
+ const labels = {
2393
+ google: 'Sign up with Google',
2394
+ linkedin: 'Sign up with LinkedIn',
2395
+ apple: 'Sign up with Apple',
2396
+ microsoft: 'Sign up with Microsoft',
2397
+ github: 'Sign up with GitHub',
2398
+ emailPassword: 'Sign up with Email'
2399
+ };
2400
+ return labels[provider];
2401
+ }
2402
+ getProviderIcon(provider) {
2403
+ return undefined;
2404
+ }
2405
+ onTenantNameChange() {
2406
+ // Auto-generate slug from tenant name
2407
+ if (this.tenantName) {
2408
+ const slug = this.tenantName
2409
+ .toLowerCase()
2410
+ .replace(/[^a-z0-9\s-]/g, '')
2411
+ .replace(/\s+/g, '-')
2412
+ .replace(/-+/g, '-')
2413
+ .replace(/^-|-$/g, '');
2414
+ this.tenantSlug = slug;
2415
+ this.slugError = '';
2416
+ this.slugAvailable = false;
2417
+ }
2418
+ }
2419
+ async checkSlugAvailability() {
2420
+ if (!this.tenantSlug || this.tenantSlug.length < 3) {
2421
+ this.slugError = 'Slug must be at least 3 characters';
2422
+ return;
2423
+ }
2424
+ if (!/^[a-z0-9-]+$/.test(this.tenantSlug)) {
2425
+ this.slugError = 'Only lowercase letters, numbers, and hyphens allowed';
2426
+ return;
2427
+ }
2428
+ this.checkingSlug = true;
2429
+ this.slugError = '';
2430
+ try {
2431
+ const result = await this.auth.checkTenantSlugAvailable(this.tenantSlug);
2432
+ if (result.available) {
2433
+ this.slugAvailable = true;
2434
+ }
2435
+ else {
2436
+ this.slugError = 'This URL is already taken';
2437
+ if (result.suggestion) {
2438
+ this.slugError += `. Try: ${result.suggestion}`;
2439
+ }
2440
+ }
2441
+ }
2442
+ catch (err) {
2443
+ // Slug check failed, but don't block registration
2444
+ console.warn('Slug availability check failed:', err);
2445
+ }
2446
+ finally {
2447
+ this.checkingSlug = false;
2448
+ }
2449
+ }
2450
+ toggleAuthMethod(event) {
2451
+ event.preventDefault();
2452
+ this.useEmailPassword = !this.useEmailPassword;
2453
+ this.error = '';
2454
+ }
2455
+ isFormValid() {
2456
+ // Tenant information must be valid
2457
+ if (!this.tenantName || !this.tenantSlug) {
2458
+ return false;
2459
+ }
2460
+ if (this.slugError) {
2461
+ return false;
2462
+ }
2463
+ // If using email/password, check those fields
2464
+ if (this.useEmailPassword) {
2465
+ if (!this.displayName || !this.email || !this.password || !this.confirmPassword) {
2466
+ return false;
2467
+ }
2468
+ if (this.password.length < 8) {
2469
+ return false;
2470
+ }
2471
+ if (this.password !== this.confirmPassword) {
2472
+ return false;
2473
+ }
2474
+ }
2475
+ return true;
2476
+ }
2477
+ async onOAuthRegister(provider) {
2478
+ if (!this.isFormValid()) {
2479
+ this.error = 'Please complete the organization information';
2480
+ return;
2481
+ }
2482
+ this.loading = true;
2483
+ this.loadingText = `Signing up with ${provider}...`;
2484
+ this.error = '';
2485
+ try {
2486
+ const result = await this.auth.registerTenant({
2487
+ tenantName: this.tenantName,
2488
+ tenantSlug: this.tenantSlug,
2489
+ provider: provider
2490
+ });
2491
+ if (result.success && result.tenant && result.user) {
2492
+ this.success = 'Organization created successfully!';
2493
+ this.tenantCreated.emit({
2494
+ tenant: result.tenant,
2495
+ user: result.user
2496
+ });
2497
+ }
2498
+ else {
2499
+ this.error = result.message || 'Registration failed';
2500
+ }
2501
+ }
2502
+ catch (err) {
2503
+ this.error = err.message || 'An unexpected error occurred';
2504
+ }
2505
+ finally {
2506
+ this.loading = false;
2507
+ }
2508
+ }
2509
+ async onRegister() {
2510
+ if (!this.isFormValid()) {
2511
+ this.error = 'Please fill in all required fields correctly';
2512
+ return;
2513
+ }
2514
+ this.loading = true;
2515
+ this.loadingText = 'Creating your organization...';
2516
+ this.error = '';
2517
+ this.success = '';
2518
+ try {
2519
+ const result = await this.auth.registerTenant({
2520
+ tenantName: this.tenantName,
2521
+ tenantSlug: this.tenantSlug,
2522
+ displayName: this.displayName,
2523
+ email: this.email,
2524
+ password: this.password,
2525
+ provider: 'emailPassword'
2526
+ });
2527
+ if (result.success && result.tenant && result.user) {
2528
+ this.success = 'Organization created successfully!';
2529
+ this.tenantCreated.emit({
2530
+ tenant: result.tenant,
2531
+ user: result.user
2532
+ });
2533
+ }
2534
+ else {
2535
+ this.error = result.message || 'Registration failed';
2536
+ }
2537
+ }
2538
+ catch (err) {
2539
+ this.error = err.message || 'An unexpected error occurred';
2540
+ }
2541
+ finally {
2542
+ this.loading = false;
2543
+ }
2544
+ }
2545
+ onLoginClick(event) {
2546
+ event.preventDefault();
2547
+ this.navigateToLogin.emit();
2548
+ }
2549
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantRegisterComponent, deps: [{ token: AuthService }], target: i0.ɵɵFactoryTarget.Component });
2550
+ 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: `
2551
+ <div class="tenant-register-dialog">
2552
+ <h2 class="register-title">{{ title }}</h2>
2553
+
2554
+ <!-- Ownership Warning Message -->
2555
+ <div class="warning-box">
2556
+ <div class="warning-icon">⚠️</div>
2557
+ <div class="warning-content">
2558
+ <strong>{{ ownershipTitle }}</strong>
2559
+ <p>{{ ownershipMessage }}</p>
2560
+ </div>
2561
+ </div>
2562
+
2563
+ <form (ngSubmit)="onRegister()" class="register-form">
2564
+ <!-- Tenant Information Section -->
2565
+ <div class="section-header">{{ tenantSectionTitle }}</div>
2566
+
2567
+ <div class="form-group">
2568
+ <label for="tenantName">{{ tenantNameLabel }} *</label>
2569
+ <input
2570
+ id="tenantName"
2571
+ [(ngModel)]="tenantName"
2572
+ name="tenantName"
2573
+ [placeholder]="tenantNamePlaceholder"
2574
+ type="text"
2575
+ required
2576
+ (input)="onTenantNameChange()"
2577
+ class="form-control">
2578
+ </div>
2579
+
2580
+ <div class="form-group">
2581
+ <label for="tenantSlug">{{ tenantSlugLabel }} *</label>
2582
+ <input
2583
+ id="tenantSlug"
2584
+ [(ngModel)]="tenantSlug"
2585
+ name="tenantSlug"
2586
+ [placeholder]="tenantSlugPlaceholder"
2587
+ type="text"
2588
+ required
2589
+ pattern="[a-z0-9-]+"
2590
+ (blur)="checkSlugAvailability()"
2591
+ class="form-control"
2592
+ [class.input-error]="slugError"
2593
+ [class.input-success]="slugAvailable && tenantSlug">
2594
+ @if (tenantSlug && urlPreviewEnabled) {
2595
+ <small class="form-hint">
2596
+ {{ urlPreviewPrefix }}{{ tenantSlug }}
2597
+ @if (checkingSlug) {
2598
+ <span class="checking">Checking...</span>
2599
+ }
2600
+ @if (slugAvailable && !checkingSlug) {
2601
+ <span class="available">✓ Available</span>
2602
+ }
2603
+ @if (slugError) {
2604
+ <span class="error-text">{{ slugError }}</span>
2605
+ }
2606
+ </small>
2607
+ }
2608
+ </div>
2609
+
2610
+ <!-- User Information Section -->
2611
+ <div class="section-header">{{ userSectionTitle }}</div>
2612
+
2613
+ <!-- OAuth Providers (Primary Option) -->
2614
+ @if (oauthProviders.length > 0 && !useEmailPassword) {
2615
+ <div class="oauth-section">
2616
+ <p class="oauth-description">{{ oauthDescription }}</p>
2617
+ <div class="oauth-buttons">
2618
+ @for (provider of oauthProviders; track provider) {
2619
+ <button
2620
+ type="button"
2621
+ (click)="onOAuthRegister(provider)"
2622
+ [disabled]="loading || !isFormValid()"
2623
+ class="btn btn-oauth btn-{{ provider }}">
2624
+ @if (getProviderIcon(provider)) {
2625
+ <span class="oauth-icon">
2626
+ {{ getProviderIcon(provider) }}
2627
+ </span>
2628
+ }
2629
+ {{ getProviderLabel(provider) }}
2630
+ </button>
2631
+ }
2632
+ </div>
2633
+ </div>
2634
+
2635
+ <!-- Switch to Email/Password -->
2636
+ @if (isProviderEnabled('emailPassword')) {
2637
+ <div class="switch-method">
2638
+ <a href="#" (click)="toggleAuthMethod($event)">
2639
+ {{ useEmailPassword ? 'Use OAuth instead' : 'Use email/password instead' }}
2640
+ </a>
2641
+ </div>
2642
+ }
2643
+ }
2644
+
2645
+ <!-- Email/Password Form (Fallback or Manual Entry) -->
2646
+ @if (useEmailPassword || oauthProviders.length === 0) {
2647
+ <div class="form-group">
2648
+ <label for="displayName">Full Name *</label>
2649
+ <input
2650
+ id="displayName"
2651
+ [(ngModel)]="displayName"
2652
+ name="displayName"
2653
+ placeholder="Enter your full name"
2654
+ type="text"
2655
+ required
2656
+ class="form-control">
2657
+ </div>
2658
+
2659
+ <div class="form-group">
2660
+ <label for="email">Email *</label>
2661
+ <input
2662
+ id="email"
2663
+ [(ngModel)]="email"
2664
+ name="email"
2665
+ placeholder="Enter your email"
2666
+ type="email"
2667
+ required
2668
+ class="form-control">
2669
+ </div>
2670
+
2671
+ <div class="form-group">
2672
+ <label for="password">Password *</label>
2673
+ <input
2674
+ id="password"
2675
+ [(ngModel)]="password"
2676
+ name="password"
2677
+ placeholder="Create a password"
2678
+ type="password"
2679
+ required
2680
+ minlength="8"
2681
+ class="form-control">
2682
+ <small class="form-hint">At least 8 characters</small>
2683
+ </div>
2684
+
2685
+ <div class="form-group">
2686
+ <label for="confirmPassword">Confirm Password *</label>
2687
+ <input
2688
+ id="confirmPassword"
2689
+ [(ngModel)]="confirmPassword"
2690
+ name="confirmPassword"
2691
+ placeholder="Confirm your password"
2692
+ type="password"
2693
+ required
2694
+ class="form-control">
2695
+ </div>
2696
+
2697
+ <button
2698
+ type="submit"
2699
+ [disabled]="loading || !isFormValid()"
2700
+ class="btn btn-primary btn-block">
2701
+ {{ loading ? 'Creating...' : submitButtonText }}
2702
+ </button>
2703
+
2704
+ <!-- Switch to OAuth -->
2705
+ @if (oauthProviders.length > 0) {
2706
+ <div class="switch-method">
2707
+ <a href="#" (click)="toggleAuthMethod($event)">
2708
+ Use OAuth instead
2709
+ </a>
2710
+ </div>
2711
+ }
2712
+ }
2713
+ </form>
2714
+
2715
+ <!-- Error Message -->
2716
+ @if (error) {
2717
+ <div class="error-message">
2718
+ {{ error }}
2719
+ </div>
2720
+ }
2721
+
2722
+ <!-- Success Message -->
2723
+ @if (success) {
2724
+ <div class="success-message">
2725
+ {{ success }}
2726
+ </div>
2727
+ }
2728
+
2729
+ <!-- Loading Overlay -->
2730
+ @if (loading) {
2731
+ <div class="loading-overlay">
2732
+ <div class="spinner"></div>
2733
+ <p class="loading-text">{{ loadingText }}</p>
2734
+ </div>
2735
+ }
2736
+
2737
+ <!-- Login Link -->
2738
+ <div class="login-link">
2739
+ {{ loginLinkText }}
2740
+ <a href="#" (click)="onLoginClick($event)">{{ loginLinkAction }}</a>
2741
+ </div>
2742
+ </div>
2743
+ `, 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}.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}.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: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.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: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.MinLengthValidator, selector: "[minlength][formControlName],[minlength][formControl],[minlength][ngModel]", inputs: ["minlength"] }, { kind: "directive", type: i2.PatternValidator, selector: "[pattern][formControlName],[pattern][formControl],[pattern][ngModel]", inputs: ["pattern"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
2744
+ }
2745
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantRegisterComponent, decorators: [{
2746
+ type: Component,
2747
+ args: [{ selector: 'lib-tenant-register', standalone: true, imports: [CommonModule, FormsModule], template: `
2748
+ <div class="tenant-register-dialog">
2749
+ <h2 class="register-title">{{ title }}</h2>
2750
+
2751
+ <!-- Ownership Warning Message -->
2752
+ <div class="warning-box">
2753
+ <div class="warning-icon">⚠️</div>
2754
+ <div class="warning-content">
2755
+ <strong>{{ ownershipTitle }}</strong>
2756
+ <p>{{ ownershipMessage }}</p>
2757
+ </div>
2758
+ </div>
2759
+
2760
+ <form (ngSubmit)="onRegister()" class="register-form">
2761
+ <!-- Tenant Information Section -->
2762
+ <div class="section-header">{{ tenantSectionTitle }}</div>
2763
+
2764
+ <div class="form-group">
2765
+ <label for="tenantName">{{ tenantNameLabel }} *</label>
2766
+ <input
2767
+ id="tenantName"
2768
+ [(ngModel)]="tenantName"
2769
+ name="tenantName"
2770
+ [placeholder]="tenantNamePlaceholder"
2771
+ type="text"
2772
+ required
2773
+ (input)="onTenantNameChange()"
2774
+ class="form-control">
2775
+ </div>
2776
+
2777
+ <div class="form-group">
2778
+ <label for="tenantSlug">{{ tenantSlugLabel }} *</label>
2779
+ <input
2780
+ id="tenantSlug"
2781
+ [(ngModel)]="tenantSlug"
2782
+ name="tenantSlug"
2783
+ [placeholder]="tenantSlugPlaceholder"
2784
+ type="text"
2785
+ required
2786
+ pattern="[a-z0-9-]+"
2787
+ (blur)="checkSlugAvailability()"
2788
+ class="form-control"
2789
+ [class.input-error]="slugError"
2790
+ [class.input-success]="slugAvailable && tenantSlug">
2791
+ @if (tenantSlug && urlPreviewEnabled) {
2792
+ <small class="form-hint">
2793
+ {{ urlPreviewPrefix }}{{ tenantSlug }}
2794
+ @if (checkingSlug) {
2795
+ <span class="checking">Checking...</span>
2796
+ }
2797
+ @if (slugAvailable && !checkingSlug) {
2798
+ <span class="available">✓ Available</span>
2799
+ }
2800
+ @if (slugError) {
2801
+ <span class="error-text">{{ slugError }}</span>
2802
+ }
2803
+ </small>
2804
+ }
2805
+ </div>
2806
+
2807
+ <!-- User Information Section -->
2808
+ <div class="section-header">{{ userSectionTitle }}</div>
2809
+
2810
+ <!-- OAuth Providers (Primary Option) -->
2811
+ @if (oauthProviders.length > 0 && !useEmailPassword) {
2812
+ <div class="oauth-section">
2813
+ <p class="oauth-description">{{ oauthDescription }}</p>
2814
+ <div class="oauth-buttons">
2815
+ @for (provider of oauthProviders; track provider) {
2816
+ <button
2817
+ type="button"
2818
+ (click)="onOAuthRegister(provider)"
2819
+ [disabled]="loading || !isFormValid()"
2820
+ class="btn btn-oauth btn-{{ provider }}">
2821
+ @if (getProviderIcon(provider)) {
2822
+ <span class="oauth-icon">
2823
+ {{ getProviderIcon(provider) }}
2824
+ </span>
2825
+ }
2826
+ {{ getProviderLabel(provider) }}
2827
+ </button>
2828
+ }
2829
+ </div>
2830
+ </div>
2831
+
2832
+ <!-- Switch to Email/Password -->
2833
+ @if (isProviderEnabled('emailPassword')) {
2834
+ <div class="switch-method">
2835
+ <a href="#" (click)="toggleAuthMethod($event)">
2836
+ {{ useEmailPassword ? 'Use OAuth instead' : 'Use email/password instead' }}
2837
+ </a>
2838
+ </div>
2839
+ }
2840
+ }
2841
+
2842
+ <!-- Email/Password Form (Fallback or Manual Entry) -->
2843
+ @if (useEmailPassword || oauthProviders.length === 0) {
2844
+ <div class="form-group">
2845
+ <label for="displayName">Full Name *</label>
2846
+ <input
2847
+ id="displayName"
2848
+ [(ngModel)]="displayName"
2849
+ name="displayName"
2850
+ placeholder="Enter your full name"
2851
+ type="text"
2852
+ required
2853
+ class="form-control">
2854
+ </div>
2855
+
2856
+ <div class="form-group">
2857
+ <label for="email">Email *</label>
2858
+ <input
2859
+ id="email"
2860
+ [(ngModel)]="email"
2861
+ name="email"
2862
+ placeholder="Enter your email"
2863
+ type="email"
2864
+ required
2865
+ class="form-control">
2866
+ </div>
2867
+
2868
+ <div class="form-group">
2869
+ <label for="password">Password *</label>
2870
+ <input
2871
+ id="password"
2872
+ [(ngModel)]="password"
2873
+ name="password"
2874
+ placeholder="Create a password"
2875
+ type="password"
2876
+ required
2877
+ minlength="8"
2878
+ class="form-control">
2879
+ <small class="form-hint">At least 8 characters</small>
2880
+ </div>
2881
+
2882
+ <div class="form-group">
2883
+ <label for="confirmPassword">Confirm Password *</label>
2884
+ <input
2885
+ id="confirmPassword"
2886
+ [(ngModel)]="confirmPassword"
2887
+ name="confirmPassword"
2888
+ placeholder="Confirm your password"
2889
+ type="password"
2890
+ required
2891
+ class="form-control">
2892
+ </div>
2893
+
2894
+ <button
2895
+ type="submit"
2896
+ [disabled]="loading || !isFormValid()"
2897
+ class="btn btn-primary btn-block">
2898
+ {{ loading ? 'Creating...' : submitButtonText }}
2899
+ </button>
2900
+
2901
+ <!-- Switch to OAuth -->
2902
+ @if (oauthProviders.length > 0) {
2903
+ <div class="switch-method">
2904
+ <a href="#" (click)="toggleAuthMethod($event)">
2905
+ Use OAuth instead
2906
+ </a>
2907
+ </div>
2908
+ }
2909
+ }
2910
+ </form>
2911
+
2912
+ <!-- Error Message -->
2913
+ @if (error) {
2914
+ <div class="error-message">
2915
+ {{ error }}
2916
+ </div>
2917
+ }
2918
+
2919
+ <!-- Success Message -->
2920
+ @if (success) {
2921
+ <div class="success-message">
2922
+ {{ success }}
2923
+ </div>
2924
+ }
2925
+
2926
+ <!-- Loading Overlay -->
2927
+ @if (loading) {
2928
+ <div class="loading-overlay">
2929
+ <div class="spinner"></div>
2930
+ <p class="loading-text">{{ loadingText }}</p>
2931
+ </div>
2932
+ }
2933
+
2934
+ <!-- Login Link -->
2935
+ <div class="login-link">
2936
+ {{ loginLinkText }}
2937
+ <a href="#" (click)="onLoginClick($event)">{{ loginLinkAction }}</a>
2938
+ </div>
2939
+ </div>
2940
+ `, 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}.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}.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"] }]
2941
+ }], ctorParameters: () => [{ type: AuthService }], propDecorators: { title: [{
2942
+ type: Input
2943
+ }], providers: [{
2944
+ type: Input
2945
+ }], requireTenantName: [{
2946
+ type: Input
2947
+ }], tenantSectionTitle: [{
2948
+ type: Input
2949
+ }], tenantNameLabel: [{
2950
+ type: Input
2951
+ }], tenantNamePlaceholder: [{
2952
+ type: Input
2953
+ }], tenantSlugLabel: [{
2954
+ type: Input
2955
+ }], tenantSlugPlaceholder: [{
2956
+ type: Input
2957
+ }], urlPreviewEnabled: [{
2958
+ type: Input
2959
+ }], urlPreviewPrefix: [{
2960
+ type: Input
2961
+ }], userSectionTitle: [{
2962
+ type: Input
2963
+ }], oauthDescription: [{
2964
+ type: Input
2965
+ }], ownershipTitle: [{
2966
+ type: Input
2967
+ }], ownershipMessage: [{
2968
+ type: Input
2969
+ }], submitButtonText: [{
2970
+ type: Input
2971
+ }], loginLinkText: [{
2972
+ type: Input
2973
+ }], loginLinkAction: [{
2974
+ type: Input
2975
+ }], tenantCreated: [{
2976
+ type: Output
2977
+ }], navigateToLogin: [{
2978
+ type: Output
2979
+ }] } });
2980
+
2981
+ /**
2982
+ * Dialog wrapper for TenantLoginComponent
2983
+ *
2984
+ * Usage with Angular Material Dialog:
2985
+ * ```typescript
2986
+ * const dialogRef = this.dialog.open(TenantLoginDialogComponent, {
2987
+ * width: '450px',
2988
+ * data: {
2989
+ * providers: ['google'],
2990
+ * showTenantSelector: true
2991
+ * }
2992
+ * });
2993
+ *
2994
+ * dialogRef.afterClosed().subscribe(result => {
2995
+ * if (result && result.tenantId) {
2996
+ * console.log('Logged in to tenant:', result.tenantSlug);
2997
+ * // Redirect to dashboard
2998
+ * }
2999
+ * });
3000
+ * ```
3001
+ *
3002
+ * Usage with custom dialog service:
3003
+ * ```typescript
3004
+ * const dialog = this.dialogService.open(TenantLoginDialogComponent, {
3005
+ * providers: ['google', 'emailPassword'],
3006
+ * autoSelectSingleTenant: true
3007
+ * });
3008
+ * ```
3009
+ */
3010
+ class TenantLoginDialogComponent {
3011
+ data;
3012
+ dialogRef;
3013
+ constructor(injectedData, injectedDialogRef) {
3014
+ // Support both Angular Material Dialog and custom dialog implementations
3015
+ this.data = injectedData || {};
3016
+ this.dialogRef = injectedDialogRef;
3017
+ }
3018
+ onTenantSelected(event) {
3019
+ // Close dialog and return the selected tenant
3020
+ if (this.dialogRef && this.dialogRef.close) {
3021
+ this.dialogRef.close(event);
3022
+ }
3023
+ }
3024
+ onCreateTenant() {
3025
+ // Close dialog and signal that user wants to create a tenant
3026
+ if (this.dialogRef && this.dialogRef.close) {
3027
+ this.dialogRef.close({ action: 'create_tenant' });
3028
+ }
3029
+ }
3030
+ 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 });
3031
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: TenantLoginDialogComponent, isStandalone: true, selector: "lib-tenant-login-dialog", ngImport: i0, template: `
3032
+ <div class="dialog-wrapper">
3033
+ <lib-tenant-login
3034
+ [title]="data?.title || 'Sign In'"
3035
+ [providers]="data?.providers || ['google']"
3036
+ [showTenantSelector]="data?.showTenantSelector !== false"
3037
+ [autoSelectSingleTenant]="data?.autoSelectSingleTenant !== false"
3038
+ [allowTenantCreation]="data?.allowTenantCreation !== false"
3039
+ [tenantSelectorTitle]="data?.tenantSelectorTitle || 'Select Organization'"
3040
+ [tenantSelectorDescription]="data?.tenantSelectorDescription || 'Choose which organization you want to access:'"
3041
+ [continueButtonText]="data?.continueButtonText || 'Continue'"
3042
+ [registerLinkText]="data?.registerLinkText || 'Don\\'t have an account?'"
3043
+ [registerLinkAction]="data?.registerLinkAction || 'Sign up'"
3044
+ [createTenantLinkText]="data?.createTenantLinkText || 'Don\\'t see your organization?'"
3045
+ [createTenantLinkAction]="data?.createTenantLinkAction || 'Create New Organization'"
3046
+ (tenantSelected)="onTenantSelected($event)"
3047
+ (createTenant)="onCreateTenant()">
3048
+ </lib-tenant-login>
3049
+ </div>
3050
+ `, 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", "allowTenantCreation", "tenantSelectorTitle", "tenantSelectorDescription", "continueButtonText", "registerLinkText", "registerLinkAction", "createTenantLinkText", "createTenantLinkAction"], outputs: ["tenantSelected", "createTenant"] }] });
3051
+ }
3052
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantLoginDialogComponent, decorators: [{
3053
+ type: Component,
3054
+ args: [{ selector: 'lib-tenant-login-dialog', standalone: true, imports: [CommonModule, TenantLoginComponent], template: `
3055
+ <div class="dialog-wrapper">
3056
+ <lib-tenant-login
3057
+ [title]="data?.title || 'Sign In'"
3058
+ [providers]="data?.providers || ['google']"
3059
+ [showTenantSelector]="data?.showTenantSelector !== false"
3060
+ [autoSelectSingleTenant]="data?.autoSelectSingleTenant !== false"
3061
+ [allowTenantCreation]="data?.allowTenantCreation !== false"
3062
+ [tenantSelectorTitle]="data?.tenantSelectorTitle || 'Select Organization'"
3063
+ [tenantSelectorDescription]="data?.tenantSelectorDescription || 'Choose which organization you want to access:'"
3064
+ [continueButtonText]="data?.continueButtonText || 'Continue'"
3065
+ [registerLinkText]="data?.registerLinkText || 'Don\\'t have an account?'"
3066
+ [registerLinkAction]="data?.registerLinkAction || 'Sign up'"
3067
+ [createTenantLinkText]="data?.createTenantLinkText || 'Don\\'t see your organization?'"
3068
+ [createTenantLinkAction]="data?.createTenantLinkAction || 'Create New Organization'"
3069
+ (tenantSelected)="onTenantSelected($event)"
3070
+ (createTenant)="onCreateTenant()">
3071
+ </lib-tenant-login>
3072
+ </div>
3073
+ `, styles: [".dialog-wrapper{padding:0}\n"] }]
3074
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
3075
+ type: Optional
3076
+ }, {
3077
+ type: Inject,
3078
+ args: ['DIALOG_DATA']
3079
+ }] }, { type: undefined, decorators: [{
3080
+ type: Optional
3081
+ }, {
3082
+ type: Inject,
3083
+ args: ['DIALOG_REF']
3084
+ }] }] });
3085
+
3086
+ /**
3087
+ * Dialog wrapper for TenantRegisterComponent
3088
+ *
3089
+ * Usage with Angular Material Dialog:
3090
+ * ```typescript
3091
+ * const dialogRef = this.dialog.open(TenantRegisterDialogComponent, {
3092
+ * width: '500px',
3093
+ * data: {
3094
+ * providers: ['google'],
3095
+ * tenantNameLabel: 'Store Name'
3096
+ * }
3097
+ * });
3098
+ *
3099
+ * dialogRef.afterClosed().subscribe(result => {
3100
+ * if (result && result.tenant) {
3101
+ * console.log('Tenant created:', result.tenant);
3102
+ * }
3103
+ * });
3104
+ * ```
3105
+ *
3106
+ * Usage with custom dialog service:
3107
+ * ```typescript
3108
+ * const dialog = this.dialogService.open(TenantRegisterDialogComponent, {
3109
+ * providers: ['google', 'emailPassword']
3110
+ * });
3111
+ * ```
3112
+ */
3113
+ class TenantRegisterDialogComponent {
3114
+ data;
3115
+ dialogRef;
3116
+ constructor(injectedData, injectedDialogRef) {
3117
+ // Support both Angular Material Dialog and custom dialog implementations
3118
+ this.data = injectedData || {};
3119
+ this.dialogRef = injectedDialogRef;
3120
+ }
3121
+ onTenantCreated(event) {
3122
+ // Close dialog and return the created tenant
3123
+ if (this.dialogRef && this.dialogRef.close) {
3124
+ this.dialogRef.close(event);
3125
+ }
3126
+ }
3127
+ onNavigateToLogin() {
3128
+ // Close dialog without result (user wants to login instead)
3129
+ if (this.dialogRef && this.dialogRef.close) {
3130
+ this.dialogRef.close({ action: 'navigate_to_login' });
3131
+ }
3132
+ }
3133
+ 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 });
3134
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: TenantRegisterDialogComponent, isStandalone: true, selector: "lib-tenant-register-dialog", ngImport: i0, template: `
3135
+ <div class="dialog-wrapper">
3136
+ <lib-tenant-register
3137
+ [title]="data?.title || 'Create New Organization'"
3138
+ [providers]="data?.providers || ['google']"
3139
+ [requireTenantName]="data?.requireTenantName !== false"
3140
+ [tenantSectionTitle]="data?.tenantSectionTitle || 'Organization Information'"
3141
+ [tenantNameLabel]="data?.tenantNameLabel || 'Organization Name'"
3142
+ [tenantNamePlaceholder]="data?.tenantNamePlaceholder || 'Enter your organization name'"
3143
+ [tenantSlugLabel]="data?.tenantSlugLabel || 'Organization URL'"
3144
+ [tenantSlugPlaceholder]="data?.tenantSlugPlaceholder || 'organization-name'"
3145
+ [urlPreviewEnabled]="data?.urlPreviewEnabled !== false"
3146
+ [urlPreviewPrefix]="data?.urlPreviewPrefix || 'app.example.com/'"
3147
+ [userSectionTitle]="data?.userSectionTitle || 'Your Information'"
3148
+ [oauthDescription]="data?.oauthDescription || 'Recommended: Sign up with your Google account'"
3149
+ [ownershipTitle]="data?.ownershipTitle || 'CREATING A NEW ORGANIZATION'"
3150
+ [ownershipMessage]="data?.ownershipMessage || 'You are registering as an organization owner. If you are an employee, use Login instead.'"
3151
+ [submitButtonText]="data?.submitButtonText || 'Create Organization'"
3152
+ [loginLinkText]="data?.loginLinkText || 'Already have an account?'"
3153
+ [loginLinkAction]="data?.loginLinkAction || 'Sign in'"
3154
+ (tenantCreated)="onTenantCreated($event)"
3155
+ (navigateToLogin)="onNavigateToLogin()">
3156
+ </lib-tenant-register>
3157
+ </div>
3158
+ `, 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"] }] });
3159
+ }
3160
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantRegisterDialogComponent, decorators: [{
3161
+ type: Component,
3162
+ args: [{ selector: 'lib-tenant-register-dialog', standalone: true, imports: [CommonModule, TenantRegisterComponent], template: `
3163
+ <div class="dialog-wrapper">
3164
+ <lib-tenant-register
3165
+ [title]="data?.title || 'Create New Organization'"
3166
+ [providers]="data?.providers || ['google']"
3167
+ [requireTenantName]="data?.requireTenantName !== false"
3168
+ [tenantSectionTitle]="data?.tenantSectionTitle || 'Organization Information'"
3169
+ [tenantNameLabel]="data?.tenantNameLabel || 'Organization Name'"
3170
+ [tenantNamePlaceholder]="data?.tenantNamePlaceholder || 'Enter your organization name'"
3171
+ [tenantSlugLabel]="data?.tenantSlugLabel || 'Organization URL'"
3172
+ [tenantSlugPlaceholder]="data?.tenantSlugPlaceholder || 'organization-name'"
3173
+ [urlPreviewEnabled]="data?.urlPreviewEnabled !== false"
3174
+ [urlPreviewPrefix]="data?.urlPreviewPrefix || 'app.example.com/'"
3175
+ [userSectionTitle]="data?.userSectionTitle || 'Your Information'"
3176
+ [oauthDescription]="data?.oauthDescription || 'Recommended: Sign up with your Google account'"
3177
+ [ownershipTitle]="data?.ownershipTitle || 'CREATING A NEW ORGANIZATION'"
3178
+ [ownershipMessage]="data?.ownershipMessage || 'You are registering as an organization owner. If you are an employee, use Login instead.'"
3179
+ [submitButtonText]="data?.submitButtonText || 'Create Organization'"
3180
+ [loginLinkText]="data?.loginLinkText || 'Already have an account?'"
3181
+ [loginLinkAction]="data?.loginLinkAction || 'Sign in'"
3182
+ (tenantCreated)="onTenantCreated($event)"
3183
+ (navigateToLogin)="onNavigateToLogin()">
3184
+ </lib-tenant-register>
3185
+ </div>
3186
+ `, styles: [".dialog-wrapper{padding:0}\n"] }]
3187
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
3188
+ type: Optional
3189
+ }, {
3190
+ type: Inject,
3191
+ args: ['DIALOG_DATA']
3192
+ }] }, { type: undefined, decorators: [{
3193
+ type: Optional
3194
+ }, {
3195
+ type: Inject,
3196
+ args: ['DIALOG_REF']
3197
+ }] }] });
3198
+
1458
3199
  /*
1459
3200
  * Public API Surface of ngx-stonescriptphp-client
1460
3201
  */
@@ -1463,5 +3204,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
1463
3204
  * Generated bundle index. Do not edit.
1464
3205
  */
1465
3206
 
1466
- export { ApiConnectionService, ApiResponse, AuthService, CsrfService, DbService, LoginDialogComponent, MyEnvironmentModel, NgxStoneScriptPhpClientModule, RegisterComponent, SigninStatusService, TokenService, VerifyStatus };
3207
+ export { ApiConnectionService, ApiResponse, AuthService, CsrfService, DbService, LoginDialogComponent, MyEnvironmentModel, NgxStoneScriptPhpClientModule, RegisterComponent, SigninStatusService, TenantLoginComponent, TenantLoginDialogComponent, TenantRegisterComponent, TenantRegisterDialogComponent, TokenService, VerifyStatus };
1467
3208
  //# sourceMappingURL=progalaxyelabs-ngx-stonescriptphp-client.mjs.map