Add patreon authentication for early access releases

This commit is contained in:
James Rowe 2019-10-21 01:09:16 -06:00
parent a7057dfed3
commit 5409b32bf0
19 changed files with 728 additions and 75 deletions

39
config.linux.patreon.toml Normal file
View file

@ -0,0 +1,39 @@
installing_message = "Reminder: yuzu is an <b>experimental</b> 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"

View file

@ -0,0 +1,51 @@
installing_message = "Reminder: yuzu is an <b>experimental</b> 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"

View file

@ -36,6 +36,32 @@ pub struct PackageDescription {
pub source: PackageSource,
#[serde(default)]
pub shortcuts: Vec<PackageShortcut>,
#[serde(default)]
pub requires_authorization: Option<bool>,
#[serde(default)]
pub need_authentication_description: Option<String>,
#[serde(default)]
pub need_link_description: Option<String>,
#[serde(default)]
pub need_subscription_description: Option<String>,
#[serde(default)]
pub need_reward_tier_description: Option<String>,
}
/// Configuration for validating the JWT token
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct JWTValidation {
pub iss: Option<String>,
// This can technically be a Vec as well, but thats a pain to support atm
pub aud: Option<String>,
}
/// 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<JWTValidation>,
}
/// Describes the application itself.
@ -66,6 +92,8 @@ pub struct Config {
pub packages: Vec<PackageDescription>,
#[serde(default)]
pub hide_advanced: bool,
#[serde(default)]
pub authentication: Option<AuthenticationConfig>,
}
impl Config {

View file

@ -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<String>,
#[serde(rename = "releaseChannels", default)]
channels: Vec<String>,
#[serde(rename = "IsPatreonAccountLinked")]
is_linked: bool,
#[serde(rename = "IsPatreonSubscriptionActive")]
is_subscribed: bool,
}
fn get_text(future: impl Future<Item = reqwest::async::Response, Error = reqwest::Error>) -> impl Future<Item = String, Error = Response> {
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::<HashMap<String, String>>();
// 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::<JWTClaims>(&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()
)
}

View file

@ -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::<HashMap<String, String>>();
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("{}")
}
}))
}

View file

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

View file

@ -36,16 +36,22 @@ pub fn build_async_client() -> Result<AsyncClient, String> {
}
/// Streams a file from a HTTP server.
pub fn stream_file<F>(url: &str, mut callback: F) -> Result<(), String>
pub fn stream_file<F>(url: &str, authorization: Option<String>, mut callback: F) -> Result<(), String>
where
F: FnMut(Vec<u8>, 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

View file

@ -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<String>,
pub authorization_token: Option<String>,
}
/// 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<u8> = 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,
})
}
}

View file

@ -49,6 +49,8 @@ extern crate sysinfo;
extern crate jsonwebtoken as jwt;
extern crate base64;
mod archives;
mod config;
mod frontend;

View file

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

View file

@ -107,6 +107,7 @@ impl ReleaseSource for GithubReleases {
files.push(File {
name: string.to_string(),
url: url.to_string(),
requires_authorization: false,
});
}

108
src/sources/patreon.rs Normal file
View file

@ -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<Vec<Release>, 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<Release> = 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)
}
}

View file

