1/** 2 * Class taking care of the whole custom SSO business logic for VfB Stuttgart. 3 * Responsibilities: 4 * + login via BFN API 5 * + replacing normal login-link in shop header with custom one 6 * + adding custom-sso modal with content from layout 7 * + replacing login form in shopping-cart with custom-sso button to open the custom-sso modal 8 * + adding spin-loading-animation to submit-button in custom-sso modal 9 * + generation of forgot-password link in modal 10 * + generation of register link in modal 11 * + vfb shop business logic regarding the token, when user already logged himself via vfb shop and has token cookie 12 * + resizing custom shop logo at scrolling 13 * 14 * @author Karel Trcalek, Toni Ivanisevic 15 * company: SAP SE 16 * original creation date: 24.04.2024 17 * updated date: 28.01.2025 18 * 19 * @example '<div id="variableContainerSso" class="d-none" 20 * data-string-username="[LANGUAGE(showlangvar=shop_email)]" 21 * data-string-password="[LANGUAGE(showlangvar=shop_password)]" 22 * data-string-register="[LANGUAGE(showlangvar=shop_register)]" 23 * data-string-forgot-password="[LANGUAGE(showlangvar=shop_forgot_password)]" 24 * data-string-login="[LANGUAGE(showlangvar=shop_login)]" 25 * data-string-processing-title="[LANGUAGE(showlangvar=shop_ext_sso_loginprocessing)]" 26 * data-url-session-id="[SESSIONID|html_attr]" 27 * data-url-shop-id="[SHOPID|int]" 28 * data-test-server="[SERVER_TEST(COMMAND=IF,EQ,1)]1[SERVER_TEST(COMMAND=ELSE)]0[SERVER_TEST(COMMAND=ENDIF)]" 29 * ></div> 30 */ 31class SSOLogin { 32 variableContainerId = 'variableContainerSso' 33 debugOutput = false 34 variables = { 35 stringUsername: undefined, 36 stringPassword: undefined, 37 stringRegister: undefined, 38 stringForgotPassword: undefined, 39 stringLogin: undefined, 40 stringProcessingTitle: undefined, 41 urlSessionId: undefined, 42 urlShopId: undefined, 43 testServer: undefined 44 } 45 46 // data, texts and links for custom-sso-modal 47 urlForgotPasswordTest = 'https://testshop.vfb.de/account/password' 48 urlForgotPasswordProd = 'https://shop.vfb.de/de/passwort-vergessen' 49// urlRegister = '/interface/login/redirect/ssologin' 50 urlRegister = 'https://shop.vfb.de/de/login' 51 52 urlFormAction = '/interface/login/redirect/ssocheck' 53 modalLabelText = 'Mit bereits bestehendem VfB-Kundenkonto anmelden' 54 // data for shopping cart login area containing small button opening the custom-sso-modal 55 shoppingCartButtonLabel = 'VfB-Login' 56 shoppingCartAreaLabel = 'Anmelden/Registrieren mit VfB-Kundenkonto:' 57 shoppingCartTooltipText = 'Die VFB.DE ist ein Login für die gesamte digitale Welt des VfB Stuttgart 1893 AG' 58 shoppingCartFormToReplace = '#basketLoginMask div div.tab-content' 59 // data and replace links for logged customer in customer master data area 60 urlMasterCustomerDataProd = 'https://shop.vfb.de/de/mein-konto' 61 urlMasterCustomerDataTest = 'https://testshop.vfb.de/address' 62 // data for styling the vfb-login button in shopping-cart and internal login-screen 63 buttonLoginStateLabel = 'Bitte melde dich an' 64 65 constructor () { 66 this.initialize() 67 } 68 69 static createInstance () { 70 return new SSOLogin() 71 } 72 73 initialize () { 74 this.extractVariables() 75 this.outputDebug() 76 77 this.linkLoginRegister = document.getElementById('link-login-register') 78 if (this.linkLoginRegister) { 79 this.createButton() 80 this.appendModal() 81 this.addSSOLoginTemplate() 82 this.addLoadingAnimationOnSubmit() 83 } 84 85 // check if the internal login-screen is displayed which happens after clicking on event with event-status button configured requiring logged customer 86 if (document.getElementsByClassName('page-state-8a').length > 0) { 87 this.addSSOButtonToLoginTab() 88 } 89 } 90 91 // remove automatically displayed login-form from login-tab in shop state 8a and enable only login via button 92 // opening custom-sso modal window to perform external login via SSO 93 addSSOButtonToLoginTab () { 94 // remove the Login/Anmelden tab-list ul element 95 const loginTabList = document.querySelector('main.content-main > ul:first-of-type') 96 if (loginTabList) { 97 loginTabList.remove() 98 } 99 100 // remove the internal login-screen within the first div containing the internal login-mask 101 const targetElement = document.querySelector('#login-tab >
101 div:first-of-type') 102 if (targetElement) { 103 targetElement.innerHTML = this.generateSsoLoginTemplate(this.buttonLoginStateLabel) 104 } 105 } 106 107 createButton () { 108 const newButton = document.createElement('a') 109 newButton.className = this.linkLoginRegister.className 110 newButton.classList.add('login-with-sso') 111 newButton.id = 'link-login-with-sso' 112 newButton.dataset.bsToggle = 'modal' 113 newButton.dataset.bsTarget = '#ssoLoginModal' 114 newButton.innerHTML = this.linkLoginRegister.innerHTML 115 this.linkLoginRegister.parentNode.replaceChild(newButton, this.linkLoginRegister) 116 } 117 118 extractVariables () { 119 const variableContainer = document.getElementById(this.variableContainerId) 120 if (!variableContainer) return 121 122 this.variables = { ...variableContainer.dataset } 123 } 124 125 getForgottenPasswordUrl () { 126 return this.variables.testServer === '1' ? this.urlForgotPasswordTest : this.urlForgotPasswordProd 127 } 128 129 outputDebug () { 130 if (this.debugOutput) { 131 console.table(this.variables) 132 } 133 } 134 135 addLoadingAnimationOnSubmit () { 136 const modalSsoSubmitForm = document.getElementById('startSSOLoginProcess') 137 modalSsoSubmitForm.addEventListener('submit', () => { 138 this.startSubmitLoadingAnimation() 139 }) 140 } 141 142 startSubmitLoadingAnimation () { 143 const ssoLoginSubmitButton = $('#submitStartExternalSSOProcess') 144 const ssoLoginUsername = $('#extSsoLoginUsername') 145 const ssoLoginPassword = $('#extSsoLoginPassword') 146 const loginProcessingTitle = this.variables.stringProcessingTitle 147 148 if (ssoLoginUsername.val().length !== 0 && ssoLoginPassword.val().length !== 0) { 149 ssoLoginSubmitButton.prop('disabled', true) 150 ssoLoginUsername.attr('readonly', 'readonly') 151 ssoLoginPassword.attr('readonly', 'readonly') 152 153 ssoLoginSubmitButton.css('background-color', '#D40824') 154 ssoLoginSubmitButton.css('border-color', '#D40824') 155 ssoLoginSubmitButton.html( 156 '<i class=\'fa fa-circle-o-notch fa-spin\'></i> ' + loginProcessingTitle 157 ) 158 } 159 } 160 161 generateSsoLoginTemplate (buttonLabel = '') { 162 return ` 163 <p class="mb-2"> 164 ${this.shoppingCartAreaLabel} 165 <i class="fa fa-question-circle" data-bs-toggle="tooltip" title="" data-bs-original-title="${this.shoppingCartTooltipText}" aria-label="${this.shoppingCartTooltipText}"></i> 166 </p> 167 <p> 168 <button type="button" class="btn btn-warning login-with-sso" style="background-color: #D40824; border-color: #D40824; color: #FFFFFF" 169 data-bs-toggle="modal" data-bs-target="#ssoLoginModal"> 170 ${buttonLabel} 171 </button> 172 </p> 173 ` 174 } 175 176 /** 177 * Generates the content of the custom modal window with login form containing input field for email, password and submit button 178 * @returns {string} 179 */ 180 generateSSOModal () { 181 return ` 182 <div class="modal fade" id="ssoLoginModal" tabindex="-1" aria-labelledby="ssoLoginModalLabel" aria-hidden="true" data-bs-keyboard="true"> 183 <div class="modal-dialog"> 184 <div class="modal-content"> 185 <div class="modal-header"> 186 <h5 class="modal-title" id="ssoLoginModalLabel">${this.modalLabelText}</h5> 187 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> 188 </div> 189 <form method="post" id="startSSOLoginProcess" action="${this.urlFormAction}?wes=${this.variables.urlSessionId}&shopid=${this.variables.urlShopId}"> 190 <div class="modal-body"> 191 <div class="mb-3"> 192 <label for="extSsoLoginUsername">${this.variables.stringUsername}<span class="text-danger">*</span></label> 193 <input type="email" name="username" class="form-control" id="extSsoLoginUsername" required> 194 </div> 195 <div class="mb-3"> 196 <label for="extSsoLoginPassword">${this.variables.stringPassword}<span class="text-danger">*</span></label> 197 <input type="password" name="password" class="form-control" id="extSsoLoginPassword" required> 198 </div> 199 <div> 200 <div class="d-flex justify-content-between bg-body"> 201 <div class="bg-body"><a href="${this.urlRegister}?wes=${this.variables.urlSessionId}&shopid=${this.variables.urlShopId}" class="float-end" target="">${this.variables.stringRegister}</a></div> 202 <div class="bg-body"><a href="${this.getForgottenPasswordUrl()}" class="float-end" target="_blank">${this.variables.stringForgotPassword}</a></div> 203 </div> 204 </div> 205 </div> 206 <div class="modal-footer pt-4"> 207 <button type="submit" id="submitStartExternalSSOProcess" data-string-processing-title="${this.variables.stringProcessingTitle}" class="btn btn-primary mx-auto w-100 py-2">${this.variables.stringLogin}</button> 208 </div> 209 </form> 210 </div> 211 </div> 212 </div> 213 ` 214 } 215 216 appendModal () { 217 document.body.insertAdjacentHTML('beforeend', this.generateSSOModal()) 218 } 219 220 /** 221 * Generates the custom sso template inside shopping-cart when customer is not logged in 222 */ 223 addSSOLoginTemplate () { 224 const targetElement = document.querySelector(this.shoppingCartFormToReplace) 225 if (targetElement) { 226 targetElement.innerHTML = this.generateSsoLoginTemplate(this.shoppingCartButtonLabel) 227 } 228 } 229} 230 231/** 232 * Vanilla JS function to read cookie value 233 * @param name 234 * @returns {string} 235 */ 236function getCookie (name) { 237 const value = `; ${document.cookie}` 238 const parts = value.split(`; ${name}=`) 239 if (parts.length === 2) return parts.pop().split(';').shift() 240} 241 242/** 243 * Performs automatic login via SSO custom bundle when the customer is already logged-in in VfB-Shop 244 * and his access token is available as value in cookie set by vfb shop 245 */ 246const logInCustomerOnWebsiteLoad = () => { 247 if (document.cookie.indexOf('token') > -1 && document.cookie.indexOf('userLoggedInAutomaticallyPerSSO') === -1) { 248 const userAccessToken = getCookie('token')
249 const elem = $('#myModal') 250 const shopId = elem.data('shopid') 251 const wes = elem.data('sessionid') 252 document.cookie = `userLoggedInAutomaticallyPerSSO=1; max-age=${60 * 60};` 253 254 window.location = '/interface/login/redirect/ssocheck?shopid=' + shopId + '&at=' + userAccessToken + '&wes=' + wes 255 } 256} 257 258/** 259 * Performs automatic login via SSO custom bundle when customer clicks on "Anmelden" button which opens 260 * custom sso modal. In this case if the cookie with access token of user is available, 261 * we perform the sso login directly via this access-token not forcing the customer to enter 262 * his sso credentials again, and we log him in directly 263 */ 264const logInCustomerOnSSOModalClick = () => { 265 if (document.getElementById('link-login-with-sso') && document.cookie.indexOf('token') > -1) { 266 const loginLinkInHeader = document.getElementById('link-login-with-sso') 267 // prevent custom sso modal from opening, since we automatically log the user in 268 loginLinkInHeader.removeAttribute('data-bs-toggle') 269 loginLinkInHeader.removeAttribute('data-bs-target') 270 loginLinkInHeader.setAttribute('href', '') 271 272 loginLinkInHeader.addEventListener('click', (event) => { 273 event.preventDefault() 274 const userAccessToken = getCookie('token') 275 const elem = $('#myModal') 276 const shopId = elem.data('shopid') 277 const wes = elem.data('sessionid') 278 window.location = '/interface/login/redirect/ssocheck?shopid=' + shopId + '&at=' + userAccessToken + '&wes=' + wes 279 }) 280 } 281} 282 283const additionalDomReadyFunctions = () => { 284 const headings = document.querySelectorAll('article h2')
285 headings.forEach(heading => { 286 const parts = heading.textContent.split(' - ') 287 if (parts.length === 2) { 288 heading.innerHTML = `${parts[0]} -<br>${parts[1]}` 289 } 290 }) 291 292 document.querySelectorAll('#pageEvents .event-container .event-title:not(:has(.text-delimiter)').forEach(eventName => { 293 const parts = eventName.textContent.split(' - ') 294 if (parts.length === 2) { 295 eventName.innerHTML = `${parts[0]} -<br>${parts[1]}` 296 } 297 }) 298 299 // add form-check-input class to checkboxes 300 const checkboxes = document.querySelectorAll('input[type="checkbox"]:not(.form-check-input)') 301 checkboxes.forEach(checkbox => { 302 checkbox.classList.add('form-check-input') 303 }) 304 305 // remove inline style from edit icons 306 const elements = document.querySelectorAll('.invoice-title > a > .fa-edit') 307 elements.forEach(element => { 308 element.removeAttribute('style') 309 }) 310} 311 312/** 313 * Resize the main shop logo upon scrolling down 314 */ 315window.addEventListener('scroll', function () { 316 const navbarBrand = document.querySelector('.navbar-brand') 317 const logo = navbarBrand.querySelector('img') 318 if (window.scrollY === 0) { 319 logo.classList.remove('--small') 320 } else { 321 logo.classList.add('--small') 322 } 323}) 324 325/** 326 * Replaces original url from Meine Daten Menu-Link with custom one depending on if testserver is 1 or 0 327 * pass the sso instance to have access to data-div values and preconfigured test or prod links 328 */ 329const replaceMyAccountAddressUrl = (sso) => { 330 const myAccountAddresses = document.getElementById('myAccountAddresses') 331 if (myAccountAddresses === null) { 332 return 333 } 334 335 if (myAccountAddresses.getElementsByTagName('a').length === 0) { 336 return 337 } 338 339 let myAccountAddressLink = myAccountAddresses.querySelector('a:first-of-type') 340 let customAddressLink = sso.variables.testServer === '1' ? sso.urlMasterCustomerDataTest : sso.urlMasterCustomerDataProd 341 myAccountAddressLink.setAttribute('href', customAddressLink) 342 myAccountAddressLink.setAttribute('target', '_blank') 343} 344 345/** 346 * Replaces original url of Addresses Link in dropdown menu with custom one depending on if testserver is 1 or 0 347 * pass the sso instance to have access to data-div values and preconfigured test or prod links 348 */ 349const replaceDropDownAddressUrl = (sso) => { 350 let dropDownAddressLink = document.getElementById('dropDownMenuAddresses') 351 if (dropDownAddressLink === null) { 352 return 353 } 354 355 let customAddressLink = sso.variables.testServer === '1' ? sso.urlMasterCustomerDataTest : sso.urlMasterCustomerDataProd 356 dropDownAddressLink.setAttribute('href', customAddressLink) 357 dropDownAddressLink.setAttribute('target', '_blank') 358} 359 360/** 361 * Adds new custom Click-Event onto Logout/Abmelden-Link in the Dropdown menu of online-shop with logged customer. 362 * If the user manually logouts, the cookie token will be removed so that he can use another user credentials to login 363 */ 364const removeTokenCookie = () => { 365 let dropDownLogoutLink = document.getElementById('dropDownMenuLogout') 366 if (dropDownLogoutLink === null) { 367 return 368 } 369 370 dropDownLogoutLink.addEventListener('click', () => { 371 if (document.cookie.indexOf('token') > -1) { 372 document.cookie = `token=0; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.vfb.de;` 373 } 374 if (document.cookie.indexOf('userLoggedInAutomaticallyPerSSO') > -1) { 375 document.cookie = `userLoggedInAutomaticallyPerSSO=0; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;` 376 } 377 }) 378} 379 380/** 381 * remove menu-entry allowing to change customers password 382 */ 383const removeMyAccountPasswordMenu = () => { 384 if (document.getElementById('myAccountPassword') === null) { 385 return 386 } 387 388 document.getElementById('myAccountPassword').remove() 389} 390 391/** 392 * remove link from drop-down menu allowing to change customers password 393 */ 394const removeMyAccountPasswordDropDownLink = () => { 395 if (document.getElementById('dropDownMenuPassword') === null) { 396 return 397 } 398 399 document.getElementById('dropDownMenuPassword').remove() 400} 401 402/** 403 * Main method initializing and running the custom sso logic 404 */ 405document.addEventListener('DOMContentLoaded', () => { 406 const sso = SSOLogin.createInstance() 407 // read token cookie from logged user in vfb-shop and if logged-in, execute automatic sso login in online-shop 408 logInCustomerOnWebsiteLoad() 409 // check if Anmelden-Link is on page and cookie with user access-token exists. if so, execute automatic sso-login via click on anmelden-link 410 logInCustomerOnSSOModalClick() 411 // remove menu-entry allowing to change customers password 412 removeMyAccountPasswordMenu() 413 // remove password link from dropdown menu 414 removeMyAccountPasswordDropDownLink() 415 // replace url of address-menu in my-account area with custom url 416 replaceMyAccountAddressUrl(sso) 417 // replace url of address link in dropdown-menu with custom url 418 replaceDropDownAddressUrl(sso) 419 // add onclick-fallback on internal logout-link in online-shop dropdown menu (when customer logged-in) to clean up the cookie named token if set 420 removeTokenCookie() 421 422 additionalDomReadyFunctions() 423})
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.