@stack-spot/auth-react 2.13.0 → 2.14.1-beta.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.
- package/CHANGELOG.md +242 -235
- package/out/index.d.ts +4 -103
- package/out/index.js +20 -780
- package/out/index.js.map +1 -1
- package/out/index.mjs +4 -780
- package/out/index.mjs.map +1 -1
- package/package.json +39 -39
- package/rollup.config.mjs +37 -37
- package/src/Authenticated.tsx +65 -65
- package/src/IDPLogin.tsx +102 -102
- package/src/Login.tsx +206 -206
- package/src/SSOLogin.tsx +39 -39
- package/src/SessionManager.ts +296 -296
- package/src/dictionary.ts +48 -48
- package/src/hooks.ts +34 -34
- package/src/index.ts +5 -5
- package/src/last-login-type.ts +20 -20
- package/src/provider-icons/Github.tsx +21 -21
- package/src/provider-icons/Google.tsx +17 -17
- package/src/provider-icons/Microsoft.tsx +10 -10
- package/src/types.ts +29 -29
- package/src/utils/cookies.ts +24 -24
- package/src/utils/redirect.ts +20 -20
- package/tsconfig.json +11 -11
package/src/SessionManager.ts
CHANGED
|
@@ -1,296 +1,296 @@
|
|
|
1
|
-
import { AccessTokenPayload, AuthConfig, AuthManager, GenericLogger, Session, ThirdPartyAuthType, ThirdPartyLoginParams } from '@stack-spot/auth'
|
|
2
|
-
import { sessionCookie } from './utils/cookies'
|
|
3
|
-
import { redirect } from './utils/redirect'
|
|
4
|
-
|
|
5
|
-
const sessionKey = 'session'
|
|
6
|
-
|
|
7
|
-
interface SessionManagerConfig extends Pick<AuthConfig, 'accountUrl' | 'authUrl' | 'clientId' | 'defaultTenant' | 'retry' | 'retryDelay' | 'logger'> {
|
|
8
|
-
/**
|
|
9
|
-
* The URL to redirect to when the user logs out.
|
|
10
|
-
* @default location.origin
|
|
11
|
-
*/
|
|
12
|
-
loginUrl?: string,
|
|
13
|
-
/**
|
|
14
|
-
* The URL to redirect to when the login completes in the authentication app. If not provided, will be the same as `loginUrl`.
|
|
15
|
-
* @default loginUrl
|
|
16
|
-
*/
|
|
17
|
-
redirectUrl?: string,
|
|
18
|
-
/**
|
|
19
|
-
* Forbidden authentication types to this Session Manager.
|
|
20
|
-
*/
|
|
21
|
-
blockedAuthTypes?: ThirdPartyAuthType[]
|
|
22
|
-
/**
|
|
23
|
-
* A URL to send login events to (observability).
|
|
24
|
-
*/
|
|
25
|
-
rdUrl?: string,
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
type AuthExtraData = { from?: string | null, finalRedirect?: string | null }
|
|
29
|
-
|
|
30
|
-
type ChangeListener = (session: Session | undefined) => void
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Controls the current session in a browser.
|
|
34
|
-
*
|
|
35
|
-
* This should not be used under a Node.JS environment.
|
|
36
|
-
*
|
|
37
|
-
* This is a singleton. To create the first instance or recover the current one, use `SessionManager.create`.
|
|
38
|
-
*/
|
|
39
|
-
export class SessionManager {
|
|
40
|
-
private current: Session | undefined
|
|
41
|
-
private readonly auth: AuthManager<AuthExtraData>
|
|
42
|
-
private config: SessionManagerConfig
|
|
43
|
-
private changeListeners: ChangeListener[] = []
|
|
44
|
-
private logger: GenericLogger
|
|
45
|
-
static instance: SessionManager | undefined
|
|
46
|
-
|
|
47
|
-
private constructor(config: SessionManagerConfig) {
|
|
48
|
-
config.loginUrl ||= location.origin
|
|
49
|
-
const redirectUrl = (config.redirectUrl || config.loginUrl).replace(/([^/])$/, '$1/') // the trailing "/" is required by Stackspot IAM.
|
|
50
|
-
this.config = config
|
|
51
|
-
this.auth = new AuthManager<AuthExtraData>({
|
|
52
|
-
...config,
|
|
53
|
-
redirectUrl,
|
|
54
|
-
storage: localStorage,
|
|
55
|
-
sessionPersistence: {
|
|
56
|
-
load: () => localStorage.getItem(sessionKey),
|
|
57
|
-
save: (session) => localStorage.setItem(sessionKey, session),
|
|
58
|
-
},
|
|
59
|
-
})
|
|
60
|
-
this.logger = this.auth.config.logger
|
|
61
|
-
SessionManager.instance = this
|
|
62
|
-
|
|
63
|
-
// Keep session in sync with other app's session
|
|
64
|
-
addEventListener('focus', () => this.validateSharedSession())
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
static create(config: SessionManagerConfig) {
|
|
68
|
-
return SessionManager.instance ?? new SessionManager(config)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
private setSession(session: Session | undefined) {
|
|
72
|
-
this.current = session
|
|
73
|
-
this.changeListeners.forEach(l => l(session))
|
|
74
|
-
if (session) this.setSessionCookie(session)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async restoreSession() {
|
|
78
|
-
const session = await this.auth.restoreSession()
|
|
79
|
-
this.logger.log('Validating shared session.')
|
|
80
|
-
const sessionValid = await this.validateSharedSession(session)
|
|
81
|
-
this.setSession(sessionValid ? session : undefined)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async validateSharedSession(session: Session | undefined = this.current): Promise<boolean> {
|
|
85
|
-
|
|
86
|
-
// skipping because authentication is in progress
|
|
87
|
-
if (this.urlHasThirdPartyLoginData()) {
|
|
88
|
-
this.logger.log('Session is invalid because there\'s another authentication in progress.')
|
|
89
|
-
return false
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const sharedSessionCookie = sessionCookie.get()
|
|
93
|
-
|
|
94
|
-
// It has been logged out on another portal, so logout on this one too
|
|
95
|
-
if (!sharedSessionCookie) {
|
|
96
|
-
this.logger.log('Session is invalid because no shared session cookie was found, i.e, a logout was performed in another portal. Forcing log off.')
|
|
97
|
-
session && await this.logout()
|
|
98
|
-
return false
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const isDifferentSessionActive = sharedSessionCookie.sub != session?.getTokenData().sub
|
|
102
|
-
const isSharedSessionTypeBlocked = this.config.blockedAuthTypes?.includes(sharedSessionCookie.type)
|
|
103
|
-
if (isSharedSessionTypeBlocked) {
|
|
104
|
-
this.logger.log('Session is invalid because shared sessions have been blocked in the SessionManager\'s configuration (blockedAuthTypes).')
|
|
105
|
-
return false
|
|
106
|
-
} else if (isDifferentSessionActive || !session) {
|
|
107
|
-
this.logger.log(isDifferentSessionActive
|
|
108
|
-
? 'Session is invalid because a different session is already active.'
|
|
109
|
-
: 'Session is invalid because it\'s undefined.'
|
|
110
|
-
)
|
|
111
|
-
this.logger.log('Starting login with tenant from the session cookie.')
|
|
112
|
-
await this.startThirdPartyLoginUsingTenant(sharedSessionCookie)
|
|
113
|
-
return false
|
|
114
|
-
}
|
|
115
|
-
return true
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
hasSession() {
|
|
119
|
-
return !!this.current && !this.current.isExpired()
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
getSession() {
|
|
123
|
-
if (!this.hasSession()) {
|
|
124
|
-
this.endSession()
|
|
125
|
-
throw new Error('Session is not available, redirecting to login.')
|
|
126
|
-
}
|
|
127
|
-
return this.current!
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async endSession(redirectToLogin = true) {
|
|
131
|
-
this.current = undefined
|
|
132
|
-
localStorage.removeItem(sessionKey)
|
|
133
|
-
sessionCookie.delete()
|
|
134
|
-
if (redirectToLogin && this.config.loginUrl) await redirect(this.config.loginUrl)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async restartSession() {
|
|
138
|
-
await this.logout({ endSession: false })
|
|
139
|
-
this.current = undefined
|
|
140
|
-
localStorage.removeItem(sessionKey)
|
|
141
|
-
await this.restoreSession()
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async logout({ endSession }: { endSession?: boolean } | undefined = { endSession: true }) {
|
|
145
|
-
try {
|
|
146
|
-
await this.current?.logout()
|
|
147
|
-
} catch (error) {
|
|
148
|
-
// eslint-disable-next-line no-console
|
|
149
|
-
console.error(`Could not logout from IDM.\n${error}`)
|
|
150
|
-
}
|
|
151
|
-
if (!endSession) return
|
|
152
|
-
await this.endSession()
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
async startThirdPartyLogin(data: ThirdPartyLoginParams) {
|
|
156
|
-
const params = new URLSearchParams(location.search)
|
|
157
|
-
const authUrl = await this.auth.startThirdPartyLogin(data, {
|
|
158
|
-
from: location.href,
|
|
159
|
-
finalRedirect: params.get('finalRedirect'),
|
|
160
|
-
})
|
|
161
|
-
await redirect(authUrl)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
urlHasThirdPartyLoginData() {
|
|
165
|
-
const url = new URL(location.toString())
|
|
166
|
-
return url.searchParams.has('state') && !url.searchParams.has('error')
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async startThirdPartyLoginUsingTenant(data: ThirdPartyLoginParams) {
|
|
170
|
-
const params = new URLSearchParams(location.search)
|
|
171
|
-
const cookie = sessionCookie.get()
|
|
172
|
-
if (!cookie || !cookie.tenant) {
|
|
173
|
-
this.logger.log('Login out because no tenant information is available in the following data:', JSON.stringify(data))
|
|
174
|
-
//If no tenant is available we should log out the user
|
|
175
|
-
await this.logout()
|
|
176
|
-
return
|
|
177
|
-
}
|
|
178
|
-
const authUrl = await this.auth.getThirdPartyLoginFromTenant(data, cookie.tenant, {
|
|
179
|
-
from: location.href,
|
|
180
|
-
finalRedirect: params.get('finalRedirect'),
|
|
181
|
-
}
|
|
182
|
-
)
|
|
183
|
-
await redirect(authUrl)
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
async completeThirdPartyLogin() {
|
|
187
|
-
const url = new URL(location.toString())
|
|
188
|
-
if (url.searchParams.has('error')) {
|
|
189
|
-
throw new Error(`Error while signing in: ${url.searchParams.get('error_description')}`)
|
|
190
|
-
}
|
|
191
|
-
const { session, data: { from, finalRedirect } } = await this.auth.completeThirdPartyLogin(location.search)
|
|
192
|
-
this.setSession(session)
|
|
193
|
-
history.replaceState(null, '', from || location.toString().replace(/\?.*$/, ''))
|
|
194
|
-
this.sendLoginEventRd(this.current?.getTokenData())
|
|
195
|
-
if (finalRedirect) await redirect(finalRedirect)
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
getEmailForLogin() {
|
|
199
|
-
const session = sessionCookie.get()
|
|
200
|
-
return session?.type == 'sso' ? session.email : undefined
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
async switchAccount(accountId: string) {
|
|
204
|
-
this.logger.log('Switching accounts', accountId, this.current?.getTokenData().account_id_v2)
|
|
205
|
-
try {
|
|
206
|
-
this.current && await this.auth.switchAccount(accountId, this.current)
|
|
207
|
-
} catch (error) {
|
|
208
|
-
this.logger.error('Error while switching accounts', error)
|
|
209
|
-
throw error
|
|
210
|
-
}
|
|
211
|
-
this.setSession(this.current)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
onChange(listener: ChangeListener) {
|
|
215
|
-
this.changeListeners.push(listener)
|
|
216
|
-
return () => {
|
|
217
|
-
const index = this.changeListeners.indexOf(listener)
|
|
218
|
-
if (index != -1) this.changeListeners.splice(index, 1)
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
private setSessionCookie(session: Session) {
|
|
223
|
-
const { email, account_type, sub, tenant } = session.getTokenData()
|
|
224
|
-
const { provider, refresh_expires_in } = session.getSessionData()
|
|
225
|
-
if (!email || !sub || !tenant) return
|
|
226
|
-
const isFreemium = account_type == 'FREEMIUM'
|
|
227
|
-
const cookieAttributes = { 'Max-Age': refresh_expires_in, path: '/' }
|
|
228
|
-
if (isFreemium) {
|
|
229
|
-
sessionCookie.set({ type: 'idp', provider: provider!, sub, tenant }, cookieAttributes)
|
|
230
|
-
} else {
|
|
231
|
-
sessionCookie.set({ email, type: 'sso', sub, tenant }, cookieAttributes)
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
private async sendLoginEventRd(tokenData?: AccessTokenPayload) {
|
|
236
|
-
if (!this.config.rdUrl) return
|
|
237
|
-
|
|
238
|
-
if (!tokenData) {
|
|
239
|
-
// eslint-disable-next-line no-console
|
|
240
|
-
console.error('Unable to trigger login hook. No sessionEmail or name identified.')
|
|
241
|
-
return
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const { email, name, account_type, client_id, account_name, trial_account_status } = tokenData
|
|
245
|
-
const isLoginAI = client_id === "stackspot-portal-ai"
|
|
246
|
-
const isLoginEDP = client_id === "stackspot-portal"
|
|
247
|
-
|
|
248
|
-
if (!isLoginAI && !isLoginEDP && trial_account_status === 'PENDING') return
|
|
249
|
-
|
|
250
|
-
const leadType = account_type === 'FREEMIUM' ? 'TRIAL' : 'ENTERPRISE'
|
|
251
|
-
|
|
252
|
-
const rdObject = {
|
|
253
|
-
event_type: 'CONVERSION',
|
|
254
|
-
event_family: 'CDP',
|
|
255
|
-
payload: {
|
|
256
|
-
email,
|
|
257
|
-
name,
|
|
258
|
-
conversion_identifier: isLoginAI ? 'login_ai' : 'login_edp',
|
|
259
|
-
cf_leadtype: leadType,
|
|
260
|
-
cf_account_name: leadType === 'TRIAL' ? leadType : account_name,
|
|
261
|
-
},
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const response = await fetch(this.config.rdUrl, {
|
|
265
|
-
method: 'POST',
|
|
266
|
-
body: JSON.stringify(rdObject),
|
|
267
|
-
headers: {
|
|
268
|
-
'content-type': 'application/json',
|
|
269
|
-
},
|
|
270
|
-
})
|
|
271
|
-
const data = await response.json()
|
|
272
|
-
|
|
273
|
-
if (!response.ok) {
|
|
274
|
-
// eslint-disable-next-line no-console
|
|
275
|
-
console.error('Error while sending event to RD Station', data)
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
async getTrialEnabledProviders() {
|
|
280
|
-
try {
|
|
281
|
-
const response = await fetch(`${this.config.accountUrl}/v1/accounts/trial/sso`)
|
|
282
|
-
const trialProviders = await response.json()
|
|
283
|
-
|
|
284
|
-
if (!response.ok) {
|
|
285
|
-
// eslint-disable-next-line no-console
|
|
286
|
-
console.error('Error while fetching available login providers', trialProviders)
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const providerKeys = Object.keys(trialProviders || {})
|
|
290
|
-
return providerKeys.filter(key => trialProviders[key] === true)
|
|
291
|
-
} catch (error) {
|
|
292
|
-
console.error('Error while fetching available login providers', error)
|
|
293
|
-
return []
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}
|
|
1
|
+
import { AccessTokenPayload, AuthConfig, AuthManager, GenericLogger, Session, ThirdPartyAuthType, ThirdPartyLoginParams } from '@stack-spot/auth'
|
|
2
|
+
import { sessionCookie } from './utils/cookies'
|
|
3
|
+
import { redirect } from './utils/redirect'
|
|
4
|
+
|
|
5
|
+
const sessionKey = 'session'
|
|
6
|
+
|
|
7
|
+
interface SessionManagerConfig extends Pick<AuthConfig, 'accountUrl' | 'authUrl' | 'clientId' | 'defaultTenant' | 'retry' | 'retryDelay' | 'logger'> {
|
|
8
|
+
/**
|
|
9
|
+
* The URL to redirect to when the user logs out.
|
|
10
|
+
* @default location.origin
|
|
11
|
+
*/
|
|
12
|
+
loginUrl?: string,
|
|
13
|
+
/**
|
|
14
|
+
* The URL to redirect to when the login completes in the authentication app. If not provided, will be the same as `loginUrl`.
|
|
15
|
+
* @default loginUrl
|
|
16
|
+
*/
|
|
17
|
+
redirectUrl?: string,
|
|
18
|
+
/**
|
|
19
|
+
* Forbidden authentication types to this Session Manager.
|
|
20
|
+
*/
|
|
21
|
+
blockedAuthTypes?: ThirdPartyAuthType[]
|
|
22
|
+
/**
|
|
23
|
+
* A URL to send login events to (observability).
|
|
24
|
+
*/
|
|
25
|
+
rdUrl?: string,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type AuthExtraData = { from?: string | null, finalRedirect?: string | null }
|
|
29
|
+
|
|
30
|
+
type ChangeListener = (session: Session | undefined) => void
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Controls the current session in a browser.
|
|
34
|
+
*
|
|
35
|
+
* This should not be used under a Node.JS environment.
|
|
36
|
+
*
|
|
37
|
+
* This is a singleton. To create the first instance or recover the current one, use `SessionManager.create`.
|
|
38
|
+
*/
|
|
39
|
+
export class SessionManager {
|
|
40
|
+
private current: Session | undefined
|
|
41
|
+
private readonly auth: AuthManager<AuthExtraData>
|
|
42
|
+
private config: SessionManagerConfig
|
|
43
|
+
private changeListeners: ChangeListener[] = []
|
|
44
|
+
private logger: GenericLogger
|
|
45
|
+
static instance: SessionManager | undefined
|
|
46
|
+
|
|
47
|
+
private constructor(config: SessionManagerConfig) {
|
|
48
|
+
config.loginUrl ||= location.origin
|
|
49
|
+
const redirectUrl = (config.redirectUrl || config.loginUrl).replace(/([^/])$/, '$1/') // the trailing "/" is required by Stackspot IAM.
|
|
50
|
+
this.config = config
|
|
51
|
+
this.auth = new AuthManager<AuthExtraData>({
|
|
52
|
+
...config,
|
|
53
|
+
redirectUrl,
|
|
54
|
+
storage: localStorage,
|
|
55
|
+
sessionPersistence: {
|
|
56
|
+
load: () => localStorage.getItem(sessionKey),
|
|
57
|
+
save: (session) => localStorage.setItem(sessionKey, session),
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
this.logger = this.auth.config.logger
|
|
61
|
+
SessionManager.instance = this
|
|
62
|
+
|
|
63
|
+
// Keep session in sync with other app's session
|
|
64
|
+
addEventListener('focus', () => this.validateSharedSession())
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static create(config: SessionManagerConfig) {
|
|
68
|
+
return SessionManager.instance ?? new SessionManager(config)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private setSession(session: Session | undefined) {
|
|
72
|
+
this.current = session
|
|
73
|
+
this.changeListeners.forEach(l => l(session))
|
|
74
|
+
if (session) this.setSessionCookie(session)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async restoreSession() {
|
|
78
|
+
const session = await this.auth.restoreSession()
|
|
79
|
+
this.logger.log('Validating shared session.')
|
|
80
|
+
const sessionValid = await this.validateSharedSession(session)
|
|
81
|
+
this.setSession(sessionValid ? session : undefined)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async validateSharedSession(session: Session | undefined = this.current): Promise<boolean> {
|
|
85
|
+
|
|
86
|
+
// skipping because authentication is in progress
|
|
87
|
+
if (this.urlHasThirdPartyLoginData()) {
|
|
88
|
+
this.logger.log('Session is invalid because there\'s another authentication in progress.')
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const sharedSessionCookie = sessionCookie.get()
|
|
93
|
+
|
|
94
|
+
// It has been logged out on another portal, so logout on this one too
|
|
95
|
+
if (!sharedSessionCookie) {
|
|
96
|
+
this.logger.log('Session is invalid because no shared session cookie was found, i.e, a logout was performed in another portal. Forcing log off.')
|
|
97
|
+
session && await this.logout()
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const isDifferentSessionActive = sharedSessionCookie.sub != session?.getTokenData().sub
|
|
102
|
+
const isSharedSessionTypeBlocked = this.config.blockedAuthTypes?.includes(sharedSessionCookie.type)
|
|
103
|
+
if (isSharedSessionTypeBlocked) {
|
|
104
|
+
this.logger.log('Session is invalid because shared sessions have been blocked in the SessionManager\'s configuration (blockedAuthTypes).')
|
|
105
|
+
return false
|
|
106
|
+
} else if (isDifferentSessionActive || !session) {
|
|
107
|
+
this.logger.log(isDifferentSessionActive
|
|
108
|
+
? 'Session is invalid because a different session is already active.'
|
|
109
|
+
: 'Session is invalid because it\'s undefined.'
|
|
110
|
+
)
|
|
111
|
+
this.logger.log('Starting login with tenant from the session cookie.')
|
|
112
|
+
await this.startThirdPartyLoginUsingTenant(sharedSessionCookie)
|
|
113
|
+
return false
|
|
114
|
+
}
|
|
115
|
+
return true
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
hasSession() {
|
|
119
|
+
return !!this.current && !this.current.isExpired()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
getSession() {
|
|
123
|
+
if (!this.hasSession()) {
|
|
124
|
+
this.endSession()
|
|
125
|
+
throw new Error('Session is not available, redirecting to login.')
|
|
126
|
+
}
|
|
127
|
+
return this.current!
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async endSession(redirectToLogin = true) {
|
|
131
|
+
this.current = undefined
|
|
132
|
+
localStorage.removeItem(sessionKey)
|
|
133
|
+
sessionCookie.delete()
|
|
134
|
+
if (redirectToLogin && this.config.loginUrl) await redirect(this.config.loginUrl)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async restartSession() {
|
|
138
|
+
await this.logout({ endSession: false })
|
|
139
|
+
this.current = undefined
|
|
140
|
+
localStorage.removeItem(sessionKey)
|
|
141
|
+
await this.restoreSession()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async logout({ endSession }: { endSession?: boolean } | undefined = { endSession: true }) {
|
|
145
|
+
try {
|
|
146
|
+
await this.current?.logout()
|
|
147
|
+
} catch (error) {
|
|
148
|
+
// eslint-disable-next-line no-console
|
|
149
|
+
console.error(`Could not logout from IDM.\n${error}`)
|
|
150
|
+
}
|
|
151
|
+
if (!endSession) return
|
|
152
|
+
await this.endSession()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async startThirdPartyLogin(data: ThirdPartyLoginParams) {
|
|
156
|
+
const params = new URLSearchParams(location.search)
|
|
157
|
+
const authUrl = await this.auth.startThirdPartyLogin(data, {
|
|
158
|
+
from: location.href,
|
|
159
|
+
finalRedirect: params.get('finalRedirect'),
|
|
160
|
+
})
|
|
161
|
+
await redirect(authUrl)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
urlHasThirdPartyLoginData() {
|
|
165
|
+
const url = new URL(location.toString())
|
|
166
|
+
return url.searchParams.has('state') && !url.searchParams.has('error')
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async startThirdPartyLoginUsingTenant(data: ThirdPartyLoginParams) {
|
|
170
|
+
const params = new URLSearchParams(location.search)
|
|
171
|
+
const cookie = sessionCookie.get()
|
|
172
|
+
if (!cookie || !cookie.tenant) {
|
|
173
|
+
this.logger.log('Login out because no tenant information is available in the following data:', JSON.stringify(data))
|
|
174
|
+
//If no tenant is available we should log out the user
|
|
175
|
+
await this.logout()
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
const authUrl = await this.auth.getThirdPartyLoginFromTenant(data, cookie.tenant, {
|
|
179
|
+
from: location.href,
|
|
180
|
+
finalRedirect: params.get('finalRedirect'),
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
await redirect(authUrl)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async completeThirdPartyLogin() {
|
|
187
|
+
const url = new URL(location.toString())
|
|
188
|
+
if (url.searchParams.has('error')) {
|
|
189
|
+
throw new Error(`Error while signing in: ${url.searchParams.get('error_description')}`)
|
|
190
|
+
}
|
|
191
|
+
const { session, data: { from, finalRedirect } } = await this.auth.completeThirdPartyLogin(location.search)
|
|
192
|
+
this.setSession(session)
|
|
193
|
+
history.replaceState(null, '', from || location.toString().replace(/\?.*$/, ''))
|
|
194
|
+
this.sendLoginEventRd(this.current?.getTokenData())
|
|
195
|
+
if (finalRedirect) await redirect(finalRedirect)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
getEmailForLogin() {
|
|
199
|
+
const session = sessionCookie.get()
|
|
200
|
+
return session?.type == 'sso' ? session.email : undefined
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async switchAccount(accountId: string) {
|
|
204
|
+
this.logger.log('Switching accounts', accountId, this.current?.getTokenData().account_id_v2)
|
|
205
|
+
try {
|
|
206
|
+
this.current && await this.auth.switchAccount(accountId, this.current)
|
|
207
|
+
} catch (error) {
|
|
208
|
+
this.logger.error('Error while switching accounts', error)
|
|
209
|
+
throw error
|
|
210
|
+
}
|
|
211
|
+
this.setSession(this.current)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
onChange(listener: ChangeListener) {
|
|
215
|
+
this.changeListeners.push(listener)
|
|
216
|
+
return () => {
|
|
217
|
+
const index = this.changeListeners.indexOf(listener)
|
|
218
|
+
if (index != -1) this.changeListeners.splice(index, 1)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private setSessionCookie(session: Session) {
|
|
223
|
+
const { email, account_type, sub, tenant } = session.getTokenData()
|
|
224
|
+
const { provider, refresh_expires_in } = session.getSessionData()
|
|
225
|
+
if (!email || !sub || !tenant) return
|
|
226
|
+
const isFreemium = account_type == 'FREEMIUM'
|
|
227
|
+
const cookieAttributes = { 'Max-Age': refresh_expires_in, path: '/' }
|
|
228
|
+
if (isFreemium) {
|
|
229
|
+
sessionCookie.set({ type: 'idp', provider: provider!, sub, tenant }, cookieAttributes)
|
|
230
|
+
} else {
|
|
231
|
+
sessionCookie.set({ email, type: 'sso', sub, tenant }, cookieAttributes)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private async sendLoginEventRd(tokenData?: AccessTokenPayload) {
|
|
236
|
+
if (!this.config.rdUrl) return
|
|
237
|
+
|
|
238
|
+
if (!tokenData) {
|
|
239
|
+
// eslint-disable-next-line no-console
|
|
240
|
+
console.error('Unable to trigger login hook. No sessionEmail or name identified.')
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const { email, name, account_type, client_id, account_name, trial_account_status } = tokenData
|
|
245
|
+
const isLoginAI = client_id === "stackspot-portal-ai"
|
|
246
|
+
const isLoginEDP = client_id === "stackspot-portal"
|
|
247
|
+
|
|
248
|
+
if (!isLoginAI && !isLoginEDP && trial_account_status === 'PENDING') return
|
|
249
|
+
|
|
250
|
+
const leadType = account_type === 'FREEMIUM' ? 'TRIAL' : 'ENTERPRISE'
|
|
251
|
+
|
|
252
|
+
const rdObject = {
|
|
253
|
+
event_type: 'CONVERSION',
|
|
254
|
+
event_family: 'CDP',
|
|
255
|
+
payload: {
|
|
256
|
+
email,
|
|
257
|
+
name,
|
|
258
|
+
conversion_identifier: isLoginAI ? 'login_ai' : 'login_edp',
|
|
259
|
+
cf_leadtype: leadType,
|
|
260
|
+
cf_account_name: leadType === 'TRIAL' ? leadType : account_name,
|
|
261
|
+
},
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const response = await fetch(this.config.rdUrl, {
|
|
265
|
+
method: 'POST',
|
|
266
|
+
body: JSON.stringify(rdObject),
|
|
267
|
+
headers: {
|
|
268
|
+
'content-type': 'application/json',
|
|
269
|
+
},
|
|
270
|
+
})
|
|
271
|
+
const data = await response.json()
|
|
272
|
+
|
|
273
|
+
if (!response.ok) {
|
|
274
|
+
// eslint-disable-next-line no-console
|
|
275
|
+
console.error('Error while sending event to RD Station', data)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async getTrialEnabledProviders() {
|
|
280
|
+
try {
|
|
281
|
+
const response = await fetch(`${this.config.accountUrl}/v1/accounts/trial/sso`)
|
|
282
|
+
const trialProviders = await response.json()
|
|
283
|
+
|
|
284
|
+
if (!response.ok) {
|
|
285
|
+
// eslint-disable-next-line no-console
|
|
286
|
+
console.error('Error while fetching available login providers', trialProviders)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const providerKeys = Object.keys(trialProviders || {})
|
|
290
|
+
return providerKeys.filter(key => trialProviders[key] === true)
|
|
291
|
+
} catch (error) {
|
|
292
|
+
console.error('Error while fetching available login providers', error)
|
|
293
|
+
return []
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|