feat: add auth
This commit is contained in:
187
src/auth.rs
Normal file
187
src/auth.rs
Normal file
@@ -0,0 +1,187 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use actix_web::{Responder, get};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Serialize, Serializer};
|
||||
use sqlx::query;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use crate::{db, helper, models::auth};
|
||||
|
||||
#[get("/api/scp/auth/is_logged_in")]
|
||||
pub async fn is_scp_logged_in() -> impl Responder {
|
||||
if let Ok(pool) = db::get_pool().await {
|
||||
let token = query!("SELECT * FROM tokens WHERE is_refresh = ?", 1)
|
||||
.fetch_one(&pool)
|
||||
.await;
|
||||
if let Ok(_token) = token {
|
||||
return true.to_string();
|
||||
}
|
||||
}
|
||||
false.to_string()
|
||||
}
|
||||
|
||||
#[get("/api/scp/auth/start_flow")]
|
||||
pub async fn start_flow() -> impl Responder {
|
||||
let url = start_device_flow().await.unwrap();
|
||||
url
|
||||
}
|
||||
|
||||
async fn start_device_flow() -> Result<String, Box<dyn Error>> {
|
||||
let url = "https://www.servercontrolpanel.de/realms/scp/protocol/openid-connect/auth/device";
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let response = client
|
||||
.post(url)
|
||||
.form(&[("client_id", "scp"), ("scope", "offline_access openid")])
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let openid_response = response.json::<auth::OpenidResponse>().await?;
|
||||
|
||||
let _handle = start_polling_thread(openid_response.clone());
|
||||
|
||||
Ok(openid_response.verification_uri_complete)
|
||||
}
|
||||
|
||||
fn start_polling_thread(openid_response: auth::OpenidResponse) -> JoinHandle<()> {
|
||||
let client = reqwest::Client::new();
|
||||
let device_code = openid_response.device_code;
|
||||
let interval = openid_response.interval;
|
||||
let token_url = "https://www.servercontrolpanel.de/realms/scp/protocol/openid-connect/token";
|
||||
let expiration_time =
|
||||
SystemTime::now() + Duration::from_secs(openid_response.expires_in as u64);
|
||||
|
||||
let thread = tokio::spawn(async move {
|
||||
loop {
|
||||
if expiration_time <= SystemTime::now() {
|
||||
println!("Token expired!");
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_secs(interval as u64)).await;
|
||||
let response = client
|
||||
.post(token_url)
|
||||
.form(&[
|
||||
("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
|
||||
("device_code", &device_code),
|
||||
("client_id", "scp"),
|
||||
])
|
||||
.send()
|
||||
.await
|
||||
.expect("Could not poll login!");
|
||||
|
||||
if !response.status().is_success() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let openid_response = response
|
||||
.json::<auth::OpenidTokenResponse>()
|
||||
.await
|
||||
.expect("Could not deserialize response!");
|
||||
|
||||
let pool = db::get_pool().await.expect("Could not get DB Pool");
|
||||
let token_expiry =
|
||||
SystemTime::now() + Duration::from_secs(openid_response.expires_in as u64);
|
||||
let token_expiry: DateTime<Utc> = token_expiry.into();
|
||||
let token_expiry = token_expiry.to_rfc3339();
|
||||
|
||||
query!(
|
||||
"INSERT INTO tokens (token, is_refresh,expires_at) VALUES (?, 0, ?)",
|
||||
openid_response.access_token,
|
||||
token_expiry
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("Could not insert token into database");
|
||||
|
||||
let refresh_token_expiry = SystemTime::now() + Duration::from_hours(24 * 30);
|
||||
let refresh_token_expiry: DateTime<Utc> = refresh_token_expiry.into();
|
||||
let refresh_token_expiry = refresh_token_expiry.to_rfc3339();
|
||||
|
||||
query!(
|
||||
"INSERT INTO tokens (token, is_refresh,expires_at) VALUES (?, 1, ?)",
|
||||
openid_response.refresh_token,
|
||||
refresh_token_expiry
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("Could not insert token into database");
|
||||
}
|
||||
});
|
||||
thread
|
||||
}
|
||||
|
||||
pub async fn create_new_access_token() -> Result<(), Box<dyn Error>> {
|
||||
let client = reqwest::Client::new();
|
||||
let url = "https://www.servercontrolpanel.de/realms/scp/protocol/openid-connect/token";
|
||||
let refresh_token = helper::get_refresh_token().await?;
|
||||
|
||||
let response = client
|
||||
.post(url)
|
||||
.form(&[
|
||||
("client_id", "scp"),
|
||||
("refresh_token", &refresh_token),
|
||||
("grant_type", "refresh_token"),
|
||||
])
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let token_response = response.json::<auth::OpenidTokenResponse>().await?;
|
||||
|
||||
let pool = db::get_pool().await.expect("Could not get DB Pool");
|
||||
let token_expiry = SystemTime::now() + Duration::from_secs(token_response.expires_in as u64);
|
||||
let token_expiry: DateTime<Utc> = token_expiry.into();
|
||||
let token_expiry = token_expiry.to_rfc3339();
|
||||
|
||||
query!(
|
||||
"INSERT INTO tokens (token, is_refresh,expires_at) VALUES (?, 0, ?)",
|
||||
token_response.access_token,
|
||||
token_expiry
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("Could not insert token into database");
|
||||
|
||||
let refresh_token_expiry = SystemTime::now() + Duration::from_hours(24 * 30);
|
||||
let refresh_token_expiry: DateTime<Utc> = refresh_token_expiry.into();
|
||||
let refresh_token_expiry = refresh_token_expiry.to_rfc3339();
|
||||
|
||||
query!(
|
||||
"UPDATE tokens SET token = ?, expires_at = ? WHERE is_refresh = 1 AND token = ?",
|
||||
token_response.refresh_token,
|
||||
refresh_token_expiry,
|
||||
refresh_token
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("Could not update refresh token in database");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[get("/api/auth/user")]
|
||||
pub async fn get_user() -> impl Responder {
|
||||
let client = reqwest::Client::new();
|
||||
let url = "https://www.servercontrolpanel.de/realms/scp/protocol/openid-connect/userinfo";
|
||||
let token = helper::get_access_token()
|
||||
.await
|
||||
.expect("Could not get access token");
|
||||
|
||||
let response = client
|
||||
.get(url)
|
||||
.bearer_auth(token)
|
||||
.send()
|
||||
.await
|
||||
.expect("Could not get user info");
|
||||
|
||||
let user = response
|
||||
.json::<auth::User>()
|
||||
.await
|
||||
.expect("Could not parse user info");
|
||||
|
||||
let str: String = serde_json::to_string(&user).unwrap();
|
||||
str
|
||||
}
|
||||
45
src/db.rs
Normal file
45
src/db.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use std::{env, str::FromStr, sync::Mutex, time::Duration};
|
||||
|
||||
use sqlx::{
|
||||
ConnectOptions, Sqlite, SqlitePool,
|
||||
pool::PoolConnection,
|
||||
sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteSynchronous},
|
||||
};
|
||||
|
||||
static DATABASE_POOL: Mutex<Option<SqlitePool>> = Mutex::new(None);
|
||||
|
||||
pub async fn get_pool() -> Result<SqlitePool, sqlx::Error> {
|
||||
let existing_pool = {
|
||||
let lock = DATABASE_POOL.lock().unwrap();
|
||||
lock.clone()
|
||||
};
|
||||
|
||||
if let Some(pool) = existing_pool {
|
||||
return Ok(pool);
|
||||
}
|
||||
|
||||
let dburl = env::var("DATABASE_URL").unwrap_or_else(|_| "sqlite://./data.db".to_string());
|
||||
let mut options = SqliteConnectOptions::from_str(&dburl)?;
|
||||
options = options
|
||||
.create_if_missing(true)
|
||||
.journal_mode(SqliteJournalMode::Wal)
|
||||
.synchronous(SqliteSynchronous::Normal)
|
||||
.busy_timeout(Duration::from_secs(5));
|
||||
|
||||
let pool = SqlitePool::connect_with(options).await?;
|
||||
|
||||
{
|
||||
let mut lock = DATABASE_POOL.lock().unwrap();
|
||||
if lock.is_none() {
|
||||
*lock = Some(pool.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
pub async fn migrate() -> Result<(), sqlx::Error> {
|
||||
let pool = get_pool().await?;
|
||||
sqlx::migrate!("./migrations").run(&pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
32
src/helper.rs
Normal file
32
src/helper.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use std::error::Error;
|
||||
|
||||
use scp_core::apis::configuration;
|
||||
use sqlx::query;
|
||||
|
||||
use crate::db;
|
||||
|
||||
pub async fn get_refresh_token() -> Result<String, Box<dyn Error>> {
|
||||
let pool = db::get_pool().await?;
|
||||
let tok = query!("SELECT * FROM tokens WHERE is_refresh = 1 ORDER BY expires_at DESC LIMIT 1")
|
||||
.fetch_one(&pool)
|
||||
.await?
|
||||
.token;
|
||||
|
||||
Ok(tok)
|
||||
}
|
||||
|
||||
pub async fn get_access_token() -> Result<String, Box<dyn Error>> {
|
||||
let pool = db::get_pool().await?;
|
||||
let tok = query!("SELECT * FROM tokens WHERE is_refresh = 0 ORDER BY expires_at DESC LIMIT 1")
|
||||
.fetch_one(&pool)
|
||||
.await?
|
||||
.token;
|
||||
|
||||
Ok(tok)
|
||||
}
|
||||
|
||||
pub async fn get_authed_api_config() -> Result<configuration::Configuration, Box<dyn Error>> {
|
||||
let mut conf = configuration::Configuration::default();
|
||||
conf.bearer_access_token = Some(get_access_token().await?);
|
||||
Ok(conf)
|
||||
}
|
||||
53
src/jobs.rs
Normal file
53
src/jobs.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use std::{error::Error, time::Duration};
|
||||
|
||||
use quartz::{Job, Scheduler, Trigger};
|
||||
use sqlx::query;
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
use crate::{auth, db};
|
||||
pub fn init_scheduler(rt: Handle) -> Scheduler {
|
||||
let scheduler = Scheduler::new();
|
||||
|
||||
let trigger_every_5min =
|
||||
Trigger::with_identity("5min_trigger", "default_group").every(Duration::from_mins(5));
|
||||
|
||||
let trigger_every_3min =
|
||||
Trigger::with_identity("3min_trigger", "default_group").every(Duration::from_mins(3));
|
||||
|
||||
let rt_cleanup = rt.clone();
|
||||
let cleanup_tokens_job = Job::with_identity("cleanup_tokens_job", "default_group", move || {
|
||||
let rt = rt_cleanup.clone();
|
||||
rt.spawn(async move {
|
||||
let _ = cleanup_old_tokens().await;
|
||||
});
|
||||
});
|
||||
|
||||
let rt_refresh = rt.clone();
|
||||
let refresh_access_token_job =
|
||||
Job::with_identity("refresh_access_token_job", "default_group", move || {
|
||||
let rt = rt_refresh.clone();
|
||||
rt.spawn(async move {
|
||||
let _ = refresh_access_token().await;
|
||||
});
|
||||
});
|
||||
|
||||
scheduler.schedule_job(cleanup_tokens_job, trigger_every_5min);
|
||||
scheduler.schedule_job(refresh_access_token_job, trigger_every_3min);
|
||||
|
||||
scheduler
|
||||
}
|
||||
|
||||
pub async fn cleanup_old_tokens() -> Result<(), Box<dyn Error>> {
|
||||
let pool = db::get_pool().await?;
|
||||
let now = chrono::Utc::now();
|
||||
let now = now.to_rfc3339();
|
||||
query!("DELETE FROM tokens WHERE expires_at < ?", now)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn refresh_access_token() -> Result<(), Box<dyn Error>> {
|
||||
auth::create_new_access_token().await?;
|
||||
Ok(())
|
||||
}
|
||||
24
src/main.rs
24
src/main.rs
@@ -1,10 +1,16 @@
|
||||
use std::{env, error::Error};
|
||||
use std::env;
|
||||
|
||||
use actix_files::Files;
|
||||
use actix_web::{App, HttpResponse, HttpServer, Responder, get, web};
|
||||
use scp_core::apis::configuration::Configuration;
|
||||
use scp_core::apis::default_api;
|
||||
|
||||
mod auth;
|
||||
mod db;
|
||||
mod helper;
|
||||
mod jobs;
|
||||
mod models;
|
||||
|
||||
#[get("/api/hello")]
|
||||
async fn hello() -> impl Responder {
|
||||
HttpResponse::Ok().json(serde_json::json!({ "message": "hello" }))
|
||||
@@ -18,7 +24,6 @@ async fn spa_fallback() -> actix_web::Result<actix_files::NamedFile> {
|
||||
async fn ping_netcup() -> impl Responder {
|
||||
let config = Configuration::default();
|
||||
let res = default_api::api_ping_get(&config).await;
|
||||
dbg!(&res);
|
||||
|
||||
res.is_ok().to_string()
|
||||
}
|
||||
@@ -30,10 +35,18 @@ async fn main() -> std::io::Result<()> {
|
||||
.parse::<u16>()
|
||||
.unwrap();
|
||||
|
||||
HttpServer::new(|| {
|
||||
db::migrate().await.expect("Error migrating!");
|
||||
|
||||
let rt_handle = tokio::runtime::Handle::current();
|
||||
let scheduler = jobs::init_scheduler(rt_handle.clone());
|
||||
|
||||
let res = HttpServer::new(|| {
|
||||
App::new()
|
||||
.service(hello)
|
||||
.service(ping_netcup)
|
||||
.service(auth::is_scp_logged_in)
|
||||
.service(auth::start_flow)
|
||||
.service(auth::get_user)
|
||||
.service(
|
||||
Files::new("/", "./frontend/dist")
|
||||
.index_file("index.html")
|
||||
@@ -43,5 +56,8 @@ async fn main() -> std::io::Result<()> {
|
||||
})
|
||||
.bind(("127.0.0.1", port))?
|
||||
.run()
|
||||
.await
|
||||
.await;
|
||||
|
||||
scheduler.shutdown();
|
||||
res
|
||||
}
|
||||
|
||||
60
src/models/auth.rs
Normal file
60
src/models/auth.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use serde::Deserialize;
|
||||
use serde_derive::Serialize;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
||||
pub struct User {
|
||||
pub autologin_token_used: String,
|
||||
pub sub: String,
|
||||
#[serde(rename = "email_verified")]
|
||||
pub email_verified: bool,
|
||||
pub webservice_password_used: String,
|
||||
pub secure_mode_active: String,
|
||||
pub roles: Vec<String>,
|
||||
pub name: String,
|
||||
pub id: String,
|
||||
#[serde(rename = "preferred_username")]
|
||||
pub preferred_username: String,
|
||||
#[serde(rename = "given_name")]
|
||||
pub given_name: String,
|
||||
#[serde(rename = "family_name")]
|
||||
pub family_name: String,
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OpenidResponse {
|
||||
#[serde(rename = "device_code")]
|
||||
pub device_code: String,
|
||||
#[serde(rename = "user_code")]
|
||||
pub user_code: String,
|
||||
#[serde(rename = "verification_uri")]
|
||||
pub verification_uri: String,
|
||||
#[serde(rename = "verification_uri_complete")]
|
||||
pub verification_uri_complete: String,
|
||||
#[serde(rename = "expires_in")]
|
||||
pub expires_in: i64,
|
||||
pub interval: i64,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OpenidTokenResponse {
|
||||
#[serde(rename = "access_token")]
|
||||
pub access_token: String,
|
||||
#[serde(rename = "expires_in")]
|
||||
pub expires_in: i64,
|
||||
#[serde(rename = "refresh_expires_in")]
|
||||
pub refresh_expires_in: i64,
|
||||
#[serde(rename = "refresh_token")]
|
||||
pub refresh_token: String,
|
||||
#[serde(rename = "token_type")]
|
||||
pub token_type: String,
|
||||
#[serde(rename = "not-before-policy")]
|
||||
pub not_before_policy: i64,
|
||||
#[serde(rename = "session_state")]
|
||||
pub session_state: String,
|
||||
pub scope: String,
|
||||
}
|
||||
1
src/models/mod.rs
Normal file
1
src/models/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod auth;
|
||||
Reference in New Issue
Block a user