diff --git a/config.linux.patreon.toml b/config.linux.patreon.toml new file mode 100644 index 0000000..14ca6e6 --- /dev/null +++ b/config.linux.patreon.toml @@ -0,0 +1,39 @@ +installing_message = "Reminder: yuzu is an experimental emulator. Stuff will break!" +hide_advanced = true + +[[authentication]] + # Base64 encoded version of the public key for validating the JWT token. Must be in DER format + pub_key_base64 = "MIIBCgKCAQEAs5K6s49JVV9LBMzDrkORsoPSYsv1sCXDtxjp4pn8p0uPSvJAsbNNmdIgCjfSULzbHLM28MblnI4zYP8ZgKtkjdg+Ic5WQbS5iBAkf18zMafpOrotTArLsgZSmUfNYt0SOiN17D+sq/Ov/CKXRM9CttKkEbanBTVqkx7sxsHVbkI6tDvkboSaNeVPHzHlfAbvGrUo5cbAFCB/KnRsoxr+g7jLKTxU1w4xb/pIs91h80AXV/yZPXL6ItPM3/0noIRXjmoeYWf2sFQaFALNB2Kef0p6/hoHYUQP04ZSIL3Q+v13z5X2YJIlI4eLg+iD25QYm9V8oP3+Xro4vd47a0/maQIDAQAB" + # URL to authenticate against. This must return a JWT token with their permissions and a custom claim patreonInfo with the following structure + # "patreonInfo": { "linked": false, "activeSubscription": false } + # If successful, the frontend will use this JWT token as a Bearer Authentication when requesting the binaries to download + auth_url = "https://api.yuzu-emu.org/jwt/installer/" + [[authentication.validation]] + iss = "citra-core" + aud = "installer" + +[[packages]] +name = "yuzu" +default = true +requires_authorization = false +description = "The yuzu Mainline is for plebs. Please upgrade to patreon to git gud." +default = true + [packages.source] + name = "github" + match = "^yuzu-linux-[0-9]*-[0-9a-f]*.tar.xz$" + [packages.source.config] + repo = "yuzu-emu/yuzu-nightly" + +[[packages]] +name = "yuzu Early Access" +description = "The build for all those epic Chads out there who didn't have to steal 5 dollar from their mom to pay for this." +# Displayed when the package has no authentication for the user +need_authentication_description = "Click here to sign in with your yuzu account for Early Access" +# Displayed when the package has an authentication, but the user is not authorized +need_authorization_description = "You are signed in, but you do not have a current subscription! Click here for more details" +requires_authorization = true + [packages.source] + name = "github" + match = "^yuzu-linux-[0-9]*-[0-9a-f]*.tar.xz$" + [packages.source.config] + repo = "yuzu-emu/yuzu-canary" diff --git a/config.windows.patreon.toml b/config.windows.patreon.toml new file mode 100644 index 0000000..7ade327 --- /dev/null +++ b/config.windows.patreon.toml @@ -0,0 +1,51 @@ +installing_message = "Reminder: yuzu is an experimental emulator. Stuff will break!" +hide_advanced = true + +[authentication] +# Base64 encoded version of the public key for validating the JWT token. Must be in DER format +pub_key_base64 = "MIIBCgKCAQEAs5K6s49JVV9LBMzDrkORsoPSYsv1sCXDtxjp4pn8p0uPSvJAsbNNmdIgCjfSULzbHLM28MblnI4zYP8ZgKtkjdg+Ic5WQbS5iBAkf18zMafpOrotTArLsgZSmUfNYt0SOiN17D+sq/Ov/CKXRM9CttKkEbanBTVqkx7sxsHVbkI6tDvkboSaNeVPHzHlfAbvGrUo5cbAFCB/KnRsoxr+g7jLKTxU1w4xb/pIs91h80AXV/yZPXL6ItPM3/0noIRXjmoeYWf2sFQaFALNB2Kef0p6/hoHYUQP04ZSIL3Q+v13z5X2YJIlI4eLg+iD25QYm9V8oP3+Xro4vd47a0/maQIDAQAB" +# URL to authenticate against. This must return a JWT token with their permissions and a custom claim patreonInfo with the following structure +# "patreonInfo": { "linked": false, "activeSubscription": false } +# If successful, the frontend will use this JWT token as a Bearer Authentication when requesting the binaries to download +auth_url = "https://api.yuzu-emu.org/jwt/installer/" + [authentication.validation] + iss = "citra-core" + aud = "installer" + +[[packages]] +name = "yuzu" +default = true +requires_authorization = false +description = "The yuzu Mainline is for plebs. Please upgrade to patreon to git gud." + [packages.source] + name = "github" + match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.zip$" + [packages.source.config] + repo = "yuzu-emu/yuzu-nightly" + [[packages.shortcuts]] + name = "yuzu" + relative_path = "mainline/yuzu.exe" + description = "Launch yuzu (Mainline version)" + +[[packages]] +name = "yuzu Early Access" +description = "The build for all those epic Chads out there who didn't have to steal 5 dollar from their mom to pay for this." +# Displayed when the package has no authentication for the user +need_authentication_description = "Click here to sign in with your yuzu account for Early Access" +# Displayed when the package has an authentication, but the user has not linked their account +need_link_description = "You are signed in, but you need to link your Patreon account! Click here for more details" +# Displayed when the package has an authentication, but the user has not linked their account +need_subscription_description = "You are signed in, but you need to link your Patreon account! Click here for more details" +# Displayed when the package has an authentication, but the user has not linked their account +need_reward_tier_description = "You are signed in, but are not backing an eligible reward tier! Click here for more details" +requires_authorization = true + [packages.source] + name = "patreon" + match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.zip$" + [packages.source.config] + repo = "earlyaccess" + [[packages.shortcuts]] + name = "yuzu Early Access" + relative_path = "earlyaccess/yuzu.exe" + description = "Launch yuzu Early Access" + diff --git a/src/config.rs b/src/config.rs index e5ac346..a1aeea5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -36,6 +36,32 @@ pub struct PackageDescription { pub source: PackageSource, #[serde(default)] pub shortcuts: Vec, + #[serde(default)] + pub requires_authorization: Option, + #[serde(default)] + pub need_authentication_description: Option, + #[serde(default)] + pub need_link_description: Option, + #[serde(default)] + pub need_subscription_description: Option, + #[serde(default)] + pub need_reward_tier_description: Option, +} + +/// Configuration for validating the JWT token +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct JWTValidation { + pub iss: Option, + // This can technically be a Vec as well, but thats a pain to support atm + pub aud: Option, +} + +/// The configuration for this release. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AuthenticationConfig { + pub pub_key_base64: String, + pub auth_url: String, + pub validation: Option, } /// Describes the application itself. @@ -66,6 +92,8 @@ pub struct Config { pub packages: Vec, #[serde(default)] pub hide_advanced: bool, + #[serde(default)] + pub authentication: Option, } impl Config { diff --git a/src/frontend/rest/services/authentication.rs b/src/frontend/rest/services/authentication.rs new file mode 100644 index 0000000..cdbb2fa --- /dev/null +++ b/src/frontend/rest/services/authentication.rs @@ -0,0 +1,187 @@ + +use http::build_async_client; + +use hyper::header::{ContentLength, ContentType}; +use reqwest::header::{USER_AGENT}; +use futures::{Stream, Future}; +use jwt::{decode, Validation, Algorithm}; + +use frontend::rest::services::{WebService, Request, Response, default_future}; +use frontend::rest::services::Future as InternalFuture; +use logging::LoggingErrors; +use url::form_urlencoded; +use std::collections::HashMap; +use std::sync::Arc; + +/// claims struct, it needs to derive `Serialize` and/or `Deserialize` +#[derive(Debug, Serialize, Deserialize)] +struct JWTClaims { + sub: String, + iss: String, + aud: String, + exp: usize, + #[serde(default)] + roles: Vec, + #[serde(rename = "releaseChannels", default)] + channels: Vec, + #[serde(rename = "IsPatreonAccountLinked")] + is_linked: bool, + #[serde(rename = "IsPatreonSubscriptionActive")] + is_subscribed: bool, +} + +fn get_text(future: impl Future) -> impl Future { + future.map(|mut response| { + // Get the body of the response + match response.status() { + reqwest::StatusCode::OK => + Ok(response.text() + .map_err(|e| { + error!("Error while converting the response to text {:?}", e); + Response::new() + .with_status(hyper::StatusCode::InternalServerError) + })), + _ => { + error!("Error wrong response code from server {:?}", response.status()); + Err(Response::new() + .with_status(hyper::StatusCode::InternalServerError)) + } + } + }) + .map_err(|err| { + error!("Error cannot get text on errored stream {:?}", err); + Response::new() + .with_status(hyper::StatusCode::InternalServerError) + }) + .and_then(|x| x) + .flatten() +} + +pub fn handle(service: &WebService, _req: Request) -> InternalFuture { + let framework = service.framework.read().log_expect("InstallerFramework has been dirtied"); + let credentials = framework.database.credentials.clone(); + let config = framework.config.clone().unwrap(); + + // If authentication isn't configured, just return immediately + if config.authentication.is_none() { + return default_future(Response::new().with_status(hyper::Ok).with_body("{}")); + } + + // Create moveable framework references so that the lambdas can write to them later + let write_cred_fw = Arc::clone(&service.framework); + + Box::new( + _req.body().concat2().map(move |body| { + let req = form_urlencoded::parse(body.as_ref()) + .into_owned() + .collect::>(); + + // Determine which credentials we should use + let (username, token) = { + let req_username = req.get("username").unwrap(); + let req_token = req.get("token").unwrap(); + // if the user didn't provide credentials, and theres nothing stored in the database, return an early error + let req_cred_valid = !req_username.is_empty() && !req_token.is_empty(); + let stored_cred_valid = !credentials.username.is_empty() && !credentials.token.is_empty(); + if !req_cred_valid && !stored_cred_valid { + info!("No passed in credential and no stored credentials to validate"); + return default_future(Response::new().with_status(hyper::BadRequest)); + } + if req_cred_valid { + (req.get("username").unwrap().clone(), req.get("token").unwrap().clone()) + } else { + (credentials.username.clone(), credentials.token.clone()) + } + }; + + let authentication = config.authentication.unwrap(); + + // Get the public key for this authentication url + let pub_key = if authentication.pub_key_base64.is_empty() { + vec![] + } else { + match base64::decode(&authentication.pub_key_base64) { + Ok(v) => v, + Err(err) => { + error!("Configured public key was not empty and did not decode as base64 {:?}", err); + return default_future(Response::new().with_status(hyper::StatusCode::InternalServerError)); + }, + } + }; + + // Build the HTTP client up + let client = match build_async_client() { + Ok(v) => v, + Err(_) => { + return default_future(Response::new().with_status(hyper::StatusCode::InternalServerError)); + }, + }; + + // call the authentication URL to see if we are authenticated + Box::new(get_text( + client.post(&authentication.auth_url) + .header(USER_AGENT, "liftinstall (j-selby)") + .header("X-USERNAME", username.clone()) + .header("X-TOKEN", token.clone()) + .send() + ).map(move |body| { + // Configure validation for audience and issuer if the configuration provides it + let validation = match authentication.validation { + Some(v) => { + let mut valid = Validation::new(Algorithm::RS256); + valid.iss = v.iss; + if v.aud.is_some() { + valid.set_audience(&v.aud.unwrap()); + } + valid + } + None => Validation::default() + }; + + // Verify the JWT token + let tok = match decode::(&body, pub_key.as_slice(), &validation) { + Ok(v) => v, + Err(v) => { + error!("Error while decoding the JWT. error: {:?} str: {:?}", v, &body); + return Err(Response::new().with_status(hyper::StatusCode::InternalServerError)); + }, + }; + + { + // Store the validated username and password into the installer database + let mut framework = write_cred_fw.write().log_expect("InstallerFramework has been dirtied"); + framework.database.credentials.username = username.clone(); + framework.database.credentials.token = token.clone(); + // And store the JWT token temporarily in the + framework.authorization_token = Some(body.clone()); + } + + // Convert the json to a string and return the json token + match serde_json::to_string(&tok.claims) { + Ok(v) => Ok(v), + Err(e) => { + error!("Error while converting the claims to JSON string: {:?}", e); + Err(Response::new().with_status(hyper::StatusCode::InternalServerError)) + } + } + }) + .and_then(|res| res) + .map(|out| { + // Finally return the JSON with the response + info!("successfully verified username and token"); + Response::new() + .with_header(ContentLength(out.len() as u64)) + .with_header(ContentType::json()) + .with_status(hyper::StatusCode::Ok) + .with_body(out) + }) + .or_else(|err| { + // Convert the Err value into an Ok value since the error code from this HTTP request is an Ok(response) + Ok(err) + }) + ) + }) + // Flatten the internal future into the output response future + .flatten() + ) +} \ No newline at end of file diff --git a/src/frontend/rest/services/browser.rs b/src/frontend/rest/services/browser.rs new file mode 100644 index 0000000..32885cc --- /dev/null +++ b/src/frontend/rest/services/browser.rs @@ -0,0 +1,29 @@ + + +use frontend::rest::services::{WebService, Request, Response}; +use frontend::rest::services::Future as InternalFuture; +use futures::{Stream, Future}; +use url::form_urlencoded; +use std::collections::HashMap; +use hyper::header::ContentType; + +pub fn handle(_service: &WebService, _req: Request) -> InternalFuture { + Box::new( + _req.body().concat2().map(move |body| { + let req = form_urlencoded::parse(body.as_ref()) + .into_owned() + .collect::>(); + if webbrowser::open( req.get("url").unwrap()).is_ok() { + Response::new() + .with_status(hyper::Ok) + .with_header(ContentType::json()) + .with_body("{}") + } else { + Response::new() + .with_status(hyper::BadRequest) + .with_header(ContentType::json()) + .with_body("{}") + } + })) +} + diff --git a/src/frontend/rest/services/mod.rs b/src/frontend/rest/services/mod.rs index 50eda2d..1b7473e 100644 --- a/src/frontend/rest/services/mod.rs +++ b/src/frontend/rest/services/mod.rs @@ -22,6 +22,7 @@ use futures::sink::Sink; mod attributes; mod authentication; +mod browser; mod config; mod default_path; mod dark_mode; @@ -140,6 +141,7 @@ impl Service for WebService { (Method::Get, "/api/installation-status") => installation_status::handle(self, req), (Method::Post, "/api/check-auth") => authentication::handle(self, req), (Method::Post, "/api/start-install") => install::handle(self, req), + (Method::Post, "/api/open-browser") => browser::handle(self, req), (Method::Post, "/api/uninstall") => uninstall::handle(self, req), (Method::Post, "/api/update-updater") => update_updater::handle(self, req), (Method::Get, _) => static_files::handle(self, req), diff --git a/src/http.rs b/src/http.rs index 1a813d5..e892d5e 100644 --- a/src/http.rs +++ b/src/http.rs @@ -36,16 +36,22 @@ pub fn build_async_client() -> Result { } /// Streams a file from a HTTP server. -pub fn stream_file(url: &str, mut callback: F) -> Result<(), String> +pub fn stream_file(url: &str, authorization: Option, mut callback: F) -> Result<(), String> where F: FnMut(Vec, u64) -> (), { assert_ssl(url)?; - let mut client = build_client()? - .get(url) - .send() - .map_err(|x| format!("Failed to GET resource: {:?}", x))?; + let mut client = if authorization.is_some() { + build_client()?.get(url) + .header("Authorization", format!("Bearer {}", authorization.unwrap())) + .send() + .map_err(|x| format!("Failed to GET resource: {:?}", x))? + } else { + build_client()?.get(url) + .send() + .map_err(|x| format!("Failed to GET resource: {:?}", x))? + }; let size = match client.headers().get(CONTENT_LENGTH) { Some(ref v) => v diff --git a/src/installer.rs b/src/installer.rs index f7d9dce..d0c9f10 100644 --- a/src/installer.rs +++ b/src/installer.rs @@ -92,6 +92,7 @@ pub struct InstallerFramework { // If we just completed an uninstall, and we should clean up after ourselves. pub burn_after_exit: bool, pub launcher_path: Option, + pub authorization_token: Option, } /// Contains basic properties on the status of the session. Subset of InstallationFramework. @@ -262,7 +263,7 @@ impl InstallerFramework { let mut downloaded = 0; let mut data_storage: Vec = Vec::new(); - http::stream_file(tool, |data, size| { + http::stream_file(tool, None, |data, size| { { data_storage.extend_from_slice(&data); } @@ -440,6 +441,7 @@ impl InstallerFramework { is_launcher: false, burn_after_exit: false, launcher_path: None, + authorization_token: None, } } @@ -467,6 +469,7 @@ impl InstallerFramework { is_launcher: false, burn_after_exit: false, launcher_path: None, + authorization_token: None, }) } } diff --git a/src/main.rs b/src/main.rs index 6efa301..22f04a2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,6 +49,8 @@ extern crate sysinfo; extern crate jsonwebtoken as jwt; +extern crate base64; + mod archives; mod config; mod frontend; diff --git a/src/native/interop.cpp b/src/native/interop.cpp index 18be048..7121614 100644 --- a/src/native/interop.cpp +++ b/src/native/interop.cpp @@ -45,7 +45,7 @@ extern "C" int saveShortcut( const wchar_t *args, const wchar_t *workingDir) { - char *errStr = NULL; + const char *errStr = NULL; HRESULT h; IShellLink *shellLink = NULL; IPersistFile *persistFile = NULL; diff --git a/src/sources/github/mod.rs b/src/sources/github/mod.rs index 35c0f43..a8a75a7 100644 --- a/src/sources/github/mod.rs +++ b/src/sources/github/mod.rs @@ -107,6 +107,7 @@ impl ReleaseSource for GithubReleases { files.push(File { name: string.to_string(), url: url.to_string(), + requires_authorization: false, }); } diff --git a/src/sources/patreon.rs b/src/sources/patreon.rs new file mode 100644 index 0000000..b2b888b --- /dev/null +++ b/src/sources/patreon.rs @@ -0,0 +1,108 @@ +//! github/mod.rs +//! +//! Contains the Github API implementation of a release source. + +use sources::types::*; +use http::build_client; +use reqwest::header::USER_AGENT; +use reqwest::StatusCode; + +pub struct PatreonReleases {} + +/// The configuration for this release. +#[derive(Serialize, Deserialize)] +struct PatreonConfig { + repo: String, +} + +impl PatreonReleases { + pub fn new() -> Self { + PatreonReleases {} + } +} + +impl ReleaseSource for PatreonReleases { + fn get_current_releases(&self, _config: &TomlValue) -> Result, String> { + let config: PatreonConfig = match _config.clone().try_into() { + Ok(v) => v, + Err(v) => return Err(format!("Failed to parse release config: {:?}", v)), + }; + + let mut results: Vec = Vec::new(); + + // Build the HTTP client up + let client = build_client()?; + let mut response = client + .get(&format!( + "https://api.yuzu-emu.org/downloads/{}/", + config.repo + )) + .header(USER_AGENT, "liftinstall (j-selby)") + .send() + .map_err(|x| format!("Error while sending HTTP request: {:?}", x))?; + + match response.status() { + StatusCode::OK => {} + StatusCode::FORBIDDEN => { + return Err( + "You are not eligible to download this release".to_string(), + ); + } + _ => { + return Err(format!("Bad status code: {:?}.", response.status())); + } + } + + let body = response + .text() + .map_err(|x| format!("Failed to decode HTTP response body: {:?}", x))?; + + let result: serde_json::Value = serde_json::from_str(&body) + .map_err(|x| format!("Failed to parse response: {:?}", x))?; + + // Parse JSON from server + let mut files = Vec::new(); + + let id: u64 = match result["version"].as_u64() { + Some(v) => v, + None => return Err("JSON payload missing information about ID".to_string()), + }; + + let downloads = match result["files"].as_array() { + Some(v) => v, + None => return Err("JSON payload not an array".to_string()), + }; + + for file in downloads.iter() { + let string = match file["name"].as_str() { + Some(v) => v, + None => { + return Err( + "JSON payload missing information about release name".to_string() + ); + } + }; + + let url = match file["url"].as_str() { + Some(v) => v, + None => { + return Err( + "JSON payload missing information about release URL".to_string() + ); + } + }; + + files.push(File { + name: string.to_string(), + url: url.to_string(), + requires_authorization: true, + }); + } + + results.push(Release { + version: Version::new_number(id), + files, + }); + Ok(results) + } +} diff --git a/src/sources/types.rs b/src/sources/types.rs index e45656a..285c5d7 100644 --- a/src/sources/types.rs +++ b/src/sources/types.rs @@ -66,6 +66,7 @@ impl Ord for Version { pub struct File { pub name: String, pub url: String, + pub requires_authorization: bool, } impl File {} diff --git a/src/tasks/download_pkg.rs b/src/tasks/download_pkg.rs index 0817dc4..3262ec0 100644 --- a/src/tasks/download_pkg.rs +++ b/src/tasks/download_pkg.rs @@ -54,7 +54,7 @@ impl Task for DownloadPackageTask { let mut downloaded = 0; let mut data_storage: Vec = Vec::new(); - stream_file(&file.url, |data, size| { + stream_file(&file.url, context.authorization_token.clone(), |data, size| { { data_storage.extend_from_slice(&data); } diff --git a/ui/src/main.js b/ui/src/main.js index 076dafa..3bef367 100644 --- a/ui/src/main.js +++ b/ui/src/main.js @@ -84,6 +84,13 @@ var app = new Vue({ attrs: base_attributes, config: {}, install_location: '', + username: '', + token: '', + jwt_token: {}, + is_authenticated: false, + is_linked: false, + is_subscribed: false, + has_reward_tier: false, // If the option to pick an install location should be provided show_install_location: true, metadata: { @@ -111,6 +118,38 @@ var app = new Vue({ } ) }, + check_authentication: function (success, error) { + var that = this; + var app = this.$root; + + app.ajax('/api/check-auth', function (auth) { + that.jwt_token = auth; + that.is_authenticated = Object.keys(that.jwt_token).length !== 0 && that.jwt_token.constructor === Object; + if (that.is_authenticated) { + // Give all permissions to vip roles + if (that.jwt_token.roles.indexOf("vip") > -1) { + that.is_linked = true; + that.is_subscribed = true; + that.has_reward_tier = true; + } else { + that.is_linked = that.jwt_token.isPatreonAccountLinked; + that.is_subscribed = that.jwt_token.isPatreonSubscriptionActive; + that.has_reward_tier = that.jwt_token.releaseChannels.indexOf("early-release") > -1; + } + } + if (success) { + success(); + } + }, function (e) { + if (error) { + error(); + } + }, { + "username": app.$data.username, + "token": app.$data.token + }) + }, + ajax: ajax, stream_ajax: stream_ajax } diff --git a/ui/src/router.js b/ui/src/router.js index 8381ecf..29db2f4 100644 --- a/ui/src/router.js +++ b/ui/src/router.js @@ -7,6 +7,7 @@ import ErrorView from './views/ErrorView.vue' import InstallPackages from './views/InstallPackages.vue' import CompleteView from './views/CompleteView.vue' import ModifyView from './views/ModifyView.vue' +import AuthenticationView from './views/AuthenticationView.vue' Vue.use(Router) @@ -47,6 +48,11 @@ export default new Router({ name: 'modify', component: ModifyView }, + { + path: '/authentication', + name: 'authentication', + component: AuthenticationView + }, { path: '/', redirect: '/config' diff --git a/ui/src/views/AuthenticationView.vue b/ui/src/views/AuthenticationView.vue new file mode 100644 index 0000000..df7ea2e --- /dev/null +++ b/ui/src/views/AuthenticationView.vue @@ -0,0 +1,124 @@ + + + diff --git a/ui/src/views/DownloadConfig.vue b/ui/src/views/DownloadConfig.vue index 045150b..fd95dad 100644 --- a/ui/src/views/DownloadConfig.vue +++ b/ui/src/views/DownloadConfig.vue @@ -29,29 +29,27 @@ export default { this.$root.ajax('/api/config', function (e) { that.$root.config = e - that.choose_next_state() + // Update the updater if needed + if (that.$root.config.new_tool) { + this.$router.push('/install/updater') + return + } + + that.$root.check_authentication(that.choose_next_state, that.choose_next_state) }, function (e) { - console.error('Got error while downloading config: ' + - e) + console.error('Got error while downloading config: ' + e) if (that.$root.metadata.is_launcher) { // Just launch the target application that.$root.exit() } else { that.$router.replace({ name: 'showerr', - params: { msg: 'Got error while downloading config: ' + - e } }) + params: { msg: 'Got error while downloading config: ' + e } }) } }) }, choose_next_state: function () { var app = this.$root - // Update the updater if needed - if (app.config.new_tool) { - this.$router.push('/install/updater') - return - } - if (app.metadata.preexisting_install) { app.install_location = app.metadata.install_path diff --git a/ui/src/views/SelectPackages.vue b/ui/src/views/SelectPackages.vue index 110706b..9772c40 100644 --- a/ui/src/views/SelectPackages.vue +++ b/ui/src/views/SelectPackages.vue @@ -1,63 +1,87 @@