From efbbd4385fd8ea5d06fd7d0e707a352182ae458f Mon Sep 17 00:00:00 2001 From: XXdaugonXX Date: Sat, 13 Aug 2022 20:22:44 +0200 Subject: [PATCH] V3.0.0 Recoding from new source from dscalzi --- .github/workflows/build.yml | 35 + .travis.yml | 45 - LICENSE.txt | 2 +- README.md | 26 +- app/app.ejs | 2 + app/assets/css/launcher.css | 622 ++-- app/assets/images/LoginWithMicrosoft.png | Bin 3295 -> 0 bytes app/assets/images/icons/microsoft.svg | 7 + app/assets/images/icons/mojang.svg | 5 + app/assets/images/icons/news.svg | 14 - app/assets/js/assetguard.js | 9 +- app/assets/js/authmanager.js | 344 +- app/assets/js/configmanager.js | 122 +- app/assets/js/discordwrapper.js | 2 +- app/assets/js/distromanager.js | 9 +- app/assets/js/dropinmodutil.js | 15 +- app/assets/js/ipcconstants.js | 28 + app/assets/js/microsoft.js | 191 - app/assets/js/mojang.js | 299 -- app/assets/js/processbuilder.js | 6 +- app/assets/js/scripts/landing.js | 502 +-- app/assets/js/scripts/login.js | 164 +- app/assets/js/scripts/loginOptions.js | 50 + app/assets/js/scripts/overlay.js | 6 + app/assets/js/scripts/settings.js | 247 +- app/assets/js/scripts/uibinder.js | 90 +- app/assets/js/scripts/uicore.js | 13 +- app/assets/js/scripts/welcome.js | 5 +- app/assets/lang/en_US.json | 2 +- app/landing.ejs | 79 +- app/login.ejs | 1 - app/loginOptions.ejs | 34 + app/overlay.ejs | 2 +- app/settings.ejs | 121 +- app/waiting.ejs | 8 + app/welcome.ejs | 8 +- build/icon.png | Bin 1816707 -> 145930 bytes dev-app-update.yml | 2 +- docs/MicrosoftAuth.md | 35 + docs/distro.md | 5 - electron-builder.yml | 10 +- index.js | 159 +- package-lock.json | 4291 ++++++++++++---------- package.json | 27 +- 44 files changed, 3604 insertions(+), 4040 deletions(-) create mode 100644 .github/workflows/build.yml delete mode 100644 .travis.yml delete mode 100644 app/assets/images/LoginWithMicrosoft.png create mode 100644 app/assets/images/icons/microsoft.svg create mode 100644 app/assets/images/icons/mojang.svg delete mode 100644 app/assets/images/icons/news.svg create mode 100644 app/assets/js/ipcconstants.js delete mode 100644 app/assets/js/microsoft.js delete mode 100644 app/assets/js/mojang.js create mode 100644 app/assets/js/scripts/loginOptions.js create mode 100644 app/loginOptions.ejs create mode 100644 app/waiting.ejs create mode 100644 docs/MicrosoftAuth.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000000..d7336703a4 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,35 @@ +name: Build + +on: push + +jobs: + release: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [macos-latest, ubuntu-latest, windows-latest] + + steps: + - name: Check out Git repository + uses: actions/checkout@v1 + + - name: Set up Node + uses: actions/setup-node@v1 + with: + node-version: 16 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.x + + - name: Install Dependencies + run: npm ci + shell: bash + + - name: Build + env: + GH_TOKEN: ${{ secrets.github_token }} + run: npm run dist + shell: bash \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 1818e4edf1..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,45 +0,0 @@ -matrix: - include: - - os: osx - osx_image: xcode12.2 - language: node_js - node_js: "14" - env: - - ELECTRON_CACHE=$HOME/.cache/electron - - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder - - ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES=true - - CSC_IDENTITY_AUTO_DISCOVERY=false - - - os: linux - services: docker - language: generic - node_js: "14" - env: - - ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES=true - -cache: - directories: - - node_modules - - $HOME/.cache/electron - - $HOME/.cache/electron-builder - -script: - - | - if [ "$TRAVIS_OS_NAME" == "linux" ]; then - ENVS=`env | grep -iE '(DEBUG|NODE_|ELECTRON_|YARN_|NPM_|CI|CIRCLE|TRAVIS|APPVEYOR_|CSC_|_TOKEN|_KEY|AWS_|STRIP|BUILD_)' | sed -n '/^[^\t]/s/=.*//p' | sed '/^$/d' | sed 's/^/-e /g' | tr '\n' ' '` - docker run $ENVS --rm \ - -v ${PWD}:/project \ - -v ~/.cache/electron:/root/.cache/electron \ - -v ~/.cache/electron-builder:/root/.cache/electron-builder \ - electronuserland/builder:wine \ - /bin/bash -c "node -v && npm ci && npm run cilinux" - else - npm run cidarwin - fi - -before_cache: - - rm -rf $HOME/.cache/electron-builder/wine - -branches: - except: - - "/^v\\d+\\.\\d+\\.\\d+$/" \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt index a7ce69e2f2..1455dbbbb9 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017-2021 Daniel D. Scalzi +Copyright (c) 2017-2022 Daniel D. Scalzi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 7f28966cc5..52d4283e4e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@

aventium softworks

-

Zayviel Launcher

+

Helios Launcher

