You've already forked AstralRinth
forked from didirus/AstralRinth
Fix creator payouts + scheduler (#686)
This commit is contained in:
@@ -50,9 +50,7 @@ pub fn config(cfg: &mut ServiceConfig) {
|
|||||||
.service(resend_verify_email)
|
.service(resend_verify_email)
|
||||||
.service(set_email)
|
.service(set_email)
|
||||||
.service(verify_email)
|
.service(verify_email)
|
||||||
.service(subscribe_newsletter)
|
.service(subscribe_newsletter),
|
||||||
.service(login_from_minecraft)
|
|
||||||
.configure(super::minecraft::config),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1177,65 +1175,6 @@ pub async fn auth_callback(
|
|||||||
Ok(res?)
|
Ok(res?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct MinecraftLogin {
|
|
||||||
pub flow: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("login/minecraft")]
|
|
||||||
pub async fn login_from_minecraft(
|
|
||||||
req: HttpRequest,
|
|
||||||
client: Data<PgPool>,
|
|
||||||
file_host: Data<Arc<dyn FileHost + Send + Sync>>,
|
|
||||||
redis: Data<deadpool_redis::Pool>,
|
|
||||||
login: web::Json<MinecraftLogin>,
|
|
||||||
) -> Result<HttpResponse, AuthenticationError> {
|
|
||||||
let flow = Flow::get(&login.flow, &redis).await?;
|
|
||||||
|
|
||||||
// Extract cookie header from request
|
|
||||||
if let Some(Flow::MicrosoftLogin {
|
|
||||||
access_token: token,
|
|
||||||
}) = flow
|
|
||||||
{
|
|
||||||
Flow::remove(&login.flow, &redis).await?;
|
|
||||||
let provider = AuthProvider::Microsoft;
|
|
||||||
let oauth_user = provider.get_user(&token).await?;
|
|
||||||
let user_id_opt = provider.get_user_id(&oauth_user.id, &**client).await?;
|
|
||||||
|
|
||||||
let mut transaction = client.begin().await?;
|
|
||||||
|
|
||||||
let user_id = if let Some(user_id) = user_id_opt {
|
|
||||||
let user = crate::database::models::User::get_id(user_id, &**client, &redis)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
|
||||||
|
|
||||||
if user.totp_secret.is_some() {
|
|
||||||
let flow = Flow::Login2FA { user_id: user.id }
|
|
||||||
.insert(Duration::minutes(30), &redis)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
return Ok(HttpResponse::Ok().json(serde_json::json!({
|
|
||||||
"error": "2fa_required",
|
|
||||||
"flow": flow
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
|
|
||||||
user_id
|
|
||||||
} else {
|
|
||||||
oauth_user
|
|
||||||
.create_account(provider, &mut transaction, &client, &file_host, &redis)
|
|
||||||
.await?
|
|
||||||
};
|
|
||||||
|
|
||||||
let session = issue_session(req, user_id, &mut transaction, &redis).await?;
|
|
||||||
Ok(HttpResponse::Ok().json(serde_json::json!({
|
|
||||||
"code": session.session
|
|
||||||
})))
|
|
||||||
} else {
|
|
||||||
Err(AuthenticationError::InvalidCredentials)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct DeleteAuthProvider {
|
pub struct DeleteAuthProvider {
|
||||||
pub provider: AuthProvider,
|
pub provider: AuthProvider,
|
||||||
|
|||||||
@@ -1,160 +0,0 @@
|
|||||||
//! Main authentication flow for Hydra
|
|
||||||
use crate::{auth::minecraft::stages, auth::templates, parse_var};
|
|
||||||
|
|
||||||
// use crate::db::RuntimeState;
|
|
||||||
use crate::database::models::flow_item::Flow;
|
|
||||||
use crate::queue::socket::ActiveSockets;
|
|
||||||
use actix_web::http::StatusCode;
|
|
||||||
use actix_web::{get, web, HttpResponse};
|
|
||||||
use chrono::Duration;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use serde_json::json;
|
|
||||||
use tokio::sync::RwLock;
|
|
||||||
|
|
||||||
macro_rules! ws_conn_try {
|
|
||||||
($ctx:literal $status:path, $res:expr => $ws_conn:expr) => {
|
|
||||||
match $res {
|
|
||||||
Ok(res) => res,
|
|
||||||
Err(err) => {
|
|
||||||
let error = format!("In {}: {err}", $ctx);
|
|
||||||
let render = super::Error::render_string(&error);
|
|
||||||
let _ = $ws_conn.text(render.clone()).await;
|
|
||||||
let _ = $ws_conn.close(None).await;
|
|
||||||
return Err(templates::ErrorPage {
|
|
||||||
code: $status,
|
|
||||||
message: render,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct Query {
|
|
||||||
pub code: String,
|
|
||||||
pub state: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("callback")]
|
|
||||||
pub async fn route(
|
|
||||||
db: web::Data<RwLock<ActiveSockets>>,
|
|
||||||
info: web::Query<Query>,
|
|
||||||
redis: web::Data<deadpool_redis::Pool>,
|
|
||||||
) -> Result<HttpResponse, templates::ErrorPage> {
|
|
||||||
let public_url = parse_var::<String>("SELF_ADDR").unwrap_or(format!(
|
|
||||||
"http://{}",
|
|
||||||
parse_var::<String>("BIND_ADDR").unwrap()
|
|
||||||
));
|
|
||||||
let client_id = parse_var::<String>("MICROSOFT_CLIENT_ID").unwrap();
|
|
||||||
let client_secret = parse_var::<String>("MICROSOFT_CLIENT_SECRET").unwrap();
|
|
||||||
|
|
||||||
let code = &info.code;
|
|
||||||
|
|
||||||
let mut ws_conn = {
|
|
||||||
let db = db.read().await;
|
|
||||||
|
|
||||||
let mut x = db
|
|
||||||
.auth_sockets
|
|
||||||
.get_mut(&info.state)
|
|
||||||
.ok_or_else(|| templates::ErrorPage {
|
|
||||||
code: StatusCode::BAD_REQUEST,
|
|
||||||
message: "Invalid state sent, you probably need to get a new websocket".to_string(),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
x.value_mut().clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
ws_conn_try!(
|
|
||||||
"Removing login flow" StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
Flow::remove(code, &redis).await
|
|
||||||
=> ws_conn
|
|
||||||
);
|
|
||||||
|
|
||||||
let access_token = ws_conn_try!(
|
|
||||||
"OAuth token exchange" StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
stages::access_token::fetch_token(
|
|
||||||
public_url,
|
|
||||||
code,
|
|
||||||
&client_id,
|
|
||||||
&client_secret,
|
|
||||||
).await
|
|
||||||
=> ws_conn
|
|
||||||
);
|
|
||||||
|
|
||||||
let stages::xbl_signin::XBLLogin {
|
|
||||||
token: xbl_token,
|
|
||||||
uhs,
|
|
||||||
} = ws_conn_try!(
|
|
||||||
"XBox Live token exchange" StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
stages::xbl_signin::login_xbl(&access_token.access_token).await
|
|
||||||
=> ws_conn
|
|
||||||
);
|
|
||||||
|
|
||||||
let xsts_response = ws_conn_try!(
|
|
||||||
"XSTS token exchange" StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
stages::xsts_token::fetch_token(&xbl_token).await
|
|
||||||
=> ws_conn
|
|
||||||
);
|
|
||||||
|
|
||||||
match xsts_response {
|
|
||||||
stages::xsts_token::XSTSResponse::Unauthorized(err) => {
|
|
||||||
let _ = ws_conn
|
|
||||||
.text(super::Error::render_string(&format!(
|
|
||||||
"Error getting XBox Live token: {err}"
|
|
||||||
)))
|
|
||||||
.await;
|
|
||||||
let _ = ws_conn.close(None).await;
|
|
||||||
|
|
||||||
Err(templates::ErrorPage {
|
|
||||||
code: StatusCode::FORBIDDEN,
|
|
||||||
message: err,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
stages::xsts_token::XSTSResponse::Success { token: xsts_token } => {
|
|
||||||
let bearer_token = &ws_conn_try!(
|
|
||||||
"Bearer token flow" StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
stages::bearer_token::fetch_bearer(&xsts_token, &uhs)
|
|
||||||
.await
|
|
||||||
=> ws_conn
|
|
||||||
);
|
|
||||||
|
|
||||||
let player_info = &ws_conn_try!(
|
|
||||||
"No Minecraft account for profile. Make sure you own the game and have set a username through the official Minecraft launcher." StatusCode::BAD_REQUEST,
|
|
||||||
stages::player_info::fetch_info(bearer_token)
|
|
||||||
.await
|
|
||||||
=> ws_conn
|
|
||||||
);
|
|
||||||
|
|
||||||
let flow = &ws_conn_try!(
|
|
||||||
"Error creating microsoft login request flow." StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
Flow::MicrosoftLogin {
|
|
||||||
access_token: access_token.access_token.clone(),
|
|
||||||
}
|
|
||||||
.insert(Duration::hours(1), &redis)
|
|
||||||
.await
|
|
||||||
=> ws_conn
|
|
||||||
);
|
|
||||||
|
|
||||||
ws_conn
|
|
||||||
.text(
|
|
||||||
json!({
|
|
||||||
"token": bearer_token,
|
|
||||||
"refresh_token": &access_token.refresh_token,
|
|
||||||
"expires_after": 86400,
|
|
||||||
"flow": flow,
|
|
||||||
}).to_string()
|
|
||||||
)
|
|
||||||
.await.map_err(|_| templates::ErrorPage {
|
|
||||||
code: StatusCode::BAD_REQUEST,
|
|
||||||
message: "Failed to send login details to launcher. Try restarting the login process!".to_string(),
|
|
||||||
})?;
|
|
||||||
let _ = ws_conn.close(None).await;
|
|
||||||
|
|
||||||
Ok(templates::Success {
|
|
||||||
name: &player_info.name,
|
|
||||||
icon: &format!("https://mc-heads.net/avatar/{}/128", &player_info.id),
|
|
||||||
}
|
|
||||||
.render())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
//! Login route for Hydra, redirects to the Microsoft login page before going to the redirect route
|
|
||||||
use crate::{auth::minecraft::stages::login_redirect, auth::templates, parse_var};
|
|
||||||
use actix_web::http::StatusCode;
|
|
||||||
use actix_web::{get, web, HttpResponse};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct Query {
|
|
||||||
pub id: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct AuthorizationInit {
|
|
||||||
pub url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("init")]
|
|
||||||
pub async fn route(info: web::Query<Query>) -> Result<HttpResponse, templates::ErrorPage> {
|
|
||||||
let conn_id = info.0.id.ok_or_else(|| templates::ErrorPage {
|
|
||||||
code: StatusCode::BAD_REQUEST,
|
|
||||||
message: "No socket ID provided (open a web socket at the / route for one)".to_string(),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let public_url = parse_var::<String>("SELF_ADDR").unwrap_or(format!(
|
|
||||||
"http://{}",
|
|
||||||
parse_var::<String>("BIND_ADDR").unwrap()
|
|
||||||
));
|
|
||||||
let client_id = parse_var::<String>("MICROSOFT_CLIENT_ID").unwrap();
|
|
||||||
|
|
||||||
let url = login_redirect::get_url(&public_url, &conn_id, &client_id);
|
|
||||||
|
|
||||||
Ok(HttpResponse::TemporaryRedirect()
|
|
||||||
.append_header(("Location", &*url))
|
|
||||||
.json(AuthorizationInit { url }))
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
mod auth;
|
|
||||||
mod login;
|
|
||||||
mod refresh;
|
|
||||||
mod socket;
|
|
||||||
mod stages;
|
|
||||||
|
|
||||||
use actix_web::http::StatusCode;
|
|
||||||
use actix_web::web::{scope, ServiceConfig};
|
|
||||||
use actix_web::HttpResponse;
|
|
||||||
use serde_json::json;
|
|
||||||
use std::fmt::{Display, Formatter};
|
|
||||||
|
|
||||||
/// Error message
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Error {
|
|
||||||
pub code: StatusCode,
|
|
||||||
pub reason: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Error {
|
|
||||||
pub fn render_string(reason: &str) -> String {
|
|
||||||
json!({ "error": reason }).to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"{}",
|
|
||||||
json!({
|
|
||||||
"error": self.reason
|
|
||||||
})
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl actix_web::ResponseError for Error {
|
|
||||||
fn status_code(&self) -> StatusCode {
|
|
||||||
self.code
|
|
||||||
}
|
|
||||||
|
|
||||||
fn error_response(&self) -> HttpResponse {
|
|
||||||
HttpResponse::build(self.code).json(json!({
|
|
||||||
"error": self.reason
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn config(cfg: &mut ServiceConfig) {
|
|
||||||
cfg.service(
|
|
||||||
scope("minecraft")
|
|
||||||
.service(auth::route)
|
|
||||||
.service(login::route)
|
|
||||||
.service(refresh::route)
|
|
||||||
.service(socket::route),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
//! Refresh token route
|
|
||||||
use super::stages;
|
|
||||||
use crate::parse_var;
|
|
||||||
use actix_web::http::StatusCode;
|
|
||||||
use actix_web::{post, web, HttpResponse};
|
|
||||||
use serde::Deserialize;
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct Body {
|
|
||||||
refresh_token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("refresh")]
|
|
||||||
pub async fn route(body: web::Json<Body>) -> Result<HttpResponse, super::Error> {
|
|
||||||
let public_url = parse_var::<String>("SELF_ADDR").unwrap_or(format!(
|
|
||||||
"http://{}",
|
|
||||||
parse_var::<String>("BIND_ADDR").unwrap()
|
|
||||||
));
|
|
||||||
let client_id = parse_var::<String>("MICROSOFT_CLIENT_ID").unwrap();
|
|
||||||
let client_secret = parse_var::<String>("MICROSOFT_CLIENT_SECRET").unwrap();
|
|
||||||
|
|
||||||
let access_token = stages::access_token::refresh_token(
|
|
||||||
&public_url,
|
|
||||||
&body.refresh_token,
|
|
||||||
&client_id,
|
|
||||||
&client_secret,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| super::Error {
|
|
||||||
code: StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
reason: "Error with OAuth token exchange".to_string(),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let stages::xbl_signin::XBLLogin {
|
|
||||||
token: xbl_token,
|
|
||||||
uhs,
|
|
||||||
} = stages::xbl_signin::login_xbl(&access_token.access_token)
|
|
||||||
.await
|
|
||||||
.map_err(|_| super::Error {
|
|
||||||
code: StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
reason: "Error with XBox Live token exchange".to_string(),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let xsts_response = stages::xsts_token::fetch_token(&xbl_token)
|
|
||||||
.await
|
|
||||||
.map_err(|_| super::Error {
|
|
||||||
code: StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
reason: "Error with XSTS token exchange".to_string(),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
match xsts_response {
|
|
||||||
stages::xsts_token::XSTSResponse::Unauthorized(err) => Err(super::Error {
|
|
||||||
code: StatusCode::UNAUTHORIZED,
|
|
||||||
reason: format!("Error getting XBox Live token: {err}"),
|
|
||||||
}),
|
|
||||||
stages::xsts_token::XSTSResponse::Success { token: xsts_token } => {
|
|
||||||
let bearer_token = stages::bearer_token::fetch_bearer(&xsts_token, &uhs)
|
|
||||||
.await
|
|
||||||
.map_err(|_| super::Error {
|
|
||||||
code: StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
reason: "Error with Bearer token flow".to_string(),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(&json!({
|
|
||||||
"token": bearer_token,
|
|
||||||
"refresh_token": &access_token.refresh_token,
|
|
||||||
"expires_after": 86400
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
use crate::database::models::flow_item::Flow;
|
|
||||||
use crate::queue::socket::ActiveSockets;
|
|
||||||
use actix_web::web::Payload;
|
|
||||||
use actix_web::{get, web, HttpRequest, HttpResponse};
|
|
||||||
use actix_ws::{Closed, Session};
|
|
||||||
use chrono::Duration;
|
|
||||||
use tokio::sync::RwLock;
|
|
||||||
|
|
||||||
#[get("ws")]
|
|
||||||
pub async fn route(
|
|
||||||
req: HttpRequest,
|
|
||||||
body: Payload,
|
|
||||||
db: web::Data<RwLock<ActiveSockets>>,
|
|
||||||
redis: web::Data<deadpool_redis::Pool>,
|
|
||||||
) -> Result<HttpResponse, actix_web::Error> {
|
|
||||||
let (res, session, _msg_stream) = actix_ws::handle(&req, body)?;
|
|
||||||
let _ = sock(session, db, redis).await;
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn sock(
|
|
||||||
mut ws_stream: Session,
|
|
||||||
db: web::Data<RwLock<ActiveSockets>>,
|
|
||||||
redis: web::Data<deadpool_redis::Pool>,
|
|
||||||
) -> Result<(), Closed> {
|
|
||||||
if let Ok(state) = Flow::MinecraftAuth
|
|
||||||
.insert(Duration::minutes(30), &redis)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
ws_stream
|
|
||||||
.text(serde_json::json!({ "login_code": state }).to_string())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let db = db.write().await;
|
|
||||||
db.auth_sockets.insert(state, ws_stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
//! Get access token from code
|
|
||||||
use serde::Deserialize;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
const OAUTH_TOKEN_URL: &str = "https://login.live.com/oauth20_token.srf";
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct Tokens {
|
|
||||||
pub access_token: String,
|
|
||||||
pub refresh_token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_token(
|
|
||||||
public_uri: String,
|
|
||||||
code: &str,
|
|
||||||
client_id: &str,
|
|
||||||
client_secret: &str,
|
|
||||||
) -> Result<Tokens, reqwest::Error> {
|
|
||||||
let redirect_uri = format!("{}/v2/auth/minecraft/callback", public_uri);
|
|
||||||
|
|
||||||
let mut params = HashMap::new();
|
|
||||||
params.insert("client_id", client_id);
|
|
||||||
params.insert("client_secret", client_secret);
|
|
||||||
params.insert("code", code);
|
|
||||||
params.insert("grant_type", "authorization_code");
|
|
||||||
params.insert("redirect_uri", redirect_uri.as_str());
|
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let result = client
|
|
||||||
.post(OAUTH_TOKEN_URL)
|
|
||||||
.form(¶ms)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.json::<Tokens>()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn refresh_token(
|
|
||||||
public_uri: &str,
|
|
||||||
refresh_token: &str,
|
|
||||||
client_id: &str,
|
|
||||||
client_secret: &str,
|
|
||||||
) -> Result<Tokens, reqwest::Error> {
|
|
||||||
let redirect_uri = format!("{}/v2/auth/minecraft/callback", public_uri);
|
|
||||||
|
|
||||||
let mut params = HashMap::new();
|
|
||||||
params.insert("client_id", client_id);
|
|
||||||
params.insert("client_secret", client_secret);
|
|
||||||
params.insert("refresh_token", refresh_token);
|
|
||||||
params.insert("grant_type", "refresh_token");
|
|
||||||
params.insert("redirect_uri", &redirect_uri);
|
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let result = client
|
|
||||||
.post(OAUTH_TOKEN_URL)
|
|
||||||
.form(¶ms)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.json::<Tokens>()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
//! Minecraft bearer token
|
|
||||||
use crate::auth::AuthenticationError;
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
const MCSERVICES_AUTH_URL: &str = "https://api.minecraftservices.com/launcher/login";
|
|
||||||
|
|
||||||
pub async fn fetch_bearer(token: &str, uhs: &str) -> Result<String, AuthenticationError> {
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let body = client
|
|
||||||
.post(MCSERVICES_AUTH_URL)
|
|
||||||
.json(&json!({
|
|
||||||
"xtoken": format!("XBL3.0 x={};{}", uhs, token),
|
|
||||||
"platform": "PC_LAUNCHER"
|
|
||||||
}))
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.text()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
serde_json::from_str::<serde_json::Value>(&body)?
|
|
||||||
.get("access_token")
|
|
||||||
.and_then(serde_json::Value::as_str)
|
|
||||||
.map(String::from)
|
|
||||||
.ok_or(AuthenticationError::Custom(format!(
|
|
||||||
"Response didn't contain valid bearer token. body: {body}"
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
//! Login redirect step
|
|
||||||
pub fn get_url(public_uri: &str, conn_id: &str, client_id: &str) -> String {
|
|
||||||
format!(
|
|
||||||
"https://login.live.com/oauth20_authorize.srf?client_id={client_id}&response_type=code&redirect_uri={}&scope={}&state={conn_id}&prompt=select_account&cobrandid=8058f65d-ce06-4c30-9559-473c9275a65d",
|
|
||||||
urlencoding::encode(&format!("{}/v2/auth/minecraft/callback", public_uri)),
|
|
||||||
urlencoding::encode("XboxLive.signin offline_access")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
//! MSA authentication stages
|
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
|
|
||||||
pub mod access_token;
|
|
||||||
pub mod bearer_token;
|
|
||||||
pub mod login_redirect;
|
|
||||||
pub mod player_info;
|
|
||||||
pub mod xbl_signin;
|
|
||||||
pub mod xsts_token;
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref REQWEST_CLIENT: reqwest::Client = {
|
|
||||||
let mut headers = reqwest::header::HeaderMap::new();
|
|
||||||
let header = reqwest::header::HeaderValue::from_str(&format!(
|
|
||||||
"modrinth/labrinth/{} (support@modrinth.com)",
|
|
||||||
env!("CARGO_PKG_VERSION")
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
headers.insert(reqwest::header::USER_AGENT, header);
|
|
||||||
reqwest::Client::builder()
|
|
||||||
.tcp_keepalive(Some(std::time::Duration::from_secs(10)))
|
|
||||||
.default_headers(headers)
|
|
||||||
.build()
|
|
||||||
.expect("Reqwest Client Building Failed")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
//! Fetch player info for display
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
const PROFILE_URL: &str = "https://api.minecraftservices.com/minecraft/profile";
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct PlayerInfo {
|
|
||||||
pub id: String,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for PlayerInfo {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
id: "606e2ff0ed7748429d6ce1d3321c7838".to_string(),
|
|
||||||
name: String::from("???"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_info(token: &str) -> Result<PlayerInfo, reqwest::Error> {
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let resp = client
|
|
||||||
.get(PROFILE_URL)
|
|
||||||
.header(reqwest::header::AUTHORIZATION, format!("Bearer {token}"))
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.error_for_status()?
|
|
||||||
.json()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(resp)
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
//! Signin for XBox Live
|
|
||||||
|
|
||||||
use crate::auth::AuthenticationError;
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
const XBL_AUTH_URL: &str = "https://user.auth.xboxlive.com/user/authenticate";
|
|
||||||
|
|
||||||
// Deserialization
|
|
||||||
pub struct XBLLogin {
|
|
||||||
pub token: String,
|
|
||||||
pub uhs: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Impl
|
|
||||||
pub async fn login_xbl(token: &str) -> Result<XBLLogin, AuthenticationError> {
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let body = client
|
|
||||||
.post(XBL_AUTH_URL)
|
|
||||||
.header(reqwest::header::ACCEPT, "application/json")
|
|
||||||
.header("x-xbl-contract-version", "1")
|
|
||||||
.json(&json!({
|
|
||||||
"Properties": {
|
|
||||||
"AuthMethod": "RPS",
|
|
||||||
"SiteName": "user.auth.xboxlive.com",
|
|
||||||
"RpsTicket": format!("d={token}")
|
|
||||||
},
|
|
||||||
"RelyingParty": "http://auth.xboxlive.com",
|
|
||||||
"TokenType": "JWT"
|
|
||||||
}))
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.text()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let json = serde_json::from_str::<serde_json::Value>(&body)?;
|
|
||||||
let token = Some(&json)
|
|
||||||
.and_then(|it| it.get("Token")?.as_str().map(String::from))
|
|
||||||
.ok_or(AuthenticationError::Custom(
|
|
||||||
"XBL response didn't contain valid token".to_string(),
|
|
||||||
))?;
|
|
||||||
let uhs = Some(&json)
|
|
||||||
.and_then(|it| {
|
|
||||||
it.get("DisplayClaims")?
|
|
||||||
.get("xui")?
|
|
||||||
.get(0)?
|
|
||||||
.get("uhs")?
|
|
||||||
.as_str()
|
|
||||||
.map(String::from)
|
|
||||||
})
|
|
||||||
.ok_or(AuthenticationError::Custom(
|
|
||||||
"XBL response didn't contain valid user hash".to_string(),
|
|
||||||
))?;
|
|
||||||
|
|
||||||
Ok(XBLLogin { token, uhs })
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
use crate::auth::AuthenticationError;
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
const XSTS_AUTH_URL: &str = "https://xsts.auth.xboxlive.com/xsts/authorize";
|
|
||||||
|
|
||||||
pub enum XSTSResponse {
|
|
||||||
Unauthorized(String),
|
|
||||||
Success { token: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_token(token: &str) -> Result<XSTSResponse, AuthenticationError> {
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let resp = client
|
|
||||||
.post(XSTS_AUTH_URL)
|
|
||||||
.header(reqwest::header::ACCEPT, "application/json")
|
|
||||||
.json(&json!({
|
|
||||||
"Properties": {
|
|
||||||
"SandboxId": "RETAIL",
|
|
||||||
"UserTokens": [
|
|
||||||
token
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"RelyingParty": "rp://api.minecraftservices.com/",
|
|
||||||
"TokenType": "JWT"
|
|
||||||
}))
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
let status = resp.status();
|
|
||||||
|
|
||||||
let body = resp.text().await?;
|
|
||||||
let json = serde_json::from_str::<serde_json::Value>(&body)?;
|
|
||||||
|
|
||||||
if status.is_success() {
|
|
||||||
Ok(json
|
|
||||||
.get("Token")
|
|
||||||
.and_then(|x| x.as_str().map(String::from))
|
|
||||||
.map(|it| XSTSResponse::Success { token: it })
|
|
||||||
.unwrap_or(XSTSResponse::Unauthorized(
|
|
||||||
"XSTS response didn't contain valid token!".to_string(),
|
|
||||||
)))
|
|
||||||
} else {
|
|
||||||
Ok(XSTSResponse::Unauthorized(
|
|
||||||
#[allow(clippy::unreadable_literal)]
|
|
||||||
match json.get("XErr").and_then(|x| x.as_i64()) {
|
|
||||||
Some(2148916238) => {
|
|
||||||
String::from("This Microsoft account is underage and is not linked to a family.")
|
|
||||||
},
|
|
||||||
Some(2148916235) => {
|
|
||||||
String::from("XBOX Live/Minecraft is not available in your country.")
|
|
||||||
},
|
|
||||||
Some(2148916233) => String::from("This account does not have a valid XBOX Live profile. Please buy Minecraft and try again!"),
|
|
||||||
Some(2148916236) | Some(2148916237) => String::from("This account needs adult verification on Xbox page."),
|
|
||||||
_ => String::from("Unknown error code"),
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
pub mod checks;
|
pub mod checks;
|
||||||
pub mod email;
|
pub mod email;
|
||||||
pub mod flows;
|
pub mod flows;
|
||||||
pub mod minecraft;
|
|
||||||
pub mod pats;
|
pub mod pats;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
mod templates;
|
mod templates;
|
||||||
@@ -48,9 +47,7 @@ pub enum AuthenticationError {
|
|||||||
#[error("Invalid state sent, you probably need to get a new websocket")]
|
#[error("Invalid state sent, you probably need to get a new websocket")]
|
||||||
SocketError,
|
SocketError,
|
||||||
#[error("Invalid callback URL specified")]
|
#[error("Invalid callback URL specified")]
|
||||||
Url,
|
Url
|
||||||
#[error("{0}")]
|
|
||||||
Custom(String),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl actix_web::ResponseError for AuthenticationError {
|
impl actix_web::ResponseError for AuthenticationError {
|
||||||
@@ -69,7 +66,6 @@ impl actix_web::ResponseError for AuthenticationError {
|
|||||||
AuthenticationError::Url => StatusCode::BAD_REQUEST,
|
AuthenticationError::Url => StatusCode::BAD_REQUEST,
|
||||||
AuthenticationError::FileHosting(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
AuthenticationError::FileHosting(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
AuthenticationError::DuplicateUser => StatusCode::BAD_REQUEST,
|
AuthenticationError::DuplicateUser => StatusCode::BAD_REQUEST,
|
||||||
AuthenticationError::Custom(..) => StatusCode::BAD_REQUEST,
|
|
||||||
AuthenticationError::SocketError => StatusCode::BAD_REQUEST,
|
AuthenticationError::SocketError => StatusCode::BAD_REQUEST,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,7 +86,6 @@ impl actix_web::ResponseError for AuthenticationError {
|
|||||||
AuthenticationError::Url => "url_error",
|
AuthenticationError::Url => "url_error",
|
||||||
AuthenticationError::FileHosting(..) => "file_hosting",
|
AuthenticationError::FileHosting(..) => "file_hosting",
|
||||||
AuthenticationError::DuplicateUser => "duplicate_user",
|
AuthenticationError::DuplicateUser => "duplicate_user",
|
||||||
AuthenticationError::Custom(..) => "custom",
|
|
||||||
AuthenticationError::SocketError => "socket",
|
AuthenticationError::SocketError => "socket",
|
||||||
},
|
},
|
||||||
description: &self.to_string(),
|
description: &self.to_string(),
|
||||||
|
|||||||
@@ -34,9 +34,6 @@ pub enum Flow {
|
|||||||
confirm_email: String,
|
confirm_email: String,
|
||||||
},
|
},
|
||||||
MinecraftAuth,
|
MinecraftAuth,
|
||||||
MicrosoftLogin {
|
|
||||||
access_token: String,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Flow {
|
impl Flow {
|
||||||
|
|||||||
38
src/main.rs
38
src/main.rs
@@ -335,25 +335,25 @@ async fn main() -> std::io::Result<()> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// {
|
{
|
||||||
// let pool_ref = pool.clone();
|
let pool_ref = pool.clone();
|
||||||
// let redis_ref = redis_pool.clone();
|
let redis_ref = redis_pool.clone();
|
||||||
// let client_ref = clickhouse.clone();
|
let client_ref = clickhouse.clone();
|
||||||
// scheduler.run(std::time::Duration::from_secs(60 * 60 * 6), move || {
|
scheduler.run(std::time::Duration::from_secs(60 * 60 * 6), move || {
|
||||||
// let pool_ref = pool_ref.clone();
|
let pool_ref = pool_ref.clone();
|
||||||
// let redis_ref = redis_ref.clone();
|
let redis_ref = redis_ref.clone();
|
||||||
// let client_ref = client_ref.clone();
|
let client_ref = client_ref.clone();
|
||||||
//
|
|
||||||
// async move {
|
async move {
|
||||||
// info!("Done running payouts");
|
info!("Done running payouts");
|
||||||
// let result = process_payout(&pool_ref, &redis_ref, &client_ref).await;
|
let result = process_payout(&pool_ref, &redis_ref, &client_ref).await;
|
||||||
// if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
// warn!("Payouts run failed: {:?}", e);
|
warn!("Payouts run failed: {:?}", e);
|
||||||
// }
|
}
|
||||||
// info!("Done running payouts");
|
info!("Done running payouts");
|
||||||
// }
|
}
|
||||||
// });
|
});
|
||||||
// }
|
}
|
||||||
|
|
||||||
let ip_salt = Pepper {
|
let ip_salt = Pepper {
|
||||||
pepper: models::ids::Base62Id(models::ids::random_base62(11)).to_string(),
|
pepper: models::ids::Base62Id(models::ids::random_base62(11)).to_string(),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::models::projects::MonetizationStatus;
|
|||||||
use crate::routes::ApiError;
|
use crate::routes::ApiError;
|
||||||
use crate::util::env::parse_var;
|
use crate::util::env::parse_var;
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use chrono::{DateTime, Datelike, Duration, Utc, Weekday};
|
use chrono::{DateTime, Datelike, Duration, Utc, Weekday, NaiveDateTime, NaiveDate, NaiveTime};
|
||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
@@ -287,7 +287,12 @@ pub async fn process_payout(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|x| (x.project_id, x.page_views))
|
.map(|x| (x.project_id, x.page_views))
|
||||||
.collect::<HashMap<u64, u64>>();
|
.collect::<HashMap<u64, u64>>();
|
||||||
views_values.extend(downloads_values);
|
|
||||||
|
for (key, value) in downloads_values.iter() {
|
||||||
|
let counter = views_values.entry(*key).or_insert(0);
|
||||||
|
*counter += *value;
|
||||||
|
}
|
||||||
|
|
||||||
let multipliers: PayoutMultipliers = PayoutMultipliers {
|
let multipliers: PayoutMultipliers = PayoutMultipliers {
|
||||||
sum: downloads_sum + views_sum,
|
sum: downloads_sum + views_sum,
|
||||||
values: views_values,
|
values: views_values,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ impl super::Validator for ForgeValidator {
|
|||||||
|
|
||||||
fn validate(
|
fn validate(
|
||||||
&self,
|
&self,
|
||||||
archive: &mut ZipArchive<Cursor<bytes::Bytes>>,
|
_archive: &mut ZipArchive<Cursor<bytes::Bytes>>,
|
||||||
) -> Result<ValidationResult, ValidationError> {
|
) -> Result<ValidationResult, ValidationError> {
|
||||||
// if archive.by_name("META-INF/mods.toml").is_err() {
|
// if archive.by_name("META-INF/mods.toml").is_err() {
|
||||||
// return Ok(ValidationResult::Warning(
|
// return Ok(ValidationResult::Warning(
|
||||||
|
|||||||
Reference in New Issue
Block a user