Merge branch 'dscalzi:master' into master
This commit is contained in:
commit
6d5e7aa7ac
24
.github/workflows/build.yml
vendored
24
.github/workflows/build.yml
vendored
@ -1,4 +1,4 @@
|
|||||||
name: Build/release
|
name: Build
|
||||||
|
|
||||||
on: push
|
on: push
|
||||||
|
|
||||||
@ -14,16 +14,22 @@ jobs:
|
|||||||
- name: Check out Git repository
|
- name: Check out Git repository
|
||||||
uses: actions/checkout@v1
|
uses: actions/checkout@v1
|
||||||
|
|
||||||
- name: Install Node.js, NPM and Yarn
|
- name: Set up Node
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
node-version: 16
|
||||||
|
|
||||||
- name: Build/release Electron app
|
- name: Set up Python
|
||||||
uses: samuelmeuli/action-electron-builder@v1
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.github_token }}
|
python-version: 3.x
|
||||||
|
|
||||||
# If the commit is tagged with a version (e.g. "v1.0.0"),
|
- name: Install Dependencies
|
||||||
# release the app after building
|
run: npm ci
|
||||||
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
shell: bash
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.github_token }}
|
||||||
|
run: npm run dist
|
||||||
|
shell: bash
|
@ -1,6 +1,6 @@
|
|||||||
MIT License
|
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
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
11
README.md
11
README.md
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<em><h5 align="center">(formerly Electron Launcher)</h5></em>
|
<em><h5 align="center">(formerly Electron Launcher)</h5></em>
|
||||||
|
|
||||||
[<p align="center"><img src="https://img.shields.io/travis/dscalzi/HeliosLauncher.svg?style=for-the-badge" alt="travis">](https://travis-ci.org/dscalzi/HeliosLauncher) [<img src="https://img.shields.io/github/downloads/dscalzi/HeliosLauncher/total.svg?style=for-the-badge" alt="downloads">](https://github.com/dscalzi/HeliosLauncher/releases) <img src="https://forthebadge.com/images/badges/winter-is-coming.svg" height="28px" alt="stark"></p>
|
[<p align="center"><img src="https://img.shields.io/github/workflow/status/dscalzi/HeliosLauncher/Build.svg?style=for-the-badge" alt="gh actions">](https://github.com/dscalzi/HeliosLauncher/actions) [<img src="https://img.shields.io/github/downloads/dscalzi/HeliosLauncher/total.svg?style=for-the-badge" alt="downloads">](https://github.com/dscalzi/HeliosLauncher/releases) <img src="https://forthebadge.com/images/badges/winter-is-coming.svg" height="28px" alt="winter-is-coming"></p>
|
||||||
|
|
||||||
<p align="center">Join modded servers without worrying about installing Java, Forge, or other mods. We'll handle that for you.</p>
|
<p align="center">Join modded servers without worrying about installing Java, Forge, or other mods. We'll handle that for you.</p>
|
||||||
|
|
||||||
@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
* 🔒 Full account management.
|
* 🔒 Full account management.
|
||||||
* Add multiple accounts and easily switch between them.
|
* 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.
|
* Credentials are never stored and transmitted directly to Mojang.
|
||||||
* 📂 Efficient asset management.
|
* 📂 Efficient asset management.
|
||||||
* Receive client updates as soon as we release them.
|
* Receive client updates as soon as we release them.
|
||||||
@ -54,7 +55,7 @@ If you download from the [Releases](https://github.com/dscalzi/HeliosLauncher/re
|
|||||||
| Platform | File |
|
| Platform | File |
|
||||||
| -------- | ---- |
|
| -------- | ---- |
|
||||||
| Windows x64 | `Helios-Launcher-setup-VERSION.exe` |
|
| Windows x64 | `Helios-Launcher-setup-VERSION.exe` |
|
||||||
| macOS x64 | `Helios-Launcher-setup-VERSION.dmg` |
|
| macOS x64 | `Helios-Launcher-setup-VERSION-x64.dmg` |
|
||||||
| macOS arm64 | `Helios-Launcher-setup-VERSION-arm64.dmg` |
|
| macOS arm64 | `Helios-Launcher-setup-VERSION-arm64.dmg` |
|
||||||
| Linux x64 | `Helios-Launcher-setup-VERSION.AppImage` |
|
| Linux x64 | `Helios-Launcher-setup-VERSION.AppImage` |
|
||||||
|
|
||||||
@ -83,7 +84,7 @@ This section details the setup of a basic developmentment environment.
|
|||||||
|
|
||||||
**System Requirements**
|
**System Requirements**
|
||||||
|
|
||||||
* [Node.js][nodejs] v14
|
* [Node.js][nodejs] v16
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -180,13 +181,15 @@ Note that you **cannot** open the DevTools window while using this debug configu
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
For instructions on setting up Microsoft Authentication, see https://github.com/dscalzi/HeliosLauncher/blob/master/docs/MicrosoftAuth.md.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
* [Wiki][wiki]
|
* [Wiki][wiki]
|
||||||
* [Nebula (Create Distribution.json)][nebula]
|
* [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.
|
The best way to contact the developers is on Discord.
|
||||||
|
|
||||||
|
@ -31,6 +31,8 @@
|
|||||||
<div id="main">
|
<div id="main">
|
||||||
<%- include('welcome') %>
|
<%- include('welcome') %>
|
||||||
<%- include('login') %>
|
<%- include('login') %>
|
||||||
|
<%- include('waiting') %>
|
||||||
|
<%- include('loginOptions') %>
|
||||||
<%- include('settings') %>
|
<%- include('settings') %>
|
||||||
<%- include('landing') %>
|
<%- include('landing') %>
|
||||||
</div>
|
</div>
|
||||||
|
@ -222,6 +222,7 @@ body, button {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.50);
|
||||||
}
|
}
|
||||||
|
|
||||||
#welcomeContent {
|
#welcomeContent {
|
||||||
@ -872,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) *
|
* Settings View (sttings.ejs) *
|
||||||
@ -1269,45 +1439,65 @@ input:checked + .toggleSwitchSlider:before {
|
|||||||
* Settings View (Account Tab)
|
* Settings View (Account Tab)
|
||||||
* * */
|
* * */
|
||||||
|
|
||||||
/* Add account button styles. */
|
.settingsAuthAccountTypeContainer {
|
||||||
#settingsAddAccount {
|
display: flex;
|
||||||
background: rgba(0, 0, 0, 0.25);
|
|
||||||
border: 1px solid rgba(126, 126, 126, 0.57);
|
|
||||||
border-radius: 3px;
|
|
||||||
height: 50px;
|
|
||||||
width: 75%;
|
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;
|
text-align: left;
|
||||||
padding: 0px 50px;
|
padding: 2px 0px;
|
||||||
|
color: white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: 0.25s ease;
|
transition: 0.25s ease;
|
||||||
}
|
}
|
||||||
#settingsAddAccount:hover,
|
.settingsAddAuthAccount:hover,
|
||||||
#settingsAddAccount:focus {
|
.settingsAddAuthAccount:focus {
|
||||||
background: rgba(54, 54, 54, 0.25);
|
text-shadow: 0px 0px 20px white, 0px 0px 20px white, 0px 0px 20px white;
|
||||||
text-shadow: 0px 0px 20px white;
|
|
||||||
}
|
}
|
||||||
|
.settingsAddAuthAccount:active {
|
||||||
/* Settings auth accounts header. */
|
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);
|
||||||
#settingsCurrentAccountsHeader {
|
color: rgba(255, 255, 255, 0.75);
|
||||||
margin: 20px 0px;
|
}
|
||||||
|
.settingsAddAuthAccount:disabled {
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Auth account list container styles. */
|
/* Auth account list container styles. */
|
||||||
#settingsCurrentAccounts {
|
.settingsCurrentAccounts {
|
||||||
margin-bottom: 5%;
|
margin-bottom: 5%;
|
||||||
}
|
}
|
||||||
#settingsCurrentAccounts > .settingsAuthAccount:not(:last-child) {
|
.settingsCurrentAccounts > .settingsAuthAccount:not(:last-child) {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
#settingsCurrentAccounts > .settingsAuthAccount:not(:first-child) {
|
.settingsCurrentAccounts > .settingsAuthAccount:not(:first-child) {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Auth account shared styles. */
|
/* Auth account shared styles. */
|
||||||
.settingsAuthAccount {
|
.settingsAuthAccount {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 75%;
|
|
||||||
background: rgba(0, 0, 0, 0.25);
|
background: rgba(0, 0, 0, 0.25);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
border: 1px solid rgba(126, 126, 126, 0.57);
|
border: 1px solid rgba(126, 126, 126, 0.57);
|
||||||
|
7
app/assets/images/icons/microsoft.svg
Normal file
7
app/assets/images/icons/microsoft.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
|
||||||
|
<path fill="#f3f3f3" d="M0 0h23v23H0z" />
|
||||||
|
<path fill="#f35325" d="M1 1h10v10H1z" />
|
||||||
|
<path fill="#81bc06" d="M12 1h10v10H12z" />
|
||||||
|
<path fill="#05a6f0" d="M1 12h10v10H1z" />
|
||||||
|
<path fill="#ffba08" d="M12 12h10v10H12z" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 303 B |
5
app/assets/images/icons/mojang.svg
Normal file
5
app/assets/images/icons/mojang.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 9.677 9.667">
|
||||||
|
<path d="M-26.332-12.098h2.715c-1.357.18-2.574 1.23-2.715 2.633z" fill="#fff" />
|
||||||
|
<path d="M2.598.022h7.07L9.665 7c-.003 1.334-1.113 2.46-2.402 2.654H0V2.542C.134 1.2 1.3.195 2.598.022z" fill="#db2331" />
|
||||||
|
<path d="M1.54 2.844c.314-.76 1.31-.46 1.954-.528.785-.083 1.503.272 2.1.758l.164-.9c.327.345.587.756.964 1.052.28.254.655-.342.86-.013.42.864.408 1.86.54 2.795l-.788-.373C6.9 4.17 5.126 3.052 3.656 3.685c-1.294.592-1.156 2.65.06 3.255 1.354.703 2.953.51 4.405.292-.07.42-.34.87-.834.816l-4.95.002c-.5.055-.886-.413-.838-.89l.04-4.315z" fill="#fff" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 664 B |
@ -5,6 +5,7 @@ const child_process = require('child_process')
|
|||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
const EventEmitter = require('events')
|
const EventEmitter = require('events')
|
||||||
const fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
|
const StreamZip = require('node-stream-zip')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const Registry = require('winreg')
|
const Registry = require('winreg')
|
||||||
const request = require('request')
|
const request = require('request')
|
||||||
@ -15,13 +16,6 @@ const ConfigManager = require('./configmanager')
|
|||||||
const DistroManager = require('./distromanager')
|
const DistroManager = require('./distromanager')
|
||||||
const isDev = require('./isdev')
|
const isDev = require('./isdev')
|
||||||
|
|
||||||
// Constants
|
|
||||||
// const PLATFORM_MAP = {
|
|
||||||
// win32: '-windows-x64.tar.gz',
|
|
||||||
// darwin: '-macosx-x64.tar.gz',
|
|
||||||
// linux: '-linux-x64.tar.gz'
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Classes
|
// Classes
|
||||||
|
|
||||||
/** Class representing a base asset. */
|
/** Class representing a base asset. */
|
||||||
@ -222,42 +216,6 @@ class JavaGuard extends EventEmitter {
|
|||||||
this.mcVersion = mcVersion
|
this.mcVersion = mcVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
// /**
|
|
||||||
// * @typedef OracleJREData
|
|
||||||
// * @property {string} uri The base uri of the JRE.
|
|
||||||
// * @property {{major: string, update: string, build: string}} version Object containing version information.
|
|
||||||
// */
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Resolves the latest version of Oracle's JRE and parses its download link.
|
|
||||||
// *
|
|
||||||
// * @returns {Promise.<OracleJREData>} Promise which resolved to an object containing the JRE download data.
|
|
||||||
// */
|
|
||||||
// static _latestJREOracle(){
|
|
||||||
|
|
||||||
// const url = 'https://www.oracle.com/technetwork/java/javase/downloads/jre8-downloads-2133155.html'
|
|
||||||
// const regex = /https:\/\/.+?(?=\/java)\/java\/jdk\/([0-9]+u[0-9]+)-(b[0-9]+)\/([a-f0-9]{32})?\/jre-\1/
|
|
||||||
|
|
||||||
// return new Promise((resolve, reject) => {
|
|
||||||
// request(url, (err, resp, body) => {
|
|
||||||
// if(!err){
|
|
||||||
// const arr = body.match(regex)
|
|
||||||
// const verSplit = arr[1].split('u')
|
|
||||||
// resolve({
|
|
||||||
// uri: arr[0],
|
|
||||||
// version: {
|
|
||||||
// major: verSplit[0],
|
|
||||||
// update: verSplit[1],
|
|
||||||
// build: arr[2]
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// } else {
|
|
||||||
// resolve(null)
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef OpenJDKData
|
* @typedef OpenJDKData
|
||||||
* @property {string} uri The base uri of the JRE.
|
* @property {string} uri The base uri of the JRE.
|
||||||
@ -281,30 +239,41 @@ class JavaGuard extends EventEmitter {
|
|||||||
if(process.platform === 'darwin') {
|
if(process.platform === 'darwin') {
|
||||||
return this._latestCorretto(major)
|
return this._latestCorretto(major)
|
||||||
} else {
|
} else {
|
||||||
return this._latestAdoptOpenJDK(major)
|
return this._latestAdoptium(major)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static _latestAdoptOpenJDK(major) {
|
static _latestAdoptium(major) {
|
||||||
|
|
||||||
|
const majorNum = Number(major)
|
||||||
const sanitizedOS = process.platform === 'win32' ? 'windows' : (process.platform === 'darwin' ? 'mac' : process.platform)
|
const sanitizedOS = process.platform === 'win32' ? 'windows' : (process.platform === 'darwin' ? 'mac' : process.platform)
|
||||||
|
const url = `https://api.adoptium.net/v3/assets/latest/${major}/hotspot?vendor=eclipse`
|
||||||
const url = `https://api.adoptopenjdk.net/v2/latestAssets/nightly/openjdk${major}?os=${sanitizedOS}&arch=x64&heap_size=normal&openjdk_impl=hotspot&type=jre`
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
request({url, json: true}, (err, resp, body) => {
|
request({url, json: true}, (err, resp, body) => {
|
||||||
if(!err && body.length > 0){
|
if(!err && body.length > 0){
|
||||||
resolve({
|
|
||||||
uri: body[0].binary_link,
|
const targetBinary = body.find(entry => {
|
||||||
size: body[0].binary_size,
|
return entry.version.major === majorNum
|
||||||
name: body[0].binary_name
|
&& entry.binary.os === sanitizedOS
|
||||||
|
&& entry.binary.image_type === 'jdk'
|
||||||
|
&& entry.binary.architecture === 'x64'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if(targetBinary != null) {
|
||||||
|
resolve({
|
||||||
|
uri: targetBinary.binary.package.link,
|
||||||
|
size: targetBinary.binary.package.size,
|
||||||
|
name: targetBinary.binary.package.name
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
resolve(null)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
resolve(null)
|
resolve(null)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static _latestCorretto(major) {
|
static _latestCorretto(major) {
|
||||||
@ -839,6 +808,7 @@ class JavaGuard extends EventEmitter {
|
|||||||
pathSet1 = new Set([
|
pathSet1 = new Set([
|
||||||
...pathSet1,
|
...pathSet1,
|
||||||
...(await JavaGuard._scanFileSystem('C:\\Program Files\\Java')),
|
...(await JavaGuard._scanFileSystem('C:\\Program Files\\Java')),
|
||||||
|
...(await JavaGuard._scanFileSystem('C:\\Program Files\\Eclipse Foundation')),
|
||||||
...(await JavaGuard._scanFileSystem('C:\\Program Files\\AdoptOpenJDK'))
|
...(await JavaGuard._scanFileSystem('C:\\Program Files\\AdoptOpenJDK'))
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@ -1583,21 +1553,7 @@ class AssetGuard extends EventEmitter {
|
|||||||
this.java = new DLTracker([jre], jre.size, (a, self) => {
|
this.java = new DLTracker([jre], jre.size, (a, self) => {
|
||||||
if(verData.name.endsWith('zip')){
|
if(verData.name.endsWith('zip')){
|
||||||
|
|
||||||
const zip = new AdmZip(a.to)
|
this._extractJdkZip(a.to, dataDir, self)
|
||||||
const pos = path.join(dataDir, zip.getEntries()[0].entryName)
|
|
||||||
zip.extractAllToAsync(dataDir, true, (err) => {
|
|
||||||
if(err){
|
|
||||||
console.log(err)
|
|
||||||
self.emit('complete', 'java', JavaGuard.javaExecFromRoot(pos))
|
|
||||||
} else {
|
|
||||||
fs.unlink(a.to, err => {
|
|
||||||
if(err){
|
|
||||||
console.log(err)
|
|
||||||
}
|
|
||||||
self.emit('complete', 'java', JavaGuard.javaExecFromRoot(pos))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Tar.gz
|
// Tar.gz
|
||||||
@ -1638,67 +1594,31 @@ class AssetGuard extends EventEmitter {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// _enqueueOracleJRE(dataDir){
|
async _extractJdkZip(zipPath, runtimeDir, self) {
|
||||||
// return new Promise((resolve, reject) => {
|
|
||||||
// JavaGuard._latestJREOracle().then(verData => {
|
|
||||||
// if(verData != null){
|
|
||||||
|
|
||||||
// const combined = verData.uri + PLATFORM_MAP[process.platform]
|
const zip = new StreamZip.async({
|
||||||
|
file: zipPath,
|
||||||
|
storeEntries: true
|
||||||
|
})
|
||||||
|
|
||||||
// const opts = {
|
let pos = ''
|
||||||
// url: combined,
|
try {
|
||||||
// headers: {
|
const entries = await zip.entries()
|
||||||
// 'Cookie': 'oraclelicense=accept-securebackup-cookie'
|
pos = path.join(runtimeDir, Object.keys(entries)[0])
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// request.head(opts, (err, resp, body) => {
|
console.log('Extracting jdk..')
|
||||||
// if(err){
|
await zip.extract(null, runtimeDir)
|
||||||
// resolve(false)
|
console.log('Cleaning up..')
|
||||||
// } else {
|
await fs.remove(zipPath)
|
||||||
// dataDir = path.join(dataDir, 'runtime', 'x64')
|
console.log('Jdk extraction complete.')
|
||||||
// const name = combined.substring(combined.lastIndexOf('/')+1)
|
|
||||||
// const fDir = path.join(dataDir, name)
|
|
||||||
// const jre = new Asset(name, null, parseInt(resp.headers['content-length']), opts, fDir)
|
|
||||||
// this.java = new DLTracker([jre], jre.size, (a, self) => {
|
|
||||||
// let h = null
|
|
||||||
// fs.createReadStream(a.to)
|
|
||||||
// .on('error', err => console.log(err))
|
|
||||||
// .pipe(zlib.createGunzip())
|
|
||||||
// .on('error', err => console.log(err))
|
|
||||||
// .pipe(tar.extract(dataDir, {
|
|
||||||
// map: (header) => {
|
|
||||||
// if(h == null){
|
|
||||||
// h = header.name
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }))
|
|
||||||
// .on('error', err => console.log(err))
|
|
||||||
// .on('finish', () => {
|
|
||||||
// fs.unlink(a.to, err => {
|
|
||||||
// if(err){
|
|
||||||
// console.log(err)
|
|
||||||
// }
|
|
||||||
// if(h.indexOf('/') > -1){
|
|
||||||
// h = h.substring(0, h.indexOf('/'))
|
|
||||||
// }
|
|
||||||
// const pos = path.join(dataDir, h)
|
|
||||||
// self.emit('complete', 'java', JavaGuard.javaExecFromRoot(pos))
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
|
|
||||||
// })
|
} catch(err) {
|
||||||
// resolve(true)
|
console.log(err)
|
||||||
// }
|
} finally {
|
||||||
// })
|
zip.close()
|
||||||
|
self.emit('complete', 'java', JavaGuard.javaExecFromRoot(pos))
|
||||||
// } else {
|
}
|
||||||
// resolve(false)
|
}
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
// _enqueueMojangJRE(dir){
|
// _enqueueMojangJRE(dir){
|
||||||
// return new Promise((resolve, reject) => {
|
// return new Promise((resolve, reject) => {
|
||||||
|
@ -10,15 +10,18 @@
|
|||||||
*/
|
*/
|
||||||
// Requirements
|
// Requirements
|
||||||
const ConfigManager = require('./configmanager')
|
const ConfigManager = require('./configmanager')
|
||||||
const LoggerUtil = require('./loggerutil')
|
const { LoggerUtil } = require('helios-core')
|
||||||
const Mojang = require('./mojang')
|
const { RestResponseStatus } = require('helios-core/common')
|
||||||
const logger = LoggerUtil('%c[AuthManager]', 'color: #a02d2a; font-weight: bold')
|
const { MojangRestAPI, mojangErrorDisplayable, MojangErrorCode } = require('helios-core/mojang')
|
||||||
const loggerSuccess = LoggerUtil('%c[AuthManager]', 'color: #209b07; font-weight: bold')
|
const { MicrosoftAuth, microsoftErrorDisplayable, MicrosoftErrorCode } = require('helios-core/microsoft')
|
||||||
|
const { AZURE_CLIENT_ID } = require('./ipcconstants')
|
||||||
|
|
||||||
|
const log = LoggerUtil.getLogger('AuthManager')
|
||||||
|
|
||||||
// Functions
|
// Functions
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add an account. This will authenticate the given credentials with Mojang's
|
* 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
|
* authserver. The resultant data will be stored as an auth account in the
|
||||||
* configuration database.
|
* configuration database.
|
||||||
*
|
*
|
||||||
@ -26,40 +29,172 @@ const loggerSuccess = LoggerUtil('%c[AuthManager]', 'color: #209b07; font-weight
|
|||||||
* @param {string} password The account password.
|
* @param {string} password The account password.
|
||||||
* @returns {Promise.<Object>} Promise which resolves the resolved authenticated account object.
|
* @returns {Promise.<Object>} Promise which resolves the resolved authenticated account object.
|
||||||
*/
|
*/
|
||||||
exports.addAccount = async function(username, password){
|
exports.addMojangAccount = async function(username, password) {
|
||||||
try {
|
try {
|
||||||
const session = await Mojang.authenticate(username, password, ConfigManager.getClientToken())
|
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){
|
if(session.selectedProfile != null){
|
||||||
const ret = ConfigManager.addAuthAccount(session.selectedProfile.id, session.accessToken, username, session.selectedProfile.name)
|
const ret = ConfigManager.addMojangAuthAccount(session.selectedProfile.id, session.accessToken, username, session.selectedProfile.name)
|
||||||
if(ConfigManager.getClientToken() == null){
|
if(ConfigManager.getClientToken() == null){
|
||||||
ConfigManager.setClientToken(session.clientToken)
|
ConfigManager.setClientToken(session.clientToken)
|
||||||
}
|
}
|
||||||
ConfigManager.save()
|
ConfigManager.save()
|
||||||
return ret
|
return ret
|
||||||
} else {
|
} else {
|
||||||
throw new Error('NotPaidAccount')
|
return Promise.reject(mojangErrorDisplayable(MojangErrorCode.ERROR_NOT_PAID))
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return Promise.reject(mojangErrorDisplayable(response.mojangErrorCode))
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err){
|
} catch (err){
|
||||||
return Promise.reject(err)
|
log.error(err)
|
||||||
|
return Promise.reject(mojangErrorDisplayable(MojangErrorCode.UNKNOWN))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
|
||||||
|
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 {
|
||||||
|
accessTokenRaw = entryCode
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove an account. This will invalidate the access token associated
|
* 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 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} authCode The authCode obtained from microsoft.
|
||||||
|
* @returns {Promise.<Object>} Promise which resolves the resolved authenticated account object.
|
||||||
|
*/
|
||||||
|
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.
|
* with the account and then remove it from the database.
|
||||||
*
|
*
|
||||||
* @param {string} uuid The UUID of the account to be removed.
|
* @param {string} uuid The UUID of the account to be removed.
|
||||||
* @returns {Promise.<void>} Promise which resolves to void when the action is complete.
|
* @returns {Promise.<void>} Promise which resolves to void when the action is complete.
|
||||||
*/
|
*/
|
||||||
exports.removeAccount = async function(uuid){
|
exports.removeMojangAccount = async function(uuid){
|
||||||
try {
|
try {
|
||||||
const authAcc = ConfigManager.getAuthAccount(uuid)
|
const authAcc = ConfigManager.getAuthAccount(uuid)
|
||||||
await Mojang.invalidate(authAcc.accessToken, ConfigManager.getClientToken())
|
const response = await MojangRestAPI.invalidate(authAcc.accessToken, ConfigManager.getClientToken())
|
||||||
|
if(response.responseStatus === RestResponseStatus.SUCCESS) {
|
||||||
|
ConfigManager.removeAuthAccount(uuid)
|
||||||
|
ConfigManager.save()
|
||||||
|
return Promise.resolve()
|
||||||
|
} else {
|
||||||
|
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 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.<void>} Promise which resolves to void when the action is complete.
|
||||||
|
*/
|
||||||
|
exports.removeMicrosoftAccount = async function(uuid){
|
||||||
|
try {
|
||||||
ConfigManager.removeAuthAccount(uuid)
|
ConfigManager.removeAuthAccount(uuid)
|
||||||
ConfigManager.save()
|
ConfigManager.save()
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
} catch (err){
|
} catch (err){
|
||||||
|
log.error('Error while removing account', err)
|
||||||
return Promise.reject(err)
|
return Promise.reject(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -69,31 +204,112 @@ exports.removeAccount = async function(uuid){
|
|||||||
* we will attempt to refresh the access token and update that value. If that fails, a
|
* we will attempt to refresh the access token and update that value. If that fails, a
|
||||||
* new login will be required.
|
* new login will be required.
|
||||||
*
|
*
|
||||||
* **Function is WIP**
|
* @returns {Promise.<boolean>} 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.<boolean>} Promise which resolves to true if the access token is valid,
|
||||||
|
* otherwise false.
|
||||||
|
*/
|
||||||
|
async function validateSelectedMicrosoftAccount(){
|
||||||
|
const current = ConfigManager.getSelectedAccount()
|
||||||
|
const now = new Date().getTime()
|
||||||
|
const mcExpiresAt = Date.parse(current.expiresAt)
|
||||||
|
const mcExpired = now >= mcExpiresAt
|
||||||
|
|
||||||
|
if(!mcExpired) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MC token expired. Check MS token.
|
||||||
|
|
||||||
|
const msExpiresAt = Date.parse(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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the selected auth account.
|
||||||
*
|
*
|
||||||
* @returns {Promise.<boolean>} Promise which resolves to true if the access token is valid,
|
* @returns {Promise.<boolean>} Promise which resolves to true if the access token is valid,
|
||||||
* otherwise false.
|
* otherwise false.
|
||||||
*/
|
*/
|
||||||
exports.validateSelected = async function(){
|
exports.validateSelected = async function(){
|
||||||
const current = ConfigManager.getSelectedAccount()
|
const current = ConfigManager.getSelectedAccount()
|
||||||
const isValid = await Mojang.validate(current.accessToken, ConfigManager.getClientToken())
|
|
||||||
if(!isValid){
|
if(current.type === 'microsoft') {
|
||||||
try {
|
return await validateSelectedMicrosoftAccount()
|
||||||
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?
|
|
||||||
}
|
|
||||||
logger.log('Account access token is invalid.')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
loggerSuccess.log('Account access token validated.')
|
|
||||||
return true
|
|
||||||
} else {
|
} else {
|
||||||
loggerSuccess.log('Account access token validated.')
|
return await validateSelectedMojangAccount()
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -318,20 +318,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} uuid The uuid of the authenticated account.
|
||||||
* @param {string} accessToken The new Access Token.
|
* @param {string} accessToken The new Access Token.
|
||||||
*
|
*
|
||||||
* @returns {Object} The authenticated account object created by this action.
|
* @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].accessToken = accessToken
|
||||||
|
config.authenticationDatabase[uuid].type = 'mojang' // For gradual conversion.
|
||||||
return config.authenticationDatabase[uuid]
|
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} uuid The uuid of the authenticated account.
|
||||||
* @param {string} accessToken The accessToken of the authenticated account.
|
* @param {string} accessToken The accessToken of the authenticated account.
|
||||||
@ -340,9 +341,10 @@ exports.updateAuthAccount = function(uuid, accessToken){
|
|||||||
*
|
*
|
||||||
* @returns {Object} The authenticated account object created by this action.
|
* @returns {Object} The authenticated account object created by this action.
|
||||||
*/
|
*/
|
||||||
exports.addAuthAccount = function(uuid, accessToken, username, displayName){
|
exports.addMojangAuthAccount = function(uuid, accessToken, username, displayName){
|
||||||
config.selectedAccount = uuid
|
config.selectedAccount = uuid
|
||||||
config.authenticationDatabase[uuid] = {
|
config.authenticationDatabase[uuid] = {
|
||||||
|
type: 'mojang',
|
||||||
accessToken,
|
accessToken,
|
||||||
username: username.trim(),
|
username: username.trim(),
|
||||||
uuid: uuid.trim(),
|
uuid: uuid.trim(),
|
||||||
@ -351,6 +353,58 @@ exports.addAuthAccount = function(uuid, accessToken, username, displayName){
|
|||||||
return config.authenticationDatabase[uuid]
|
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]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove an authenticated account from the database. If the account
|
* Remove an authenticated account from the database. If the account
|
||||||
* was also the selected account, a new one will be selected. If there
|
* was also the selected account, a new one will be selected. If there
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// Work in progress
|
// Work in progress
|
||||||
const logger = require('./loggerutil')('%c[DiscordWrapper]', 'color: #7289da; font-weight: bold')
|
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 client
|
||||||
let activity
|
let activity
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
const fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
const path = require('path')
|
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 #1: File Name (without .disabled, if any)
|
||||||
// Group #2: File Extension (jar, zip, or litemod)
|
// Group #2: File Extension (jar, zip, or litemod)
|
||||||
@ -95,14 +96,16 @@ exports.addDropinMods = function(files, modsdir) {
|
|||||||
* @returns {Promise.<boolean>} True if the mod was deleted, otherwise false.
|
* @returns {Promise.<boolean>} True if the mod was deleted, otherwise false.
|
||||||
*/
|
*/
|
||||||
exports.deleteDropinMod = async function(modsDir, fullName){
|
exports.deleteDropinMod = async function(modsDir, fullName){
|
||||||
try {
|
|
||||||
await shell.trashItem(path.join(modsDir, fullName))
|
const res = await ipcRenderer.invoke(SHELL_OPCODE.TRASH_ITEM, path.join(modsDir, fullName))
|
||||||
return true
|
|
||||||
} catch(error) {
|
if(!res.result) {
|
||||||
shell.beep()
|
shell.beep()
|
||||||
console.error('Error deleting drop-in mod.', error)
|
console.error('Error deleting drop-in mod.', res.error)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
28
app/assets/js/ipcconstants.js
Normal file
28
app/assets/js/ipcconstants.js
Normal file
@ -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 = '1ce6e35a-126f-48fd-97fb-54d143ac6d45'
|
||||||
|
// 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'
|
||||||
|
}
|
@ -1,271 +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: 'session.minecraft.net',
|
|
||||||
status: 'grey',
|
|
||||||
name: 'Multiplayer Session Service',
|
|
||||||
essential: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
service: 'authserver.mojang.com',
|
|
||||||
status: 'grey',
|
|
||||||
name: 'Authentication Service',
|
|
||||||
essential: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
service: 'textures.minecraft.net',
|
|
||||||
status: 'grey',
|
|
||||||
name: 'Minecraft Skins',
|
|
||||||
essential: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
service: 'api.mojang.com',
|
|
||||||
status: 'grey',
|
|
||||||
name: 'Public API',
|
|
||||||
essential: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
service: 'minecraft.net',
|
|
||||||
status: 'grey',
|
|
||||||
name: 'Minecraft.net',
|
|
||||||
essential: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
service: 'account.mojang.com',
|
|
||||||
status: 'grey',
|
|
||||||
name: 'Mojang Accounts Website',
|
|
||||||
essential: false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// 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 = function(){
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
request.get('https://status.mojang.com/check',
|
|
||||||
{
|
|
||||||
json: true,
|
|
||||||
timeout: 2500
|
|
||||||
},
|
|
||||||
function(error, response, body){
|
|
||||||
|
|
||||||
if(error || response.statusCode !== 200){
|
|
||||||
logger.warn('Unable to retrieve Mojang status.')
|
|
||||||
logger.debug('Error while retrieving Mojang statuses:', error)
|
|
||||||
//reject(error || response.statusCode)
|
|
||||||
for(let i=0; i<statuses.length; i++){
|
|
||||||
statuses[i].status = 'grey'
|
|
||||||
}
|
|
||||||
resolve(statuses)
|
|
||||||
} else {
|
|
||||||
for(let i=0; i<body.length; i++){
|
|
||||||
const key = Object.keys(body[i])[0]
|
|
||||||
inner:
|
|
||||||
for(let j=0; j<statuses.length; j++){
|
|
||||||
if(statuses[j].service === key) {
|
|
||||||
statuses[j].status = body[i][key]
|
|
||||||
break inner
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resolve(statuses)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Authenticate a user with their Mojang credentials.
|
|
||||||
*
|
|
||||||
* @param {string} username The user's username, this is often an email.
|
|
||||||
* @param {string} password The user's password.
|
|
||||||
* @param {string} clientToken The launcher's Client Token.
|
|
||||||
* @param {boolean} requestUser Optional. Adds user object to the reponse.
|
|
||||||
* @param {Object} agent Optional. Provided by default. Adds user info to the response.
|
|
||||||
*
|
|
||||||
* @see http://wiki.vg/Authentication#Authenticate
|
|
||||||
*/
|
|
||||||
exports.authenticate = function(username, password, clientToken, requestUser = true, agent = minecraftAgent){
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
|
|
||||||
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'})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
@ -4,13 +4,13 @@
|
|||||||
// Requirements
|
// Requirements
|
||||||
const cp = require('child_process')
|
const cp = require('child_process')
|
||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
const {URL} = require('url')
|
const { URL } = require('url')
|
||||||
|
const { MojangRestAPI, getServerStatus } = require('helios-core/mojang')
|
||||||
|
|
||||||
// Internal Requirements
|
// Internal Requirements
|
||||||
const DiscordWrapper = require('./assets/js/discordwrapper')
|
const DiscordWrapper = require('./assets/js/discordwrapper')
|
||||||
const Mojang = require('./assets/js/mojang')
|
|
||||||
const ProcessBuilder = require('./assets/js/processbuilder')
|
const ProcessBuilder = require('./assets/js/processbuilder')
|
||||||
const ServerStatus = require('./assets/js/serverstatus')
|
const { RestResponseStatus, isDisplayableError } = require('helios-core/common')
|
||||||
|
|
||||||
// Launch Elements
|
// Launch Elements
|
||||||
const launch_content = document.getElementById('launch_content')
|
const launch_content = document.getElementById('launch_content')
|
||||||
@ -21,7 +21,7 @@ const launch_details_text = document.getElementById('launch_details_text')
|
|||||||
const server_selection_button = document.getElementById('server_selection_button')
|
const server_selection_button = document.getElementById('server_selection_button')
|
||||||
const user_text = document.getElementById('user_text')
|
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 */
|
/* Launch Progress Wrapper Functions */
|
||||||
|
|
||||||
@ -165,8 +165,15 @@ const refreshMojangStatuses = async function(){
|
|||||||
let tooltipEssentialHTML = ''
|
let tooltipEssentialHTML = ''
|
||||||
let tooltipNonEssentialHTML = ''
|
let tooltipNonEssentialHTML = ''
|
||||||
|
|
||||||
try {
|
const response = await MojangRestAPI.status()
|
||||||
const statuses = await Mojang.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
|
greenCount = 0
|
||||||
greyCount = 0
|
greyCount = 0
|
||||||
|
|
||||||
@ -175,12 +182,12 @@ const refreshMojangStatuses = async function(){
|
|||||||
|
|
||||||
if(service.essential){
|
if(service.essential){
|
||||||
tooltipEssentialHTML += `<div class="mojangStatusContainer">
|
tooltipEssentialHTML += `<div class="mojangStatusContainer">
|
||||||
<span class="mojangStatusIcon" style="color: ${Mojang.statusToHex(service.status)};">•</span>
|
<span class="mojangStatusIcon" style="color: ${MojangRestAPI.statusToHex(service.status)};">•</span>
|
||||||
<span class="mojangStatusName">${service.name}</span>
|
<span class="mojangStatusName">${service.name}</span>
|
||||||
</div>`
|
</div>`
|
||||||
} else {
|
} else {
|
||||||
tooltipNonEssentialHTML += `<div class="mojangStatusContainer">
|
tooltipNonEssentialHTML += `<div class="mojangStatusContainer">
|
||||||
<span class="mojangStatusIcon" style="color: ${Mojang.statusToHex(service.status)};">•</span>
|
<span class="mojangStatusIcon" style="color: ${MojangRestAPI.statusToHex(service.status)};">•</span>
|
||||||
<span class="mojangStatusName">${service.name}</span>
|
<span class="mojangStatusName">${service.name}</span>
|
||||||
</div>`
|
</div>`
|
||||||
}
|
}
|
||||||
@ -206,14 +213,9 @@ const refreshMojangStatuses = async function(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
loggerLanding.warn('Unable to refresh Mojang service status.')
|
|
||||||
loggerLanding.debug(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('mojangStatusEssentialContainer').innerHTML = tooltipEssentialHTML
|
document.getElementById('mojangStatusEssentialContainer').innerHTML = tooltipEssentialHTML
|
||||||
document.getElementById('mojangStatusNonEssentialContainer').innerHTML = tooltipNonEssentialHTML
|
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){
|
const refreshServerStatus = async function(fade = false){
|
||||||
@ -225,11 +227,11 @@ const refreshServerStatus = async function(fade = false){
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const serverURL = new URL('my://' + serv.getAddress())
|
const serverURL = new URL('my://' + serv.getAddress())
|
||||||
const servStat = await ServerStatus.getStatus(serverURL.hostname, serverURL.port)
|
|
||||||
if(servStat.online){
|
const servStat = await getServerStatus(47, serverURL.hostname, Number(serverURL.port))
|
||||||
|
console.log(servStat)
|
||||||
pLabel = 'PLAYERS'
|
pLabel = 'PLAYERS'
|
||||||
pVal = servStat.onlinePlayers + '/' + servStat.maxPlayers
|
pVal = servStat.players.online + '/' + servStat.players.max
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
loggerLanding.warn('Unable to refresh server status, assuming offline.')
|
loggerLanding.warn('Unable to refresh server status, assuming offline.')
|
||||||
@ -291,7 +293,7 @@ function asyncSystemScan(mcVersion, launchAfter = true){
|
|||||||
toggleLaunchArea(true)
|
toggleLaunchArea(true)
|
||||||
setLaunchPercentage(0, 100)
|
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))
|
const forkEnv = JSON.parse(JSON.stringify(process.env))
|
||||||
forkEnv.CONFIG_DIRECT_PATH = ConfigManager.getLauncherDirectory()
|
forkEnv.CONFIG_DIRECT_PATH = ConfigManager.getLauncherDirectory()
|
||||||
@ -323,7 +325,7 @@ function asyncSystemScan(mcVersion, launchAfter = true){
|
|||||||
// Show this information to the user.
|
// Show this information to the user.
|
||||||
setOverlayContent(
|
setOverlayContent(
|
||||||
'No Compatible<br>Java Installation Found',
|
'No Compatible<br>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? By installing, you accept <a href="http://www.oracle.com/technetwork/java/javase/terms/license/index.html">Oracle\'s license agreement</a>.',
|
'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 Java',
|
||||||
'Install Manually'
|
'Install Manually'
|
||||||
)
|
)
|
||||||
@ -493,8 +495,8 @@ function dlAsync(login = true){
|
|||||||
toggleLaunchArea(true)
|
toggleLaunchArea(true)
|
||||||
setLaunchPercentage(0, 100)
|
setLaunchPercentage(0, 100)
|
||||||
|
|
||||||
const loggerAEx = LoggerUtil('%c[AEx]', 'color: #353232; font-weight: bold')
|
const loggerAEx = LoggerUtil1('%c[AEx]', 'color: #353232; font-weight: bold')
|
||||||
const loggerLaunchSuite = LoggerUtil('%c[LaunchSuite]', 'color: #000668; font-weight: bold')
|
const loggerLaunchSuite = LoggerUtil1('%c[LaunchSuite]', 'color: #000668; font-weight: bold')
|
||||||
|
|
||||||
const forkEnv = JSON.parse(JSON.stringify(process.env))
|
const forkEnv = JSON.parse(JSON.stringify(process.env))
|
||||||
forkEnv.CONFIG_DIRECT_PATH = ConfigManager.getLauncherDirectory()
|
forkEnv.CONFIG_DIRECT_PATH = ConfigManager.getLauncherDirectory()
|
||||||
|
@ -21,7 +21,7 @@ const loginForm = document.getElementById('loginForm')
|
|||||||
// Control variables.
|
// Control variables.
|
||||||
let lu = false, lp = false
|
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')
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -154,79 +154,6 @@ function formDisabled(v){
|
|||||||
loginRememberOption.disabled = 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 loginViewOnSuccess = VIEWS.landing
|
||||||
let loginViewOnCancel = VIEWS.settings
|
let loginViewOnCancel = VIEWS.settings
|
||||||
let loginViewCancelHandler
|
let loginViewCancelHandler
|
||||||
@ -262,7 +189,7 @@ loginButton.addEventListener('click', () => {
|
|||||||
// Show loading stuff.
|
// Show loading stuff.
|
||||||
loginLoading(true)
|
loginLoading(true)
|
||||||
|
|
||||||
AuthManager.addAccount(loginUsername.value, loginPassword.value).then((value) => {
|
AuthManager.addMojangAccount(loginUsername.value, loginPassword.value).then((value) => {
|
||||||
updateSelectedAccount(value)
|
updateSelectedAccount(value)
|
||||||
loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.loggingIn'), Lang.queryJS('login.success'))
|
loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.loggingIn'), Lang.queryJS('login.success'))
|
||||||
$('.circle-loader').toggleClass('load-complete')
|
$('.circle-loader').toggleClass('load-complete')
|
||||||
@ -285,16 +212,28 @@ loginButton.addEventListener('click', () => {
|
|||||||
formDisabled(false)
|
formDisabled(false)
|
||||||
})
|
})
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}).catch((err) => {
|
}).catch((displayableError) => {
|
||||||
loginLoading(false)
|
loginLoading(false)
|
||||||
const errF = resolveError(err)
|
|
||||||
setOverlayContent(errF.title, errF.desc, Lang.queryJS('login.tryAgain'))
|
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.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setOverlayContent(actualDisplayableError.title, actualDisplayableError.desc, Lang.queryJS('login.tryAgain'))
|
||||||
setOverlayHandler(() => {
|
setOverlayHandler(() => {
|
||||||
formDisabled(false)
|
formDisabled(false)
|
||||||
toggleOverlay(false)
|
toggleOverlay(false)
|
||||||
})
|
})
|
||||||
toggleOverlay(true)
|
toggleOverlay(true)
|
||||||
loggerLogin.log('Error while logging in.', err)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
})
|
50
app/assets/js/scripts/loginOptions.js
Normal file
50
app/assets/js/scripts/loginOptions.js
Normal file
@ -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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -197,6 +197,9 @@ document.getElementById('accountSelectConfirm').addEventListener('click', () =>
|
|||||||
const authAcc = ConfigManager.setSelectedAccount(listings[i].getAttribute('uuid'))
|
const authAcc = ConfigManager.setSelectedAccount(listings[i].getAttribute('uuid'))
|
||||||
ConfigManager.save()
|
ConfigManager.save()
|
||||||
updateSelectedAccount(authAcc)
|
updateSelectedAccount(authAcc)
|
||||||
|
if(getCurrentView() === VIEWS.settings) {
|
||||||
|
prepareSettings()
|
||||||
|
}
|
||||||
toggleOverlay(false)
|
toggleOverlay(false)
|
||||||
validateSelectedAccount()
|
validateSelectedAccount()
|
||||||
return
|
return
|
||||||
@ -207,6 +210,9 @@ document.getElementById('accountSelectConfirm').addEventListener('click', () =>
|
|||||||
const authAcc = ConfigManager.setSelectedAccount(listings[0].getAttribute('uuid'))
|
const authAcc = ConfigManager.setSelectedAccount(listings[0].getAttribute('uuid'))
|
||||||
ConfigManager.save()
|
ConfigManager.save()
|
||||||
updateSelectedAccount(authAcc)
|
updateSelectedAccount(authAcc)
|
||||||
|
if(getCurrentView() === VIEWS.settings) {
|
||||||
|
prepareSettings()
|
||||||
|
}
|
||||||
toggleOverlay(false)
|
toggleOverlay(false)
|
||||||
validateSelectedAccount()
|
validateSelectedAccount()
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ const semver = require('semver')
|
|||||||
|
|
||||||
const { JavaGuard } = require('./assets/js/assetguard')
|
const { JavaGuard } = require('./assets/js/assetguard')
|
||||||
const DropinModUtil = require('./assets/js/dropinmodutil')
|
const DropinModUtil = require('./assets/js/dropinmodutil')
|
||||||
|
const { MSFT_OPCODE, MSFT_REPLY_TYPE, MSFT_ERROR } = require('./assets/js/ipcconstants')
|
||||||
|
|
||||||
const settingsState = {
|
const settingsState = {
|
||||||
invalid: new Set()
|
invalid: new Set()
|
||||||
@ -85,7 +86,7 @@ bindFileSelectors()
|
|||||||
/**
|
/**
|
||||||
* Bind value validators to the settings UI elements. These will
|
* Bind value validators to the settings UI elements. These will
|
||||||
* validate against the criteria defined in the ConfigManager (if
|
* validate against the criteria defined in the ConfigManager (if
|
||||||
* and). If the value is invalid, the UI will reflect this and saving
|
* any). If the value is invalid, the UI will reflect this and saving
|
||||||
* will be disabled until the value is corrected. This is an automated
|
* will be disabled until the value is corrected. This is an automated
|
||||||
* process. More complex UI may need to be bound separately.
|
* process. More complex UI may need to be bound separately.
|
||||||
*/
|
*/
|
||||||
@ -314,8 +315,11 @@ settingsNavDone.onclick = () => {
|
|||||||
* Account Management Tab
|
* Account Management Tab
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Bind the add account button.
|
const msftLoginLogger = LoggerUtil.getLogger('Microsoft Login')
|
||||||
document.getElementById('settingsAddAccount').onclick = (e) => {
|
const msftLogoutLogger = LoggerUtil.getLogger('Microsoft Logout')
|
||||||
|
|
||||||
|
// Bind the add mojang account button.
|
||||||
|
document.getElementById('settingsAddMojangAccount').onclick = (e) => {
|
||||||
switchView(getCurrentView(), VIEWS.login, 500, 500, () => {
|
switchView(getCurrentView(), VIEWS.login, 500, 500, () => {
|
||||||
loginViewOnCancel = VIEWS.settings
|
loginViewOnCancel = VIEWS.settings
|
||||||
loginViewOnSuccess = VIEWS.settings
|
loginViewOnSuccess = VIEWS.settings
|
||||||
@ -323,6 +327,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
|
* Bind functionality for the account selection buttons. If another account
|
||||||
* is selected, the UI of the previously selected account will be updated.
|
* is selected, the UI of the previously selected account will be updated.
|
||||||
@ -367,7 +467,6 @@ function bindAuthAccountLogOut(){
|
|||||||
setOverlayHandler(() => {
|
setOverlayHandler(() => {
|
||||||
processLogOut(val, isLastAccount)
|
processLogOut(val, isLastAccount)
|
||||||
toggleOverlay(false)
|
toggleOverlay(false)
|
||||||
switchView(getCurrentView(), VIEWS.login)
|
|
||||||
})
|
})
|
||||||
setDismissHandler(() => {
|
setDismissHandler(() => {
|
||||||
toggleOverlay(false)
|
toggleOverlay(false)
|
||||||
@ -381,6 +480,7 @@ function bindAuthAccountLogOut(){
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let msAccDomElementCache
|
||||||
/**
|
/**
|
||||||
* Process a log out.
|
* Process a log out.
|
||||||
*
|
*
|
||||||
@ -391,19 +491,91 @@ function processLogOut(val, isLastAccount){
|
|||||||
const parent = val.closest('.settingsAuthAccount')
|
const parent = val.closest('.settingsAuthAccount')
|
||||||
const uuid = parent.getAttribute('uuid')
|
const uuid = parent.getAttribute('uuid')
|
||||||
const prevSelAcc = ConfigManager.getSelectedAccount()
|
const prevSelAcc = ConfigManager.getSelectedAccount()
|
||||||
AuthManager.removeAccount(uuid).then(() => {
|
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){
|
if(!isLastAccount && uuid === prevSelAcc.uuid){
|
||||||
const selAcc = ConfigManager.getSelectedAccount()
|
const selAcc = ConfigManager.getSelectedAccount()
|
||||||
refreshAuthAccountSelected(selAcc.uuid)
|
refreshAuthAccountSelected(selAcc.uuid)
|
||||||
updateSelectedAccount(selAcc)
|
updateSelectedAccount(selAcc)
|
||||||
validateSelectedAccount()
|
validateSelectedAccount()
|
||||||
}
|
}
|
||||||
|
if(isLastAccount) {
|
||||||
|
loginOptionsCancelEnabled(false)
|
||||||
|
loginOptionsViewOnLoginSuccess = VIEWS.settings
|
||||||
|
loginOptionsViewOnLoginCancel = VIEWS.loginOptions
|
||||||
|
switchView(getCurrentView(), VIEWS.loginOptions)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
$(parent).fadeOut(250, () => {
|
$(parent).fadeOut(250, () => {
|
||||||
parent.remove()
|
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
|
* Refreshes the status of the selected account on the auth account
|
||||||
* elements.
|
* elements.
|
||||||
@ -425,7 +597,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.
|
* Add auth account elements for each one stored in the authentication database.
|
||||||
@ -438,11 +611,13 @@ function populateAuthAccounts(){
|
|||||||
}
|
}
|
||||||
const selectedUUID = ConfigManager.getSelectedAccount().uuid
|
const selectedUUID = ConfigManager.getSelectedAccount().uuid
|
||||||
|
|
||||||
let authAccountStr = ''
|
let microsoftAuthAccountStr = ''
|
||||||
|
let mojangAuthAccountStr = ''
|
||||||
|
|
||||||
authKeys.map((val) => {
|
authKeys.forEach((val) => {
|
||||||
const acc = authAccounts[val]
|
const acc = authAccounts[val]
|
||||||
authAccountStr += `<div class="settingsAuthAccount" uuid="${acc.uuid}">
|
|
||||||
|
const accHtml = `<div class="settingsAuthAccount" uuid="${acc.uuid}">
|
||||||
<div class="settingsAuthAccountLeft">
|
<div class="settingsAuthAccountLeft">
|
||||||
<img class="settingsAuthAccountImage" alt="${acc.displayName}" src="https://mc-heads.net/body/${acc.uuid}/60">
|
<img class="settingsAuthAccountImage" alt="${acc.displayName}" src="https://mc-heads.net/body/${acc.uuid}/60">
|
||||||
</div>
|
</div>
|
||||||
@ -465,9 +640,17 @@ function populateAuthAccounts(){
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`
|
</div>`
|
||||||
|
|
||||||
|
if(acc.type === 'microsoft') {
|
||||||
|
microsoftAuthAccountStr += accHtml
|
||||||
|
} else {
|
||||||
|
mojangAuthAccountStr += accHtml
|
||||||
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
settingsCurrentAccounts.innerHTML = authAccountStr
|
settingsCurrentMicrosoftAccounts.innerHTML = microsoftAuthAccountStr
|
||||||
|
settingsCurrentMojangAccounts.innerHTML = mojangAuthAccountStr
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -16,9 +16,11 @@ let fatalStartupError = false
|
|||||||
// Mapping of each view to their container IDs.
|
// Mapping of each view to their container IDs.
|
||||||
const VIEWS = {
|
const VIEWS = {
|
||||||
landing: '#landingContainer',
|
landing: '#landingContainer',
|
||||||
|
loginOptions: '#loginOptionsContainer',
|
||||||
login: '#loginContainer',
|
login: '#loginContainer',
|
||||||
settings: '#settingsContainer',
|
settings: '#settingsContainer',
|
||||||
welcome: '#welcomeContainer'
|
welcome: '#welcomeContainer',
|
||||||
|
waiting: '#waitingContainer'
|
||||||
}
|
}
|
||||||
|
|
||||||
// The currently shown view container.
|
// The currently shown view container.
|
||||||
@ -86,8 +88,11 @@ function showMainUI(data){
|
|||||||
currentView = VIEWS.landing
|
currentView = VIEWS.landing
|
||||||
$(VIEWS.landing).fadeIn(1000)
|
$(VIEWS.landing).fadeIn(1000)
|
||||||
} else {
|
} else {
|
||||||
currentView = VIEWS.login
|
loginOptionsCancelEnabled(false)
|
||||||
$(VIEWS.login).fadeIn(1000)
|
loginOptionsViewOnLoginSuccess = VIEWS.landing
|
||||||
|
loginOptionsViewOnLoginCancel = VIEWS.loginOptions
|
||||||
|
currentView = VIEWS.loginOptions
|
||||||
|
$(VIEWS.loginOptions).fadeIn(1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -329,20 +334,46 @@ async function validateSelectedAccount(){
|
|||||||
'Select Another Account'
|
'Select Another Account'
|
||||||
)
|
)
|
||||||
setOverlayHandler(() => {
|
setOverlayHandler(() => {
|
||||||
|
|
||||||
|
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
|
document.getElementById('loginUsername').value = selectedAcc.username
|
||||||
validateEmail(selectedAcc.username)
|
validateEmail(selectedAcc.username)
|
||||||
loginViewOnSuccess = getCurrentView()
|
}
|
||||||
loginViewOnCancel = getCurrentView()
|
|
||||||
if(accLen > 0){
|
loginOptionsViewOnLoginSuccess = getCurrentView()
|
||||||
loginViewCancelHandler = () => {
|
loginOptionsViewOnLoginCancel = VIEWS.loginOptions
|
||||||
ConfigManager.addAuthAccount(selectedAcc.uuid, selectedAcc.accessToken, selectedAcc.username, selectedAcc.displayName)
|
|
||||||
|
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()
|
ConfigManager.save()
|
||||||
validateSelectedAccount()
|
validateSelectedAccount()
|
||||||
}
|
}
|
||||||
loginCancelEnabled(true)
|
loginOptionsCancelEnabled(true)
|
||||||
|
} else {
|
||||||
|
loginOptionsCancelEnabled(false)
|
||||||
}
|
}
|
||||||
toggleOverlay(false)
|
toggleOverlay(false)
|
||||||
switchView(getCurrentView(), VIEWS.login)
|
switchView(getCurrentView(), VIEWS.loginOptions)
|
||||||
})
|
})
|
||||||
setDismissHandler(() => {
|
setDismissHandler(() => {
|
||||||
if(accLen > 1){
|
if(accLen > 1){
|
||||||
|
@ -9,11 +9,12 @@ const $ = require('jquery')
|
|||||||
const {ipcRenderer, shell, webFrame} = require('electron')
|
const {ipcRenderer, shell, webFrame} = require('electron')
|
||||||
const remote = require('@electron/remote')
|
const remote = require('@electron/remote')
|
||||||
const isDev = require('./assets/js/isdev')
|
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 loggerUICore = LoggerUtil1('%c[UICore]', 'color: #000668; font-weight: bold')
|
||||||
const loggerAutoUpdater = LoggerUtil('%c[AutoUpdater]', 'color: #000668; font-weight: bold')
|
const loggerAutoUpdater = LoggerUtil1('%c[AutoUpdater]', 'color: #000668; font-weight: bold')
|
||||||
const loggerAutoUpdaterSuccess = LoggerUtil('%c[AutoUpdater]', 'color: #209b07; font-weight: bold')
|
const loggerAutoUpdaterSuccess = LoggerUtil1('%c[AutoUpdater]', 'color: #209b07; font-weight: bold')
|
||||||
|
|
||||||
// Log deprecation and process warnings.
|
// Log deprecation and process warnings.
|
||||||
process.traceProcessWarnings = true
|
process.traceProcessWarnings = true
|
||||||
@ -49,7 +50,7 @@ if(!isDev){
|
|||||||
loggerAutoUpdaterSuccess.log('New update available', info.version)
|
loggerAutoUpdaterSuccess.log('New update available', info.version)
|
||||||
|
|
||||||
if(process.platform === 'darwin'){
|
if(process.platform === 'darwin'){
|
||||||
info.darwindownload = `https://github.com/dscalzi/HeliosLauncher/releases/download/v${info.version}/helioslauncher-setup-${info.version}${process.arch === 'arm64' ? '-arm64' : ''}.dmg`
|
info.darwindownload = `https://github.com/dscalzi/HeliosLauncher/releases/download/v${info.version}/Helios-Launcher-setup-${info.version}${process.arch === 'arm64' ? '-arm64' : '-x64'}.dmg`
|
||||||
showUpdateUI(info)
|
showUpdateUI(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,5 +2,8 @@
|
|||||||
* Script for welcome.ejs
|
* Script for welcome.ejs
|
||||||
*/
|
*/
|
||||||
document.getElementById('welcomeButton').addEventListener('click', e => {
|
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)
|
||||||
})
|
})
|
34
app/loginOptions.ejs
Normal file
34
app/loginOptions.ejs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<div id="loginOptionsContainer" style="display: none;">
|
||||||
|
<div id="loginOptionsContent">
|
||||||
|
<div class="loginOptionsMainContent">
|
||||||
|
<h2>Login Options</h2>
|
||||||
|
<div class="loginOptionActions">
|
||||||
|
<div class="loginOptionButtonContainer">
|
||||||
|
<button id="loginOptionMicrosoft" class="loginOptionButton">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 23 23">
|
||||||
|
<path fill="#f35325" d="M1 1h10v10H1z" />
|
||||||
|
<path fill="#81bc06" d="M12 1h10v10H12z" />
|
||||||
|
<path fill="#05a6f0" d="M1 12h10v10H1z" />
|
||||||
|
<path fill="#ffba08" d="M12 12h10v10H12z" />
|
||||||
|
</svg>
|
||||||
|
<span>Login with Microsoft</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="loginOptionButtonContainer">
|
||||||
|
<button id="loginOptionMojang" class="loginOptionButton">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 9.677 9.667">
|
||||||
|
<path d="M-26.332-12.098h2.715c-1.357.18-2.574 1.23-2.715 2.633z" fill="#fff" />
|
||||||
|
<path d="M2.598.022h7.07L9.665 7c-.003 1.334-1.113 2.46-2.402 2.654H0V2.542C.134 1.2 1.3.195 2.598.022z" fill="#db2331" />
|
||||||
|
<path d="M1.54 2.844c.314-.76 1.31-.46 1.954-.528.785-.083 1.503.272 2.1.758l.164-.9c.327.345.587.756.964 1.052.28.254.655-.342.86-.013.42.864.408 1.86.54 2.795l-.788-.373C6.9 4.17 5.126 3.052 3.656 3.685c-1.294.592-1.156 2.65.06 3.255 1.354.703 2.953.51 4.405.292-.07.42-.34.87-.834.816l-4.95.002c-.5.055-.886-.413-.838-.89l.04-4.315z" fill="#fff" />
|
||||||
|
</svg>
|
||||||
|
<span>Login with Mojang</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="loginOptionCancelContainer" style="display: none;">
|
||||||
|
<button id="loginOptionCancelButton">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="./assets/js/scripts/loginOptions.js"></script>
|
||||||
|
</div>
|
@ -28,16 +28,45 @@
|
|||||||
<span class="settingsTabHeaderText">Account Settings</span>
|
<span class="settingsTabHeaderText">Account Settings</span>
|
||||||
<span class="settingsTabHeaderDesc">Add new accounts or manage existing ones.</span>
|
<span class="settingsTabHeaderDesc">Add new accounts or manage existing ones.</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="settingsAddAccountContainer">
|
<div class="settingsAuthAccountTypeContainer">
|
||||||
<button id="settingsAddAccount">
|
<div class="settingsAuthAccountTypeHeader">
|
||||||
<span id="settingsAddAccountText">+ Add Account</span>
|
<div class="settingsAuthAccountTypeHeaderLeft">
|
||||||
</button>
|
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 23 23">
|
||||||
|
<path fill="#f35325" d="M1 1h10v10H1z" />
|
||||||
|
<path fill="#81bc06" d="M12 1h10v10H12z" />
|
||||||
|
<path fill="#05a6f0" d="M1 12h10v10H1z" />
|
||||||
|
<path fill="#ffba08" d="M12 12h10v10H12z" />
|
||||||
|
</svg>
|
||||||
|
<span>Microsoft</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="settingsCurrentAccountsHeader">
|
<div class="settingsAuthAccountTypeHeaderRight">
|
||||||
<span class="settingsFieldTitle">Current Accounts</span>
|
<button class="settingsAddAuthAccount" id="settingsAddMicrosoftAccount">+ Add Microsoft Account</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settingsCurrentAccounts" id="settingsCurrentMicrosoftAccounts">
|
||||||
|
<!-- Microsoft auth accounts populated here. -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settingsAuthAccountTypeContainer">
|
||||||
|
<div class="settingsAuthAccountTypeHeader">
|
||||||
|
<div class="settingsAuthAccountTypeHeaderLeft">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 9.677 9.667">
|
||||||
|
<path d="M-26.332-12.098h2.715c-1.357.18-2.574 1.23-2.715 2.633z" fill="#fff" />
|
||||||
|
<path d="M2.598.022h7.07L9.665 7c-.003 1.334-1.113 2.46-2.402 2.654H0V2.542C.134 1.2 1.3.195 2.598.022z" fill="#db2331" />
|
||||||
|
<path d="M1.54 2.844c.314-.76 1.31-.46 1.954-.528.785-.083 1.503.272 2.1.758l.164-.9c.327.345.587.756.964 1.052.28.254.655-.342.86-.013.42.864.408 1.86.54 2.795l-.788-.373C6.9 4.17 5.126 3.052 3.656 3.685c-1.294.592-1.156 2.65.06 3.255 1.354.703 2.953.51 4.405.292-.07.42-.34.87-.834.816l-4.95.002c-.5.055-.886-.413-.838-.89l.04-4.315z" fill="#fff" />
|
||||||
|
</svg>
|
||||||
|
<span>Mojang</span>
|
||||||
|
</div>
|
||||||
|
<div class="settingsAuthAccountTypeHeaderRight">
|
||||||
|
<button class="settingsAddAuthAccount" id="settingsAddMojangAccount">+ Add Mojang Account</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settingsCurrentAccounts" id="settingsCurrentMojangAccounts">
|
||||||
|
<!-- Mojang auth accounts populated here. -->
|
||||||
</div>
|
</div>
|
||||||
<div id="settingsCurrentAccounts">
|
|
||||||
<!-- Auth accounts populated here. -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="settingsTabMinecraft" class="settingsTab" style="display: none;">
|
<div id="settingsTabMinecraft" class="settingsTab" style="display: none;">
|
||||||
|
8
app/waiting.ejs
Normal file
8
app/waiting.ejs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<div id="waitingContainer" style="display: none;">
|
||||||
|
<div id="waitingContent">
|
||||||
|
<div class="waitingSpinner"></div>
|
||||||
|
<div id="waitingTextContainer">
|
||||||
|
<h2>Waiting for Microsoft..</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
35
docs/MicrosoftAuth.md
Normal file
35
docs/MicrosoftAuth.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Microsoft Authentication
|
||||||
|
|
||||||
|
Authenticating with Microsoft is fully supported by Helios Launcher.
|
||||||
|
|
||||||
|
## Acquiring an Azure Client ID
|
||||||
|
|
||||||
|
1. Navigate to https://portal.azure.com
|
||||||
|
2. In the search bar, search for **Azure Active Directory**.
|
||||||
|
3. In Azure Active Directory, go to **App Registrations** on the left pane (Under *Manage*).
|
||||||
|
4. Click **New Registration**.
|
||||||
|
- Set **Name** to be your launcher's name.
|
||||||
|
- Set **Supported account types** to *Accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)*
|
||||||
|
- Leave **Redirect URI** blank.
|
||||||
|
- Register the application.
|
||||||
|
5. You should be on the application's management page. If not, Navigate back to **App Registrations**. Select the application you just registered.
|
||||||
|
6. Click **Authentication** on the left pane (Under *Manage*).
|
||||||
|
7. Click **Add Platform**.
|
||||||
|
- Select **Mobile and desktop applications**.
|
||||||
|
- Choose `https://login.microsoftonline.com/common/oauth2/nativeclient` as the **Redirect URI**.
|
||||||
|
- Select **Configure** to finish adding the platform.
|
||||||
|
8. Navigate back to **Overview**.
|
||||||
|
9. Copy **Application (client) ID**.
|
||||||
|
|
||||||
|
|
||||||
|
Reference: https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app
|
||||||
|
|
||||||
|
## Adding the Azure Client ID to Helios Launcher.
|
||||||
|
|
||||||
|
In `app/assets/js/ipcconstants.js` you'll find **`AZURE_CLIENT_ID`**. Set it to your application's id.
|
||||||
|
|
||||||
|
Note: Azure Client ID is NOT a secret value and **can** be stored in git. Reference: https://stackoverflow.com/questions/57306964/are-azure-active-directorys-tenantid-and-clientid-considered-secrets
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
You can now authenticate with Microsoft through the launcher.
|
@ -2,13 +2,13 @@ appId: 'helioslauncher'
|
|||||||
productName: 'Helios Launcher'
|
productName: 'Helios Launcher'
|
||||||
artifactName: '${productName}-setup-${version}.${ext}'
|
artifactName: '${productName}-setup-${version}.${ext}'
|
||||||
|
|
||||||
copyright: 'Copyright © 2018-2021 Daniel Scalzi'
|
copyright: 'Copyright © 2018-2022 Daniel Scalzi'
|
||||||
|
|
||||||
asar: true
|
asar: true
|
||||||
compression: 'maximum'
|
compression: 'maximum'
|
||||||
|
|
||||||
files:
|
files:
|
||||||
- '!{dist,.gitignore,.vscode,docs,dev-app-update.yml,.travis.yml,.nvmrc,.eslintrc.json,build.js}'
|
- '!{dist,.gitignore,.vscode,docs,dev-app-update.yml,.nvmrc,.eslintrc.json}'
|
||||||
|
|
||||||
extraResources:
|
extraResources:
|
||||||
- 'libraries'
|
- 'libraries'
|
||||||
|
131
index.js
131
index.js
@ -2,7 +2,7 @@ const remoteMain = require('@electron/remote/main')
|
|||||||
remoteMain.initialize()
|
remoteMain.initialize()
|
||||||
|
|
||||||
// Requirements
|
// Requirements
|
||||||
const { app, BrowserWindow, ipcMain, Menu } = require('electron')
|
const { app, BrowserWindow, ipcMain, Menu, shell } = require('electron')
|
||||||
const autoUpdater = require('electron-updater').autoUpdater
|
const autoUpdater = require('electron-updater').autoUpdater
|
||||||
const ejse = require('ejs-electron')
|
const ejse = require('ejs-electron')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
@ -10,6 +10,7 @@ const isDev = require('./app/assets/js/isdev')
|
|||||||
const path = require('path')
|
const path = require('path')
|
||||||
const semver = require('semver')
|
const semver = require('semver')
|
||||||
const { pathToFileURL } = require('url')
|
const { pathToFileURL } = require('url')
|
||||||
|
const { AZURE_CLIENT_ID, MSFT_OPCODE, MSFT_REPLY_TYPE, MSFT_ERROR, SHELL_OPCODE } = require('./app/assets/js/ipcconstants')
|
||||||
|
|
||||||
// Setup auto updater.
|
// Setup auto updater.
|
||||||
function initAutoUpdater(event, data) {
|
function initAutoUpdater(event, data) {
|
||||||
@ -84,12 +85,136 @@ ipcMain.on('distributionIndexDone', (event, res) => {
|
|||||||
event.sender.send('distributionIndexDone', res)
|
event.sender.send('distributionIndexDone', res)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Handle trash item.
|
||||||
|
ipcMain.handle(SHELL_OPCODE.TRASH_ITEM, async (event, ...args) => {
|
||||||
|
try {
|
||||||
|
await shell.trashItem(args[0])
|
||||||
|
return {
|
||||||
|
result: true
|
||||||
|
}
|
||||||
|
} catch(error) {
|
||||||
|
return {
|
||||||
|
result: false,
|
||||||
|
error: error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Disable hardware acceleration.
|
// Disable hardware acceleration.
|
||||||
// https://electronjs.org/docs/tutorial/offscreen-rendering
|
// https://electronjs.org/docs/tutorial/offscreen-rendering
|
||||||
app.disableHardwareAcceleration()
|
app.disableHardwareAcceleration()
|
||||||
|
|
||||||
// https://github.com/electron/electron/issues/18397
|
|
||||||
app.allowRendererProcessReuse = true
|
const REDIRECT_URI_PREFIX = 'https://login.microsoftonline.com/common/oauth2/nativeclient?'
|
||||||
|
|
||||||
|
// Microsoft Auth Login
|
||||||
|
let msftAuthWindow
|
||||||
|
let msftAuthSuccess
|
||||||
|
let msftAuthViewSuccess
|
||||||
|
let msftAuthViewOnClose
|
||||||
|
ipcMain.on(MSFT_OPCODE.OPEN_LOGIN, (ipcEvent, ...arguments_) => {
|
||||||
|
if (msftAuthWindow) {
|
||||||
|
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.ALREADY_OPEN, msftAuthViewOnClose)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msftAuthSuccess = false
|
||||||
|
msftAuthViewSuccess = arguments_[0]
|
||||||
|
msftAuthViewOnClose = arguments_[1]
|
||||||
|
msftAuthWindow = new BrowserWindow({
|
||||||
|
title: 'Microsoft Login',
|
||||||
|
backgroundColor: '#222222',
|
||||||
|
width: 520,
|
||||||
|
height: 600,
|
||||||
|
frame: true,
|
||||||
|
icon: getPlatformIcon('SealCircle')
|
||||||
|
})
|
||||||
|
|
||||||
|
msftAuthWindow.on('closed', () => {
|
||||||
|
msftAuthWindow = undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
msftAuthWindow.on('close', () => {
|
||||||
|
if(!msftAuthSuccess) {
|
||||||
|
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.NOT_FINISHED, msftAuthViewOnClose)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
msftAuthWindow.webContents.on('did-navigate', (_, uri) => {
|
||||||
|
if (uri.startsWith(REDIRECT_URI_PREFIX)) {
|
||||||
|
let queries = uri.substring(REDIRECT_URI_PREFIX.length).split('#', 1).toString().split('&')
|
||||||
|
let queryMap = {}
|
||||||
|
|
||||||
|
queries.forEach(query => {
|
||||||
|
const [name, value] = query.split('=')
|
||||||
|
queryMap[name] = decodeURI(value)
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.SUCCESS, queryMap, msftAuthViewSuccess)
|
||||||
|
|
||||||
|
msftAuthSuccess = true
|
||||||
|
msftAuthWindow.close()
|
||||||
|
msftAuthWindow = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
msftAuthWindow.removeMenu()
|
||||||
|
msftAuthWindow.loadURL(`https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?prompt=select_account&client_id=${AZURE_CLIENT_ID}&response_type=code&scope=XboxLive.signin%20offline_access&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Microsoft Auth Logout
|
||||||
|
let msftLogoutWindow
|
||||||
|
let msftLogoutSuccess
|
||||||
|
let msftLogoutSuccessSent
|
||||||
|
ipcMain.on(MSFT_OPCODE.OPEN_LOGOUT, (ipcEvent, uuid, isLastAccount) => {
|
||||||
|
if (msftLogoutWindow) {
|
||||||
|
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.ALREADY_OPEN)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msftLogoutSuccess = false
|
||||||
|
msftLogoutSuccessSent = false
|
||||||
|
msftLogoutWindow = new BrowserWindow({
|
||||||
|
title: 'Microsoft Logout',
|
||||||
|
backgroundColor: '#222222',
|
||||||
|
width: 520,
|
||||||
|
height: 600,
|
||||||
|
frame: true,
|
||||||
|
icon: getPlatformIcon('SealCircle')
|
||||||
|
})
|
||||||
|
|
||||||
|
msftLogoutWindow.on('closed', () => {
|
||||||
|
msftLogoutWindow = undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
msftLogoutWindow.on('close', () => {
|
||||||
|
if(!msftLogoutSuccess) {
|
||||||
|
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.NOT_FINISHED)
|
||||||
|
} else if(!msftLogoutSuccessSent) {
|
||||||
|
msftLogoutSuccessSent = true
|
||||||
|
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.SUCCESS, uuid, isLastAccount)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
msftLogoutWindow.webContents.on('did-navigate', (_, uri) => {
|
||||||
|
if(uri.startsWith('https://login.microsoftonline.com/common/oauth2/v2.0/logoutsession')) {
|
||||||
|
msftLogoutSuccess = true
|
||||||
|
setTimeout(() => {
|
||||||
|
if(!msftLogoutSuccessSent) {
|
||||||
|
msftLogoutSuccessSent = true
|
||||||
|
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.SUCCESS, uuid, isLastAccount)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(msftLogoutWindow) {
|
||||||
|
msftLogoutWindow.close()
|
||||||
|
msftLogoutWindow = null
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
msftLogoutWindow.removeMenu()
|
||||||
|
msftLogoutWindow.loadURL('https://login.microsoftonline.com/common/oauth2/v2.0/logout')
|
||||||
|
})
|
||||||
|
|
||||||
// Keep a global reference of the window object, if you don't, the window will
|
// Keep a global reference of the window object, if you don't, the window will
|
||||||
// be closed automatically when the JavaScript object is garbage collected.
|
// be closed automatically when the JavaScript object is garbage collected.
|
||||||
|
6850
package-lock.json
generated
6850
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "helioslauncher",
|
"name": "helioslauncher",
|
||||||
"version": "1.8.0",
|
"version": "1.9.0",
|
||||||
"productName": "Helios Launcher",
|
"productName": "Helios Launcher",
|
||||||
"description": "Modded Minecraft Launcher",
|
"description": "Modded Minecraft Launcher",
|
||||||
"author": "Daniel Scalzi (https://github.com/dscalzi/)",
|
"author": "Daniel Scalzi (https://github.com/dscalzi/)",
|
||||||
@ -20,28 +20,31 @@
|
|||||||
"lint": "eslint --config .eslintrc.json ."
|
"lint": "eslint --config .eslintrc.json ."
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "14.x.x"
|
"node": "16.x.x"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron/remote": "^2.0.1",
|
"@electron/remote": "^2.0.5",
|
||||||
"adm-zip": "^0.5.7",
|
"adm-zip": "^0.5.9",
|
||||||
"async": "^3.2.1",
|
"async": "^3.2.3",
|
||||||
"discord-rpc": "^3.2.0",
|
"discord-rpc-patch": "^4.0.1",
|
||||||
"ejs": "^3.1.6",
|
"ejs": "^3.1.6",
|
||||||
"ejs-electron": "^2.1.1",
|
"ejs-electron": "^2.1.1",
|
||||||
"electron-updater": "^4.3.9",
|
"electron-updater": "^4.6.5",
|
||||||
"fs-extra": "^10.0.0",
|
"fs-extra": "^10.0.1",
|
||||||
"github-syntax-dark": "^0.5.0",
|
"github-syntax-dark": "^0.5.0",
|
||||||
|
"got": "^11.8.3",
|
||||||
|
"helios-core": "~0.1.0",
|
||||||
"jquery": "^3.6.0",
|
"jquery": "^3.6.0",
|
||||||
|
"node-stream-zip": "^1.15.0",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"semver": "^7.3.5",
|
"semver": "^7.3.5",
|
||||||
"tar-fs": "^2.1.1",
|
"tar-fs": "^2.1.1",
|
||||||
"winreg": "^1.2.4"
|
"winreg": "^1.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "^13.5.1",
|
"electron": "^17.1.0",
|
||||||
"electron-builder": "^22.11.7",
|
"electron-builder": "^22.14.13",
|
||||||
"eslint": "^7.32.0"
|
"eslint": "^8.10.0"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
Loading…
Reference in New Issue
Block a user