PageSourceSearch

https://tickets.vfb.de/ticket-logos/images/69f0aeb5d03ac.js

js vfb.de collected 2026-09-24 17:39:18 UTC 17,035 bytes, 423 lines download raw bytes

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.