mirror of
https://github.com/ellmau/adf-obdd.git
synced 2025-12-20 09:39:38 +01:00
Add API for adding/deleting users; login; logout
This commit is contained in:
parent
38737bc725
commit
620e86e10b
1080
Cargo.lock
generated
1080
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
24
flake.lock
generated
24
flake.lock
generated
@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1667395993,
|
||||
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
|
||||
"lastModified": 1678901627,
|
||||
"narHash": "sha256-U02riOqrKKzwjsxc/400XnElV+UtPUQWpANPlyazjH0=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
|
||||
"rev": "93a2b84fc4b70d9e089d029deacc3583435c2ed6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -33,11 +33,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1672441588,
|
||||
"narHash": "sha256-jx5kxOyeObnVD44HRebKYL3cjWrcKhhcDmEYm0/naDY=",
|
||||
"lastModified": 1679966490,
|
||||
"narHash": "sha256-k0jV+y1jawE6w4ZvKgXDNg4+O9NNtcaWwzw8gufv0b4=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "6a0d2701705c3cf6f42c15aa92b7885f1f8a477f",
|
||||
"rev": "5b7cd5c39befee629be284970415b6eb3b0ff000",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -49,11 +49,11 @@
|
||||
},
|
||||
"nixpkgs-unstable": {
|
||||
"locked": {
|
||||
"lastModified": 1672350804,
|
||||
"narHash": "sha256-jo6zkiCabUBn3ObuKXHGqqORUMH27gYDIFFfLq5P4wg=",
|
||||
"lastModified": 1679944645,
|
||||
"narHash": "sha256-e5Qyoe11UZjVfgRfwNoSU57ZeKuEmjYb77B9IVW7L/M=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "677ed08a50931e38382dbef01cba08a8f7eac8f6",
|
||||
"rev": "4bb072f0a8b267613c127684e099a70e1f6ff106",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -82,11 +82,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1672626196,
|
||||
"narHash": "sha256-BfdLrMqxqa4YA1I1wgPBQyu4FPzL0Tp4WI2C5S6BuYo=",
|
||||
"lastModified": 1680056830,
|
||||
"narHash": "sha256-WB4KD8oLSxAAtmXYSzwVwQusC2Gy5vTEln1uTt0GI2k=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "c8bf9c162bb3f734cf357846e995eb70b94e2bcd",
|
||||
"rev": "c8d8d05b8100d451243b614d950fa3f966c1fcc2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@ -71,7 +71,7 @@ impl Adf {
|
||||
ac: vec![Term(0); parser.dict_size()],
|
||||
rng: Adf::default_rng(),
|
||||
};
|
||||
(0..parser.dict_size()).into_iter().for_each(|value| {
|
||||
(0..parser.dict_size()).for_each(|value| {
|
||||
log::trace!("adding variable {}", Var(value));
|
||||
result.bdd.variable(Var(value));
|
||||
});
|
||||
|
||||
@ -556,7 +556,7 @@ impl Bdd {
|
||||
|
||||
/// Counts how often another roBDD uses a [variable][crate::datatypes::Var], which occurs in this roBDD.
|
||||
pub fn active_var_impact(&self, var: Var, termlist: &[Term]) -> usize {
|
||||
(0..termlist.len()).into_iter().fold(0usize, |acc, idx| {
|
||||
(0..termlist.len()).fold(0usize, |acc, idx| {
|
||||
if self
|
||||
.var_dependencies(termlist[var.value()])
|
||||
.contains(&Var(idx))
|
||||
|
||||
@ -20,6 +20,10 @@ derive_more = "0.99.17"
|
||||
env_logger = "0.9"
|
||||
log = "0.4"
|
||||
serde = "1"
|
||||
mongodb = "2.4.0"
|
||||
actix-identity = "0.5.2"
|
||||
argon2 = "0.5.0"
|
||||
actix-session = { version="0.7.2", features = ["cookie-session"] }
|
||||
|
||||
[features]
|
||||
cors_for_local_development = []
|
||||
|
||||
@ -1,9 +1,23 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use actix_files as fs;
|
||||
use actix_identity::{Identity, IdentityMiddleware};
|
||||
use actix_session::config::PersistentSession;
|
||||
use actix_session::storage::CookieSessionStore;
|
||||
use actix_session::SessionMiddleware;
|
||||
use actix_web::cookie::Key;
|
||||
use actix_web::rt::task::spawn_blocking;
|
||||
use actix_web::rt::time::timeout;
|
||||
use actix_web::{post, web, App, HttpServer, Responder, ResponseError};
|
||||
use actix_web::{
|
||||
delete, post, web, App, HttpMessage, HttpRequest, HttpResponse, HttpServer, Responder,
|
||||
ResponseError,
|
||||
};
|
||||
use adf_bdd::datatypes::Term;
|
||||
use argon2::password_hash::rand_core::OsRng;
|
||||
use argon2::password_hash::SaltString;
|
||||
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
||||
use mongodb::results::DeleteResult;
|
||||
use mongodb::{bson::doc, options::IndexOptions, Client, IndexModel};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use derive_more::{Display, Error};
|
||||
@ -15,13 +29,149 @@ use adf_bdd::adf::{Adf, DoubleLabeledGraph};
|
||||
use adf_bdd::adfbiodivine::Adf as BdAdf;
|
||||
use adf_bdd::parser::AdfParser;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
const THIRTY_MINUTES: actix_web::cookie::time::Duration =
|
||||
actix_web::cookie::time::Duration::minutes(30);
|
||||
|
||||
const ASSET_DIRECTORY: &str = "./assets";
|
||||
|
||||
const DB_NAME: &str = "adf-obdd";
|
||||
const USER_COLL: &str = "users";
|
||||
const ADF_COLL: &str = "adf-problems";
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct User {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
// Creates an index on the "username" field to force the values to be unique.
|
||||
async fn create_username_index(client: &Client) {
|
||||
let options = IndexOptions::builder().unique(true).build();
|
||||
let model = IndexModel::builder()
|
||||
.keys(doc! { "username": 1 })
|
||||
.options(options)
|
||||
.build();
|
||||
client
|
||||
.database(DB_NAME)
|
||||
.collection::<User>(USER_COLL)
|
||||
.create_index(model, None)
|
||||
.await
|
||||
.expect("creating an index should succeed");
|
||||
}
|
||||
|
||||
// Add new user
|
||||
#[post("/register")]
|
||||
async fn register(client: web::Data<Client>, user: web::Json<User>) -> impl Responder {
|
||||
let mut user: User = user.into_inner();
|
||||
let user_coll = client.database(DB_NAME).collection(USER_COLL);
|
||||
|
||||
let user_exists: bool = user_coll
|
||||
.find_one(doc! { "username": &user.username }, None)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.is_some();
|
||||
|
||||
if user_exists {
|
||||
return HttpResponse::Conflict()
|
||||
.body("Username is already taken. Please pick another one!");
|
||||
}
|
||||
|
||||
let pw = &user.password;
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let hashed_pw = Argon2::default()
|
||||
.hash_password(pw.as_bytes(), &salt)
|
||||
.expect("Error while hashing password!")
|
||||
.to_string();
|
||||
|
||||
user.password = hashed_pw;
|
||||
|
||||
let result = user_coll.insert_one(user, None).await;
|
||||
match result {
|
||||
Ok(_) => HttpResponse::Ok().body("Registration successful!"),
|
||||
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
// Remove user
|
||||
#[delete("/delete")]
|
||||
async fn delete_account(client: web::Data<Client>, identity: Option<Identity>) -> impl Responder {
|
||||
let user_coll: mongodb::Collection<User> = client.database(DB_NAME).collection(USER_COLL);
|
||||
|
||||
match identity.map(|id| id.id()) {
|
||||
None => HttpResponse::Unauthorized().body("You need to login to delete your account."),
|
||||
Some(Err(err)) => HttpResponse::InternalServerError().body(err.to_string()),
|
||||
Some(Ok(username)) => {
|
||||
match user_coll
|
||||
.delete_one(doc! { "username": username }, None)
|
||||
.await
|
||||
{
|
||||
Ok(DeleteResult {
|
||||
deleted_count: 0, ..
|
||||
}) => HttpResponse::InternalServerError().body("Account could not be deleted."),
|
||||
Ok(DeleteResult {
|
||||
deleted_count: 1, ..
|
||||
}) => HttpResponse::Ok().body("Account deleted."),
|
||||
Ok(_) => unreachable!(
|
||||
"delete_one removes at most one entry so all cases are covered already"
|
||||
),
|
||||
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Login
|
||||
#[post("/login")]
|
||||
async fn login(
|
||||
req: HttpRequest,
|
||||
client: web::Data<Client>,
|
||||
user_data: web::Json<User>,
|
||||
) -> impl Responder {
|
||||
let username = &user_data.username;
|
||||
let pw = &user_data.password;
|
||||
let user_coll: mongodb::Collection<User> = client.database(DB_NAME).collection(USER_COLL);
|
||||
match user_coll
|
||||
.find_one(doc! { "username": username }, None)
|
||||
.await
|
||||
{
|
||||
Ok(Some(user)) => {
|
||||
let stored_hash = PasswordHash::new(&user.password).unwrap();
|
||||
let pw_valid = Argon2::default()
|
||||
.verify_password(pw.as_bytes(), &stored_hash)
|
||||
.is_ok();
|
||||
|
||||
if pw_valid {
|
||||
Identity::login(&req.extensions(), username.to_string()).unwrap();
|
||||
HttpResponse::Ok().body("Login successful!")
|
||||
} else {
|
||||
HttpResponse::BadRequest().body("Invalid email or password")
|
||||
}
|
||||
}
|
||||
Ok(None) => HttpResponse::NotFound().body(format!(
|
||||
"No user found with username {}",
|
||||
&user_data.username
|
||||
)),
|
||||
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("/logout")]
|
||||
async fn logout(id: Identity) -> impl Responder {
|
||||
id.logout();
|
||||
|
||||
HttpResponse::Ok().body("Logout successful!")
|
||||
}
|
||||
|
||||
type Ac = Option<Vec<Term>>;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
enum Parsing {
|
||||
Naive,
|
||||
Hybrid,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, Serialize)]
|
||||
enum Strategy {
|
||||
ParseOnly,
|
||||
Ground,
|
||||
@ -32,6 +182,30 @@ enum Strategy {
|
||||
StableNogood,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AcsPerStrategy {
|
||||
parse_only: Ac,
|
||||
ground: Ac,
|
||||
complete: Ac,
|
||||
stable: Ac,
|
||||
stable_counting_a: Ac,
|
||||
stable_counting_b: Ac,
|
||||
stable_nogood: Ac,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AdfProblem {
|
||||
code: String,
|
||||
parsing_used: Parsing,
|
||||
adf: Adf,
|
||||
acs_per_strategy: AcsPerStrategy,
|
||||
}
|
||||
|
||||
// #[get("/")]
|
||||
// fn index() -> impl Responder {
|
||||
|
||||
// }
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SolveReqBody {
|
||||
code: String,
|
||||
@ -39,11 +213,6 @@ struct SolveReqBody {
|
||||
strategy: Strategy,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SolveResBody {
|
||||
graph: DoubleLabeledGraph,
|
||||
}
|
||||
|
||||
fn solve(req_body: web::Json<SolveReqBody>) -> impl Responder {
|
||||
let input = &req_body.code;
|
||||
let parsing = &req_body.parsing;
|
||||
@ -72,7 +241,7 @@ fn solve(req_body: web::Json<SolveReqBody>) -> impl Responder {
|
||||
|
||||
log::debug!("{:?}", adf);
|
||||
|
||||
let acs = match strategy {
|
||||
let acs: Vec<Ac> = match strategy {
|
||||
Strategy::ParseOnly => vec![None],
|
||||
Strategy::Ground => vec![Some(adf.grounded())],
|
||||
Strategy::Complete => adf.complete().map(Some).collect(),
|
||||
@ -125,30 +294,54 @@ async fn main() -> std::io::Result<()> {
|
||||
.filter_level(log::LevelFilter::Debug)
|
||||
.init();
|
||||
|
||||
#[cfg(feature = "cors_for_local_development")]
|
||||
let server = HttpServer::new(|| {
|
||||
// setup mongodb
|
||||
let mongodb_uri =
|
||||
std::env::var("MONGODB_URI").unwrap_or_else(|_| "mongodb://localhost:27017".into());
|
||||
let client = Client::with_uri_str(mongodb_uri)
|
||||
.await
|
||||
.expect("failed to connect to mongodb");
|
||||
create_username_index(&client).await;
|
||||
|
||||
// cookie secret ket
|
||||
let secret_key = Key::generate();
|
||||
|
||||
let server = HttpServer::new(move || {
|
||||
let app = App::new();
|
||||
|
||||
#[cfg(feature = "cors_for_local_development")]
|
||||
let cors = Cors::default()
|
||||
.allow_any_origin()
|
||||
.allowed_origin("http://localhost:1234")
|
||||
.allow_any_method()
|
||||
.allow_any_header()
|
||||
.max_age(3600);
|
||||
|
||||
App::new()
|
||||
.wrap(cors)
|
||||
.service(solve_with_timeout)
|
||||
// this mus be last to not override anything
|
||||
.service(fs::Files::new("/", "./assets").index_file("index.html"))
|
||||
})
|
||||
.bind(("0.0.0.0", 8080))?
|
||||
.run()
|
||||
.await;
|
||||
#[cfg(feature = "cors_for_local_development")]
|
||||
let app = app.wrap(cors);
|
||||
|
||||
#[cfg(not(feature = "cors_for_local_development"))]
|
||||
let server = HttpServer::new(|| {
|
||||
App::new()
|
||||
#[cfg(feature = "cors_for_local_development")]
|
||||
let cookie_secure = false;
|
||||
#[cfg(not(feature = "cors_for_local_development"))]
|
||||
let cookie_secure = true;
|
||||
|
||||
app.app_data(web::Data::new(client.clone()))
|
||||
.wrap(IdentityMiddleware::default())
|
||||
.wrap(
|
||||
SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone())
|
||||
.cookie_name("adf-obdd-service-auth".to_owned())
|
||||
.cookie_secure(cookie_secure)
|
||||
.session_lifecycle(PersistentSession::default().session_ttl(THIRTY_MINUTES))
|
||||
.build(),
|
||||
)
|
||||
.service(
|
||||
web::scope("/users")
|
||||
.service(register)
|
||||
.service(delete_account)
|
||||
.service(login)
|
||||
.service(logout),
|
||||
)
|
||||
.service(solve_with_timeout)
|
||||
// this mus be last to not override anything
|
||||
.service(fs::Files::new("/", "./assets").index_file("index.html"))
|
||||
.service(fs::Files::new("/", ASSET_DIRECTORY).index_file("index.html"))
|
||||
})
|
||||
.bind(("0.0.0.0", 8080))?
|
||||
.run()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user