feat(backend): users with passwords
This commit is contained in:
parent
07c76423ac
commit
b480cf2e64
12 changed files with 280 additions and 71 deletions
|
@ -1,6 +1,6 @@
|
||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="dev backend" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
<configuration default="false" name="dev backend" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||||
<option name="command" value="run -- serve ./example_vault --clean --no-browser --reinitialize --rescan-mode mirror" />
|
<option name="command" value="run -- serve ./example_vault --clean --no-browser --reinitialize --rescan-mode mirror --secret upend" />
|
||||||
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
||||||
<envs />
|
<envs />
|
||||||
<option name="emulateTerminal" value="true" />
|
<option name="emulateTerminal" value="true" />
|
||||||
|
|
55
Cargo.lock
generated
55
Cargo.lock
generated
|
@ -487,6 +487,18 @@ version = "1.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6"
|
checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "argon2"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"blake2",
|
||||||
|
"cpufeatures",
|
||||||
|
"password-hash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrayref"
|
name = "arrayref"
|
||||||
version = "0.3.7"
|
version = "0.3.7"
|
||||||
|
@ -555,6 +567,12 @@ version = "0.21.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53"
|
checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64ct"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
|
@ -567,6 +585,15 @@ version = "2.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
|
checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blake2"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "blake2b_simd"
|
name = "blake2b_simd"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
@ -863,9 +890,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.9"
|
version = "0.2.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
|
checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
@ -1110,6 +1137,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer",
|
"block-buffer",
|
||||||
"crypto-common",
|
"crypto-common",
|
||||||
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1745,9 +1773,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.147"
|
version = "0.2.153"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
|
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libsqlite3-sys"
|
name = "libsqlite3-sys"
|
||||||
|
@ -2254,6 +2282,17 @@ version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "944553dd59c802559559161f9816429058b869003836120e262e8caec061b7ae"
|
checksum = "944553dd59c802559559161f9816429058b869003836120e262e8caec061b7ae"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "password-hash"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"rand_core",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.14"
|
version = "1.0.14"
|
||||||
|
@ -2914,6 +2953,12 @@ version = "0.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "subtle"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.109"
|
version = "1.0.109"
|
||||||
|
@ -3379,6 +3424,7 @@ name = "upend-db"
|
||||||
version = "0.0.2"
|
version = "0.0.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"argon2",
|
||||||
"chrono",
|
"chrono",
|
||||||
"diesel",
|
"diesel",
|
||||||
"diesel_migrations",
|
"diesel_migrations",
|
||||||
|
@ -3394,6 +3440,7 @@ dependencies = [
|
||||||
"nonempty",
|
"nonempty",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"password-hash",
|
||||||
"rayon",
|
"rayon",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
@ -4,5 +4,4 @@ pub struct UpEndConfig {
|
||||||
pub desktop_enabled: bool,
|
pub desktop_enabled: bool,
|
||||||
pub trust_executables: bool,
|
pub trust_executables: bool,
|
||||||
pub secret: String,
|
pub secret: String,
|
||||||
pub key: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ use std::collections::HashMap;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, Mutex};
|
||||||
use tracing::trace;
|
use tracing::trace;
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
use tracing_subscriber::filter::{EnvFilter, LevelFilter};
|
use tracing_subscriber::filter::{EnvFilter, LevelFilter};
|
||||||
|
@ -80,7 +80,7 @@ enum Commands {
|
||||||
entity: String,
|
entity: String,
|
||||||
/// The attribute of the entry.
|
/// The attribute of the entry.
|
||||||
attribute: String,
|
attribute: String,
|
||||||
/// The value; its type will be heurestically determined.
|
/// The value; its type will be heuristically determined.
|
||||||
value: String,
|
value: String,
|
||||||
/// Output format
|
/// Output format
|
||||||
#[arg(short, long, default_value = "tsv")]
|
#[arg(short, long, default_value = "tsv")]
|
||||||
|
@ -172,10 +172,6 @@ struct ServeArgs {
|
||||||
#[arg(long, env = "UPEND_SECRET")]
|
#[arg(long, env = "UPEND_SECRET")]
|
||||||
secret: Option<String>,
|
secret: Option<String>,
|
||||||
|
|
||||||
/// Authentication key users must supply.
|
|
||||||
#[arg(long, env = "UPEND_KEY")]
|
|
||||||
key: Option<String>,
|
|
||||||
|
|
||||||
/// Allowed host/domain name the API can serve.
|
/// Allowed host/domain name the API can serve.
|
||||||
#[arg(long, env = "UPEND_ALLOW_HOST")]
|
#[arg(long, env = "UPEND_ALLOW_HOST")]
|
||||||
allow_host: Vec<String>,
|
allow_host: Vec<String>,
|
||||||
|
@ -415,9 +411,9 @@ async fn main() -> Result<()> {
|
||||||
})),
|
})),
|
||||||
desktop_enabled: !args.no_desktop,
|
desktop_enabled: !args.no_desktop,
|
||||||
trust_executables: args.trust_executables,
|
trust_executables: args.trust_executables,
|
||||||
key: args.key,
|
|
||||||
secret,
|
secret,
|
||||||
},
|
},
|
||||||
|
public: Arc::new(Mutex::new(upend.connection()?.get_users()?.len() == 0)),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start HTTP server
|
// Start HTTP server
|
||||||
|
|
|
@ -26,7 +26,7 @@ use serde_json::json;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::convert::{TryFrom, TryInto};
|
use std::convert::{TryFrom, TryInto};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
use tracing::{debug, info, trace};
|
use tracing::{debug, info, trace};
|
||||||
|
@ -57,25 +57,90 @@ pub struct State {
|
||||||
pub job_container: jobs::JobContainer,
|
pub job_container: jobs::JobContainer,
|
||||||
pub preview_store: Option<Arc<PreviewStore>>,
|
pub preview_store: Option<Arc<PreviewStore>>,
|
||||||
pub preview_thread_pool: Option<Arc<rayon::ThreadPool>>,
|
pub preview_thread_pool: Option<Arc<rayon::ThreadPool>>,
|
||||||
|
pub public: Arc<Mutex<bool>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct JwtClaims {
|
struct JwtClaims {
|
||||||
|
user: String,
|
||||||
exp: usize,
|
exp: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct LoginRequest {
|
pub struct UserPayload {
|
||||||
key: String,
|
username: String,
|
||||||
|
password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/api/auth/login")]
|
#[post("/api/auth/login")]
|
||||||
pub async fn login(
|
pub async fn login(
|
||||||
state: web::Data<State>,
|
state: web::Data<State>,
|
||||||
payload: web::Json<LoginRequest>,
|
payload: web::Json<UserPayload>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
if state.config.key.is_none() || Some(&payload.key) == state.config.key.as_ref() {
|
let conn = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
match conn.authenticate_user(&payload.username, &payload.password) {
|
||||||
|
Ok(()) => {
|
||||||
|
let token = create_token(&payload.username, &state.config.secret)?;
|
||||||
|
Ok(HttpResponse::Ok().json(json!({ "token": token })))
|
||||||
|
}
|
||||||
|
Err(e) => Err(ErrorUnauthorized(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/api/auth/register")]
|
||||||
|
pub async fn register(
|
||||||
|
req: HttpRequest,
|
||||||
|
state: web::Data<State>,
|
||||||
|
payload: web::Json<UserPayload>,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
|
check_auth(&req, &state)?;
|
||||||
|
|
||||||
|
let conn = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
match conn.set_user(&payload.username, &payload.password) {
|
||||||
|
Ok(_) => {
|
||||||
|
*state.public.lock().unwrap() = false;
|
||||||
|
let token = create_token(&payload.username, &state.config.secret)?;
|
||||||
|
Ok(HttpResponse::Ok().json(json!({ "token": token })))
|
||||||
|
}
|
||||||
|
Err(e) => Err(ErrorInternalServerError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_auth(req: &HttpRequest, state: &State) -> Result<Option<String>, actix_web::Error> {
|
||||||
|
if *state.public.lock().unwrap() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(auth_header) = req.headers().get("Authorization") {
|
||||||
|
let auth_header = auth_header.to_str().map_err(|err| {
|
||||||
|
ErrorBadRequest(format!("Invalid value in Authorization header: {err:?}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// decode Bearer
|
||||||
|
if !auth_header.starts_with("Bearer ") {
|
||||||
|
return Err(ErrorUnauthorized("Invalid token type."));
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = jsonwebtoken::decode::<JwtClaims>(
|
||||||
|
auth_header.trim_start_matches("Bearer "),
|
||||||
|
&jsonwebtoken::DecodingKey::from_secret(state.config.secret.as_ref()),
|
||||||
|
&jsonwebtoken::Validation::default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
match token {
|
||||||
|
Ok(token) => Ok(Some(token.claims.user)),
|
||||||
|
Err(err) => Err(ErrorUnauthorized(format!("Invalid token: {err:?}"))),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(ErrorUnauthorized("Authorization required."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_token(username: &str, secret: &str) -> Result<String, Error> {
|
||||||
let claims = JwtClaims {
|
let claims = JwtClaims {
|
||||||
|
user: username.to_string(),
|
||||||
exp: (SystemTime::now()
|
exp: (SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.map_err(ErrorInternalServerError)?
|
.map_err(ErrorInternalServerError)?
|
||||||
|
@ -83,41 +148,12 @@ pub async fn login(
|
||||||
+ 7 * 24 * 60 * 60) as usize,
|
+ 7 * 24 * 60 * 60) as usize,
|
||||||
};
|
};
|
||||||
|
|
||||||
let token = jsonwebtoken::encode(
|
jsonwebtoken::encode(
|
||||||
&jsonwebtoken::Header::default(),
|
&jsonwebtoken::Header::default(),
|
||||||
&claims,
|
&claims,
|
||||||
&jsonwebtoken::EncodingKey::from_secret(state.config.secret.as_ref()),
|
&jsonwebtoken::EncodingKey::from_secret(secret.as_ref()),
|
||||||
)
|
)
|
||||||
.map_err(ErrorInternalServerError)?;
|
.map_err(ErrorInternalServerError)
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(json!({ "token": token })))
|
|
||||||
} else {
|
|
||||||
Err(ErrorUnauthorized("Incorrect token."))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_auth(req: &HttpRequest, state: &State) -> Result<(), actix_web::Error> {
|
|
||||||
if let Some(key) = &state.config.key {
|
|
||||||
if let Some(auth_header) = req.headers().get("Authorization") {
|
|
||||||
let auth_header = auth_header.to_str().map_err(|err| {
|
|
||||||
ErrorBadRequest(format!("Invalid value in Authorization header: {err:?}"))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let token = jsonwebtoken::decode::<JwtClaims>(
|
|
||||||
auth_header,
|
|
||||||
&jsonwebtoken::DecodingKey::from_secret(key.as_ref()),
|
|
||||||
&jsonwebtoken::Validation::default(),
|
|
||||||
);
|
|
||||||
|
|
||||||
token
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|err| ErrorUnauthorized(format!("Invalid token: {err:?}")))
|
|
||||||
} else {
|
|
||||||
Err(ErrorUnauthorized("Authorization required."))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -128,10 +164,13 @@ pub struct RawRequest {
|
||||||
|
|
||||||
#[get("/api/raw/{hash}")]
|
#[get("/api/raw/{hash}")]
|
||||||
pub async fn get_raw(
|
pub async fn get_raw(
|
||||||
|
req: HttpRequest,
|
||||||
state: web::Data<State>,
|
state: web::Data<State>,
|
||||||
web::Query(query): web::Query<RawRequest>,
|
web::Query(query): web::Query<RawRequest>,
|
||||||
hash: web::Path<String>,
|
hash: web::Path<String>,
|
||||||
) -> Result<impl Responder, Error> {
|
) -> Result<impl Responder, Error> {
|
||||||
|
check_auth(&req, &state)?;
|
||||||
|
|
||||||
let address =
|
let address =
|
||||||
Address::decode(&b58_decode(hash.into_inner()).map_err(ErrorInternalServerError)?)
|
Address::decode(&b58_decode(hash.into_inner()).map_err(ErrorInternalServerError)?)
|
||||||
.map_err(ErrorInternalServerError)?;
|
.map_err(ErrorInternalServerError)?;
|
||||||
|
@ -218,9 +257,12 @@ pub async fn get_raw(
|
||||||
|
|
||||||
#[head("/api/raw/{hash}")]
|
#[head("/api/raw/{hash}")]
|
||||||
pub async fn head_raw(
|
pub async fn head_raw(
|
||||||
|
req: HttpRequest,
|
||||||
state: web::Data<State>,
|
state: web::Data<State>,
|
||||||
hash: web::Path<String>,
|
hash: web::Path<String>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
|
check_auth(&req, &state)?;
|
||||||
|
|
||||||
let address =
|
let address =
|
||||||
Address::decode(&b58_decode(hash.into_inner()).map_err(ErrorInternalServerError)?)
|
Address::decode(&b58_decode(hash.into_inner()).map_err(ErrorInternalServerError)?)
|
||||||
.map_err(ErrorInternalServerError)?;
|
.map_err(ErrorInternalServerError)?;
|
||||||
|
@ -254,10 +296,13 @@ pub async fn head_raw(
|
||||||
|
|
||||||
#[get("/api/thumb/{hash}")]
|
#[get("/api/thumb/{hash}")]
|
||||||
pub async fn get_thumbnail(
|
pub async fn get_thumbnail(
|
||||||
|
req: HttpRequest,
|
||||||
state: web::Data<State>,
|
state: web::Data<State>,
|
||||||
hash: web::Path<String>,
|
hash: web::Path<String>,
|
||||||
web::Query(query): web::Query<HashMap<String, String>>,
|
web::Query(query): web::Query<HashMap<String, String>>,
|
||||||
) -> Result<Either<NamedFile, HttpResponse>, Error> {
|
) -> Result<Either<NamedFile, HttpResponse>, Error> {
|
||||||
|
check_auth(&req, &state)?;
|
||||||
|
|
||||||
#[cfg(feature = "previews")]
|
#[cfg(feature = "previews")]
|
||||||
if let Some(preview_store) = &state.preview_store {
|
if let Some(preview_store) = &state.preview_store {
|
||||||
let hash = hash.into_inner();
|
let hash = hash.into_inner();
|
||||||
|
@ -299,7 +344,13 @@ pub async fn get_thumbnail(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/api/query")]
|
#[post("/api/query")]
|
||||||
pub async fn get_query(state: web::Data<State>, query: String) -> Result<HttpResponse, Error> {
|
pub async fn get_query(
|
||||||
|
req: HttpRequest,
|
||||||
|
state: web::Data<State>,
|
||||||
|
query: String,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
|
check_auth(&req, &state)?;
|
||||||
|
|
||||||
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||||
|
|
||||||
let in_query: Query = query.parse().map_err(ErrorBadRequest)?;
|
let in_query: Query = query.parse().map_err(ErrorBadRequest)?;
|
||||||
|
@ -341,9 +392,12 @@ impl EntriesAsHash for Vec<Entry> {
|
||||||
|
|
||||||
#[get("/api/obj/{address_str}")]
|
#[get("/api/obj/{address_str}")]
|
||||||
pub async fn get_object(
|
pub async fn get_object(
|
||||||
|
req: HttpRequest,
|
||||||
state: web::Data<State>,
|
state: web::Data<State>,
|
||||||
address: web::Path<Address>,
|
address: web::Path<Address>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
|
check_auth(&req, &state)?;
|
||||||
|
|
||||||
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||||
let address = address.into_inner();
|
let address = address.into_inner();
|
||||||
|
|
||||||
|
@ -736,7 +790,12 @@ pub async fn get_address(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/api/all/attributes")]
|
#[get("/api/all/attributes")]
|
||||||
pub async fn get_all_attributes(state: web::Data<State>) -> Result<HttpResponse, Error> {
|
pub async fn get_all_attributes(
|
||||||
|
req: HttpRequest,
|
||||||
|
state: web::Data<State>,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
|
check_auth(&req, &state)?;
|
||||||
|
|
||||||
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||||
let attributes = web::block(move || connection.get_all_attributes())
|
let attributes = web::block(move || connection.get_all_attributes())
|
||||||
.await?
|
.await?
|
||||||
|
@ -779,6 +838,8 @@ pub async fn list_hier(
|
||||||
path: web::Path<String>,
|
path: web::Path<String>,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
|
check_auth(&req, &state)?;
|
||||||
|
|
||||||
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||||
if path.is_empty() {
|
if path.is_empty() {
|
||||||
Ok(HttpResponse::MovedPermanently()
|
Ok(HttpResponse::MovedPermanently()
|
||||||
|
@ -802,7 +863,11 @@ pub async fn list_hier(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/api/hier_roots")]
|
#[get("/api/hier_roots")]
|
||||||
pub async fn list_hier_roots(state: web::Data<State>) -> Result<HttpResponse, Error> {
|
pub async fn list_hier_roots(
|
||||||
|
req: HttpRequest,
|
||||||
|
state: web::Data<State>,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
|
check_auth(&req, &state)?;
|
||||||
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||||
|
|
||||||
let result = web::block(move || {
|
let result = web::block(move || {
|
||||||
|
@ -859,13 +924,15 @@ pub async fn api_refresh(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/api/stats/vault")]
|
#[get("/api/stats/vault")]
|
||||||
pub async fn vault_stats(state: web::Data<State>) -> Result<HttpResponse, Error> {
|
pub async fn vault_stats(req: HttpRequest, state: web::Data<State>) -> Result<HttpResponse, Error> {
|
||||||
|
check_auth(&req, &state)?;
|
||||||
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||||
Ok(HttpResponse::Ok().json(connection.get_stats().map_err(ErrorInternalServerError)?))
|
Ok(HttpResponse::Ok().json(connection.get_stats().map_err(ErrorInternalServerError)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/api/stats/store")]
|
#[get("/api/stats/store")]
|
||||||
pub async fn store_stats(state: web::Data<State>) -> Result<HttpResponse, Error> {
|
pub async fn store_stats(req: HttpRequest, state: web::Data<State>) -> Result<HttpResponse, Error> {
|
||||||
|
check_auth(&req, &state)?;
|
||||||
Ok(HttpResponse::Ok().json(json!({
|
Ok(HttpResponse::Ok().json(json!({
|
||||||
"main": state.store.stats().map_err(ErrorInternalServerError)?
|
"main": state.store.stats().map_err(ErrorInternalServerError)?
|
||||||
})))
|
})))
|
||||||
|
@ -878,9 +945,11 @@ pub struct JobsRequest {
|
||||||
|
|
||||||
#[get("/api/jobs")]
|
#[get("/api/jobs")]
|
||||||
pub async fn get_jobs(
|
pub async fn get_jobs(
|
||||||
|
req: HttpRequest,
|
||||||
state: web::Data<State>,
|
state: web::Data<State>,
|
||||||
web::Query(query): web::Query<JobsRequest>,
|
web::Query(query): web::Query<JobsRequest>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
|
check_auth(&req, &state)?;
|
||||||
let jobs = state
|
let jobs = state
|
||||||
.job_container
|
.job_container
|
||||||
.get_jobs()
|
.get_jobs()
|
||||||
|
@ -907,12 +976,14 @@ pub async fn get_info(state: web::Data<State>) -> Result<HttpResponse, Error> {
|
||||||
upend_db::common::build::PKG_VERSION,
|
upend_db::common::build::PKG_VERSION,
|
||||||
build::PKG_VERSION
|
build::PKG_VERSION
|
||||||
),
|
),
|
||||||
"desktop": state.config.desktop_enabled
|
"desktop": state.config.desktop_enabled,
|
||||||
|
"public": *state.public.lock().unwrap(),
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/api/options")]
|
#[get("/api/options")]
|
||||||
pub async fn get_options(state: web::Data<State>) -> Result<HttpResponse, Error> {
|
pub async fn get_options(req: HttpRequest, state: web::Data<State>) -> Result<HttpResponse, Error> {
|
||||||
|
check_auth(&req, &state)?;
|
||||||
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||||
Ok(HttpResponse::Ok().json(
|
Ok(HttpResponse::Ok().json(
|
||||||
connection
|
connection
|
||||||
|
@ -940,7 +1011,11 @@ pub async fn put_options(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/api/migration/user-entries")]
|
#[get("/api/migration/user-entries")]
|
||||||
pub async fn get_user_entries(state: web::Data<State>) -> Result<HttpResponse, Error> {
|
pub async fn get_user_entries(
|
||||||
|
req: HttpRequest,
|
||||||
|
state: web::Data<State>,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
|
check_auth(&req, &state)?;
|
||||||
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||||
|
|
||||||
let result = web::block(move || connection.get_explicit_entries())
|
let result = web::block(move || connection.get_explicit_entries())
|
||||||
|
@ -1226,11 +1301,11 @@ mod tests {
|
||||||
desktop_enabled: false,
|
desktop_enabled: false,
|
||||||
trust_executables: false,
|
trust_executables: false,
|
||||||
secret: "secret".to_string(),
|
secret: "secret".to_string(),
|
||||||
key: None,
|
|
||||||
},
|
},
|
||||||
job_container,
|
job_container,
|
||||||
preview_store: None,
|
preview_store: None,
|
||||||
preview_thread_pool: None,
|
preview_thread_pool: None,
|
||||||
|
public: Arc::new(Mutex::new(true)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@ where
|
||||||
.app_data(actix_web::web::Data::new(state))
|
.app_data(actix_web::web::Data::new(state))
|
||||||
.wrap(actix_web::middleware::Logger::default().exclude("/api/jobs"))
|
.wrap(actix_web::middleware::Logger::default().exclude("/api/jobs"))
|
||||||
.service(routes::login)
|
.service(routes::login)
|
||||||
|
.service(routes::register)
|
||||||
.service(routes::get_raw)
|
.service(routes::get_raw)
|
||||||
.service(routes::head_raw)
|
.service(routes::head_raw)
|
||||||
.service(routes::get_thumbnail)
|
.service(routes::get_thumbnail)
|
||||||
|
|
|
@ -33,6 +33,9 @@ diesel = { version = "1.4", features = [
|
||||||
] }
|
] }
|
||||||
diesel_migrations = "1.4"
|
diesel_migrations = "1.4"
|
||||||
libsqlite3-sys = { version = "^0", features = ["bundled"] }
|
libsqlite3-sys = { version = "^0", features = ["bundled"] }
|
||||||
|
password-hash = "0.5.0"
|
||||||
|
argon2 = "0.5.3"
|
||||||
|
|
||||||
|
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|
1
db/migrations/upend/01_users/down.sql
Normal file
1
db/migrations/upend/01_users/down.sql
Normal file
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE users;
|
7
db/migrations/upend/01_users/up.sql
Normal file
7
db/migrations/upend/01_users/up.sql
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
CREATE TABLE users
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
username VARCHAR NOT NULL,
|
||||||
|
password VARCHAR NOT NULL,
|
||||||
|
UNIQUE (username)
|
||||||
|
);
|
|
@ -1,4 +1,4 @@
|
||||||
use super::schema::{data, meta};
|
use super::schema::{data, meta, users};
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
|
@ -23,3 +23,11 @@ pub struct MetaValue {
|
||||||
pub key: String,
|
pub key: String,
|
||||||
pub value: String,
|
pub value: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Queryable, Insertable, Serialize, Clone, Debug)]
|
||||||
|
#[table_name = "users"]
|
||||||
|
pub struct UserValue {
|
||||||
|
pub id: i32,
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
|
@ -20,4 +20,10 @@ table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
allow_tables_to_appear_in_same_query!(data, meta,);
|
table! {
|
||||||
|
users (id) {
|
||||||
|
id -> Integer,
|
||||||
|
username -> Text,
|
||||||
|
password -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ use crate::inner::models;
|
||||||
use crate::inner::schema::data;
|
use crate::inner::schema::data;
|
||||||
use crate::util::LoggerSink;
|
use crate::util::LoggerSink;
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
|
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use diesel::r2d2::{self, ConnectionManager};
|
use diesel::r2d2::{self, ConnectionManager};
|
||||||
use diesel::result::{DatabaseErrorKind, Error};
|
use diesel::result::{DatabaseErrorKind, Error};
|
||||||
|
@ -273,6 +274,55 @@ impl UpEndConnection {
|
||||||
Ok(VaultOptions { blob_mode })
|
Ok(VaultOptions { blob_mode })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_users(&self) -> Result<Vec<String>> {
|
||||||
|
use crate::inner::schema::users::dsl;
|
||||||
|
|
||||||
|
let _lock = self.lock.read().unwrap();
|
||||||
|
let conn = self.pool.get()?;
|
||||||
|
|
||||||
|
let result = dsl::users.select(dsl::username).load::<String>(&conn)?;
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_user(&self, username: &str, password: &str) -> Result<bool> {
|
||||||
|
use crate::inner::schema::users::dsl;
|
||||||
|
|
||||||
|
let salt = password_hash::SaltString::generate(&mut password_hash::rand_core::OsRng);
|
||||||
|
let argon2 = Argon2::default();
|
||||||
|
let hashed_password = argon2
|
||||||
|
.hash_password(password.as_ref(), &salt)
|
||||||
|
.map_err(|e| anyhow!(e))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let _lock = self.lock.write().unwrap();
|
||||||
|
let conn = self.pool.get()?;
|
||||||
|
let result = diesel::replace_into(dsl::users)
|
||||||
|
.values((
|
||||||
|
dsl::username.eq(username),
|
||||||
|
dsl::password.eq(hashed_password),
|
||||||
|
))
|
||||||
|
.execute(&conn)?;
|
||||||
|
Ok(result > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn authenticate_user(&self, username: &str, password: &str) -> Result<()> {
|
||||||
|
use crate::inner::schema::users::dsl;
|
||||||
|
|
||||||
|
let conn = self.pool.get()?;
|
||||||
|
let user_result = dsl::users
|
||||||
|
.filter(dsl::username.eq(username))
|
||||||
|
.load::<models::UserValue>(&conn)?;
|
||||||
|
|
||||||
|
let user = user_result.first().ok_or(anyhow!("User not found"))?;
|
||||||
|
|
||||||
|
let parsed_hash = PasswordHash::new(&user.password).map_err(|e| anyhow!(e))?;
|
||||||
|
let argon2 = Argon2::default();
|
||||||
|
argon2
|
||||||
|
.verify_password(password.as_ref(), &parsed_hash)
|
||||||
|
.map_err(|e| anyhow!(e))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn retrieve_entry(&self, hash: &UpMultihash) -> Result<Option<Entry>> {
|
pub fn retrieve_entry(&self, hash: &UpMultihash) -> Result<Option<Entry>> {
|
||||||
use crate::inner::schema::data::dsl::*;
|
use crate::inner::schema::data::dsl::*;
|
||||||
|
|
||||||
|
@ -602,6 +652,22 @@ mod test {
|
||||||
assert_eq!(result[0].entity, edge_entity);
|
assert_eq!(result[0].entity, edge_entity);
|
||||||
assert_eq!(result[0].value, EntryValue::Address(random_entity));
|
assert_eq!(result[0].value, EntryValue::Address(random_entity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_users() {
|
||||||
|
let tempdir = TempDir::new().unwrap();
|
||||||
|
let result = UpEndDatabase::open(&tempdir, false).unwrap();
|
||||||
|
let db = result.db;
|
||||||
|
|
||||||
|
let connection = db.connection().unwrap();
|
||||||
|
|
||||||
|
assert!(connection.authenticate_user("thm", "hunter2").is_err());
|
||||||
|
connection.set_user("thm", "hunter2").unwrap();
|
||||||
|
connection.authenticate_user("thm", "hunter2").unwrap();
|
||||||
|
assert!(connection.authenticate_user("thm", "password").is_err());
|
||||||
|
connection.set_user("thm", "password").unwrap();
|
||||||
|
connection.authenticate_user("thm", "password").unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
|
Loading…
Reference in a new issue