+ +
(formerly Electron Launcher)
[

travis](https://travis-ci.org/XXdaugonXX/ZayvielLauncher) [downloads](https://github.com/XXdaguonXX/ZayvielLauncher/releases) @@ -13,6 +15,7 @@ * 🔒 Full account management. * Add multiple accounts and easily switch between them. + * Microsoft (OAuth 2.0) + Mojang (Yggdrasil) authentication fully supported. * Credentials are never stored and transmitted directly to Mojang. * 📂 Efficient asset management. * Receive client updates as soon as we release them. @@ -20,7 +23,6 @@ * ☕ **Automatic Java validation.** * If you have an incompatible version of Java installed, we'll install the right one *for you*. * You do not need to have Java installed to run the launcher. -* 📰 News feed natively built into the launcher. * ⚙️ Intuitive settings management, including a Java control panel. * Supports all of our servers. * Switch between server configurations with ease. @@ -52,7 +54,8 @@ If you download from the [Releases](https://github.com/XXdaugonXX/ZayvielLaunche | Platform | File | | -------- | ---- | | Windows x64 | `Zayviel-Launcher-setup-VERSION.exe` | -| macOS | `Zayviel-Launcher-setup-VERSION.dmg` | +| macOS x64 | `Zayviel-Launcher-setup-VERSION-x64.dmg` | +| macOS arm64 | `Zayviel-Launcher-setup-VERSION-arm64.dmg` | | Linux x64 | `Zayviel-Launcher-setup-VERSION.AppImage` | ## Console @@ -80,7 +83,7 @@ This section details the setup of a basic developmentment environment. **System Requirements** -* [Node.js][nodejs] v14 +* [Node.js][nodejs] v16 --- @@ -175,14 +178,9 @@ Note that you **cannot** open the DevTools window while using this debug configu ### Note on Third-Party Usage -You may use this software in your own project so long as the following conditions are met. - -* Credit is expressly given to the original authors (Daniel Scalzi). - * Include a link to the original source on the launcher's About page. - * Credit the authors and provide a link to the original source in any publications or download pages. -* The source code remain **public** as a fork of this repository. +Please give credit to the original author and provide a link to the original source. This is free software, please do at least this much. -We reserve the right to update these conditions at any time, please check back periodically. +For instructions on setting up Microsoft Authentication, see https://github.com/dscalzi/HeliosLauncher/blob/master/docs/MicrosoftAuth.md. --- @@ -190,7 +188,7 @@ We reserve the right to update these conditions at any time, please check back p * [Wiki][wiki] * [Nebula (Create Distribution.json)][nebula] -* [v2 Rewrite Branch (WIP)][v2branch] +* [v2 Rewrite Branch (Inactive)][v2branch] The best way to contact the developers is on Discord. @@ -207,6 +205,6 @@ The best way to contact the developers is on Discord. [rendererprocess]: https://electronjs.org/docs/tutorial/application-architecture#main-and-renderer-processes 'Renderer Process' [chromedebugger]: https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome 'Debugger for Chrome' [discord]: https://discord.gg/zNWUXdt 'Discord' -[wiki]: https://github.com/XXdaugonXX/ZayvielLauncher/wiki 'wiki' +[wiki]: https://github.com/dscalzi/HeliosLauncher/wiki 'wiki' [nebula]: https://github.com/dscalzi/Nebula 'dscalzi/Nebula' -[v2branch]: https://github.com/dscalzi/HeliosLauncher/tree/ts-refactor 'v2 branch' \ No newline at end of file +[v2branch]: https://github.com/dscalzi/HeliosLauncher/tree/ts-refactor 'v2 branch' diff --git a/app/app.ejs b/app/app.ejs index 7b8445424d..020ab9e1fa 100644 --- a/app/app.ejs +++ b/app/app.ejs @@ -31,6 +31,8 @@

<%- include('welcome') %> <%- include('login') %> + <%- include('waiting') %> + <%- include('loginOptions') %> <%- include('settings') %> <%- include('landing') %>
diff --git a/app/assets/css/launcher.css b/app/assets/css/launcher.css index cea7655e51..79152cee03 100644 --- a/app/assets/css/launcher.css +++ b/app/assets/css/launcher.css @@ -222,6 +222,7 @@ body, button { align-items: center; height: 100%; width: 100%; + background: rgba(0, 0, 0, 0.50); } #welcomeContent { @@ -859,19 +860,6 @@ body, button { transform: rotate(45deg); } -#loginMSButton { - border-color: transparent; - background-color: transparent; - cursor: pointer; - font-family: 'Avenir Medium'; - font-size: 12px; - font-weight: bold; - margin-bottom: 253px; - margin-top: -250px; - color: rgba(255, 255, 255, 0.75); -} - - /* #login_filter { height: calc(100% - 22px); @@ -885,6 +873,175 @@ body, button { } */ +/******************************************************************************* + * * + * Waiting View (waiting.ejs) * + * * + ******************************************************************************/ + +#waitingContainer { + position: relative; + display: flex; + justify-content: center; + align-items: center; + height: 100%; + width: 100%; + transition: filter 0.25s ease; + background: rgba(0, 0, 0, 0.50); +} + +#waitingContent { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 50%; + top: -10%; + position: relative; +} + +.waitingSpinner:before { + transform: rotateX(60deg) rotateY(45deg) rotateZ(45deg); + animation: 750ms rotateBefore infinite linear reverse; +} +.waitingSpinner:after { + transform: rotateX(240deg) rotateY(45deg) rotateZ(45deg); + animation: 750ms rotateAfter infinite linear; +} +.waitingSpinner:before, +.waitingSpinner:after { + box-sizing: border-box; + content: ''; + display: block; + position: fixed; + top: calc(50% - 5em); + /* left: 50%; */ + margin-top: -5em; + margin-left: -5em; + width: 10em; + height: 10em; + transform-style: preserve-3d; + transform-origin: 50%; + transform: rotateY(50%); + perspective-origin: 50% 50%; + perspective: 340px; + background-size: 10em 10em; + background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjI2NnB4IiBoZWlnaHQ9IjI5N3B4IiB2aWV3Qm94PSIwIDAgMjY2IDI5NyIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczpza2V0Y2g9Imh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaC9ucyI+CiAgICA8dGl0bGU+c3Bpbm5lcjwvdGl0bGU+CiAgICA8ZGVzY3JpcHRpb24+Q3JlYXRlZCB3aXRoIFNrZXRjaCAoaHR0cDovL3d3dy5ib2hlbWlhbmNvZGluZy5jb20vc2tldGNoKTwvZGVzY3JpcHRpb24+CiAgICA8ZGVmcz48L2RlZnM+CiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4KICAgICAgICA8cGF0aCBkPSJNMTcxLjUwNzgxMywzLjI1MDAwMDM4IEMyMjYuMjA4MTgzLDEyLjg1NzcxMTEgMjk3LjExMjcyMiw3MS40OTEyODIzIDI1MC44OTU1OTksMTA4LjQxMDE1NSBDMjE2LjU4MjAyNCwxMzUuODIwMzEgMTg2LjUyODQwNSw5Ny4wNjI0OTY0IDE1Ni44MDA3NzQsODUuNzczNDM0NiBDMTI3LjA3MzE0Myw3NC40ODQzNzIxIDc2Ljg4ODQ2MzIsODQuMjE2MTQ2MiA2MC4xMjg5MDY1LDEwOC40MTAxNTMgQy0xNS45ODA0Njg1LDIxOC4yODEyNDcgMTQ1LjI3NzM0NCwyOTYuNjY3OTY4IDE0NS4yNzczNDQsMjk2LjY2Nzk2OCBDMTQ1LjI3NzM0NCwyOTYuNjY3OTY4IC0yNS40NDkyMTg3LDI1Ny4yNDIxOTggMy4zOTg0Mzc1LDEwOC40MTAxNTUgQzE2LjMwNzA2NjEsNDEuODExNDE3NCA4NC43Mjc1ODI5LC0xMS45OTIyOTg1IDE3MS41MDc4MTMsMy4yNTAwMDAzOCBaIiBpZD0iUGF0aC0xIiBmaWxsPSIjZmZmZmZmIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICA8L2c+Cjwvc3ZnPg==); +} + +#waitingTextContainer { + position: fixed; + top: 50%; +} + +@keyframes rotateBefore { + from { + transform: rotateX(60deg) rotateY(45deg) rotateZ(0deg); + } + to { + transform: rotateX(60deg) rotateY(45deg) rotateZ(-360deg); + } +} + +@keyframes rotateAfter { + from { + transform: rotateX(240deg) rotateY(45deg) rotateZ(0deg); + } + to { + transform: rotateX(240deg) rotateY(45deg) rotateZ(360deg); + } +} + +/******************************************************************************* + * * + * Login Options View (loginOptions.ejs) * + * * + ******************************************************************************/ + +#loginOptionsContainer { + position: relative; + display: flex; + justify-content: center; + align-items: center; + height: 100%; + width: 100%; + transition: filter 0.25s ease; + background: rgba(0, 0, 0, 0.50); +} + +#loginOptionsContent { + border-radius: 3px; + position: relative; + top: -5%; +} + +.loginOptionsMainContent { + display: flex; + flex-direction: column; + align-items: center; +} + +.loginOptionActions { + display: flex; + flex-direction: column; + row-gap: 10px; +} + +.loginOptionButtonContainer { + width: 16em; +} + +.loginOptionButton { + background: rgba(0, 0, 0, 0.25); + border: 1px solid rgba(126, 126, 126, 0.57); + border-radius: 3px; + height: 50px; + width: 100%; + text-align: left; + padding: 0px 25px; + cursor: pointer; + outline: none; + transition: 0.25s ease; + display: flex; + align-items: center; + column-gap: 5px; +} +.loginOptionButton:hover, +.loginOptionButton:focus { + background: rgba(54, 54, 54, 0.25); + text-shadow: 0px 0px 20px white; +} + +#loginOptionCancelContainer { + position: absolute; + bottom: -100px; +} + +#loginOptionCancelButton { + background: none; + border: none; + padding: 2px 0px; + font-size: 16px; + font-weight: bold; + color: lightgrey; + cursor: pointer; + outline: none; + transition: 0.25s ease; +} +#loginOptionCancelButton:hover, +#loginOptionCancelButton:focus { + text-shadow: 0px 0px 20px lightgrey; +} +#loginOptionCancelButton:active { + text-shadow: 0px 0px 20px rgba(211, 211, 211, 0.75); + color: rgba(211, 211, 211, 0.75); +} +#loginOptionCancelButton:disabled { + color: rgba(211, 211, 211, 0.75); + pointer-events: none; +} + + /******************************************************************************* * * * Settings View (sttings.ejs) * @@ -1282,45 +1439,65 @@ input:checked + .toggleSwitchSlider:before { * Settings View (Account Tab) * * */ -/* Add account button styles. */ -#settingsAddAccount { - background: rgba(0, 0, 0, 0.25); - border: 1px solid rgba(126, 126, 126, 0.57); - border-radius: 3px; - height: 50px; +.settingsAuthAccountTypeContainer { + display: flex; width: 75%; + flex-direction: column; +} + +.settingsAuthAccountTypeHeader { + display: flex; + align-items: center; + width: 100%; + justify-content: space-between; + padding: 10px 0px; + border-bottom: 1px solid #ffffff85; + margin-bottom: 30px; +} + +.settingsAuthAccountTypeHeaderLeft { + display: flex; + column-gap: 5px; +} + +/* Settings add account button styles. */ +.settingsAddAuthAccount { + background: none; + border: none; text-align: left; - padding: 0px 50px; + padding: 2px 0px; + color: white; cursor: pointer; outline: none; transition: 0.25s ease; } -#settingsAddAccount:hover, -#settingsAddAccount:focus { - background: rgba(54, 54, 54, 0.25); - text-shadow: 0px 0px 20px white; +.settingsAddAuthAccount:hover, +.settingsAddAuthAccount:focus { + text-shadow: 0px 0px 20px white, 0px 0px 20px white, 0px 0px 20px white; } - -/* Settings auth accounts header. */ -#settingsCurrentAccountsHeader { - margin: 20px 0px; +.settingsAddAuthAccount:active { + text-shadow: 0px 0px 20px rgba(255, 255, 255, 0.75), 0px 0px 20px rgba(255, 255, 255, 0.75), 0px 0px 20px rgba(255, 255, 255, 0.75); + color: rgba(255, 255, 255, 0.75); +} +.settingsAddAuthAccount:disabled { + color: rgba(255, 255, 255, 0.75); + pointer-events: none; } /* Auth account list container styles. */ -#settingsCurrentAccounts { +.settingsCurrentAccounts { margin-bottom: 5%; } -#settingsCurrentAccounts > .settingsAuthAccount:not(:last-child) { +.settingsCurrentAccounts > .settingsAuthAccount:not(:last-child) { margin-bottom: 10px; } -#settingsCurrentAccounts > .settingsAuthAccount:not(:first-child) { +.settingsCurrentAccounts > .settingsAuthAccount:not(:first-child) { margin-top: 10px; } /* Auth account shared styles. */ .settingsAuthAccount { display: flex; - width: 75%; background: rgba(0, 0, 0, 0.25); border-radius: 3px; border: 1px solid rgba(126, 126, 126, 0.57); @@ -2347,187 +2524,6 @@ input:checked + .toggleSwitchSlider:before { display: inline-flex; } -/******************************************************************************* - * * - * Landing View (News Styles) * - * * - ******************************************************************************/ - -/* Main container. */ -#newsContainer { - position: absolute; - top: 100%; - height: 100%; - width: 100%; - transition: top 2s ease; - display: flex; - align-items: flex-end; - justify-content: center; -} - -/* News content container. */ -#newsContent { - height: 82vh; - width: 100%; - display: flex; - -webkit-user-select: initial; - position: relative; -} - -/* Drop shadow displayed when content is scrolled out of view. */ -#newsContent:before { - content: ''; - background: linear-gradient(rgba(0, 0, 0, 0.25), transparent); - width: 100%; - height: 5px; - position: absolute; - opacity: 0; - transition: opacity 0.25s ease; -} -#newsContent[scrolled]:before { - opacity: 1; -} - -/* News article status container (left). */ -#newsStatusContainer { - width: calc(30% - 60px); - height: calc(100% - 30px); - padding: 15px 15px 15px 45px; - display: flex; - flex-direction: column; - justify-content: space-between; - position: relative; -} - -/* News status content. */ -#newsStatusContent { - display: flex; - flex-direction: column; - align-items: flex-end; -} - -/* News title wrapper. */ -#newsTitleContainer { - display: flex; - max-width: 90%; -} - -/* News article title styles. */ -#newsArticleTitle { - font-size: 18px; - font-weight: bold; - font-family: 'Avenir Medium'; - color: white; - text-decoration: none; - transition: 0.25s ease; - outline: none; - text-align: right; -} -#newsArticleTitle:hover, -#newsArticleTitle:focus { - text-shadow: 0px 0px 20px white; -} -#newsArticleTitle:active { - color: #c7c7c7; - text-shadow: 0px 0px 20px #c7c7c7; -} - -/* News meta container. */ -#newsMetaContainer { - display: flex; - flex-direction: column; -} - -/* Date and author wrappers. */ -#newsArticleDateWrapper, -#newsArticleAuthorWrapper { - display: flex; - justify-content: flex-end; -} - -/* Date and author shared styles. */ -#newsArticleDate, -#newsArticleAuthor { - display: inline-block; - font-size: 10px; - padding: 0px 5px; - font-weight: bold; - border-radius: 2px; -} - -/* Date styles. */ -#newsArticleDate { - background: white; - color: black; - margin-top: 5px; -} - -/* Author styles. */ -#newsArticleAuthor { - background: #a02d2a; -} - -/* News article comments styles. */ -#newsArticleComments { - margin-top: 5px; - display: inline-block; - font-size: 10px; - color: #ffffff; - text-decoration: none; - transition: 0.25s ease; - outline: none; - text-align: right; -} -#newsArticleComments:focus, -#newsArticleComments:hover { - color: #e0e0e0; -} -#newsArticleComments:active { - color: #c7c7c7; -} - -/* Article content container (right). */ -#newsArticleContainer { - width: calc(100% - 25px); - height: 100%; - margin: 0px 0px 0px 25px; -} - -/* Article content styles. */ -#newsArticleContentScrollable { - font-size: 12px; - overflow-y: scroll; - height: 100%; - padding: 0px 15px 0px 15px; -} -#newsArticleContentScrollable img, -#newsArticleContentScrollable iframe { - max-width: 95%; - display: block; - margin: 0 auto; -} -#newsArticleContentScrollable a { - color: rgba(202, 202, 202, 0.75); - transition: 0.25s ease; - outline: none; -} -#newsArticleContentScrollable a:hover, -#newsArticleContentScrollable a:focus { - color: rgba(255, 255, 255, 0.75); -} -#newsArticleContentScrollable a:active { - color: rgba(165, 165, 165, 0.75); -} -#newsArticleContentScrollable::-webkit-scrollbar { - width: 2px; -} -#newsArticleContentScrollable::-webkit-scrollbar-track { - display: none; -} -#newsArticleContentScrollable::-webkit-scrollbar-thumb { - border-radius: 10px; - box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.50); -} .bbCodeSpoilerButton { background: none; border: none; @@ -2553,118 +2549,9 @@ input:checked + .toggleSwitchSlider:before { border-bottom: 1px solid white; } - -#newsArticleContentWrapper { - width: 80%; -} - -.newsArticleSpacerTop { - height: 15px; -} - -/* Div to add spacing at the end of a news article. */ -.newsArticleSpacerBot { - height: 30px; -} - -/* News navigation container. */ -#newsNavigationContainer { - display: flex; - justify-content: center; - align-items: center; - margin-bottom: 10px; - -webkit-user-select: none; - position: absolute; - bottom: 15px; - right: 0px; -} - -/* Navigation status span. */ -#newsNavigationStatus { - font-size: 12px; - margin: 0px 15px; -} - -/* Left and right navigation button styles. */ -#newsNavigateLeft, -#newsNavigateRight { - background: none; - border: none; - outline: none; - height: 20px; - cursor: pointer; -} -#newsNavigateLeft:hover #newsNavigationLeftSVG, -#newsNavigateLeft:focus #newsNavigationLeftSVG, -#newsNavigateRight:hover #newsNavigationRightSVG, -#newsNavigateRight:focus #newsNavigationRightSVG { - -webkit-filter: drop-shadow(0px 0px 2px #fff); -} -#newsNavigateLeft:active #newsNavigationLeftSVG .arrowLine, -#newsNavigateRight:active #newsNavigationRightSVG .arrowLine { - stroke: #c7c7c7; -} -#newsNavigateLeft:active #newsNavigationLeftSVG, -#newsNavigateRight:active #newsNavigationRightSVG { - -webkit-filter: drop-shadow(0px 0px 2px #c7c7c7); -} -#newsNavigateLeft:disabled #newsNavigationLeftSVG .arrowLine, -#newsNavigateRight:disabled #newsNavigationRightSVG .arrowLine { - stroke: rgba(255, 255, 255, 0.75); -} -#newsNavigationLeftSVG { - transform: rotate(-90deg); - width: 15px; -} -#newsNavigationRightSVG { - transform: rotate(90deg); - width: 15px; -} - -/* News error (message) container. */ -#newsErrorContainer { - height: 100%; - display: flex; - align-items: center; - flex-direction: column; - justify-content: center; -} -#newsErrorFailed { - display: flex; - align-items: center; - flex-direction: column; - justify-content: center; -} - -/* News error content (message). */ -.newsErrorContent { - font-size: 20px; -} -#newsErrorLoading { - display: flex; - width: 168.92px; -} #nELoadSpan { white-space: pre; } -/* News error retry button styles. */ -#newsErrorRetry { - font-size: 12px; - font-weight: bold; - cursor: pointer; - background: none; - border: none; - outline: none; - transition: 0.25s ease; -} -#newsErrorRetry:focus, -#newsErrorRetry:hover { - text-shadow: 0px 0px 20px white; -} -#newsErrorRetry:active { - color: #c7c7c7; - text-shadow: 0px 0px 20px #c7c7c7; -} /******************************************************************************* * * @@ -3141,70 +3028,6 @@ input:checked + .toggleSwitchSlider:before { color: #848484; } -/* * * -* Landing View (Bottom Styles) | Center Content -* * */ - -/* Button which opens the news view. */ -#newsButton { - background: none; - border: none; - cursor: pointer; - outline: none; -} -#newsButton:hover #newsButtonText, -#newsButton:focus #newsButtonText { - text-shadow: 0px 0px 20px #fff, 0px 0px 20px #fff; -} -#newsButton:active { - color: #c7c7c7; - text-shadow: 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7; -} - -#newsButton:hover #newsButtonSVG, -#newsButton:focus #newsButtonSVG { - -webkit-filter: drop-shadow(0px 0px 2px #fff); -} -#newsButton:active #newsButtonSVG .arrowLine { - stroke: #c7c7c7; -} -#newsButton:active #newsButtonSVG { - -webkit-filter: drop-shadow(0px 0px 2px #c7c7c7); -} -#newsButton:disabled #newsButtonSVG .arrowLine { - stroke: rgba(255, 255, 255, 0.75); -} - -/* Icon which indicates there is new news. */ -#newsButtonAlert { - width: 5px; - height: 5px; - position: absolute; - border-radius: 50%; - background: red; - right: -1px; - top: 50%; -} - -/* Arrow image which floats above the news button. */ -#newsButtonSVG { - height: 11px; - margin-left: -2px; - transition: 0.25s ease; -} - -/* Span which contains the news button text. */ -#newsButtonText { - color: white; - font-weight: 900; - letter-spacing: 2px; - text-shadow: 0px 0px 0px #bebcbb; - font-size: 11px; - line-height: 30px; - display: flex; - transition: 0.25s ease; -} - /* * * * Landing View (Bottom Styles) | Right Content * * */ @@ -3787,29 +3610,4 @@ input:checked + .toggleSwitchSlider:before { /* Class which is applied when the spinner image is spinning. */ .rotating { animation: rotating 10s linear infinite; -} - -#loginMSButton { - background: none; - border: none; - margin-top: 5px; - margin-bottom: 10px; - transform: scale(0.8, 0.8); - opacity: 1; -} -#loginMSButton:disabled { - color: rgba(255, 255, 255, 0.75); - pointer-events: none; -} - -#loginMSButton:hover, -#loginMSButton:focus { - text-shadow: 0px 0px 20px #fff; - opacity: 0.8; - outline: none; -} - -#loginMSButton:active { - color: #c7c7c7; - text-shadow: 0px 0px 20px #c7c7c7; -} \ No newline at end of file +} \ No newline at end of file diff --git a/app/assets/images/LoginWithMicrosoft.png b/app/assets/images/LoginWithMicrosoft.png deleted file mode 100644 index adc281da9fecdc1d090f4e7af91b0b985ee998b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3295 zcmai1XIK-;whaVGKq8SMp@Y&SbTA+sLQ@g~sE86;By_=mVCYB>~vG zCPlg!kg60x6r_ejjkLqV`@ZkJU-#Z0vuF1H*36o{=f__2=k1#)2pc~e004j(qOacp z02nOJ*7{(UGy8>2h;lYD5bvP$02Nfhl{1IQMb|_Z0H{hlcWlRe=CgXDEr|fY`S-sE zL+_&!M*x7`+wi*XUH|(V*&LNT_k?;mZkk*U6~EqTFN-cRhDxBJwuQ0aWKR7dB{^`W zQl&D6Qq?I{nlT(!q^$ZB?>HLAK6ruaQxC zestO_aqit3U4i=itIE~t zs5tw9MCh(f?fXI7(;VM^+n&!YUZ=I3Vx%!p#lJwRl_cl_>%WMYAhKd4|A_y1=D&qc z )D?FiPU~6YL{5zwZ2{HFed0;mglKs#Xv%;NX4e`eVAtjSoYi?h+0YkB+O! z+x<;-+GNPoVww5$oH%H5S$U>E&-AGA{vUOVS4LHXJcfNO^?*m`3sCgS;pPjcX@Q)zjS$tb}B+ zwwO2_8k9?3Fbwp#{Chv~Gz8A4QsjE~$B$HlS7)!e!h}Gc(nV>!AZkZrA##^CK8pFG z12jTQZI#uoQMgS~i0F`Sp@HcD#|wQj_CagPqi;4pg=cY7^n^nLAAX^#>9qU|Qc*zs zOc3G{P|+U*RlL0aF-zM%z0q--M^2KwxD%imW^`zOOi}e=td)z^jFMPM2kBMXo6XjhI-!rL|sMkPCrjK%J%*v~I zdwD5H=2EPRJcr}*z_6}}Y#q5mC3ejPGUfg8vU~Si(ls;5zlgo@GHby4;CV@-(6EWL zvVkdP0fSh+<|ekz0Y6rgca2A@31sU7Ry(4FrJiS-#t1p;>DJm@mJur7@hQ` zZe|6U#5@CmDf*T}SxC*>6vZTu$n!k`9#Rfr_<3`#p|UQavI!>os*KP-=bvGlEqANF zgDm^gQ3x%&BF>Ub;UA&FcCW}7!t9nBoR4^TMW6B`rsS|-_7l~0q|D}-W?d-$0jW8( zegY%Z=yxJQab&#gwQJhCUhmzGkCSig;gPj!MJV1em9?mQJJ`_F-Ums4Vp>wcn3}ks zeL7mSkG?jnV9aUfb#vxXdX9@y&yy4&(7GYY1Jwfm^#wiX)>uWTv`5I%p47ilW|C&$ zhRgbG4P<)Ao0$*(xDyRkYnHr0yEk&Ta%x1!G=JETPh~+B&chNLk@9LS1gC{c*O+)23&a;Ezb{Wc zZwW!IaIOl*z?^1tq}XK(l{y+a`&<>1`vUl3R{m=IFm3miB96a1f(=YRz%qCHRU&qF zNw21vnwZ=^l?uW>>vy!pa0s!7AlN+5?a466Z>LRY#%Z>Cew%;nnl^(&0v!w{o$@YY zkC&(H`8Ev&~#eOqFC#)s~85t;PsFhOZdibk! zAKmQb_F_>TN%mZ8b2=#BS7V+r6oYtf6ya{SRU*cFJ#URi7ui^O9AuE$48SL^eOP@A zVkN0YVdwSy7Bw7+VFyN=>>-RWRLykD8=UzcL*w_1g%uAO^*9)*&ynpQ`k&if^Q^Vc zlt>8AughjJQJmhfLd&l=^DZOGm9LKicbNCB-5)rV@I7AGGFM*O3ogp)5YO`6HZ+&oKM=7=5jkMfkfQNA z`R}FfL)QK2_qCd=OQElIv&`Rk;@-j>_C!Hq5j_r*Wku>78VCYnGG55{Ejd2mfIQnV zbOORu6KwcUdxMgG3aeE*<;UX@M-#M4_P;=CsqyYRucWv^VK6(bFwbBc0q5|o;m#GsKTSTFS^RLpIRBrkS3gR({v|Ahxziz#!eR>L6+zl+B#f4J!kw zRthhN8r#BoX4)FkhaNhyS^0YzCekEsoY3zJ82l+(){v)Qz%ExV48`{7T;R2^FY8O> z#*=ENOW!;aSF95Cl@^s-%K?eh&Ar5JTj@}}C)fr5K5G{{BNbQ*_xbJksF2N#2e1n+ z-O;dx;y^R@@5^y>ZTaeHzbhTaG6AGz#_#qd#ATk7DMgP4B{+3D}0irkWoCQHh`PDPx6H{9z&SY13`^6fi8mbU!>9?#u894fz}dNPds!Tr2_#DHVlAfK3cqe`I?rC=0v66tSjw z(dMkchK2=ZRgXqxkl^{`NZ!5uQ^odQ1I_W|VGop`X*SsJhW2@I5?8u65G2MxS279E zht4Q@Mb6X{9|Tym9k-g&scQ Fe*h1x8>9dL diff --git a/app/assets/images/icons/microsoft.svg b/app/assets/images/icons/microsoft.svg new file mode 100644 index 0000000000..78a4ed9436 --- /dev/null +++ b/app/assets/images/icons/microsoft.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/assets/images/icons/mojang.svg b/app/assets/images/icons/mojang.svg new file mode 100644 index 0000000000..e1116b41ba --- /dev/null +++ b/app/assets/images/icons/mojang.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/assets/images/icons/news.svg b/app/assets/images/icons/news.svg deleted file mode 100644 index 775578d435..0000000000 --- a/app/assets/images/icons/news.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - News - - - - - - - - - \ No newline at end of file diff --git a/app/assets/js/assetguard.js b/app/assets/js/assetguard.js index ebf3be94a9..75f6bc5483 100644 --- a/app/assets/js/assetguard.js +++ b/app/assets/js/assetguard.js @@ -16,13 +16,6 @@ const ConfigManager = require('./configmanager') const DistroManager = require('./distromanager') const isDev = require('./isdev') -// Constants -// const PLATFORM_MAP = { -// win32: '-windows-x64.tar.gz', -// darwin: '-macosx-x64.tar.gz', -// linux: '-linux-x64.tar.gz' -// } - // Classes /** Class representing a base asset. */ @@ -234,7 +227,7 @@ class JavaGuard extends EventEmitter { * Fetch the last open JDK binary. * * HOTFIX: Uses Corretto 8 for macOS. - * See: https://github.com/XXdaugonXX/ZayivelLauncher/issues/70 + * See: https://github.com/dscalzi/HeliosLauncher/issues/70 * See: https://github.com/AdoptOpenJDK/openjdk-support/issues/101 * * @param {string} major The major version of Java to fetch. diff --git a/app/assets/js/authmanager.js b/app/assets/js/authmanager.js index 8b1cbeca84..3f431440b3 100644 --- a/app/assets/js/authmanager.js +++ b/app/assets/js/authmanager.js @@ -9,113 +9,192 @@ * @module authmanager */ // Requirements -const ConfigManager = require('./configmanager') -const LoggerUtil = require('./loggerutil') -const Mojang = require('./mojang') -const Microsoft = require('./microsoft') -const logger = LoggerUtil('%c[AuthManager]', 'color: #a02d2a; font-weight: bold') -const loggerSuccess = LoggerUtil('%c[AuthManager]', 'color: #209b07; font-weight: bold') - -// Validation - -async function validateSelectedMojang(selectedAccount) { - const current = selectedAccount - const isValid = await Mojang.validate(current.accessToken, ConfigManager.getClientToken()) - if(!isValid){ - try { - const session = await Mojang.refresh(current.accessToken, ConfigManager.getClientToken()) - ConfigManager.updateAuthAccount(current.uuid, session.accessToken) - ConfigManager.save() - } catch(err) { - logger.debug('Error while validating selected profile:', err) - if(err && err.error === 'ForbiddenOperationException'){ - // What do we do? +const ConfigManager = require('./configmanager') +const { LoggerUtil } = require('helios-core') +const { RestResponseStatus } = require('helios-core/common') +const { MojangRestAPI, mojangErrorDisplayable, MojangErrorCode } = require('helios-core/mojang') +const { MicrosoftAuth, microsoftErrorDisplayable, MicrosoftErrorCode } = require('helios-core/microsoft') +const { AZURE_CLIENT_ID } = require('./ipcconstants') + +const log = LoggerUtil.getLogger('AuthManager') + +// Functions + +/** + * Add a Mojang account. This will authenticate the given credentials with Mojang's + * authserver. The resultant data will be stored as an auth account in the + * configuration database. + * + * @param {string} username The account username (email if migrated). + * @param {string} password The account password. + * @returns {Promise.} Promise which resolves the resolved authenticated account object. + */ +exports.addMojangAccount = async function(username, password) { + try { + const response = await MojangRestAPI.authenticate(username, password, ConfigManager.getClientToken()) + console.log(response) + if(response.responseStatus === RestResponseStatus.SUCCESS) { + + const session = response.data + if(session.selectedProfile != null){ + const ret = ConfigManager.addMojangAuthAccount(session.selectedProfile.id, session.accessToken, username, session.selectedProfile.name) + if(ConfigManager.getClientToken() == null){ + ConfigManager.setClientToken(session.clientToken) + } + ConfigManager.save() + return ret + } else { + return Promise.reject(mojangErrorDisplayable(MojangErrorCode.ERROR_NOT_PAID)) } - logger.log('Account access token is invalid.') - return false + + } else { + return Promise.reject(mojangErrorDisplayable(response.mojangErrorCode)) } - loggerSuccess.log('Account access token validated.') - return true - } else { - loggerSuccess.log('Account access token validated.') - return true + + } catch (err){ + log.error(err) + return Promise.reject(mojangErrorDisplayable(MojangErrorCode.UNKNOWN)) } } -async function validateSelectedMicrosoft(selectedAccount) { +const AUTH_MODE = { FULL: 0, MS_REFRESH: 1, MC_REFRESH: 2 } + +/** + * Perform the full MS Auth flow in a given mode. + * + * AUTH_MODE.FULL = Full authorization for a new account. + * AUTH_MODE.MS_REFRESH = Full refresh authorization. + * AUTH_MODE.MC_REFRESH = Refresh of the MC token, reusing the MS token. + * + * @param {string} entryCode FULL-AuthCode. MS_REFRESH=refreshToken, MC_REFRESH=accessToken + * @param {*} authMode The auth mode. + * @returns An object with all auth data. AccessToken object will be null when mode is MC_REFRESH. + */ +async function fullMicrosoftAuthFlow(entryCode, authMode) { try { - const current = selectedAccount - const now = new Date().getTime() - const MCExpiresAt = Date.parse(current.expiresAt) - const MCExpired = now > MCExpiresAt - - if(MCExpired) { - const MSExpiresAt = Date.parse(ConfigManager.getMicrosoftAuth().expires_at) - const MSExpired = now > MSExpiresAt - - if (MSExpired) { - const newAccessToken = await Microsoft.refreshAccessToken(ConfigManager.getMicrosoftAuth) - ConfigManager.updateMicrosoftAuth(newAccessToken.access_token, newAccessToken.expires_at) - ConfigManager.save() - } - const newMCAccessToken = await Microsoft.authMinecraft(ConfigManager.getMicrosoftAuth().access_token) - ConfigManager.updateAuthAccount(current.uuid, newMCAccessToken.access_token, newMCAccessToken.expires_at) - ConfigManager.save() - return true + let accessTokenRaw + let accessToken + if(authMode !== AUTH_MODE.MC_REFRESH) { + const accessTokenResponse = await MicrosoftAuth.getAccessToken(entryCode, authMode === AUTH_MODE.MS_REFRESH, AZURE_CLIENT_ID) + if(accessTokenResponse.responseStatus === RestResponseStatus.ERROR) { + return Promise.reject(microsoftErrorDisplayable(accessTokenResponse.microsoftErrorCode)) + } + accessToken = accessTokenResponse.data + accessTokenRaw = accessToken.access_token } else { - return true + accessTokenRaw = entryCode } - } catch (error) { - return Promise.reject(error) + + const xblResponse = await MicrosoftAuth.getXBLToken(accessTokenRaw) + if(xblResponse.responseStatus === RestResponseStatus.ERROR) { + return Promise.reject(microsoftErrorDisplayable(xblResponse.microsoftErrorCode)) + } + const xstsResonse = await MicrosoftAuth.getXSTSToken(xblResponse.data) + if(xstsResonse.responseStatus === RestResponseStatus.ERROR) { + return Promise.reject(microsoftErrorDisplayable(xstsResonse.microsoftErrorCode)) + } + const mcTokenResponse = await MicrosoftAuth.getMCAccessToken(xstsResonse.data) + if(mcTokenResponse.responseStatus === RestResponseStatus.ERROR) { + return Promise.reject(microsoftErrorDisplayable(mcTokenResponse.microsoftErrorCode)) + } + const mcProfileResponse = await MicrosoftAuth.getMCProfile(mcTokenResponse.data.access_token) + if(mcProfileResponse.responseStatus === RestResponseStatus.ERROR) { + return Promise.reject(microsoftErrorDisplayable(mcProfileResponse.microsoftErrorCode)) + } + return { + accessToken, + accessTokenRaw, + xbl: xblResponse.data, + xsts: xstsResonse.data, + mcToken: mcTokenResponse.data, + mcProfile: mcProfileResponse.data + } + } catch(err) { + log.error(err) + return Promise.reject(microsoftErrorDisplayable(MicrosoftErrorCode.UNKNOWN)) } } -// Functions +/** + * Calculate the expiry date. Advance the expiry time by 10 seconds + * to reduce the liklihood of working with an expired token. + * + * @param {number} nowMs Current time milliseconds. + * @param {number} epiresInS Expires in (seconds) + * @returns + */ +function calculateExpiryDate(nowMs, epiresInS) { + return nowMs + ((epiresInS-10)*1000) +} /** - * Add an account. This will authenticate the given credentials with Mojang's - * authserver. The resultant data will be stored as an auth account in the - * configuration database. + * Add a Microsoft account. This will pass the provided auth code to Mojang's OAuth2.0 flow. + * The resultant data will be stored as an auth account in the configuration database. * - * @param {string} username The account username (email if migrated). - * @param {string} password The account password. + * @param {string} authCode The authCode obtained from microsoft. * @returns {Promise.} Promise which resolves the resolved authenticated account object. */ -exports.addAccount = async function(username, password){ +exports.addMicrosoftAccount = async function(authCode) { + + const fullAuth = await fullMicrosoftAuthFlow(authCode, AUTH_MODE.FULL) + + // Advance expiry by 10 seconds to avoid close calls. + const now = new Date().getTime() + + const ret = ConfigManager.addMicrosoftAuthAccount( + fullAuth.mcProfile.id, + fullAuth.mcToken.access_token, + fullAuth.mcProfile.name, + calculateExpiryDate(now, fullAuth.mcToken.expires_in), + fullAuth.accessToken.access_token, + fullAuth.accessToken.refresh_token, + calculateExpiryDate(now, fullAuth.accessToken.expires_in) + ) + ConfigManager.save() + + return ret +} + +/** + * Remove a Mojang account. This will invalidate the access token associated + * with the account and then remove it from the database. + * + * @param {string} uuid The UUID of the account to be removed. + * @returns {Promise.} Promise which resolves to void when the action is complete. + */ +exports.removeMojangAccount = async function(uuid){ try { - const session = await Mojang.authenticate(username, password, ConfigManager.getClientToken()) - if(session.selectedProfile != null){ - const ret = ConfigManager.addAuthAccount(session.selectedProfile.id, session.accessToken, username, session.selectedProfile.name) - if(ConfigManager.getClientToken() == null){ - ConfigManager.setClientToken(session.clientToken) - } + const authAcc = ConfigManager.getAuthAccount(uuid) + const response = await MojangRestAPI.invalidate(authAcc.accessToken, ConfigManager.getClientToken()) + if(response.responseStatus === RestResponseStatus.SUCCESS) { + ConfigManager.removeAuthAccount(uuid) ConfigManager.save() - return ret + return Promise.resolve() } else { - throw new Error('NotPaidAccount') + log.error('Error while removing account', response.error) + return Promise.reject(response.error) } - } catch (err){ + log.error('Error while removing account', err) return Promise.reject(err) } } /** - * Remove an account. This will invalidate the access token associated - * with the account and then remove it from the database. + * Remove a Microsoft account. It is expected that the caller will invoke the OAuth logout + * through the ipc renderer. * * @param {string} uuid The UUID of the account to be removed. * @returns {Promise.} Promise which resolves to void when the action is complete. */ -exports.removeAccount = async function(uuid){ +exports.removeMicrosoftAccount = async function(uuid){ try { - const authAcc = ConfigManager.getAuthAccount(uuid) - await Mojang.invalidate(authAcc.accessToken, ConfigManager.getClientToken()) ConfigManager.removeAuthAccount(uuid) ConfigManager.save() return Promise.resolve() } catch (err){ + log.error('Error while removing account', err) return Promise.reject(err) } } @@ -125,33 +204,112 @@ exports.removeAccount = async function(uuid){ * we will attempt to refresh the access token and update that value. If that fails, a * new login will be required. * - * **Function is WIP** + * @returns {Promise.} Promise which resolves to true if the access token is valid, + * otherwise false. + */ +async function validateSelectedMojangAccount(){ + const current = ConfigManager.getSelectedAccount() + const response = await MojangRestAPI.validate(current.accessToken, ConfigManager.getClientToken()) + + if(response.responseStatus === RestResponseStatus.SUCCESS) { + const isValid = response.data + if(!isValid){ + const refreshResponse = await MojangRestAPI.refresh(current.accessToken, ConfigManager.getClientToken()) + if(refreshResponse.responseStatus === RestResponseStatus.SUCCESS) { + const session = refreshResponse.data + ConfigManager.updateMojangAuthAccount(current.uuid, session.accessToken) + ConfigManager.save() + } else { + log.error('Error while validating selected profile:', refreshResponse.error) + log.info('Account access token is invalid.') + return false + } + log.info('Account access token validated.') + return true + } else { + log.info('Account access token validated.') + return true + } + } + +} + +/** + * Validate the selected account with Microsoft's authserver. If the account is not valid, + * we will attempt to refresh the access token and update that value. If that fails, a + * new login will be required. * * @returns {Promise.} Promise which resolves to true if the access token is valid, * otherwise false. */ -exports.validateSelected = async function(){ - const selectedAccount = ConfigManager.getSelectedAccount() - if (selectedAccount.type === 'microsoft') { - const validate = await validateSelectedMicrosoft(selectedAccount) - return validate - } else { - const validate = await validateSelectedMojang(selectedAccount) - return validate - } +async function validateSelectedMicrosoftAccount(){ + const current = ConfigManager.getSelectedAccount() + const now = new Date().getTime() + const mcExpiresAt = current.expiresAt + const mcExpired = now >= mcExpiresAt + + if(!mcExpired) { + return true + } + + // MC token expired. Check MS token. + + const msExpiresAt = current.microsoft.expires_at + const msExpired = now >= msExpiresAt + + if(msExpired) { + // MS expired, do full refresh. + try { + const res = await fullMicrosoftAuthFlow(current.microsoft.refresh_token, AUTH_MODE.MS_REFRESH) + + ConfigManager.updateMicrosoftAuthAccount( + current.uuid, + res.mcToken.access_token, + res.accessToken.access_token, + res.accessToken.refresh_token, + calculateExpiryDate(now, res.accessToken.expires_in), + calculateExpiryDate(now, res.mcToken.expires_in) + ) + ConfigManager.save() + return true + } catch(err) { + return false + } + } else { + // Only MC expired, use existing MS token. + try { + const res = await fullMicrosoftAuthFlow(current.microsoft.access_token, AUTH_MODE.MC_REFRESH) + + ConfigManager.updateMicrosoftAuthAccount( + current.uuid, + res.mcToken.access_token, + current.microsoft.access_token, + current.microsoft.refresh_token, + current.microsoft.expires_at, + calculateExpiryDate(now, res.mcToken.expires_in) + ) + ConfigManager.save() + return true + } + catch(err) { + return false + } + } } -exports.addMSAccount = async authCode => { - try { - const accessToken = await Microsoft.getAccessToken(authCode) - ConfigManager.setMicrosoftAuth(accessToken) - const MCAccessToken = await Microsoft.authMinecraft(accessToken.access_token) - const MCProfile = await Microsoft.getMCProfile(MCAccessToken.access_token) - const ret = ConfigManager.addAuthAccount(MCProfile.id, MCAccessToken.access_token, MCProfile.name, MCProfile.name, MCAccessToken.expires_at, 'microsoft') - ConfigManager.save() +/** + * Validate the selected auth account. + * + * @returns {Promise.} Promise which resolves to true if the access token is valid, + * otherwise false. + */ +exports.validateSelected = async function(){ + const current = ConfigManager.getSelectedAccount() - return ret - } catch(error) { - return Promise.reject(error) + if(current.type === 'microsoft') { + return await validateSelectedMicrosoftAccount() + } else { + return await validateSelectedMojangAccount() } -} \ No newline at end of file + +} diff --git a/app/assets/js/configmanager.js b/app/assets/js/configmanager.js index 85aaa95770..848eae7eff 100644 --- a/app/assets/js/configmanager.js +++ b/app/assets/js/configmanager.js @@ -56,7 +56,7 @@ exports.getAbsoluteMaxRAM = function(){ function resolveMaxRAM(){ const mem = os.totalmem() - return mem >= 16000000000 ? '8G' :(mem >= 8000000000 ? '4G' : (mem >= 6000000000 ? '3G' : '2G')) + return mem >= 16000000000 ? '8G' : mem >= 8000000000 ? '4G' : (mem >= 6000000000 ? '3G' : '2G') } function resolveMinRAM(){ @@ -94,17 +94,11 @@ const DEFAULT_CONFIG = { dataDirectory: dataPath } }, - newsCache: { - date: null, - content: null, - dismissed: false - }, clientToken: null, selectedServer: null, // Resolved selectedAccount: null, authenticationDatabase: {}, - modConfigurations: [], - microsoftAuth: {} + modConfigurations: [] } let config = null @@ -212,34 +206,6 @@ exports.getTempNativeFolder = function(){ // System Settings (Unconfigurable on UI) -/** - * Retrieve the news cache to determine - * whether or not there is newer news. - * - * @returns {Object} The news cache object. - */ -exports.getNewsCache = function(){ - return config.newsCache -} - -/** - * Set the new news cache object. - * - * @param {Object} newsCache The new news cache object. - */ -exports.setNewsCache = function(newsCache){ - config.newsCache = newsCache -} - -/** - * Set whether or not the news has been dismissed (checked) - * - * @param {boolean} dismissed Whether or not the news has been dismissed (checked). - */ -exports.setNewsCacheDismissed = function(dismissed){ - config.newsCache.dismissed = dismissed -} - /** * Retrieve the common directory for shared * game files (assets, libraries, etc). @@ -319,21 +285,21 @@ exports.getAuthAccount = function(uuid){ } /** - * Update the access token of an authenticated account. + * Update the access token of an authenticated mojang account. * * @param {string} uuid The uuid of the authenticated account. * @param {string} accessToken The new Access Token. * * @returns {Object} The authenticated account object created by this action. */ -exports.updateAuthAccount = function(uuid, accessToken){ +exports.updateMojangAuthAccount = function(uuid, accessToken){ config.authenticationDatabase[uuid].accessToken = accessToken - config.authenticationDatabase[uuid].expiresAt = expiresAt + config.authenticationDatabase[uuid].type = 'mojang' // For gradual conversion. return config.authenticationDatabase[uuid] } /** - * Adds an authenticated account to the database to be stored. + * Adds an authenticated mojang account to the database to be stored. * * @param {string} uuid The uuid of the authenticated account. * @param {string} accessToken The accessToken of the authenticated account. @@ -342,15 +308,66 @@ exports.updateAuthAccount = function(uuid, accessToken){ * * @returns {Object} The authenticated account object created by this action. */ - exports.addAuthAccount = function(uuid, accessToken, username, displayName, expiresAt = null, type = 'mojang'){ +exports.addMojangAuthAccount = function(uuid, accessToken, username, displayName){ config.selectedAccount = uuid config.authenticationDatabase[uuid] = { + type: 'mojang', accessToken, username: username.trim(), uuid: uuid.trim(), - displayName: displayName.trim(), - expiresAt: expiresAt, - type: type + displayName: displayName.trim() + } + return config.authenticationDatabase[uuid] +} + +/** + * Update the tokens of an authenticated microsoft account. + * + * @param {string} uuid The uuid of the authenticated account. + * @param {string} accessToken The new Access Token. + * @param {string} msAccessToken The new Microsoft Access Token + * @param {string} msRefreshToken The new Microsoft Refresh Token + * @param {date} msExpires The date when the microsoft access token expires + * @param {date} mcExpires The date when the mojang access token expires + * + * @returns {Object} The authenticated account object created by this action. + */ +exports.updateMicrosoftAuthAccount = function(uuid, accessToken, msAccessToken, msRefreshToken, msExpires, mcExpires) { + config.authenticationDatabase[uuid].accessToken = accessToken + config.authenticationDatabase[uuid].expiresAt = mcExpires + config.authenticationDatabase[uuid].microsoft.access_token = msAccessToken + config.authenticationDatabase[uuid].microsoft.refresh_token = msRefreshToken + config.authenticationDatabase[uuid].microsoft.expires_at = msExpires + return config.authenticationDatabase[uuid] +} + +/** + * Adds an authenticated microsoft account to the database to be stored. + * + * @param {string} uuid The uuid of the authenticated account. + * @param {string} accessToken The accessToken of the authenticated account. + * @param {string} name The in game name of the authenticated account. + * @param {date} mcExpires The date when the mojang access token expires + * @param {string} msAccessToken The microsoft access token + * @param {string} msRefreshToken The microsoft refresh token + * @param {date} msExpires The date when the microsoft access token expires + * + * @returns {Object} The authenticated account object created by this action. + */ +exports.addMicrosoftAuthAccount = function(uuid, accessToken, name, mcExpires, msAccessToken, msRefreshToken, msExpires) { + config.selectedAccount = uuid + config.authenticationDatabase[uuid] = { + type: 'microsoft', + accessToken, + username: name.trim(), + uuid: uuid.trim(), + displayName: name.trim(), + expiresAt: mcExpires, + microsoft: { + access_token: msAccessToken, + refresh_token: msRefreshToken, + expires_at: msExpires + } } return config.authenticationDatabase[uuid] } @@ -689,19 +706,4 @@ exports.getAllowPrerelease = function(def = false){ */ exports.setAllowPrerelease = function(allowPrerelease){ config.settings.launcher.allowPrerelease = allowPrerelease -} - -exports.setMicrosoftAuth = microsoftAuth => { - config.microsoftAuth = microsoftAuth -} - -exports.getMicrosoftAuth = () => { - return config.microsoftAuth -} - -exports.updateMicrosoftAuth = (accessToken, expiresAt) => { - config.microsoftAuth.access_token = accessToken - config.microsoftAuth.expires_at = expiresAt - - return config.microsoftAuth -} \ No newline at end of file +} \ No newline at end of file diff --git a/app/assets/js/discordwrapper.js b/app/assets/js/discordwrapper.js index 529f17be1f..7b0ef03eea 100644 --- a/app/assets/js/discordwrapper.js +++ b/app/assets/js/discordwrapper.js @@ -1,7 +1,7 @@ // Work in progress const logger = require('./loggerutil')('%c[DiscordWrapper]', 'color: #7289da; font-weight: bold') -const {Client} = require('discord-rpc') +const {Client} = require('discord-rpc-patch') let client let activity diff --git a/app/assets/js/distromanager.js b/app/assets/js/distromanager.js index c9c06368f6..d8d69bb963 100644 --- a/app/assets/js/distromanager.js +++ b/app/assets/js/distromanager.js @@ -468,13 +468,6 @@ class DistroIndex { return this.version } - /** - * @returns {string} The URL to the news RSS feed. - */ - getRSS(){ - return this.rss - } - /** * @returns {Array.} An array of declared server configurations. */ @@ -537,7 +530,7 @@ exports.pullRemote = function(){ return exports.pullLocal() } return new Promise((resolve, reject) => { - const distroURL = 'https://dgfe.dk/Modded/distrobution.json' + const distroURL = 'https://dgfe.dk/Modded/distribution.json' //const distroURL = 'https://gist.githubusercontent.com/dscalzi/53b1ba7a11d26a5c353f9d5ae484b71b/raw/' const opts = { url: distroURL, diff --git a/app/assets/js/dropinmodutil.js b/app/assets/js/dropinmodutil.js index 84ad4fa628..f816a20fce 100644 --- a/app/assets/js/dropinmodutil.js +++ b/app/assets/js/dropinmodutil.js @@ -1,6 +1,7 @@ const fs = require('fs-extra') const path = require('path') -const { shell } = require('electron') +const { ipcRenderer, shell } = require('electron') +const { SHELL_OPCODE } = require('./ipcconstants') // Group #1: File Name (without .disabled, if any) // Group #2: File Extension (jar, zip, or litemod) @@ -95,14 +96,16 @@ exports.addDropinMods = function(files, modsdir) { * @returns {Promise.} True if the mod was deleted, otherwise false. */ exports.deleteDropinMod = async function(modsDir, fullName){ - try { - await shell.trashItem(path.join(modsDir, fullName)) - return true - } catch(error) { + + const res = await ipcRenderer.invoke(SHELL_OPCODE.TRASH_ITEM, path.join(modsDir, fullName)) + + if(!res.result) { shell.beep() - console.error('Error deleting drop-in mod.', error) + console.error('Error deleting drop-in mod.', res.error) return false } + + return true } /** diff --git a/app/assets/js/ipcconstants.js b/app/assets/js/ipcconstants.js new file mode 100644 index 0000000000..a05cd3c359 --- /dev/null +++ b/app/assets/js/ipcconstants.js @@ -0,0 +1,28 @@ +// NOTE FOR THIRD-PARTY +// REPLACE THIS CLIENT ID WITH YOUR APPLICATION ID. +// SEE https://github.com/dscalzi/HeliosLauncher/blob/master/docs/MicrosoftAuth.md +exports.AZURE_CLIENT_ID = 'd5b2c390-be84-4057-a249-8a26d8f360bc' +// SEE NOTE ABOVE. + + +// Opcodes +exports.MSFT_OPCODE = { + OPEN_LOGIN: 'MSFT_AUTH_OPEN_LOGIN', + OPEN_LOGOUT: 'MSFT_AUTH_OPEN_LOGOUT', + REPLY_LOGIN: 'MSFT_AUTH_REPLY_LOGIN', + REPLY_LOGOUT: 'MSFT_AUTH_REPLY_LOGOUT' +} +// Reply types for REPLY opcode. +exports.MSFT_REPLY_TYPE = { + SUCCESS: 'MSFT_AUTH_REPLY_SUCCESS', + ERROR: 'MSFT_AUTH_REPLY_ERROR' +} +// Error types for ERROR reply. +exports.MSFT_ERROR = { + ALREADY_OPEN: 'MSFT_AUTH_ERR_ALREADY_OPEN', + NOT_FINISHED: 'MSFT_AUTH_ERR_NOT_FINISHED' +} + +exports.SHELL_OPCODE = { + TRASH_ITEM: 'TRASH_ITEM' +} \ No newline at end of file diff --git a/app/assets/js/microsoft.js b/app/assets/js/microsoft.js deleted file mode 100644 index 1bb1a8a746..0000000000 --- a/app/assets/js/microsoft.js +++ /dev/null @@ -1,191 +0,0 @@ -// Requirements -const request = require('request') -// const logger = require('./loggerutil')('%c[Microsoft]', 'color: #01a6f0; font-weight: bold') - -// Constants -const clientId = 'd5b2c390-be84-4057-a249-8a26d8f360bc' -const tokenUri = 'https://login.microsoftonline.com/consumers/oauth2/v2.0/token' -const authXBLUri = 'https://user.auth.xboxlive.com/user/authenticate' -const authXSTSUri = 'https://xsts.auth.xboxlive.com/xsts/authorize' -const authMCUri = 'https://api.minecraftservices.com/authentication/login_with_xbox' -const profileURI ='https://api.minecraftservices.com/minecraft/profile' - -// Functions -function requestPromise(uri, options) { - return new Promise((resolve, reject) => { - request(uri, options, (error, response, body) => { - if (error) { - reject(error) - } else if (response.statusCode !== 200){ - reject([response.statusCode, response.statusMessage, response]) - } else { - resolve(response) - } - }) - }) -} - -function getXBLToken(accessToken) { - return new Promise((resolve, reject) => { - const data = new Object() - - const options = { - method: 'post', - json: { - Properties: { - AuthMethod: 'RPS', - SiteName: 'user.auth.xboxlive.com', - RpsTicket: `d=${accessToken}` - }, - RelyingParty: 'http://auth.xboxlive.com', - TokenType: 'JWT' - } - } - requestPromise(authXBLUri, options).then(response => { - const body = response.body - - data.token = body.Token - data.uhs = body.DisplayClaims.xui[0].uhs - - resolve(data) - }).catch(error => { - reject(error) - }) - }) -} - -function getXSTSToken(XBLToken) { - return new Promise((resolve, reject) => { - const options = { - method: 'post', - json: { - Properties: { - SandboxId: 'RETAIL', - UserTokens: [XBLToken] - }, - RelyingParty: 'rp://api.minecraftservices.com/', - TokenType: 'JWT' - } - } - requestPromise(authXSTSUri, options).then(response => { - const body = response.body - - resolve(body.Token) - }).catch(error => { - reject(error) - }) - }) -} - -function getMCAccessToken(UHS, XSTSToken) { - return new Promise((resolve, reject) => { - const data = new Object() - const expiresAt = new Date() - - const options = { - method: 'post', - json: { - identityToken: `XBL3.0 x=${UHS};${XSTSToken}` - } - } - requestPromise(authMCUri, options).then(response => { - const body = response.body - - expiresAt.setSeconds(expiresAt.getSeconds() + body.expires_in) - data.access_token = body.access_token - data.expires_at = expiresAt - - resolve(data) - }).catch(error => { - reject(error) - }) - }) -} - -// Exports -exports.getAccessToken = authCode => { - return new Promise((resolve, reject) => { - const expiresAt = new Date() - const data = new Object() - - const options = { - method: 'post', - formData: { - client_id: clientId, - code: authCode, - scope: 'XboxLive.signin', - redirect_uri: 'https://login.microsoftonline.com/common/oauth2/nativeclient', - grant_type: 'authorization_code' - } - } - requestPromise(tokenUri, options).then(response => { - const body = JSON.parse(response.body) - expiresAt.setSeconds(expiresAt.getSeconds() + body.expires_in) - data.expires_at = expiresAt - data.access_token = body.access_token - data.refresh_token = body.refresh_token - - resolve(data) - }).catch(error => { - reject(error) - }) - }) -} - -exports.refreshAccessToken = refreshToken => { - return new Promise((resolve, reject) => { - const expiresAt = new Date() - const data = new Object() - - const options = { - method: 'post', - formData: { - client_id: clientId, - refresh_token: refreshToken, - scope: 'XboxLive.signin', - redirect_uri: 'https://login.microsoftonline.com/common/oauth2/nativeclient', - grant_type: 'refresh_token' - } - } - requestPromise(tokenUri, options).then(response => { - const body = JSON.parse(response.body) - expiresAt.setSeconds(expiresAt.getSeconds() + body.expires_in) - data.expires_at = expiresAt - data.access_token = body.access_token - - resolve(data) - }).catch(error => { - reject(error) - }) - }) -} - -exports.authMinecraft = async accessToken => { - try { - const XBLToken = await getXBLToken(accessToken) - const XSTSToken = await getXSTSToken(XBLToken.token) - const MCToken = await getMCAccessToken(XBLToken.uhs, XSTSToken) - - return MCToken - } catch (error) { - Promise.reject(error) - } -} - -exports.getMCProfile = MCAccessToken => { - return new Promise((resolve, reject) => { - const options = { - method: 'get', - headers: { - Authorization: `Bearer ${MCAccessToken}` - } - } - requestPromise(profileURI, options).then(response => { - const body = JSON.parse(response.body) - - resolve(body) - }).catch(error => { - reject(error) - }) - }) -} \ No newline at end of file diff --git a/app/assets/js/mojang.js b/app/assets/js/mojang.js deleted file mode 100644 index 8b03964b55..0000000000 --- a/app/assets/js/mojang.js +++ /dev/null @@ -1,299 +0,0 @@ -/** - * Mojang - * - * This module serves as a minimal wrapper for Mojang's REST api. - * - * @module mojang - */ -// Requirements -const request = require('request') -const logger = require('./loggerutil')('%c[Mojang]', 'color: #a02d2a; font-weight: bold') - -// Constants -const minecraftAgent = { - name: 'Minecraft', - version: 1 -} -const authpath = 'https://authserver.mojang.com' -const statuses = [ - { - service: 'mojang-multiplayer-session-service', - status: 'grey', - name: 'Multiplayer Session Service', - essential: true - }, - { - service: 'minecraft-skins', - status: 'grey', - name: 'Authentication Service', - essential: true - }, - { - service: 'textures.minecraft.net', - status: 'grey', - name: 'Minecraft Skins', - essential: false - }, - { - service: 'mojang-s-public-api', - status: 'grey', - name: 'Public API', - essential: false - }, - { - service: 'minecraft-net-website', - status: 'grey', - name: 'Minecraft.net', - essential: false - }, - { - service: 'mojang-accounts-website', - status: 'grey', - name: 'Mojang Accounts Website', - essential: false - }, - { - service: 'microsoft-o-auth-server', - status: 'grey', - name: 'Microsoft OAuth Server', - essential: true - }, - { - service: 'xbox-live-auth-server', - status: 'grey', - name: 'Xbox Live Auth Server', - essential: true - }, - { - service: 'xbox-live-gatekeeper', // Server used to give XTokens - status: 'grey', - name: 'Xbox Live Gatekeeper', - essential: true - }, - { - service: 'microsoft-minecraft-api', - status: 'grey', - name: "Minecraft API for Microsoft Accounts", - essential: true - }, - { - service: 'microsoft-minecraft-profile', - status: "grey", - name: "Minecraft Profile for Microsoft Accounts", - essential: false - } -] - -const requestURL = function (serviceURL) { return `https://raw.githubusercontent.com/GeekCornerGH/helios-status-page/master/api/${serviceURL.service}/uptime.json`} - -// Functions - -/** - * Converts a Mojang status color to a hex value. Valid statuses - * are 'green', 'yellow', 'red', and 'grey'. Grey is a custom status - * to our project which represents an unknown status. - * - * @param {string} status A valid status code. - * @returns {string} The hex color of the status code. - */ -exports.statusToHex = function(status){ - switch(status.toLowerCase()){ - case 'green': - return '#a5c325' - case 'yellow': - return '#eac918' - case 'red': - return '#c32625' - case 'grey': - default: - return '#848484' - } -} - -/** - * Retrieves the status of Mojang's services. - * The response is condensed into a single object. Each service is - * a key, where the value is an object containing a status and name - * property. - * - * @see http://wiki.vg/Mojang_API#API_Status - */ - exports.status = async function () { - return new Promise(async (resolve, reject) => { - let data = [] - for(let i=0; i { - - const body = { - agent, - username, - password, - requestUser - } - if(clientToken != null){ - body.clientToken = clientToken - } - - request.post(authpath + '/authenticate', - { - json: true, - body - }, - function(error, response, body){ - if(error){ - logger.error('Error during authentication.', error) - reject(error) - } else { - if(response.statusCode === 200){ - resolve(body) - } else { - reject(body || {code: 'ENOTFOUND'}) - } - } - }) - setTimeout(resolve, 15000) - }) -} - -/** - * Validate an access token. This should always be done before launching. - * The client token should match the one used to create the access token. - * - * @param {string} accessToken The access token to validate. - * @param {string} clientToken The launcher's client token. - * - * @see http://wiki.vg/Authentication#Validate - */ -exports.validate = function(accessToken, clientToken){ - return new Promise((resolve, reject) => { - request.post(authpath + '/validate', - { - json: true, - body: { - accessToken, - clientToken - } - }, - function(error, response, body){ - if(error){ - logger.error('Error during validation.', error) - reject(error) - } else { - if(response.statusCode === 403){ - resolve(false) - } else { - // 204 if valid - resolve(true) - } - } - }) - }) -} - -/** - * Invalidates an access token. The clientToken must match the - * token used to create the provided accessToken. - * - * @param {string} accessToken The access token to invalidate. - * @param {string} clientToken The launcher's client token. - * - * @see http://wiki.vg/Authentication#Invalidate - */ -exports.invalidate = function(accessToken, clientToken){ - return new Promise((resolve, reject) => { - request.post(authpath + '/invalidate', - { - json: true, - body: { - accessToken, - clientToken - } - }, - function(error, response, body){ - if(error){ - logger.error('Error during invalidation.', error) - reject(error) - } else { - if(response.statusCode === 204){ - resolve() - } else { - reject(body) - } - } - }) - }) -} - -/** - * Refresh a user's authentication. This should be used to keep a user logged - * in without asking them for their credentials again. A new access token will - * be generated using a recent invalid access token. See Wiki for more info. - * - * @param {string} accessToken The old access token. - * @param {string} clientToken The launcher's client token. - * @param {boolean} requestUser Optional. Adds user object to the reponse. - * - * @see http://wiki.vg/Authentication#Refresh - */ -exports.refresh = function(accessToken, clientToken, requestUser = true){ - return new Promise((resolve, reject) => { - request.post(authpath + '/refresh', - { - json: true, - body: { - accessToken, - clientToken, - requestUser - } - }, - function(error, response, body){ - if(error){ - logger.error('Error during refresh.', error) - reject(error) - } else { - if(response.statusCode === 200){ - resolve(body) - } else { - reject(body) - } - } - }) - }) -} \ No newline at end of file diff --git a/app/assets/js/processbuilder.js b/app/assets/js/processbuilder.js index 68e0011bef..46c911f453 100644 --- a/app/assets/js/processbuilder.js +++ b/app/assets/js/processbuilder.js @@ -377,7 +377,7 @@ class ProcessBuilder { // JVM Arguments First let args = this.versionData.arguments.jvm - //args.push('-Dlog4j.configurationFile=D:\\Zayviel\\game\\common\\assets\\log_configs\\client-1.12.xml') + //args.push('-Dlog4j.configurationFile=D:\\WesterosCraft\\game\\common\\assets\\log_configs\\client-1.12.xml') // Java Arguments if(process.platform === 'darwin'){ @@ -468,7 +468,7 @@ class ProcessBuilder { val = this.authUser.accessToken break case 'user_type': - val = 'mojang' + val = this.authUser.type === 'microsoft' ? 'msa' : 'mojang' break case 'version_type': val = this.versionData.type @@ -566,7 +566,7 @@ class ProcessBuilder { val = this.authUser.accessToken break case 'user_type': - val = 'mojang' + val = this.authUser.type === 'microsoft' ? 'msa' : 'mojang' break case 'user_properties': // 1.8.9 and below. val = '{}' diff --git a/app/assets/js/scripts/landing.js b/app/assets/js/scripts/landing.js index 12ead572e2..c994d489d0 100644 --- a/app/assets/js/scripts/landing.js +++ b/app/assets/js/scripts/landing.js @@ -5,13 +5,12 @@ const cp = require('child_process') const crypto = require('crypto') const { URL } = require('url') -const { getServerStatus } = require('helios-core') +const { MojangRestAPI, getServerStatus } = require('helios-core/mojang') // Internal Requirements const DiscordWrapper = require('./assets/js/discordwrapper') -const Mojang = require('./assets/js/mojang') const ProcessBuilder = require('./assets/js/processbuilder') -const ServerStatus = require('./assets/js/serverstatus') +const { RestResponseStatus, isDisplayableError } = require('helios-core/common') // Launch Elements const launch_content = document.getElementById('launch_content') @@ -22,7 +21,7 @@ const launch_details_text = document.getElementById('launch_details_text') const server_selection_button = document.getElementById('server_selection_button') const user_text = document.getElementById('user_text') -const loggerLanding = LoggerUtil('%c[Landing]', 'color: #000668; font-weight: bold') +const loggerLanding = LoggerUtil1('%c[Landing]', 'color: #000668; font-weight: bold') /* Launch Progress Wrapper Functions */ @@ -166,55 +165,57 @@ const refreshMojangStatuses = async function(){ let tooltipEssentialHTML = '' let tooltipNonEssentialHTML = '' - try { - const statuses = await Mojang.status() - greenCount = 0 - greyCount = 0 - - for(let i=0; i - - ${service.name} - ` - } else { - tooltipNonEssentialHTML += `
- - ${service.name} -
` - } - - if(service.status === 'yellow' && status !== 'red'){ - status = 'yellow' - } else if(service.status === 'red'){ - status = 'red' - } else { - if(service.status === 'grey'){ - ++greyCount - } - ++greenCount - } - + const response = await MojangRestAPI.status() + let statuses + if(response.responseStatus === RestResponseStatus.SUCCESS) { + statuses = response.data + } else { + loggerLanding.warn('Unable to refresh Mojang service status.') + statuses = MojangRestAPI.getDefaultStatuses() + } + + greenCount = 0 + greyCount = 0 + + for(let i=0; i + + ${service.name} + ` + } else { + tooltipNonEssentialHTML += `
+ + ${service.name} +
` } - if(greenCount === statuses.length){ - if(greyCount === statuses.length){ - status = 'grey' - } else { - status = 'green' + if(service.status === 'yellow' && status !== 'red'){ + status = 'yellow' + } else if(service.status === 'red'){ + status = 'red' + } else { + if(service.status === 'grey'){ + ++greyCount } + ++greenCount } - } catch (err) { - loggerLanding.warn('Unable to refresh Mojang service status.') - loggerLanding.debug(err) + } + + if(greenCount === statuses.length){ + if(greyCount === statuses.length){ + status = 'grey' + } else { + status = 'green' + } } document.getElementById('mojangStatusEssentialContainer').innerHTML = tooltipEssentialHTML document.getElementById('mojangStatusNonEssentialContainer').innerHTML = tooltipNonEssentialHTML - document.getElementById('mojang_status_icon').style.color = Mojang.statusToHex(status) + document.getElementById('mojang_status_icon').style.color = MojangRestAPI.statusToHex(status) } const refreshServerStatus = async function(fade = false){ @@ -256,8 +257,6 @@ refreshMojangStatuses() let mojangStatusListener = setInterval(() => refreshMojangStatuses(true), 300000) let serverStatusListener = setInterval(() => refreshServerStatus(true), 300000) -setTimeout(() => refreshMojangStatuses(true), 1000) // Workaround to make sure statuses are correctly shown, else its a kinda broken - /** * Shows an error overlay, toggles off the launch area. * @@ -294,7 +293,7 @@ function asyncSystemScan(mcVersion, launchAfter = true){ toggleLaunchArea(true) setLaunchPercentage(0, 100) - const loggerSysAEx = LoggerUtil('%c[SysAEx]', 'color: #353232; font-weight: bold') + const loggerSysAEx = LoggerUtil1('%c[SysAEx]', 'color: #353232; font-weight: bold') const forkEnv = JSON.parse(JSON.stringify(process.env)) forkEnv.CONFIG_DIRECT_PATH = ConfigManager.getLauncherDirectory() @@ -325,13 +324,13 @@ function asyncSystemScan(mcVersion, launchAfter = true){ // If the result is null, no valid Java installation was found. // Show this information to the user. setOverlayContent( - 'Ingen Kompatibel
Java Installation Fundet', - 'For at joine Zayviel, Skal du have en 64-bit installation af Java 8. Vil du have os til at installere en kopi? Ved at installere acceptere du Oracle\'s license agreement.', - 'Installer Java', - 'Installer Manuelt' + 'No Compatible
Java Installation Found', + 'In order to join WesterosCraft, you need a 64-bit installation of Java 8. Would you like us to install a copy?', + 'Install Java', + 'Install Manually' ) setOverlayHandler(() => { - setLaunchDetails('Forbereder Java Download..') + setLaunchDetails('Preparing Java Download..') sysAEx.send({task: 'changeContext', class: 'AssetGuard', args: [ConfigManager.getCommonDirectory(),ConfigManager.getJavaExecutable()]}) sysAEx.send({task: 'execute', function: '_enqueueOpenJDK', argsArr: [ConfigManager.getDataDirectory()]}) toggleOverlay(false) @@ -496,8 +495,8 @@ function dlAsync(login = true){ toggleLaunchArea(true) setLaunchPercentage(0, 100) - const loggerAEx = LoggerUtil('%c[AEx]', 'color: #353232; font-weight: bold') - const loggerLaunchSuite = LoggerUtil('%c[LaunchSuite]', 'color: #000668; font-weight: bold') + const loggerAEx = LoggerUtil1('%c[AEx]', 'color: #353232; font-weight: bold') + const loggerLaunchSuite = LoggerUtil1('%c[LaunchSuite]', 'color: #000668; font-weight: bold') const forkEnv = JSON.parse(JSON.stringify(process.env)) forkEnv.CONFIG_DIRECT_PATH = ConfigManager.getLauncherDirectory() @@ -684,9 +683,9 @@ function dlAsync(login = true){ const gameStateChange = function(data){ data = data.trim() if(SERVER_JOINED_REGEX.test(data)){ - DiscordWrapper.updateDetails('Udforsker Serveren!') + DiscordWrapper.updateDetails('Exploring the Realm!') } else if(GAME_JOINED_REGEX.test(data)){ - DiscordWrapper.updateDetails('Teleportere til Zayviel!') + DiscordWrapper.updateDetails('Sailing to Westeros!') } } @@ -706,7 +705,7 @@ function dlAsync(login = true){ proc.stdout.on('data', tempListener) proc.stderr.on('data', gameErrorListener) - setLaunchDetails('Færdig, Nyd dit ophold på serveren!') + setLaunchDetails('Done. Enjoy the server!') // Init Discord Hook const distro = DistroManager.getDistribution() @@ -714,7 +713,7 @@ function dlAsync(login = true){ DiscordWrapper.initRPC(distro.discord, serv.discord) hasRPC = true proc.on('close', (code, signal) => { - loggerLaunchSuite.log('Lukker ned for Discord Rich Presence..') + loggerLaunchSuite.log('Shutting down Discord Rich Presence..') DiscordWrapper.shutdownRPC() hasRPC = false proc = null @@ -724,7 +723,7 @@ function dlAsync(login = true){ } catch(err) { loggerLaunchSuite.error('Error during launch', err) - showLaunchFailure('Fejl ved opstart', 'Venligst check console (CTRL + Shift + i) for flere oplysninger.') + showLaunchFailure('Error During Launch', 'Please check the console (CTRL + Shift + i) for more details.') } } @@ -763,387 +762,4 @@ function dlAsync(login = true){ } }) }) -} - -/** - * News Loading Functions - */ - -// DOM Cache -const newsContent = document.getElementById('newsContent') -const newsArticleTitle = document.getElementById('newsArticleTitle') -const newsArticleDate = document.getElementById('newsArticleDate') -const newsArticleAuthor = document.getElementById('newsArticleAuthor') -const newsArticleComments = document.getElementById('newsArticleComments') -const newsNavigationStatus = document.getElementById('newsNavigationStatus') -const newsArticleContentScrollable = document.getElementById('newsArticleContentScrollable') -const nELoadSpan = document.getElementById('nELoadSpan') - -// News slide caches. -let newsActive = false -let newsGlideCount = 0 - -/** - * Show the news UI via a slide animation. - * - * @param {boolean} up True to slide up, otherwise false. - */ -function slide_(up){ - const lCUpper = document.querySelector('#landingContainer > #upper') - const lCLLeft = document.querySelector('#landingContainer > #lower > #left') - const lCLCenter = document.querySelector('#landingContainer > #lower > #center') - const lCLRight = document.querySelector('#landingContainer > #lower > #right') - const newsBtn = document.querySelector('#landingContainer > #lower > #center #content') - const landingContainer = document.getElementById('landingContainer') - const newsContainer = document.querySelector('#landingContainer > #newsContainer') - - newsGlideCount++ - - if(up){ - lCUpper.style.top = '-200vh' - lCLLeft.style.top = '-200vh' - lCLCenter.style.top = '-200vh' - lCLRight.style.top = '-200vh' - newsBtn.style.top = '130vh' - newsContainer.style.top = '0px' - //date.toLocaleDateString('en-US', {month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric'}) - //landingContainer.style.background = 'rgba(29, 29, 29, 0.55)' - landingContainer.style.background = 'rgba(0, 0, 0, 0.50)' - setTimeout(() => { - if(newsGlideCount === 1){ - lCLCenter.style.transition = 'none' - newsBtn.style.transition = 'none' - } - newsGlideCount-- - }, 2000) - } else { - setTimeout(() => { - newsGlideCount-- - }, 2000) - landingContainer.style.background = null - lCLCenter.style.transition = null - newsBtn.style.transition = null - newsContainer.style.top = '100%' - lCUpper.style.top = '0px' - lCLLeft.style.top = '0px' - lCLCenter.style.top = '0px' - lCLRight.style.top = '0px' - newsBtn.style.top = '10px' - } -} - -// Bind news button. -document.getElementById('newsButton').onclick = () => { - // Toggle tabbing. - if(newsActive){ - $('#landingContainer *').removeAttr('tabindex') - $('#newsContainer *').attr('tabindex', '-1') - } else { - $('#landingContainer *').attr('tabindex', '-1') - $('#newsContainer, #newsContainer *, #lower, #lower #center *').removeAttr('tabindex') - if(newsAlertShown){ - $('#newsButtonAlert').fadeOut(2000) - newsAlertShown = false - ConfigManager.setNewsCacheDismissed(true) - ConfigManager.save() - } - } - slide_(!newsActive) - newsActive = !newsActive -} - -// Array to store article meta. -let newsArr = null - -// News load animation listener. -let newsLoadingListener = null - -/** - * Set the news loading animation. - * - * @param {boolean} val True to set loading animation, otherwise false. - */ -function setNewsLoading(val){ - if(val){ - const nLStr = 'Checking for News' - let dotStr = '..' - nELoadSpan.innerHTML = nLStr + dotStr - newsLoadingListener = setInterval(() => { - if(dotStr.length >= 3){ - dotStr = '' - } else { - dotStr += '.' - } - nELoadSpan.innerHTML = nLStr + dotStr - }, 750) - } else { - if(newsLoadingListener != null){ - clearInterval(newsLoadingListener) - newsLoadingListener = null - } - } -} - -// Bind retry button. -newsErrorRetry.onclick = () => { - $('#newsErrorFailed').fadeOut(250, () => { - initNews() - $('#newsErrorLoading').fadeIn(250) - }) -} - -newsArticleContentScrollable.onscroll = (e) => { - if(e.target.scrollTop > Number.parseFloat($('.newsArticleSpacerTop').css('height'))){ - newsContent.setAttribute('scrolled', '') - } else { - newsContent.removeAttribute('scrolled') - } -} - -/** - * Reload the news without restarting. - * - * @returns {Promise.} A promise which resolves when the news - * content has finished loading and transitioning. - */ -function reloadNews(){ - return new Promise((resolve, reject) => { - $('#newsContent').fadeOut(250, () => { - $('#newsErrorLoading').fadeIn(250) - initNews().then(() => { - resolve() - }) - }) - }) -} - -let newsAlertShown = false - -/** - * Show the news alert indicating there is new news. - */ -function showNewsAlert(){ - newsAlertShown = true - $(newsButtonAlert).fadeIn(250) -} - -/** - * Initialize News UI. This will load the news and prepare - * the UI accordingly. - * - * @returns {Promise.} A promise which resolves when the news - * content has finished loading and transitioning. - */ -function initNews(){ - - return new Promise((resolve, reject) => { - setNewsLoading(true) - - let news = {} - loadNews().then(news => { - - newsArr = news.articles || null - - if(newsArr == null){ - // News Loading Failed - setNewsLoading(false) - - $('#newsErrorLoading').fadeOut(250, () => { - $('#newsErrorFailed').fadeIn(250, () => { - resolve() - }) - }) - } else if(newsArr.length === 0) { - // No News Articles - setNewsLoading(false) - - ConfigManager.setNewsCache({ - date: null, - content: null, - dismissed: false - }) - ConfigManager.save() - - $('#newsErrorLoading').fadeOut(250, () => { - $('#newsErrorNone').fadeIn(250, () => { - resolve() - }) - }) - } else { - // Success - setNewsLoading(false) - - const lN = newsArr[0] - const cached = ConfigManager.getNewsCache() - let newHash = crypto.createHash('sha1').update(lN.content).digest('hex') - let newDate = new Date(lN.date) - let isNew = false - - if(cached.date != null && cached.content != null){ - - if(new Date(cached.date) >= newDate){ - - // Compare Content - if(cached.content !== newHash){ - isNew = true - showNewsAlert() - } else { - if(!cached.dismissed){ - isNew = true - showNewsAlert() - } - } - - } else { - isNew = true - showNewsAlert() - } - - } else { - isNew = true - showNewsAlert() - } - - if(isNew){ - ConfigManager.setNewsCache({ - date: newDate.getTime(), - content: newHash, - dismissed: false - }) - ConfigManager.save() - } - - const switchHandler = (forward) => { - let cArt = parseInt(newsContent.getAttribute('article')) - let nxtArt = forward ? (cArt >= newsArr.length-1 ? 0 : cArt + 1) : (cArt <= 0 ? newsArr.length-1 : cArt - 1) - - displayArticle(newsArr[nxtArt], nxtArt+1) - } - - document.getElementById('newsNavigateRight').onclick = () => { switchHandler(true) } - document.getElementById('newsNavigateLeft').onclick = () => { switchHandler(false) } - - $('#newsErrorContainer').fadeOut(250, () => { - displayArticle(newsArr[0], 1) - $('#newsContent').fadeIn(250, () => { - resolve() - }) - }) - } - - }) - - }) -} - -/** - * Add keyboard controls to the news UI. Left and right arrows toggle - * between articles. If you are on the landing page, the up arrow will - * open the news UI. - */ -document.addEventListener('keydown', (e) => { - if(newsActive){ - if(e.key === 'ArrowRight' || e.key === 'ArrowLeft'){ - document.getElementById(e.key === 'ArrowRight' ? 'newsNavigateRight' : 'newsNavigateLeft').click() - } - // Interferes with scrolling an article using the down arrow. - // Not sure of a straight forward solution at this point. - // if(e.key === 'ArrowDown'){ - // document.getElementById('newsButton').click() - // } - } else { - if(getCurrentView() === VIEWS.landing){ - if(e.key === 'ArrowUp'){ - document.getElementById('newsButton').click() - } - } - } -}) - -/** - * Display a news article on the UI. - * - * @param {Object} articleObject The article meta object. - * @param {number} index The article index. - */ -function displayArticle(articleObject, index){ - newsArticleTitle.innerHTML = articleObject.title - newsArticleTitle.href = articleObject.link - newsArticleAuthor.innerHTML = 'by ' + articleObject.author - newsArticleDate.innerHTML = articleObject.date - newsArticleComments.innerHTML = articleObject.comments - newsArticleComments.href = articleObject.commentsLink - newsArticleContentScrollable.innerHTML = '
' + articleObject.content + '
' - Array.from(newsArticleContentScrollable.getElementsByClassName('bbCodeSpoilerButton')).forEach(v => { - v.onclick = () => { - const text = v.parentElement.getElementsByClassName('bbCodeSpoilerText')[0] - text.style.display = text.style.display === 'block' ? 'none' : 'block' - } - }) - newsNavigationStatus.innerHTML = index + ' of ' + newsArr.length - newsContent.setAttribute('article', index-1) -} - -/** - * Load news information from the RSS feed specified in the - * distribution index. - */ -function loadNews(){ - return new Promise((resolve, reject) => { - const distroData = DistroManager.getDistribution() - const newsFeed = distroData.getRSS() - const newsHost = new URL(newsFeed).origin + '/' - $.ajax({ - url: newsFeed, - success: (data) => { - const items = $(data).find('item') - const articles = [] - - for(let i=0; i { - resolve({ - articles: null - }) - }) - }) -} +} \ No newline at end of file diff --git a/app/assets/js/scripts/login.js b/app/assets/js/scripts/login.js index a6159b8bfe..724f09c4d7 100644 --- a/app/assets/js/scripts/login.js +++ b/app/assets/js/scripts/login.js @@ -17,12 +17,11 @@ const checkmarkContainer = document.getElementById('checkmarkContainer') const loginRememberOption = document.getElementById('loginRememberOption') const loginButton = document.getElementById('loginButton') const loginForm = document.getElementById('loginForm') -const loginMSButton = document.getElementById('loginMSButton') // Control variables. let lu = false, lp = false -const loggerLogin = LoggerUtil('%c[Login]', 'color: #000668; font-weight: bold') +const loggerLogin = LoggerUtil1('%c[Login]', 'color: #000668; font-weight: bold') /** @@ -155,79 +154,6 @@ function formDisabled(v){ loginRememberOption.disabled = v } -/** - * Parses an error and returns a user-friendly title and description - * for our error overlay. - * - * @param {Error | {cause: string, error: string, errorMessage: string}} err A Node.js - * error or Mojang error response. - */ -function resolveError(err){ - // Mojang Response => err.cause | err.error | err.errorMessage - // Node error => err.code | err.message - if(err.cause != null && err.cause === 'UserMigratedException') { - return { - title: Lang.queryJS('login.error.userMigrated.title'), - desc: Lang.queryJS('login.error.userMigrated.desc') - } - } else { - if(err.error != null){ - if(err.error === 'ForbiddenOperationException'){ - if(err.errorMessage != null){ - if(err.errorMessage === 'Invalid credentials. Invalid username or password.'){ - return { - title: Lang.queryJS('login.error.invalidCredentials.title'), - desc: Lang.queryJS('login.error.invalidCredentials.desc') - } - } else if(err.errorMessage === 'Invalid credentials.'){ - return { - title: Lang.queryJS('login.error.rateLimit.title'), - desc: Lang.queryJS('login.error.rateLimit.desc') - } - } - } - } - } else { - // Request errors (from Node). - if(err.code != null){ - if(err.code === 'ENOENT'){ - // No Internet. - return { - title: Lang.queryJS('login.error.noInternet.title'), - desc: Lang.queryJS('login.error.noInternet.desc') - } - } else if(err.code === 'ENOTFOUND'){ - // Could not reach server. - return { - title: Lang.queryJS('login.error.authDown.title'), - desc: Lang.queryJS('login.error.authDown.desc') - } - } - } - } - } - if(err.message != null){ - if(err.message === 'NotPaidAccount'){ - return { - title: Lang.queryJS('login.error.notPaid.title'), - desc: Lang.queryJS('login.error.notPaid.desc') - } - } else { - // Unknown error with request. - return { - title: Lang.queryJS('login.error.unknown.title'), - desc: err.message - } - } - } else { - // Unknown Mojang error. - return { - title: err.error, - desc: err.errorMessage - } - } -} - let loginViewOnSuccess = VIEWS.landing let loginViewOnCancel = VIEWS.settings let loginViewCancelHandler @@ -263,7 +189,7 @@ loginButton.addEventListener('click', () => { // Show loading stuff. loginLoading(true) - AuthManager.addAccount(loginUsername.value, loginPassword.value).then((value) => { + AuthManager.addMojangAccount(loginUsername.value, loginPassword.value).then((value) => { updateSelectedAccount(value) loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.loggingIn'), Lang.queryJS('login.success')) $('.circle-loader').toggleClass('load-complete') @@ -286,84 +212,28 @@ loginButton.addEventListener('click', () => { formDisabled(false) }) }, 1000) - }).catch((err) => { + }).catch((displayableError) => { loginLoading(false) - const errF = resolveError(err) - setOverlayContent(errF.title, errF.desc, Lang.queryJS('login.tryAgain')) - setOverlayHandler(() => { - formDisabled(false) - toggleOverlay(false) - }) - toggleOverlay(true) - loggerLogin.log('Error while logging in.', err) - }) - -}) - -loginMSButton.addEventListener('click', (event) => { - ipcRenderer.send('openMSALoginWindow', 'open') -}) - -ipcRenderer.on('MSALoginWindowReply', (event, ...args) => { - if (args[0] === 'error') { - setOverlayContent('LOGIN FAIL', 'Theres a window already open!', 'OK') - setOverlayHandler(() => { - toggleOverlay(false) - }) - toggleOverlay(true) - return - } - const queryMap = args[0] - if(queryMap.has('error')) { - let error = queryMap.get('error') - let errorDesc = queryMap.get('error_description') - setOverlayContent(error, errorDesc, 'OK') - setOverlayHandler(() => { - toggleOverlay(false) - }) - toggleOverlay(true) - return - } - - // Disable form. - formDisabled(true) - - // Show loading stuff. - loginLoading(true) + let actualDisplayableError + if(isDisplayableError(displayableError)) { + msftLoginLogger.error('Error while logging in.', displayableError) + actualDisplayableError = displayableError + } else { + // Uh oh. + msftLoginLogger.error('Unhandled error during login.', displayableError) + actualDisplayableError = { + title: 'Unknown Error During Login', + desc: 'An unknown error has occurred. Please see the console for details.' + } + } - const authCode = queryMap.get('code') - AuthManager.addMSAccount(authCode).then(account => { - updateSelectedAccount(account) - loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.loggingIn'), Lang.queryJS('login.success')) - $('.circle-loader').toggleClass('load-complete') - $('.checkmark').toggle() - setTimeout(() => { - switchView(VIEWS.login, loginViewOnSuccess, 500, 500, () => { - // Temporary workaround - if(loginViewOnSuccess === VIEWS.settings){ - prepareSettings() - } - loginViewOnSuccess = VIEWS.landing // Reset this for good measure. - loginCancelEnabled(false) // Reset this for good measure. - loginViewCancelHandler = null // Reset this for good measure. - loginUsername.value = '' - loginPassword.value = '' - $('.circle-loader').toggleClass('load-complete') - $('.checkmark').toggle() - loginLoading(false) - loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.success'), Lang.queryJS('login.login')) - formDisabled(false) - }) - }, 1000) - }).catch(error => { - loginLoading(false) - setOverlayContent('ERROR!', 'Report Plz!', Lang.queryJS('login.tryAgain')) + setOverlayContent(actualDisplayableError.title, actualDisplayableError.desc, Lang.queryJS('login.tryAgain')) setOverlayHandler(() => { formDisabled(false) toggleOverlay(false) }) toggleOverlay(true) - loggerLogin.error(error) }) + }) \ No newline at end of file diff --git a/app/assets/js/scripts/loginOptions.js b/app/assets/js/scripts/loginOptions.js new file mode 100644 index 0000000000..cdb1bc8e34 --- /dev/null +++ b/app/assets/js/scripts/loginOptions.js @@ -0,0 +1,50 @@ +const loginOptionsCancelContainer = document.getElementById('loginOptionCancelContainer') +const loginOptionMicrosoft = document.getElementById('loginOptionMicrosoft') +const loginOptionMojang = document.getElementById('loginOptionMojang') +const loginOptionsCancelButton = document.getElementById('loginOptionCancelButton') + +let loginOptionsCancellable = false + +let loginOptionsViewOnLoginSuccess +let loginOptionsViewOnLoginCancel +let loginOptionsViewOnCancel +let loginOptionsViewCancelHandler + +function loginOptionsCancelEnabled(val){ + if(val){ + $(loginOptionsCancelContainer).show() + } else { + $(loginOptionsCancelContainer).hide() + } +} + +loginOptionMicrosoft.onclick = (e) => { + switchView(getCurrentView(), VIEWS.waiting, 500, 500, () => { + ipcRenderer.send( + MSFT_OPCODE.OPEN_LOGIN, + loginOptionsViewOnLoginSuccess, + loginOptionsViewOnLoginCancel + ) + }) +} + +loginOptionMojang.onclick = (e) => { + switchView(getCurrentView(), VIEWS.login, 500, 500, () => { + loginViewOnSuccess = loginOptionsViewOnLoginSuccess + loginViewOnCancel = loginOptionsViewOnLoginCancel + loginCancelEnabled(true) + }) +} + +loginOptionsCancelButton.onclick = (e) => { + switchView(getCurrentView(), loginOptionsViewOnCancel, 500, 500, () => { + // Clear login values (Mojang login) + // No cleanup needed for Microsoft. + loginUsername.value = '' + loginPassword.value = '' + if(loginOptionsViewCancelHandler != null){ + loginOptionsViewCancelHandler() + loginOptionsViewCancelHandler = null + } + }) +} \ No newline at end of file diff --git a/app/assets/js/scripts/overlay.js b/app/assets/js/scripts/overlay.js index 22d81d62c4..cf2c5c9871 100644 --- a/app/assets/js/scripts/overlay.js +++ b/app/assets/js/scripts/overlay.js @@ -197,6 +197,9 @@ document.getElementById('accountSelectConfirm').addEventListener('click', () => const authAcc = ConfigManager.setSelectedAccount(listings[i].getAttribute('uuid')) ConfigManager.save() updateSelectedAccount(authAcc) + if(getCurrentView() === VIEWS.settings) { + prepareSettings() + } toggleOverlay(false) validateSelectedAccount() return @@ -207,6 +210,9 @@ document.getElementById('accountSelectConfirm').addEventListener('click', () => const authAcc = ConfigManager.setSelectedAccount(listings[0].getAttribute('uuid')) ConfigManager.save() updateSelectedAccount(authAcc) + if(getCurrentView() === VIEWS.settings) { + prepareSettings() + } toggleOverlay(false) validateSelectedAccount() } diff --git a/app/assets/js/scripts/settings.js b/app/assets/js/scripts/settings.js index ef912fb0e5..429ae01e3e 100644 --- a/app/assets/js/scripts/settings.js +++ b/app/assets/js/scripts/settings.js @@ -4,6 +4,7 @@ const semver = require('semver') const { JavaGuard } = require('./assets/js/assetguard') const DropinModUtil = require('./assets/js/dropinmodutil') +const { MSFT_OPCODE, MSFT_REPLY_TYPE, MSFT_ERROR } = require('./assets/js/ipcconstants') const settingsState = { invalid: new Set() @@ -179,7 +180,11 @@ function saveSettingsValues(){ if(v.type === 'number' || v.type === 'text'){ // Special Conditions if(cVal === 'JVMOptions'){ - sFn(v.value.split(' ')) + if(!v.value.trim()) { + sFn([]) + } else { + sFn(v.value.trim().split(/\s+/)) + } } else { sFn(v.value) } @@ -314,8 +319,11 @@ settingsNavDone.onclick = () => { * Account Management Tab */ -// Bind the add account button. -document.getElementById('settingsAddAccount').onclick = (e) => { +const msftLoginLogger = LoggerUtil.getLogger('Microsoft Login') +const msftLogoutLogger = LoggerUtil.getLogger('Microsoft Logout') + +// Bind the add mojang account button. +document.getElementById('settingsAddMojangAccount').onclick = (e) => { switchView(getCurrentView(), VIEWS.login, 500, 500, () => { loginViewOnCancel = VIEWS.settings loginViewOnSuccess = VIEWS.settings @@ -323,6 +331,102 @@ document.getElementById('settingsAddAccount').onclick = (e) => { }) } +// Bind the add microsoft account button. +document.getElementById('settingsAddMicrosoftAccount').onclick = (e) => { + switchView(getCurrentView(), VIEWS.waiting, 500, 500, () => { + ipcRenderer.send(MSFT_OPCODE.OPEN_LOGIN, VIEWS.settings, VIEWS.settings) + }) +} + +// Bind reply for Microsoft Login. +ipcRenderer.on(MSFT_OPCODE.REPLY_LOGIN, (_, ...arguments_) => { + if (arguments_[0] === MSFT_REPLY_TYPE.ERROR) { + + const viewOnClose = arguments_[2] + console.log(arguments_) + switchView(getCurrentView(), viewOnClose, 500, 500, () => { + + if(arguments_[1] === MSFT_ERROR.NOT_FINISHED) { + // User cancelled. + msftLoginLogger.info('Login cancelled by user.') + return + } + + // Unexpected error. + setOverlayContent( + 'Something Went Wrong', + 'Microsoft authentication failed. Please try again.', + 'OK' + ) + setOverlayHandler(() => { + toggleOverlay(false) + }) + toggleOverlay(true) + }) + } else if(arguments_[0] === MSFT_REPLY_TYPE.SUCCESS) { + const queryMap = arguments_[1] + const viewOnClose = arguments_[2] + + // Error from request to Microsoft. + if (Object.prototype.hasOwnProperty.call(queryMap, 'error')) { + switchView(getCurrentView(), viewOnClose, 500, 500, () => { + // TODO Dont know what these errors are. Just show them I guess. + // This is probably if you messed up the app registration with Azure. + console.log('Error getting authCode, is Azure application registered correctly?') + console.log(error) + console.log(error_description) + console.log('Full query map', queryMap) + let error = queryMap.error // Error might be 'access_denied' ? + let errorDesc = queryMap.error_description + setOverlayContent( + error, + errorDesc, + 'OK' + ) + setOverlayHandler(() => { + toggleOverlay(false) + }) + toggleOverlay(true) + + }) + } else { + + msftLoginLogger.info('Acquired authCode, proceeding with authentication.') + + const authCode = queryMap.code + AuthManager.addMicrosoftAccount(authCode).then(value => { + updateSelectedAccount(value) + switchView(getCurrentView(), viewOnClose, 500, 500, () => { + prepareSettings() + }) + }) + .catch((displayableError) => { + + let actualDisplayableError + if(isDisplayableError(displayableError)) { + msftLoginLogger.error('Error while logging in.', displayableError) + actualDisplayableError = displayableError + } else { + // Uh oh. + msftLoginLogger.error('Unhandled error during login.', displayableError) + actualDisplayableError = { + title: 'Unknown Error During Login', + desc: 'An unknown error has occurred. Please see the console for details.' + } + } + + switchView(getCurrentView(), viewOnClose, 500, 500, () => { + setOverlayContent(actualDisplayableError.title, actualDisplayableError.desc, Lang.queryJS('login.tryAgain')) + setOverlayHandler(() => { + toggleOverlay(false) + }) + toggleOverlay(true) + }) + }) + } + } +}) + /** * Bind functionality for the account selection buttons. If another account * is selected, the UI of the previously selected account will be updated. @@ -341,7 +445,7 @@ function bindAuthAccountSelect(){ } } val.setAttribute('selected', '') - val.innerHTML = 'Selected Account ✔' + val.innerHTML = 'Valte Konto ✔' setSelectedAccount(val.closest('.settingsAuthAccount').getAttribute('uuid')) } }) @@ -367,7 +471,6 @@ function bindAuthAccountLogOut(){ setOverlayHandler(() => { processLogOut(val, isLastAccount) toggleOverlay(false) - switchView(getCurrentView(), VIEWS.login) }) setDismissHandler(() => { toggleOverlay(false) @@ -381,6 +484,7 @@ function bindAuthAccountLogOut(){ }) } +let msAccDomElementCache /** * Process a log out. * @@ -391,19 +495,91 @@ function processLogOut(val, isLastAccount){ const parent = val.closest('.settingsAuthAccount') const uuid = parent.getAttribute('uuid') const prevSelAcc = ConfigManager.getSelectedAccount() - AuthManager.removeAccount(uuid).then(() => { - if(!isLastAccount && uuid === prevSelAcc.uuid){ - const selAcc = ConfigManager.getSelectedAccount() - refreshAuthAccountSelected(selAcc.uuid) - updateSelectedAccount(selAcc) - validateSelectedAccount() - } - }) - $(parent).fadeOut(250, () => { - parent.remove() - }) + const targetAcc = ConfigManager.getAuthAccount(uuid) + if(targetAcc.type === 'microsoft') { + msAccDomElementCache = parent + switchView(getCurrentView(), VIEWS.waiting, 500, 500, () => { + ipcRenderer.send(MSFT_OPCODE.OPEN_LOGOUT, uuid, isLastAccount) + }) + } else { + AuthManager.removeMojangAccount(uuid).then(() => { + if(!isLastAccount && uuid === prevSelAcc.uuid){ + const selAcc = ConfigManager.getSelectedAccount() + refreshAuthAccountSelected(selAcc.uuid) + updateSelectedAccount(selAcc) + validateSelectedAccount() + } + if(isLastAccount) { + loginOptionsCancelEnabled(false) + loginOptionsViewOnLoginSuccess = VIEWS.settings + loginOptionsViewOnLoginCancel = VIEWS.loginOptions + switchView(getCurrentView(), VIEWS.loginOptions) + } + }) + $(parent).fadeOut(250, () => { + parent.remove() + }) + } } +// Bind reply for Microsoft Logout. +ipcRenderer.on(MSFT_OPCODE.REPLY_LOGOUT, (_, ...arguments_) => { + if (arguments_[0] === MSFT_REPLY_TYPE.ERROR) { + switchView(getCurrentView(), VIEWS.settings, 500, 500, () => { + + if(arguments_.length > 1 && arguments_[1] === MSFT_ERROR.NOT_FINISHED) { + // User cancelled. + msftLogoutLogger.info('Logout cancelled by user.') + return + } + + // Unexpected error. + setOverlayContent( + 'Something Went Wrong', + 'Microsoft logout failed. Please try again.', + 'OK' + ) + setOverlayHandler(() => { + toggleOverlay(false) + }) + toggleOverlay(true) + }) + } else if(arguments_[0] === MSFT_REPLY_TYPE.SUCCESS) { + + const uuid = arguments_[1] + const isLastAccount = arguments_[2] + const prevSelAcc = ConfigManager.getSelectedAccount() + + msftLogoutLogger.info('Logout Successful. uuid:', uuid) + + AuthManager.removeMicrosoftAccount(uuid) + .then(() => { + if(!isLastAccount && uuid === prevSelAcc.uuid){ + const selAcc = ConfigManager.getSelectedAccount() + refreshAuthAccountSelected(selAcc.uuid) + updateSelectedAccount(selAcc) + validateSelectedAccount() + } + if(isLastAccount) { + loginOptionsCancelEnabled(false) + loginOptionsViewOnLoginSuccess = VIEWS.settings + loginOptionsViewOnLoginCancel = VIEWS.loginOptions + switchView(getCurrentView(), VIEWS.loginOptions) + } + if(msAccDomElementCache) { + msAccDomElementCache.remove() + msAccDomElementCache = null + } + }) + .finally(() => { + if(!isLastAccount) { + switchView(getCurrentView(), VIEWS.settings, 500, 500) + } + }) + + } +}) + /** * Refreshes the status of the selected account on the auth account * elements. @@ -415,7 +591,7 @@ function refreshAuthAccountSelected(uuid){ const selBtn = val.getElementsByClassName('settingsAuthAccountSelect')[0] if(uuid === val.getAttribute('uuid')){ selBtn.setAttribute('selected', '') - selBtn.innerHTML = 'Selected Account ✔' + selBtn.innerHTML = 'Valgte Konto ✔' } else { if(selBtn.hasAttribute('selected')){ selBtn.removeAttribute('selected') @@ -425,7 +601,8 @@ function refreshAuthAccountSelected(uuid){ }) } -const settingsCurrentAccounts = document.getElementById('settingsCurrentAccounts') +const settingsCurrentMicrosoftAccounts = document.getElementById('settingsCurrentMicrosoftAccounts') +const settingsCurrentMojangAccounts = document.getElementById('settingsCurrentMojangAccounts') /** * Add auth account elements for each one stored in the authentication database. @@ -438,18 +615,20 @@ function populateAuthAccounts(){ } const selectedUUID = ConfigManager.getSelectedAccount().uuid - let authAccountStr = '' + let microsoftAuthAccountStr = '' + let mojangAuthAccountStr = '' - authKeys.map((val) => { + authKeys.forEach((val) => { const acc = authAccounts[val] - authAccountStr += `
+ + const accHtml = `
${acc.displayName}
-
Username
+
Brugernavn
${acc.displayName}
@@ -458,16 +637,24 @@ function populateAuthAccounts(){
- +
- +
` + + if(acc.type === 'microsoft') { + microsoftAuthAccountStr += accHtml + } else { + mojangAuthAccountStr += accHtml + } + }) - settingsCurrentAccounts.innerHTML = authAccountStr + settingsCurrentMicrosoftAccounts.innerHTML = microsoftAuthAccountStr + settingsCurrentMojangAccounts.innerHTML = mojangAuthAccountStr } /** @@ -1201,7 +1388,7 @@ function populateVersionInformation(version, valueElement, titleElement, checkEl titleElement.style.color = '#ff886d' checkElement.style.background = '#ff886d' } else { - titleElement.innerHTML = 'Stable Release' + titleElement.innerHTML = 'Stabil Udgivelse' titleElement.style.color = null checkElement.style.background = null } @@ -1288,7 +1475,7 @@ function settingsUpdateButtonStatus(text, disabled = false, handler = null){ */ function populateSettingsUpdateInformation(data){ if(data != null){ - settingsUpdateTitle.innerHTML = `New ${isPrerelease(data.version) ? 'Pre-release' : 'Release'} Available` + settingsUpdateTitle.innerHTML = `New ${isPrerelease(data.version) ? 'Pre-release' : 'Release'} Tilgængelig` settingsUpdateChangelogCont.style.display = null settingsUpdateChangelogTitle.innerHTML = data.releaseName settingsUpdateChangelogText.innerHTML = data.releaseNotes @@ -1302,13 +1489,13 @@ function populateSettingsUpdateInformation(data){ settingsUpdateButtonStatus('Downloading..', true) } } else { - settingsUpdateTitle.innerHTML = 'You Are Running the Latest Version' + settingsUpdateTitle.innerHTML = 'Du Køre Den Nyeste Version' settingsUpdateChangelogCont.style.display = 'none' populateVersionInformation(remote.app.getVersion(), settingsUpdateVersionValue, settingsUpdateVersionTitle, settingsUpdateVersionCheck) - settingsUpdateButtonStatus('Check for Updates', false, () => { + settingsUpdateButtonStatus('Søg Efter Opdateringer', false, () => { if(!isDev){ ipcRenderer.send('autoUpdateAction', 'checkForUpdate') - settingsUpdateButtonStatus('Checking for Updates..', true) + settingsUpdateButtonStatus('Søger Efter Opdateringer..', true) } }) } diff --git a/app/assets/js/scripts/uibinder.js b/app/assets/js/scripts/uibinder.js index 55e47ce198..427b7fe275 100644 --- a/app/assets/js/scripts/uibinder.js +++ b/app/assets/js/scripts/uibinder.js @@ -16,9 +16,11 @@ let fatalStartupError = false // Mapping of each view to their container IDs. const VIEWS = { landing: '#landingContainer', + loginOptions: '#loginOptionsContainer', login: '#loginContainer', settings: '#settingsContainer', - welcome: '#welcomeContainer' + welcome: '#welcomeContainer', + waiting: '#waitingContainer' } // The currently shown view container. @@ -55,53 +57,52 @@ function getCurrentView(){ return currentView } -async function showMainUI(data){ - await require("./assets/js/mojang").status +function showMainUI(data){ + if(!isDev){ - await loggerAutoUpdater.log('Initializing..') - await ipcRenderer.send('autoUpdateAction', 'initAutoUpdater', ConfigManager.getAllowPrerelease()) + loggerAutoUpdater.log('Initializing..') + ipcRenderer.send('autoUpdateAction', 'initAutoUpdater', ConfigManager.getAllowPrerelease()) } - await prepareSettings(true) - await updateSelectedServer(data.getServer(ConfigManager.getSelectedServer())) - await refreshServerStatus() - setTimeout(async () => { + prepareSettings(true) + updateSelectedServer(data.getServer(ConfigManager.getSelectedServer())) + refreshServerStatus() + setTimeout(() => { document.getElementById('frameBar').style.backgroundColor = 'rgba(0, 0, 0, 0.5)' document.body.style.backgroundImage = `url('assets/images/backgrounds/${document.body.getAttribute('bkid')}.jpg')` - await $('#main').show() + $('#main').show() const isLoggedIn = Object.keys(ConfigManager.getAuthAccounts()).length > 0 // If this is enabled in a development environment we'll get ratelimited. // The relaunch frequency is usually far too high. if(!isDev && isLoggedIn){ - await validateSelectedAccount() + validateSelectedAccount() } if(ConfigManager.isFirstLaunch()){ currentView = VIEWS.welcome - await $(VIEWS.welcome).fadeIn(1000) + $(VIEWS.welcome).fadeIn(1000) } else { if(isLoggedIn){ currentView = VIEWS.landing - await $(VIEWS.landing).fadeIn(1000) + $(VIEWS.landing).fadeIn(1000) } else { - currentView = VIEWS.login - await $(VIEWS.login).fadeIn(1000) + loginOptionsCancelEnabled(false) + loginOptionsViewOnLoginSuccess = VIEWS.landing + loginOptionsViewOnLoginCancel = VIEWS.loginOptions + currentView = VIEWS.loginOptions + $(VIEWS.loginOptions).fadeIn(1000) } } - setTimeout(async () => { - await $('#loadingContainer').fadeOut(500, async () => { - await $('#loadSpinnerImage').removeClass('rotating') + setTimeout(() => { + $('#loadingContainer').fadeOut(500, () => { + $('#loadSpinnerImage').removeClass('rotating') }) }, 250) }, 750) - // Disable tabbing to the news container. - initNews().then(() => { - $('#newsContainer *').attr('tabindex', '-1') - }) } function showFatalStartupError(){ @@ -130,7 +131,6 @@ function showFatalStartupError(){ function onDistroRefresh(data){ updateSelectedServer(data.getServer(ConfigManager.getSelectedServer())) refreshServerStatus() - initNews() syncModConfigurations(data) } @@ -329,20 +329,46 @@ async function validateSelectedAccount(){ 'Select Another Account' ) setOverlayHandler(() => { - document.getElementById('loginUsername').value = selectedAcc.username - validateEmail(selectedAcc.username) - loginViewOnSuccess = getCurrentView() - loginViewOnCancel = getCurrentView() - if(accLen > 0){ - loginViewCancelHandler = () => { - ConfigManager.addAuthAccount(selectedAcc.uuid, selectedAcc.accessToken, selectedAcc.username, selectedAcc.displayName) + + const isMicrosoft = selectedAcc.type === 'microsoft' + + if(isMicrosoft) { + // Empty for now + } else { + // Mojang + // For convenience, pre-populate the username of the account. + document.getElementById('loginUsername').value = selectedAcc.username + validateEmail(selectedAcc.username) + } + + loginOptionsViewOnLoginSuccess = getCurrentView() + loginOptionsViewOnLoginCancel = VIEWS.loginOptions + + if(accLen > 0) { + loginOptionsViewOnCancel = getCurrentView() + loginOptionsViewCancelHandler = () => { + if(isMicrosoft) { + ConfigManager.addMicrosoftAuthAccount( + selectedAcc.uuid, + selectedAcc.accessToken, + selectedAcc.username, + selectedAcc.expiresAt, + selectedAcc.microsoft.access_token, + selectedAcc.microsoft.refresh_token, + selectedAcc.microsoft.expires_at + ) + } else { + ConfigManager.addMojangAuthAccount(selectedAcc.uuid, selectedAcc.accessToken, selectedAcc.username, selectedAcc.displayName) + } ConfigManager.save() validateSelectedAccount() } - loginCancelEnabled(true) + loginOptionsCancelEnabled(true) + } else { + loginOptionsCancelEnabled(false) } toggleOverlay(false) - switchView(getCurrentView(), VIEWS.login) + switchView(getCurrentView(), VIEWS.loginOptions) }) setDismissHandler(() => { if(accLen > 1){ diff --git a/app/assets/js/scripts/uicore.js b/app/assets/js/scripts/uicore.js index b71d9aa62d..456ec04359 100644 --- a/app/assets/js/scripts/uicore.js +++ b/app/assets/js/scripts/uicore.js @@ -9,11 +9,12 @@ const $ = require('jquery') const {ipcRenderer, shell, webFrame} = require('electron') const remote = require('@electron/remote') const isDev = require('./assets/js/isdev') -const LoggerUtil = require('./assets/js/loggerutil') +const { LoggerUtil } = require('helios-core') +const LoggerUtil1 = require('./assets/js/loggerutil') -const loggerUICore = LoggerUtil('%c[UICore]', 'color: #000668; font-weight: bold') -const loggerAutoUpdater = LoggerUtil('%c[AutoUpdater]', 'color: #000668; font-weight: bold') -const loggerAutoUpdaterSuccess = LoggerUtil('%c[AutoUpdater]', 'color: #209b07; font-weight: bold') +const loggerUICore = LoggerUtil1('%c[UICore]', 'color: #000668; font-weight: bold') +const loggerAutoUpdater = LoggerUtil1('%c[AutoUpdater]', 'color: #000668; font-weight: bold') +const loggerAutoUpdaterSuccess = LoggerUtil1('%c[AutoUpdater]', 'color: #209b07; font-weight: bold') // Log deprecation and process warnings. process.traceProcessWarnings = true @@ -49,7 +50,7 @@ if(!isDev){ loggerAutoUpdaterSuccess.log('New update available', info.version) if(process.platform === 'darwin'){ - info.darwindownload = `https://github.com/XXdaugonXX/ZayvielLauncher/releases/download/v${info.version}/zayviellauncher-setup-${info.version}${process.arch === 'arm64' ? '-arm64' : ''}.dmg` + info.darwindownload = `https://github.com/dscalzi/ZayvielLauncher/releases/download/v${info.version}/Zayviel-Launcher-setup-${info.version}${process.arch === 'arm64' ? '-arm64' : '-x64'}.dmg` showUpdateUI(info) } @@ -66,7 +67,7 @@ if(!isDev){ break case 'update-not-available': loggerAutoUpdater.log('No new update found.') - settingsUpdateButtonStatus('Check for Updates') + settingsUpdateButtonStatus('Søg Efter Opdateringer') break case 'ready': updateCheckListener = setInterval(() => { diff --git a/app/assets/js/scripts/welcome.js b/app/assets/js/scripts/welcome.js index e6ff6297fc..ed0399c353 100644 --- a/app/assets/js/scripts/welcome.js +++ b/app/assets/js/scripts/welcome.js @@ -2,5 +2,8 @@ * Script for welcome.ejs */ document.getElementById('welcomeButton').addEventListener('click', e => { - switchView(VIEWS.welcome, VIEWS.login) + loginOptionsCancelEnabled(false) // False by default, be explicit. + loginOptionsViewOnLoginSuccess = VIEWS.landing + loginOptionsViewOnLoginCancel = VIEWS.loginOptions + switchView(VIEWS.welcome, VIEWS.loginOptions) }) \ No newline at end of file diff --git a/app/assets/lang/en_US.json b/app/assets/lang/en_US.json index 5b62fba817..25b34c240e 100644 --- a/app/assets/lang/en_US.json +++ b/app/assets/lang/en_US.json @@ -1,6 +1,6 @@ { "html": { - "avatarOverlay": "Ændre" + "avatarOverlay": "Edit" }, "js": { "login": { diff --git a/app/landing.ejs b/app/landing.ejs index aa26acef11..1323431164 100644 --- a/app/landing.ejs +++ b/app/landing.ejs @@ -3,7 +3,7 @@
-
Opdatering Tilgængelig
+
Update Available
@@ -11,9 +11,9 @@ -
- -
-
- Leder Efter Nyhedder.. -
- - -
-
\ No newline at end of file diff --git a/app/login.ejs b/app/login.ejs index e167ccfef3..805b44c8ea 100644 --- a/app/login.ejs +++ b/app/login.ejs @@ -52,7 +52,6 @@
-
Need an Account? diff --git a/app/loginOptions.ejs b/app/loginOptions.ejs new file mode 100644 index 0000000000..36af37e05d --- /dev/null +++ b/app/loginOptions.ejs @@ -0,0 +1,34 @@ + \ No newline at end of file diff --git a/app/overlay.ejs b/app/overlay.ejs index 174b433c7f..0c18aef46f 100644 --- a/app/overlay.ejs +++ b/app/overlay.ejs @@ -14,7 +14,7 @@