@ -66,6 +66,7 @@ impl Ord for Version {
pub struct File {
pub name: String,
pub url: String,
pub requires_authorization: bool,
}
impl File {}

View file

@ -54,7 +54,7 @@ impl Task for DownloadPackageTask {
let mut downloaded = 0;
let mut data_storage: Vec<u8> = Vec::new();
stream_file(&file.url, |data, size| {
stream_file(&file.url, context.authorization_token.clone(), |data, size| {
{
data_storage.extend_from_slice(&data);
}

View file

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

View file

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

View file

@ -0,0 +1,124 @@
<template>
<div class="column has-padding">
<b-message type="is-info" :active.sync="browser_opened">
Page opened! Check your default browser for the page, and follow the instructions there to link your patreon account.
When you are done, enter the username and token below.
</b-message>
<p>
Before you can install this Early Access, you need to verify your account.
<a v-on:click="launch_browser('https://yuzu-emu.org/')">Click here to link your yuzu-emu.org account</a>
and paste the token below.
</p>
<br>
<div class="control">
<label for="username">Username</label>
<input class="input" type="text" v-model="$root.$data.username" placeholder="Username" id="username">
</div>
<div class="control">
<label for="token">Token</label>
<input class="input" type="password" v-model="$root.$data.token" placeholder="Token" id="token">
</div>
<br>
<b-message type="is-danger" :active.sync="invalid_login">
Login failed!
Double check that your username and token are correct and try again
</b-message>
<b-message type="is-danger" :active.sync="unlinked_patreon">
Your credentials are valid, but you still need to link your patreon!
If this is an error, then <a v-on:click="launch_browser('https://yuzu-emu.org/')">click here to link your yuzu-emu.org account</a>
</b-message>
<b-message type="is-danger" :active.sync="no_subscription">
Your patreon is linked, but you are not a current subscriber.
<a v-on:click="launch_browser('https://patreon.com/')">Log into your patreon account</a> and support the project!
</b-message>
<b-message type="is-danger" :active.sync="tier_not_selected">
Your patreon is linked, and you are supporting the project, but you must first join the Early Access reward tier!
<a v-on:click="launch_browser('https://patreon.com/')">Log into your patreon account</a> and choose to back the Early Access reward tier.
</b-message>
<div class="is-left-floating is-bottom-floating">
<p class="control">
<a class="button is-medium" v-on:click="go_back">Back</a>
</p>
</div>
<div class="is-right-floating is-bottom-floating">
<p class="control">
<a class="button is-dark is-medium" v-on:click="verify_token">Verify Token</a>
</p>
</div>
</div>
</template>
<script>
export default {
name: 'AuthenticationView',
created: function() {
// If they are already authenticated when this page is loaded,
// then we can asssume they are "clicking here for more details" and should show the appropriate error message
if (this.$root.is_authenticated) {
this.verification_opened = true;
}
},
data: function() {
return {
browser_opened: false,
verification_opened: false,
}
},
computed: {
invalid_login: function() {
return this.verification_opened && !this.$root.is_authenticated;
},
unlinked_patreon: function() {
return this.verification_opened && this.$root.is_authenticated && !this.$root.is_linked;
},
no_subscription: function() {
return this.verification_opened && this.$root.is_linked && !this.$root.is_subscribed;
},
tier_not_selected: function() {
return this.verification_opened && this.$root.is_subscribed && !this.$root.has_reward_tier;
}
},
methods: {
go_back: function () {
this.$router.go(-1)
},
launch_browser: function(url) {
const that = this;
let app = this.$root;
app.ajax('/api/open-browser', function (e) {
// only open the browser opened message if there isn't an error message currently
if (!that.verification_opened) {
that.browser_opened = true;
}
}, function (e) {}, {
"url": url,
});
},
verify_token: function() {
this.browser_opened = false;
this.$root.check_authentication(this.success, this.error);
},
success: function() {
// if they are eligible, go back to the select package page
if (this.$root.has_reward_tier) {
this.$router.go(-1);
return;
}
// They aren't currently eligible for the release, so display the error message
this.verification_opened = true;
},
error: function() {
this.verification_opened = true;
}
}
}
</script>

View file

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

View file

@ -1,63 +1,87 @@
<template>
<div class="column has-padding">
<h4 class="subtitle">Select which packages you want to install:</h4>
<h4 class="subtitle">Select which packages you want to install:</h4>
<!-- Build options -->
<div class="tile is-ancestor">
<div class="tile is-parent" v-for="Lpackage in $root.$data.config.packages" :key="Lpackage.name" :index="Lpackage.name">
<div class="tile is-child">
<div class="box clickable-box" v-on:click.capture.stop="Lpackage.default = !Lpackage.default">
<label class="checkbox">
<b-checkbox v-model="Lpackage.default">
{{ Lpackage.name }}
</b-checkbox>
<span v-if="Lpackage.installed"><i>(installed)</i></span>
</label>
<p>
{{ Lpackage.description }}
</p>
</div>
</div>
</div>
</div>
<!-- Build options -->
<div class="tile is-ancestor">
<div class="tile is-parent" v-for="Lpackage in $root.$data.config.packages" :key="Lpackage.name" :index="Lpackage.name">
<div class="tile is-child">
<div class="box clickable-box" v-if="Lpackage.requires_authorization && !$root.$data.is_authenticated" v-on:click="show_authentication">
<p>{{ Lpackage.name }}</p>
<p>
{{Lpackage.need_authentication_description}}
</p>
</div>
<div class="box clickable-box" v-else-if="Lpackage.requires_authorization && !$root.$data.is_linked" v-on:click="show_authorization">
<p>{{ Lpackage.name }}</p>
<p>
{{Lpackage.need_link_description}}
</p>
</div>
<div class="box clickable-box" v-else-if="Lpackage.requires_authorization && !$root.$data.is_subscribed" v-on:click="show_authorization">
<p>{{ Lpackage.name }}</p>
<p>
{{Lpackage.need_subscription_description}}
</p>
</div>
<div class="box clickable-box" v-else-if="Lpackage.requires_authorization && !$root.$data.has_reward_tier" v-on:click="show_authorization">
<p>{{ Lpackage.name }}</p>
<p>
{{Lpackage.need_reward_tier_description}}
</p>
</div>
<div class="box clickable-box" v-else v-on:click.capture.stop="Lpackage.default = !Lpackage.default">
<label class="checkbox">
<b-checkbox v-model="Lpackage.default">
{{ Lpackage.name }}
</b-checkbox>
<span v-if="Lpackage.installed"><i>(installed)</i></span>
</label>
<p>
{{ Lpackage.description }}
</p>
</div>
</div>
</div>
</div>
<div class="subtitle is-6" v-if="!$root.$data.metadata.preexisting_install && advanced">Install Location</div>
<div class="field has-addons" v-if="!$root.$data.metadata.preexisting_install && advanced">
<div class="control is-expanded">
<input class="input" type="text" v-model="$root.$data.install_location"
placeholder="Enter a install path here">
</div>
<div class="control">
<a class="button is-dark" v-on:click="select_file">
Select
</a>
</div>
</div>
<div class="subtitle is-6" v-if="!$root.$data.metadata.preexisting_install && advanced">Install Location</div>
<div class="field has-addons" v-if="!$root.$data.metadata.preexisting_install && advanced">
<div class="control is-expanded">
<input class="input" type="text" v-model="$root.$data.install_location"
placeholder="Enter a install path here">
</div>
<div class="control">
<a class="button is-dark" v-on:click="select_file">
Select
</a>
</div>
</div>
<div class="is-right-floating is-bottom-floating">
<div class="field is-grouped">
<p class="control">
<a class="button is-medium" v-if="!$root.$data.config.hide_advanced && !$root.$data.metadata.preexisting_install && !advanced"
v-on:click="advanced = true">Advanced...</a>
</p>
<p class="control">
<a class="button is-dark is-medium" v-if="!$root.$data.metadata.preexisting_install"
v-on:click="install">Install</a>
</p>
<p class="control">
<a class="button is-dark is-medium" v-if="$root.$data.metadata.preexisting_install"
v-on:click="install">Modify</a>
</p>
</div>
</div>
<div class="is-right-floating is-bottom-floating">
<div class="field is-grouped">
<p class="control">
<a class="button is-medium" v-if="!$root.$data.config.hide_advanced && !$root.$data.metadata.preexisting_install && !advanced"
v-on:click="advanced = true">Advanced...</a>
</p>
<p class="control">
<a class="button is-dark is-medium" v-if="!$root.$data.metadata.preexisting_install"
v-on:click="install">Install</a>
</p>
<p class="control">
<a class="button is-dark is-medium" v-if="$root.$data.metadata.preexisting_install"
v-on:click="install">Modify</a>
</p>
</div>
</div>
<div class="field is-grouped is-left-floating is-bottom-floating">
<p class="control">
<a class="button is-medium" v-if="$root.$data.metadata.preexisting_install"
v-on:click="go_back">Back</a>
</p>
</div>
</div>
<div class="field is-grouped is-left-floating is-bottom-floating">
<p class="control">
<a class="button is-medium" v-if="$root.$data.metadata.preexisting_install"
v-on:click="go_back">Back</a>
</p>
</div>
</div>
</template>
<script>
@ -77,12 +101,17 @@ export default {
}))
},
install: function () {
// TODO route instead to an authentication endpoint. if this package needs auth, then call the backend and route to the auth page
this.$router.push('/install/regular')
},
go_back: function () {
this.$router.go(-1)
}
},
show_authentication: function () {
this.$router.push('/authentication')
},
show_authorization: function () {
this.$router.push('/authentication')
},
}
}
</script>