login.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. /**
  2. * Script for login.ejs
  3. */
  4. // Validation Regexes.
  5. const validUsername = /^[a-zA-Z0-9_]{1,16}$/
  6. const basicEmail = /^\S+@\S+\.\S+$/
  7. //const validEmail = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i
  8. // Login Elements
  9. const loginCancelContainer = document.getElementById('loginCancelContainer')
  10. const loginCancelButton = document.getElementById('loginCancelButton')
  11. const loginEmailError = document.getElementById('loginEmailError')
  12. const loginUsername = document.getElementById('loginUsername')
  13. const loginPasswordError = document.getElementById('loginPasswordError')
  14. const loginPassword = document.getElementById('loginPassword')
  15. const checkmarkContainer = document.getElementById('checkmarkContainer')
  16. const loginRememberOption = document.getElementById('loginRememberOption')
  17. const loginButton = document.getElementById('loginButton')
  18. const loginForm = document.getElementById('loginForm')
  19. // Control variables.
  20. let lu = false, lp = false
  21. const loggerLogin = LoggerUtil('%c[Login]', 'color: #000668; font-weight: bold')
  22. /**
  23. * Show a login error.
  24. *
  25. * @param {HTMLElement} element The element on which to display the error.
  26. * @param {string} value The error text.
  27. */
  28. function showError(element, value){
  29. element.innerHTML = value
  30. element.style.opacity = 1
  31. }
  32. /**
  33. * Shake a login error to add emphasis.
  34. *
  35. * @param {HTMLElement} element The element to shake.
  36. */
  37. function shakeError(element){
  38. if(element.style.opacity == 1){
  39. element.classList.remove('shake')
  40. void element.offsetWidth
  41. element.classList.add('shake')
  42. }
  43. }
  44. /**
  45. * Validate that an email field is neither empty nor invalid.
  46. *
  47. * @param {string} value The email value.
  48. */
  49. function validateEmail(value){
  50. if(value){
  51. if(!basicEmail.test(value) && !validUsername.test(value)){
  52. showError(loginEmailError, '* Invalid Value')
  53. loginDisabled(true)
  54. lu = false
  55. } else {
  56. loginEmailError.style.opacity = 0
  57. lu = true
  58. if(lp){
  59. loginDisabled(false)
  60. }
  61. }
  62. } else {
  63. lu = false
  64. showError(loginEmailError, '* Required')
  65. loginDisabled(true)
  66. }
  67. }
  68. /**
  69. * Validate that the password field is not empty.
  70. *
  71. * @param {string} value The password value.
  72. */
  73. function validatePassword(value){
  74. if(value){
  75. loginPasswordError.style.opacity = 0
  76. lp = true
  77. if(lu){
  78. loginDisabled(false)
  79. }
  80. } else {
  81. lp = false
  82. showError(loginPasswordError, '* Required')
  83. loginDisabled(true)
  84. }
  85. }
  86. // Emphasize errors with shake when focus is lost.
  87. loginUsername.addEventListener('focusout', (e) => {
  88. validateEmail(e.target.value)
  89. shakeError(loginEmailError)
  90. })
  91. loginPassword.addEventListener('focusout', (e) => {
  92. validatePassword(e.target.value)
  93. shakeError(loginPasswordError)
  94. })
  95. // Validate input for each field.
  96. loginUsername.addEventListener('input', (e) => {
  97. validateEmail(e.target.value)
  98. })
  99. loginPassword.addEventListener('input', (e) => {
  100. validatePassword(e.target.value)
  101. })
  102. /**
  103. * Enable or disable the login button.
  104. *
  105. * @param {boolean} v True to enable, false to disable.
  106. */
  107. function loginDisabled(v){
  108. if(loginButton.disabled !== v){
  109. loginButton.disabled = v
  110. }
  111. }
  112. /**
  113. * Enable or disable loading elements.
  114. *
  115. * @param {boolean} v True to enable, false to disable.
  116. */
  117. function loginLoading(v){
  118. if(v){
  119. loginButton.setAttribute('loading', v)
  120. loginButton.innerHTML = loginButton.innerHTML.replace('LOGIN', 'LOGGING IN')
  121. } else {
  122. loginButton.removeAttribute('loading')
  123. loginButton.innerHTML = loginButton.innerHTML.replace('LOGGING IN', 'LOGIN')
  124. }
  125. }
  126. /**
  127. * Enable or disable login form.
  128. *
  129. * @param {boolean} v True to enable, false to disable.
  130. */
  131. function formDisabled(v){
  132. loginDisabled(v)
  133. loginCancelButton.disabled = v
  134. loginUsername.disabled = v
  135. loginPassword.disabled = v
  136. if(v){
  137. checkmarkContainer.setAttribute('disabled', v)
  138. } else {
  139. checkmarkContainer.removeAttribute('disabled')
  140. }
  141. loginRememberOption.disabled = v
  142. }
  143. /**
  144. * Parses an error and returns a user-friendly title and description
  145. * for our error overlay.
  146. *
  147. * @param {Error | {cause: string, error: string, errorMessage: string}} err A Node.js
  148. * error or Mojang error response.
  149. */
  150. function resolveError(err){
  151. // Mojang Response => err.cause | err.error | err.errorMessage
  152. // Node error => err.code | err.message
  153. if(err.cause != null && err.cause === 'UserMigratedException') {
  154. return {
  155. title: 'Error During Login:<br>Invalid Credentials',
  156. desc: 'You\'ve attempted to login with a migrated account. Try again using the account email as the username.'
  157. }
  158. } else {
  159. if(err.error != null){
  160. if(err.error === 'ForbiddenOperationException'){
  161. if(err.errorMessage != null){
  162. if(err.errorMessage === 'Invalid credentials. Invalid username or password.'){
  163. return {
  164. title: 'Error During Login:<br>Invalid Credentials',
  165. desc: 'The email or password you\'ve entered is incorrect. Please try again.'
  166. }
  167. } else if(err.errorMessage === 'Invalid credentials.'){
  168. return {
  169. title: 'Error During Login:<br>Too Many Attempts',
  170. desc: 'There have been too many login attempts with this account recently. Please try again later.'
  171. }
  172. }
  173. }
  174. }
  175. } else {
  176. // Request errors (from Node).
  177. if(err.code != null){
  178. if(err.code === 'ENOENT'){
  179. // No Internet.
  180. return {
  181. title: 'Error During Login:<br>No Internet Connection',
  182. desc: 'You must be connected to the internet in order to login. Please connect and try again.'
  183. }
  184. } else if(err.code === 'ENOTFOUND'){
  185. // Could not reach server.
  186. return {
  187. title: 'Error During Login:<br>Authentication Server Offline',
  188. desc: 'Mojang\'s authentication server is currently offline or unreachable. Please wait a bit and try again. You can check the status of the server on <a href="https://help.mojang.com/">Mojang\'s help portal</a>.'
  189. }
  190. }
  191. }
  192. }
  193. }
  194. if(err.message != null){
  195. // Unknown error with request.
  196. return {
  197. title: 'Error During Login:<br>Unknown Error',
  198. desc: err.message
  199. }
  200. } else {
  201. // Unknown Mojang error.
  202. return {
  203. title: err.error,
  204. desc: err.errorMessage
  205. }
  206. }
  207. }
  208. let loginViewOnSuccess = VIEWS.landing
  209. let loginViewOnCancel = VIEWS.settings
  210. let loginViewCancelHandler
  211. function loginCancelEnabled(val){
  212. if(val){
  213. $(loginCancelContainer).show()
  214. } else {
  215. $(loginCancelContainer).hide()
  216. }
  217. }
  218. loginCancelButton.onclick = (e) => {
  219. switchView(getCurrentView(), loginViewOnCancel, 500, 500, () => {
  220. loginCancelEnabled(false)
  221. if(loginViewCancelHandler != null){
  222. loginViewCancelHandler()
  223. loginUsername.value = ''
  224. loginPassword.value = ''
  225. loginViewCancelHandler = null
  226. }
  227. })
  228. }
  229. // Disable default form behavior.
  230. loginForm.onsubmit = () => { return false }
  231. // Bind login button behavior.
  232. loginButton.addEventListener('click', () => {
  233. // Disable form.
  234. formDisabled(true)
  235. // Show loading stuff.
  236. loginLoading(true)
  237. AuthManager.addAccount(loginUsername.value, loginPassword.value).then((value) => {
  238. updateSelectedAccount(value)
  239. loginButton.innerHTML = loginButton.innerHTML.replace('LOGGING IN', 'SUCCESS')
  240. $('.circle-loader').toggleClass('load-complete')
  241. $('.checkmark').toggle()
  242. setTimeout(() => {
  243. switchView(VIEWS.login, loginViewOnSuccess, 500, 500, () => {
  244. // Temporary workaround
  245. if(loginViewOnSuccess === VIEWS.settings){
  246. prepareSettings()
  247. }
  248. loginViewOnSuccess = VIEWS.landing // Reset this for good measure.
  249. loginCancelEnabled(false) // Reset this for good measure.
  250. loginViewCancelHandler = null // Reset this for good measure.
  251. loginUsername.value = ''
  252. loginPassword.value = ''
  253. $('.circle-loader').toggleClass('load-complete')
  254. $('.checkmark').toggle()
  255. loginLoading(false)
  256. loginButton.innerHTML = loginButton.innerHTML.replace('SUCCESS', 'LOGIN')
  257. formDisabled(false)
  258. })
  259. }, 1000)
  260. }).catch((err) => {
  261. loginLoading(false)
  262. const errF = resolveError(err)
  263. setOverlayContent(errF.title, errF.desc, 'Try Again')
  264. setOverlayHandler(() => {
  265. formDisabled(false)
  266. toggleOverlay(false)
  267. })
  268. toggleOverlay(true)
  269. loggerLogin.log('Error while logging in.', err)
  270. })
  271. })