mirror of
https://github.com/yuzu-emu/liftinstall.git
synced 2024-12-22 19:05:27 +00:00
Add patreon authentication for early access releases
This commit is contained in:
parent
a7057dfed3
commit
5409b32bf0
39
config.linux.patreon.toml
Normal file
39
config.linux.patreon.toml
Normal 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"
|
51
config.windows.patreon.toml
Normal file
51
config.windows.patreon.toml
Normal 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"
|
||||
|
|
@ -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 {
|
||||
|
|
187
src/frontend/rest/services/authentication.rs
Normal file
187
src/frontend/rest/services/authentication.rs
Normal 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()
|
||||
)
|
||||
}
|
29
src/frontend/rest/services/browser.rs
Normal file
29
src/frontend/rest/services/browser.rs
Normal 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("{}")
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
16
src/http.rs
16
src/http.rs
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,6 +49,8 @@ extern crate sysinfo;
|
|||
|
||||
extern crate jsonwebtoken as jwt;
|
||||
|
||||
extern crate base64;
|
||||
|
||||
mod archives;
|
||||
mod config;
|
||||
mod frontend;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
108
src/sources/patreon.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -66,6 +66,7 @@ impl Ord for Version {
|
|||
pub struct File {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub requires_authorization: bool,
|
||||
}
|
||||
|
||||
impl 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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
124
ui/src/views/AuthenticationView.vue
Normal file
124
ui/src/views/AuthenticationView.vue
Normal 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>
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue