Compare commits

..

14 Commits

Author SHA1 Message Date
Daniel Scalzi
f6f8a7ed3c
Add version.jar to cp until 1.17. 2022-04-03 17:33:18 -04:00
Daniel Scalzi
168b4431ae
Patches to get 1.17 working, need to revise into real solutions. 2022-04-03 17:33:18 -04:00
Daniel Scalzi
ee61ea4979
Improve handling of JVM arguments on the settings view. 2022-04-03 17:32:59 -04:00
Daniel Scalzi
4e2c9ce3ec
Set user type to msa for msft accounts. 2022-04-03 17:08:55 -04:00
Daniel Scalzi
0bc74d9c66
Electron 18. 2022-04-03 16:00:09 -04:00
Daniel Scalzi
d7d0803661
Update dependencies. 2022-03-25 15:08:38 -04:00
Daniel Scalzi
15fc12b625
Update to Electron 17, fix deleting drop-in mods from the settings view. 2022-03-04 00:02:52 -05:00
Daniel Scalzi
ccf099a5cf
Fix macOS executable name. 2022-02-15 17:45:47 -05:00
Daniel Scalzi
b092722488
Calculate expiry date should not be async. 2022-02-15 09:09:26 -05:00
Daniel Scalzi
fc289262a8
Show GitHub actions build status in the README.
Rename workflow since slash character is not supported by shields.io.
2022-02-14 02:28:16 -05:00
Daniel Scalzi
a8c92ab272
Fix branch name in comment. 2022-02-12 11:47:21 -05:00
Daniel Scalzi
f6b787c526
Update README.md 2022-02-12 10:58:19 -05:00
Daniel Scalzi
78a4f7bbf3
v1.9.0 2022-02-11 20:24:25 -05:00
Daniel Scalzi
58e68c116c
Microsoft Authentication (#216) 2022-02-11 19:51:28 -05:00
28 changed files with 1444 additions and 566 deletions

View File

@ -1,4 +1,4 @@
name: Build/release name: Build
on: push on: push

View File

@ -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

View File

@ -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` |
@ -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.

View File

@ -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>

View File

@ -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);

View 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

View 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

View File

@ -9,17 +9,19 @@
* @module authmanager * @module authmanager
*/ */
// Requirements // Requirements
const ConfigManager = require('./configmanager') const ConfigManager = require('./configmanager')
const { LoggerUtil } = require('helios-core') const { LoggerUtil } = require('helios-core')
const { MojangRestAPI, mojangErrorDisplayable, MojangErrorCode } = require('helios-core/mojang')
const { RestResponseStatus } = require('helios-core/common') const { RestResponseStatus } = require('helios-core/common')
const { MojangRestAPI, mojangErrorDisplayable, MojangErrorCode } = require('helios-core/mojang')
const { MicrosoftAuth, microsoftErrorDisplayable, MicrosoftErrorCode } = require('helios-core/microsoft')
const { AZURE_CLIENT_ID } = require('./ipcconstants')
const log = LoggerUtil.getLogger('AuthManager') 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.
* *
@ -27,7 +29,7 @@ const log = LoggerUtil.getLogger('AuthManager')
* @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 response = await MojangRestAPI.authenticate(username, password, ConfigManager.getClientToken()) const response = await MojangRestAPI.authenticate(username, password, ConfigManager.getClientToken())
console.log(response) console.log(response)
@ -35,7 +37,7 @@ exports.addAccount = async function(username, password){
const session = response.data 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)
} }
@ -55,14 +57,113 @@ exports.addAccount = async function(username, password){
} }
} }
const AUTH_MODE = { FULL: 0, MS_REFRESH: 1, MC_REFRESH: 2 }
/** /**
* Remove an account. This will invalidate the access token associated * 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))
}
}
/**
* 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)
const response = await MojangRestAPI.invalidate(authAcc.accessToken, ConfigManager.getClientToken()) const response = await MojangRestAPI.invalidate(authAcc.accessToken, ConfigManager.getClientToken())
@ -80,17 +181,33 @@ exports.removeAccount = async function(uuid){
} }
} }
/**
* 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.save()
return Promise.resolve()
} catch (err){
log.error('Error while removing account', err)
return Promise.reject(err)
}
}
/** /**
* Validate the selected account with Mojang's authserver. If the account is not valid, * Validate the selected account with Mojang's authserver. If the account is not valid,
* 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, * @returns {Promise.<boolean>} Promise which resolves to true if the access token is valid,
* otherwise false. * otherwise false.
*/ */
exports.validateSelected = async function(){ async function validateSelectedMojangAccount(){
const current = ConfigManager.getSelectedAccount() const current = ConfigManager.getSelectedAccount()
const response = await MojangRestAPI.validate(current.accessToken, ConfigManager.getClientToken()) const response = await MojangRestAPI.validate(current.accessToken, ConfigManager.getClientToken())
@ -100,7 +217,7 @@ exports.validateSelected = async function(){
const refreshResponse = await MojangRestAPI.refresh(current.accessToken, ConfigManager.getClientToken()) const refreshResponse = await MojangRestAPI.refresh(current.accessToken, ConfigManager.getClientToken())
if(refreshResponse.responseStatus === RestResponseStatus.SUCCESS) { if(refreshResponse.responseStatus === RestResponseStatus.SUCCESS) {
const session = refreshResponse.data const session = refreshResponse.data
ConfigManager.updateAuthAccount(current.uuid, session.accessToken) ConfigManager.updateMojangAuthAccount(current.uuid, session.accessToken)
ConfigManager.save() ConfigManager.save()
} else { } else {
log.error('Error while validating selected profile:', refreshResponse.error) log.error('Error while validating selected profile:', refreshResponse.error)
@ -115,4 +232,84 @@ exports.validateSelected = async function(){
} }
} }
}
/**
* 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,
* otherwise false.
*/
exports.validateSelected = async function(){
const current = ConfigManager.getSelectedAccount()
if(current.type === 'microsoft') {
return await validateSelectedMicrosoftAccount()
} else {
return await validateSelectedMojangAccount()
}
} }

View File

@ -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

View File

@ -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
} }
/** /**

View 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'
}

View File

@ -491,7 +491,7 @@ class ProcessBuilder {
val = this.authUser.accessToken val = this.authUser.accessToken
break break
case 'user_type': case 'user_type':
val = 'mojang' val = this.authUser.type === 'microsoft' ? 'msa' : 'mojang'
break break
case 'version_type': case 'version_type':
val = this.versionData.type val = this.versionData.type
@ -589,7 +589,7 @@ class ProcessBuilder {
val = this.authUser.accessToken val = this.authUser.accessToken
break break
case 'user_type': case 'user_type':
val = 'mojang' val = this.authUser.type === 'microsoft' ? 'msa' : 'mojang'
break break
case 'user_properties': // 1.8.9 and below. case 'user_properties': // 1.8.9 and below.
val = '{}' val = '{}'

View File

@ -10,7 +10,7 @@ const { MojangRestAPI, getServerStatus } = require('helios-core/mojang')
// Internal Requirements // Internal Requirements
const DiscordWrapper = require('./assets/js/discordwrapper') const DiscordWrapper = require('./assets/js/discordwrapper')
const ProcessBuilder = require('./assets/js/processbuilder') const ProcessBuilder = require('./assets/js/processbuilder')
const { RestResponseStatus } = require('helios-core/common') 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 */
@ -293,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()
@ -495,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()

View File

@ -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')
/** /**
@ -189,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')
@ -214,13 +214,26 @@ loginButton.addEventListener('click', () => {
}, 1000) }, 1000)
}).catch((displayableError) => { }).catch((displayableError) => {
loginLoading(false) loginLoading(false)
setOverlayContent(displayableError.title, displayableError.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.', displayableError)
}) })
}) })

View 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
}
})
}

View File

@ -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()
} }

View File

@ -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()
@ -179,7 +180,11 @@ function saveSettingsValues(){
if(v.type === 'number' || v.type === 'text'){ if(v.type === 'number' || v.type === 'text'){
// Special Conditions // Special Conditions
if(cVal === 'JVMOptions'){ if(cVal === 'JVMOptions'){
sFn(v.value.split(' ')) if(!v.value.trim()) {
sFn([])
} else {
sFn(v.value.trim().split(/\s+/))
}
} else { } else {
sFn(v.value) sFn(v.value)
} }
@ -314,8 +319,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 +331,102 @@ document.getElementById('settingsAddAccount').onclick = (e) => {
}) })
} }
// Bind the add microsoft account button.
document.getElementById('settingsAddMicrosoftAccount').onclick = (e) => {
switchView(getCurrentView(), VIEWS.waiting, 500, 500, () => {
ipcRenderer.send(MSFT_OPCODE.OPEN_LOGIN, VIEWS.settings, VIEWS.settings)
})
}
// Bind reply for Microsoft Login.
ipcRenderer.on(MSFT_OPCODE.REPLY_LOGIN, (_, ...arguments_) => {
if (arguments_[0] === MSFT_REPLY_TYPE.ERROR) {
const viewOnClose = arguments_[2]
console.log(arguments_)
switchView(getCurrentView(), viewOnClose, 500, 500, () => {
if(arguments_[1] === MSFT_ERROR.NOT_FINISHED) {
// User cancelled.
msftLoginLogger.info('Login cancelled by user.')
return
}
// Unexpected error.
setOverlayContent(
'Something Went Wrong',
'Microsoft authentication failed. Please try again.',
'OK'
)
setOverlayHandler(() => {
toggleOverlay(false)
})
toggleOverlay(true)
})
} else if(arguments_[0] === MSFT_REPLY_TYPE.SUCCESS) {
const queryMap = arguments_[1]
const viewOnClose = arguments_[2]
// Error from request to Microsoft.
if (Object.prototype.hasOwnProperty.call(queryMap, 'error')) {
switchView(getCurrentView(), viewOnClose, 500, 500, () => {
// TODO Dont know what these errors are. Just show them I guess.
// This is probably if you messed up the app registration with Azure.
console.log('Error getting authCode, is Azure application registered correctly?')
console.log(error)
console.log(error_description)
console.log('Full query map', queryMap)
let error = queryMap.error // Error might be 'access_denied' ?
let errorDesc = queryMap.error_description
setOverlayContent(
error,
errorDesc,
'OK'
)
setOverlayHandler(() => {
toggleOverlay(false)
})
toggleOverlay(true)
})
} else {
msftLoginLogger.info('Acquired authCode, proceeding with authentication.')
const authCode = queryMap.code
AuthManager.addMicrosoftAccount(authCode).then(value => {
updateSelectedAccount(value)
switchView(getCurrentView(), viewOnClose, 500, 500, () => {
prepareSettings()
})
})
.catch((displayableError) => {
let actualDisplayableError
if(isDisplayableError(displayableError)) {
msftLoginLogger.error('Error while logging in.', displayableError)
actualDisplayableError = displayableError
} else {
// Uh oh.
msftLoginLogger.error('Unhandled error during login.', displayableError)
actualDisplayableError = {
title: 'Unknown Error During Login',
desc: 'An unknown error has occurred. Please see the console for details.'
}
}
switchView(getCurrentView(), viewOnClose, 500, 500, () => {
setOverlayContent(actualDisplayableError.title, actualDisplayableError.desc, Lang.queryJS('login.tryAgain'))
setOverlayHandler(() => {
toggleOverlay(false)
})
toggleOverlay(true)
})
})
}
}
})
/** /**
* Bind functionality for the account selection buttons. If another account * 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 +471,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 +484,7 @@ function bindAuthAccountLogOut(){
}) })
} }
let msAccDomElementCache
/** /**
* Process a log out. * Process a log out.
* *
@ -391,19 +495,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(!isLastAccount && uuid === prevSelAcc.uuid){ if(targetAcc.type === 'microsoft') {
const selAcc = ConfigManager.getSelectedAccount() msAccDomElementCache = parent
refreshAuthAccountSelected(selAcc.uuid) switchView(getCurrentView(), VIEWS.waiting, 500, 500, () => {
updateSelectedAccount(selAcc) ipcRenderer.send(MSFT_OPCODE.OPEN_LOGOUT, uuid, isLastAccount)
validateSelectedAccount() })
} } else {
}) AuthManager.removeMojangAccount(uuid).then(() => {
$(parent).fadeOut(250, () => { if(!isLastAccount && uuid === prevSelAcc.uuid){
parent.remove() const selAcc = ConfigManager.getSelectedAccount()
}) refreshAuthAccountSelected(selAcc.uuid)
updateSelectedAccount(selAcc)
validateSelectedAccount()
}
if(isLastAccount) {
loginOptionsCancelEnabled(false)
loginOptionsViewOnLoginSuccess = VIEWS.settings
loginOptionsViewOnLoginCancel = VIEWS.loginOptions
switchView(getCurrentView(), VIEWS.loginOptions)
}
})
$(parent).fadeOut(250, () => {
parent.remove()
})
}
} }
// Bind reply for Microsoft Logout.
ipcRenderer.on(MSFT_OPCODE.REPLY_LOGOUT, (_, ...arguments_) => {
if (arguments_[0] === MSFT_REPLY_TYPE.ERROR) {
switchView(getCurrentView(), VIEWS.settings, 500, 500, () => {
if(arguments_.length > 1 && arguments_[1] === MSFT_ERROR.NOT_FINISHED) {
// User cancelled.
msftLogoutLogger.info('Logout cancelled by user.')
return
}
// Unexpected error.
setOverlayContent(
'Something Went Wrong',
'Microsoft logout failed. Please try again.',
'OK'
)
setOverlayHandler(() => {
toggleOverlay(false)
})
toggleOverlay(true)
})
} else if(arguments_[0] === MSFT_REPLY_TYPE.SUCCESS) {
const uuid = arguments_[1]
const isLastAccount = arguments_[2]
const prevSelAcc = ConfigManager.getSelectedAccount()
msftLogoutLogger.info('Logout Successful. uuid:', uuid)
AuthManager.removeMicrosoftAccount(uuid)
.then(() => {
if(!isLastAccount && uuid === prevSelAcc.uuid){
const selAcc = ConfigManager.getSelectedAccount()
refreshAuthAccountSelected(selAcc.uuid)
updateSelectedAccount(selAcc)
validateSelectedAccount()
}
if(isLastAccount) {
loginOptionsCancelEnabled(false)
loginOptionsViewOnLoginSuccess = VIEWS.settings
loginOptionsViewOnLoginCancel = VIEWS.loginOptions
switchView(getCurrentView(), VIEWS.loginOptions)
}
if(msAccDomElementCache) {
msAccDomElementCache.remove()
msAccDomElementCache = null
}
})
.finally(() => {
if(!isLastAccount) {
switchView(getCurrentView(), VIEWS.settings, 500, 500)
}
})
}
})
/** /**
* Refreshes the status of the selected account on the auth account * Refreshes the status of the selected account on the auth account
* elements. * elements.
@ -425,7 +601,8 @@ function refreshAuthAccountSelected(uuid){
}) })
} }
const settingsCurrentAccounts = document.getElementById('settingsCurrentAccounts') const settingsCurrentMicrosoftAccounts = document.getElementById('settingsCurrentMicrosoftAccounts')
const settingsCurrentMojangAccounts = document.getElementById('settingsCurrentMojangAccounts')
/** /**
* Add auth account elements for each one stored in the authentication database. * Add auth account elements for each one stored in the authentication database.
@ -438,11 +615,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 +644,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
} }
/** /**

View File

@ -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(() => {
document.getElementById('loginUsername').value = selectedAcc.username
validateEmail(selectedAcc.username) const isMicrosoft = selectedAcc.type === 'microsoft'
loginViewOnSuccess = getCurrentView()
loginViewOnCancel = getCurrentView() if(isMicrosoft) {
if(accLen > 0){ // Empty for now
loginViewCancelHandler = () => { } else {
ConfigManager.addAuthAccount(selectedAcc.uuid, selectedAcc.accessToken, selectedAcc.username, selectedAcc.displayName) // Mojang
// For convenience, pre-populate the username of the account.
document.getElementById('loginUsername').value = selectedAcc.username
validateEmail(selectedAcc.username)
}
loginOptionsViewOnLoginSuccess = getCurrentView()
loginOptionsViewOnLoginCancel = VIEWS.loginOptions
if(accLen > 0) {
loginOptionsViewOnCancel = getCurrentView()
loginOptionsViewCancelHandler = () => {
if(isMicrosoft) {
ConfigManager.addMicrosoftAuthAccount(
selectedAcc.uuid,
selectedAcc.accessToken,
selectedAcc.username,
selectedAcc.expiresAt,
selectedAcc.microsoft.access_token,
selectedAcc.microsoft.refresh_token,
selectedAcc.microsoft.expires_at
)
} else {
ConfigManager.addMojangAuthAccount(selectedAcc.uuid, selectedAcc.accessToken, selectedAcc.username, selectedAcc.displayName)
}
ConfigManager.save() 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){

View File

@ -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)
} }

View File

@ -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
View 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>

View File

@ -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">&#43; 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 class="settingsAuthAccountTypeHeaderRight">
<button class="settingsAddAuthAccount" id="settingsAddMicrosoftAccount">+ Add Microsoft Account</button>
</div>
</div>
<div class="settingsCurrentAccounts" id="settingsCurrentMicrosoftAccounts">
<!-- Microsoft auth accounts populated here. -->
</div>
</div> </div>
<div id="settingsCurrentAccountsHeader">
<span class="settingsFieldTitle">Current Accounts</span> <div class="settingsAuthAccountTypeContainer">
</div> <div class="settingsAuthAccountTypeHeader">
<div id="settingsCurrentAccounts"> <div class="settingsAuthAccountTypeHeaderLeft">
<!-- Auth accounts populated here. --> <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>
</div> </div>
<div id="settingsTabMinecraft" class="settingsTab" style="display: none;"> <div id="settingsTabMinecraft" class="settingsTab" style="display: none;">

8
app/waiting.ejs Normal file
View 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
View 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.

View File

@ -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'

144
index.js
View File

@ -2,14 +2,15 @@ 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')
const isDev = require('./app/assets/js/isdev') 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,10 +85,137 @@ 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()
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.
let win let win

756
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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/)",
@ -23,17 +23,17 @@
"node": "16.x.x" "node": "16.x.x"
}, },
"dependencies": { "dependencies": {
"@electron/remote": "^2.0.4", "@electron/remote": "^2.0.8",
"adm-zip": "^0.5.9", "adm-zip": "^0.5.9",
"async": "^3.2.3", "async": "^3.2.3",
"discord-rpc-patch": "^4.0.1", "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.6.5", "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", "got": "^11.8.3",
"helios-core": "^0.1.0-alpha.5", "helios-core": "~0.1.0",
"jquery": "^3.6.0", "jquery": "^3.6.0",
"node-stream-zip": "^1.15.0", "node-stream-zip": "^1.15.0",
"request": "^2.88.2", "request": "^2.88.2",
@ -42,9 +42,9 @@
"winreg": "^1.2.4" "winreg": "^1.2.4"
}, },
"devDependencies": { "devDependencies": {
"electron": "^16.0.8", "electron": "^18.0.1",
"electron-builder": "^22.14.13", "electron-builder": "^22.14.13",
"eslint": "^8.8.0" "eslint": "^8.12.0"
}, },
"repository": { "repository": {
"type": "git", "type": "git",