Compare commits
No commits in common. "977059c90d3c6836289c14822195c3d38e80b2b9" and "ab3ae24b56acf7edeb8bbcb22463d8630781d2a1" have entirely different histories.
977059c90d
...
ab3ae24b56
16
Makefile
16
Makefile
|
@ -1,18 +1,8 @@
|
||||||
|
|
||||||
.PHONY: server-dev server-test client-dev client-test
|
.PHONY: server client-dev
|
||||||
|
|
||||||
test:
|
server:
|
||||||
cd server && make test-oneshot
|
cd servilo && make
|
||||||
cd v-client && make test
|
|
||||||
|
|
||||||
server-dev:
|
|
||||||
cd server && make dev
|
|
||||||
|
|
||||||
server-test:
|
|
||||||
cd server && make test
|
|
||||||
|
|
||||||
client-dev:
|
client-dev:
|
||||||
cd v-client && make dev
|
cd v-client && make dev
|
||||||
|
|
||||||
client-test:
|
|
||||||
cd v-client && make test
|
|
||||||
|
|
|
@ -2,44 +2,15 @@
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 3
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "autocfg"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "common"
|
name = "common"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
"serde_yaml",
|
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hashbrown"
|
|
||||||
version = "0.12.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "indexmap"
|
|
||||||
version = "1.9.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
"hashbrown",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itoa"
|
|
||||||
version = "1.0.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.34"
|
version = "1.0.34"
|
||||||
|
@ -58,17 +29,11 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ryu"
|
|
||||||
version = "1.0.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.147"
|
version = "1.0.132"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965"
|
checksum = "8b9875c23cf305cd1fd7eb77234cbb705f21ea6a72c637a5c6db5fe4b8e7f008"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
|
@ -81,19 +46,6 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_yaml"
|
|
||||||
version = "0.9.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6d232d893b10de3eb7258ff01974d6ee20663d8e833263c99409d4b13a0209da"
|
|
||||||
dependencies = [
|
|
||||||
"indexmap",
|
|
||||||
"itoa",
|
|
||||||
"ryu",
|
|
||||||
"serde",
|
|
||||||
"unsafe-libyaml",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.84"
|
version = "1.0.84"
|
||||||
|
@ -130,9 +82,3 @@ name = "unicode-xid"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unsafe-libyaml"
|
|
||||||
version = "0.2.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c1e5fa573d8ac5f1a856f8d7be41d390ee973daf97c806b2c1a465e4e1406e68"
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,21 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "server"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
default-run = "server"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
anyhow = { version = "1" }
|
|
||||||
rand = { version = "0.8" }
|
|
||||||
rusqlite = { version = "0.26" }
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
sha2 = { version = "0.10" }
|
|
||||||
thiserror = { version = "1" }
|
|
||||||
tokio = { version = "1", features = ["full"] }
|
|
||||||
uuid = { version = "0.8", features = ["v4"] }
|
|
||||||
warp = { version = "0.3" }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
tempfile = { version = "3" }
|
|
|
@ -1,11 +0,0 @@
|
||||||
|
|
||||||
.PHONY: dev
|
|
||||||
|
|
||||||
dev:
|
|
||||||
cargo watch -x run
|
|
||||||
|
|
||||||
test:
|
|
||||||
cargo watch -x test
|
|
||||||
|
|
||||||
test-oneshot:
|
|
||||||
cargo test
|
|
|
@ -1,286 +0,0 @@
|
||||||
use crate::errors::{error, fatal, ok, AppResult};
|
|
||||||
use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ValueRef};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::{convert::Infallible, str::FromStr};
|
|
||||||
use thiserror::Error;
|
|
||||||
use uuid::{adapter::Hyphenated, Uuid};
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
use crate::errors::maybe_fail;
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum AuthenticationError {
|
|
||||||
#[error("username already exists")]
|
|
||||||
DuplicateUsername,
|
|
||||||
#[error("invitation is not valid")]
|
|
||||||
InvalidInvitation,
|
|
||||||
#[error("session token not found")]
|
|
||||||
InvalidSession,
|
|
||||||
#[error("user not found")]
|
|
||||||
UserNotFound,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
|
||||||
pub struct SessionToken(String);
|
|
||||||
|
|
||||||
impl From<&str> for SessionToken {
|
|
||||||
fn from(s: &str) -> Self {
|
|
||||||
SessionToken(s.to_owned())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for SessionToken {
|
|
||||||
type Err = Infallible;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
Ok(SessionToken(s.to_owned()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SessionToken> for String {
|
|
||||||
fn from(s: SessionToken) -> Self {
|
|
||||||
s.0.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
|
||||||
pub struct Invitation(String);
|
|
||||||
|
|
||||||
impl From<&str> for Invitation {
|
|
||||||
fn from(s: &str) -> Self {
|
|
||||||
Invitation(s.to_owned())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Invitation {
|
|
||||||
type Err = Infallible;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
Ok(Invitation(s.to_owned()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Invitation> for String {
|
|
||||||
fn from(s: Invitation) -> Self {
|
|
||||||
s.0.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
|
||||||
pub struct UserId(String);
|
|
||||||
|
|
||||||
impl From<&str> for UserId {
|
|
||||||
fn from(s: &str) -> Self {
|
|
||||||
UserId(s.to_owned())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for UserId {
|
|
||||||
type Err = Infallible;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
Ok(UserId(s.to_owned()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<UserId> for String {
|
|
||||||
fn from(s: UserId) -> Self {
|
|
||||||
s.0.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromSql for UserId {
|
|
||||||
fn column_result(val: ValueRef<'_>) -> FromSqlResult<Self> {
|
|
||||||
match val {
|
|
||||||
ValueRef::Text(t) => Ok(UserId::from(
|
|
||||||
String::from_utf8(Vec::from(t)).unwrap().as_ref(),
|
|
||||||
)),
|
|
||||||
_ => Err(FromSqlError::InvalidType),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
|
||||||
pub struct Username(String);
|
|
||||||
|
|
||||||
impl From<&str> for Username {
|
|
||||||
fn from(s: &str) -> Self {
|
|
||||||
Username(s.to_owned())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Username {
|
|
||||||
type Err = Infallible;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
Ok(Username(s.to_owned()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&Username> for String {
|
|
||||||
fn from(s: &Username) -> Self {
|
|
||||||
s.0.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Username> for String {
|
|
||||||
fn from(s: Username) -> Self {
|
|
||||||
s.0.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromSql for Username {
|
|
||||||
fn column_result(val: ValueRef<'_>) -> FromSqlResult<Self> {
|
|
||||||
match val {
|
|
||||||
ValueRef::Text(t) => Ok(Username::from(
|
|
||||||
String::from_utf8(Vec::from(t)).unwrap().as_ref(),
|
|
||||||
)),
|
|
||||||
_ => Err(FromSqlError::InvalidType),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait AuthenticationDB: Send + Sync {
|
|
||||||
fn create_user(&mut self, username: Username) -> AppResult<UserId, AuthenticationError>;
|
|
||||||
|
|
||||||
fn create_invitation(&mut self, user: UserId) -> AppResult<Invitation, AuthenticationError>;
|
|
||||||
|
|
||||||
fn authenticate(
|
|
||||||
&mut self,
|
|
||||||
invitation: Invitation,
|
|
||||||
) -> AppResult<SessionToken, AuthenticationError>;
|
|
||||||
|
|
||||||
fn delete_session(&mut self, session: SessionToken) -> AppResult<(), AuthenticationError>;
|
|
||||||
|
|
||||||
fn delete_invitation(&mut self, invitation: Invitation) -> AppResult<(), AuthenticationError>;
|
|
||||||
|
|
||||||
fn delete_user(&mut self, user: UserId) -> AppResult<(), AuthenticationError>;
|
|
||||||
|
|
||||||
fn validate_session(&self, session: SessionToken) -> AppResult<(), AuthenticationError>;
|
|
||||||
|
|
||||||
fn get_user_id(&self, username: Username) -> AppResult<UserId, AuthenticationError>;
|
|
||||||
|
|
||||||
fn list_users(&self) -> AppResult<Vec<Username>, AuthenticationError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct MemoryAuth {
|
|
||||||
users: HashMap<Username, UserId>,
|
|
||||||
inverse_users: HashMap<UserId, Username>,
|
|
||||||
invitations: HashMap<Invitation, UserId>,
|
|
||||||
sessions: HashMap<SessionToken, UserId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AuthenticationDB for MemoryAuth {
|
|
||||||
fn create_user(&mut self, username: Username) -> AppResult<UserId, AuthenticationError> {
|
|
||||||
#[cfg(test)]
|
|
||||||
let _ = maybe_fail::<AuthenticationError>(vec![])?;
|
|
||||||
|
|
||||||
let userid = UserId::from(format!("{}", Hyphenated::from_uuid(Uuid::new_v4())).as_str());
|
|
||||||
self.users.insert(username.clone(), userid.clone());
|
|
||||||
self.inverse_users.insert(userid.clone(), username);
|
|
||||||
|
|
||||||
ok(userid)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_invitation(&mut self, user: UserId) -> AppResult<Invitation, AuthenticationError> {
|
|
||||||
#[cfg(test)]
|
|
||||||
let _ = maybe_fail::<AuthenticationError>(vec![])?;
|
|
||||||
|
|
||||||
if !self.inverse_users.contains_key(&user) {
|
|
||||||
return error::<Invitation, AuthenticationError>(AuthenticationError::UserNotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
let invitation =
|
|
||||||
Invitation::from(format!("{}", Hyphenated::from_uuid(Uuid::new_v4())).as_str());
|
|
||||||
self.invitations.insert(invitation.clone(), user);
|
|
||||||
ok(invitation)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn authenticate(
|
|
||||||
&mut self,
|
|
||||||
invitation: Invitation,
|
|
||||||
) -> AppResult<SessionToken, AuthenticationError> {
|
|
||||||
#[cfg(test)]
|
|
||||||
let _ = maybe_fail::<AuthenticationError>(vec![])?;
|
|
||||||
|
|
||||||
if let Some(user) = self.invitations.get(&invitation) {
|
|
||||||
let session_token =
|
|
||||||
SessionToken::from(format!("{}", Hyphenated::from_uuid(Uuid::new_v4())).as_str());
|
|
||||||
self.sessions.insert(session_token.clone(), user.clone());
|
|
||||||
let _ = self.invitations.remove(&invitation);
|
|
||||||
ok(session_token)
|
|
||||||
} else {
|
|
||||||
error::<SessionToken, AuthenticationError>(AuthenticationError::InvalidInvitation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete_session(&mut self, session: SessionToken) -> AppResult<(), AuthenticationError> {
|
|
||||||
#[cfg(test)]
|
|
||||||
let _ = maybe_fail::<AuthenticationError>(vec![])?;
|
|
||||||
|
|
||||||
if let Some(_) = self.sessions.remove(&session) {
|
|
||||||
ok(())
|
|
||||||
} else {
|
|
||||||
error::<(), AuthenticationError>(AuthenticationError::InvalidSession)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete_invitation(&mut self, invitation: Invitation) -> AppResult<(), AuthenticationError> {
|
|
||||||
#[cfg(test)]
|
|
||||||
let _ = maybe_fail::<AuthenticationError>(vec![])?;
|
|
||||||
|
|
||||||
if let Some(_) = self.invitations.remove(&invitation) {
|
|
||||||
ok(())
|
|
||||||
} else {
|
|
||||||
error::<(), AuthenticationError>(AuthenticationError::InvalidInvitation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete_user(&mut self, user: UserId) -> AppResult<(), AuthenticationError> {
|
|
||||||
#[cfg(test)]
|
|
||||||
let _ = maybe_fail::<AuthenticationError>(vec![])?;
|
|
||||||
|
|
||||||
if let Some(username) = self.inverse_users.remove(&user) {
|
|
||||||
let _ = self.users.remove(&username);
|
|
||||||
self.invitations = self
|
|
||||||
.invitations
|
|
||||||
.iter()
|
|
||||||
.filter(|(_, value)| **value != user)
|
|
||||||
.map(|(key, value)| (key.clone(), value.clone()))
|
|
||||||
.collect::<HashMap<Invitation, UserId>>();
|
|
||||||
self.sessions = self
|
|
||||||
.sessions
|
|
||||||
.iter()
|
|
||||||
.filter(|(_, value)| **value != user)
|
|
||||||
.map(|(key, value)| (key.clone(), value.clone()))
|
|
||||||
.collect::<HashMap<SessionToken, UserId>>();
|
|
||||||
ok(())
|
|
||||||
} else {
|
|
||||||
error::<(), AuthenticationError>(AuthenticationError::UserNotFound)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_session(&self, session: SessionToken) -> AppResult<(), AuthenticationError> {
|
|
||||||
if self.sessions.contains_key(&session) {
|
|
||||||
ok(())
|
|
||||||
} else {
|
|
||||||
error::<(), AuthenticationError>(AuthenticationError::InvalidSession)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_user_id(&self, username: Username) -> AppResult<UserId, AuthenticationError> {
|
|
||||||
Ok(self
|
|
||||||
.users
|
|
||||||
.get(&username)
|
|
||||||
.map(|u| u.clone())
|
|
||||||
.ok_or(AuthenticationError::UserNotFound))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn list_users(&self) -> AppResult<Vec<Username>, AuthenticationError> {
|
|
||||||
ok(self.users.keys().cloned().collect::<Vec<Username>>())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {}
|
|
|
@ -1,3 +0,0 @@
|
||||||
fn main() {
|
|
||||||
println!("There is a tool");
|
|
||||||
}
|
|
|
@ -1,111 +0,0 @@
|
||||||
use rusqlite::{params, Connection};
|
|
||||||
use std::{
|
|
||||||
ops::{Deref, DerefMut},
|
|
||||||
path::PathBuf,
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct ManagedConnection<'a> {
|
|
||||||
pool: &'a Database,
|
|
||||||
connection: Option<Connection>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Database {
|
|
||||||
file_path: PathBuf,
|
|
||||||
pool: Arc<Mutex<Vec<Connection>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Database {
|
|
||||||
pub fn new(file_path: PathBuf) -> Result<Database, anyhow::Error> {
|
|
||||||
let mut connection = Connection::open(file_path.clone())?;
|
|
||||||
|
|
||||||
let tx = connection.transaction()?;
|
|
||||||
let version: i32 = tx.pragma_query_value(None, "user_version", |r| r.get(0))?;
|
|
||||||
if version == 0 {
|
|
||||||
tx.execute_batch(
|
|
||||||
"CREATE TABLE users (id STRING PRIMARY KEY, name TEXT);
|
|
||||||
CREATE TABLE invitations (token STRING PRIMARY KEY, user_id STRING, FOREIGN KEY(user_id) REFERENCES users(id));
|
|
||||||
CREATE TABLE sessions (token STRING PRIMARY KEY, user_id STRING, FOREIGN KEY(user_id) REFERENCES users(id));
|
|
||||||
PRAGMA user_version = 1;",
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
tx.commit()?;
|
|
||||||
|
|
||||||
Ok(Database {
|
|
||||||
file_path,
|
|
||||||
pool: Arc::new(Mutex::new(vec![connection])),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn connect<'a>(&'a self) -> Result<ManagedConnection<'a>, anyhow::Error> {
|
|
||||||
let mut pool = self.pool.lock().unwrap();
|
|
||||||
match pool.pop() {
|
|
||||||
Some(connection) => Ok(ManagedConnection {
|
|
||||||
pool: &self,
|
|
||||||
connection: Some(connection),
|
|
||||||
}),
|
|
||||||
None => {
|
|
||||||
let connection = Connection::open(self.file_path.clone())?;
|
|
||||||
Ok(ManagedConnection {
|
|
||||||
pool: &self,
|
|
||||||
connection: Some(connection),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn release(&self, connection: Connection) {
|
|
||||||
let mut pool = self.pool.lock().unwrap();
|
|
||||||
pool.push(connection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for ManagedConnection<'_> {
|
|
||||||
type Target = Connection;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Connection {
|
|
||||||
self.connection.as_ref().unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DerefMut for ManagedConnection<'_> {
|
|
||||||
fn deref_mut(&mut self) -> &mut Connection {
|
|
||||||
self.connection.as_mut().unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for ManagedConnection<'_> {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
self.pool.release(self.connection.take().unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
use tempfile::NamedTempFile;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_can_create_users() {
|
|
||||||
let path = NamedTempFile::new().unwrap().into_temp_path();
|
|
||||||
let database = Database::new(path.to_path_buf()).unwrap();
|
|
||||||
let mut connection = database.connect().unwrap();
|
|
||||||
let tr = connection.transaction().unwrap();
|
|
||||||
tr.execute(
|
|
||||||
"INSERT INTO users VALUES(?, ?)",
|
|
||||||
params!["abcdefg", "mercer"],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
tr.commit().unwrap();
|
|
||||||
|
|
||||||
let connection = database.connect().unwrap();
|
|
||||||
let id: Option<String> = connection
|
|
||||||
.query_row(
|
|
||||||
"SELECT id FROM users WHERE name = ?",
|
|
||||||
[String::from("mercer")],
|
|
||||||
|row| row.get("id"),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(id, Some(String::from("abcdefg")));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
/// This struct covers *fatal* errors for the application. Cross-functional things like full disks,
|
|
||||||
/// failing disks, database deadlocks, and so forth, which require that the whole application shut
|
|
||||||
/// down and that the administrator fix a problem.
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum FatalError {
|
|
||||||
#[error("disk is full")]
|
|
||||||
DiskFull,
|
|
||||||
#[error("io error: {0}")]
|
|
||||||
Io(std::io::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type AppResult<A, E> = Result<Result<A, E>, FatalError>;
|
|
||||||
|
|
||||||
pub fn ok<A, E>(val: A) -> AppResult<A, E> {
|
|
||||||
Ok(Ok(val))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn error<A, E>(err: E) -> AppResult<A, E> {
|
|
||||||
Ok(Err(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fatal<A, E>(err: FatalError) -> AppResult<A, E> {
|
|
||||||
Err(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub fn maybe_fail<E>(possible_errors: Vec<E>) -> AppResult<(), E> {
|
|
||||||
ok(())
|
|
||||||
}
|
|
|
@ -1,140 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::{
|
|
||||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
|
||||||
sync::{Arc, RwLock},
|
|
||||||
};
|
|
||||||
use warp::Filter;
|
|
||||||
|
|
||||||
mod authentication;
|
|
||||||
use authentication::{AuthenticationDB, AuthenticationError, MemoryAuth, UserId, Username};
|
|
||||||
|
|
||||||
mod database;
|
|
||||||
mod errors;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct AuthenticationRefused;
|
|
||||||
impl warp::reject::Reject for AuthenticationRefused {}
|
|
||||||
|
|
||||||
fn with_authentication(
|
|
||||||
auth_ctx: Arc<RwLock<impl AuthenticationDB>>,
|
|
||||||
) -> impl Filter<Extract = ((Username, UserId),), Error = warp::Rejection> + Clone {
|
|
||||||
let auth_ctx = auth_ctx.clone();
|
|
||||||
warp::header("authentication").and_then({
|
|
||||||
let auth_ctx = auth_ctx.clone();
|
|
||||||
move |auth_header: String| {
|
|
||||||
let auth_ctx = auth_ctx.clone();
|
|
||||||
async move {
|
|
||||||
if auth_header.starts_with("Basic ") {
|
|
||||||
let username = auth_header.split(" ").skip(1).collect::<String>();
|
|
||||||
match auth_ctx
|
|
||||||
.read()
|
|
||||||
.unwrap()
|
|
||||||
.get_user_id(Username::from(username.as_str()))
|
|
||||||
{
|
|
||||||
Ok(Ok(userid)) => Ok((Username::from(username.as_str()), userid)),
|
|
||||||
Ok(Err(_)) => Err(warp::reject::custom(AuthenticationRefused)),
|
|
||||||
Err(err) => panic!("{}", err),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(warp::reject::custom(AuthenticationRefused))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct ErrorResponse {
|
|
||||||
error: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct MakeUserParameters {
|
|
||||||
username: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct MakeUserResponse {
|
|
||||||
userid: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_user(
|
|
||||||
auth_ctx: Arc<RwLock<impl AuthenticationDB>>,
|
|
||||||
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
|
|
||||||
warp::path!("api" / "v1" / "users")
|
|
||||||
.and(warp::put())
|
|
||||||
.and(warp::body::json())
|
|
||||||
.map(move |params: MakeUserParameters| {
|
|
||||||
let mut auth_ctx = auth_ctx.write().unwrap();
|
|
||||||
match (*auth_ctx).create_user(Username::from(params.username.as_str())) {
|
|
||||||
Ok(Ok(userid)) => warp::reply::json(&MakeUserResponse {
|
|
||||||
userid: String::from(userid),
|
|
||||||
}),
|
|
||||||
Ok(auth_error) => warp::reply::json(&ErrorResponse {
|
|
||||||
error: format!("{:?}", auth_error),
|
|
||||||
}),
|
|
||||||
Err(err) => panic!("{}", err),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn list_users(
|
|
||||||
auth_ctx: Arc<RwLock<impl AuthenticationDB>>,
|
|
||||||
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
|
|
||||||
warp::path!("api" / "v1" / "users")
|
|
||||||
.and(warp::get())
|
|
||||||
.map(move || {
|
|
||||||
let auth_ctx = auth_ctx.read().unwrap();
|
|
||||||
match (*auth_ctx).list_users() {
|
|
||||||
Ok(Ok(users)) => warp::reply::json(
|
|
||||||
&users
|
|
||||||
.iter()
|
|
||||||
.map(|u| String::from(u))
|
|
||||||
.collect::<Vec<String>>(),
|
|
||||||
),
|
|
||||||
Ok(auth_error) => warp::reply::json(&ErrorResponse {
|
|
||||||
error: format!("{:?}", auth_error),
|
|
||||||
}),
|
|
||||||
Err(err) => panic!("{}", err),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
pub async fn main() {
|
|
||||||
let auth_ctx: Arc<RwLock<MemoryAuth>> = Arc::new(RwLock::new(Default::default()));
|
|
||||||
|
|
||||||
let echo_unauthenticated = warp::path!("api" / "v1" / "echo" / String).map(|param: String| {
|
|
||||||
println!("param: {}", param);
|
|
||||||
warp::reply::json(&vec!["unauthenticated", param.as_str()])
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
let authenticate = warp::path!("api" / "v1" / "auth" / String).map(|param: String| {
|
|
||||||
println!("param: {}", param);
|
|
||||||
warp::reply::json(¶m)
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
let echo_authenticated = warp::path!("api" / "v1" / "echo" / String)
|
|
||||||
.and(with_authentication(auth_ctx.clone()))
|
|
||||||
.map(|param: String, (username, userid)| {
|
|
||||||
println!("param: {:?}", username);
|
|
||||||
println!("param: {:?}", userid);
|
|
||||||
println!("param: {}", param);
|
|
||||||
warp::reply::json(&vec!["authed", param.as_str()])
|
|
||||||
});
|
|
||||||
|
|
||||||
let filter = list_users(auth_ctx.clone())
|
|
||||||
.or(make_user(auth_ctx.clone()))
|
|
||||||
.or(echo_authenticated)
|
|
||||||
.or(echo_unauthenticated);
|
|
||||||
|
|
||||||
let server = warp::serve(filter);
|
|
||||||
server
|
|
||||||
.run(SocketAddr::new(
|
|
||||||
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
|
|
||||||
8001,
|
|
||||||
))
|
|
||||||
.await;
|
|
||||||
}
|
|
|
@ -3,6 +3,3 @@
|
||||||
|
|
||||||
dev:
|
dev:
|
||||||
npx tauri dev
|
npx tauri dev
|
||||||
|
|
||||||
test:
|
|
||||||
npm run test
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
|
||||||
module.exports = {
|
|
||||||
preset: 'ts-jest',
|
|
||||||
testEnvironment: 'node',
|
|
||||||
};
|
|
File diff suppressed because it is too large
Load Diff
|
@ -6,8 +6,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview"
|
||||||
"test": "jest"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^1.2.0",
|
"@tauri-apps/api": "^1.2.0",
|
||||||
|
@ -16,27 +15,10 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^1.2.0",
|
"@tauri-apps/cli": "^1.2.0",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
|
||||||
"@testing-library/react": "^13.4.0",
|
|
||||||
"@testing-library/user-event": "^14.4.3",
|
|
||||||
"@types/jest": "^29.2.3",
|
|
||||||
"@types/react": "^18.0.24",
|
"@types/react": "^18.0.24",
|
||||||
"@types/react-dom": "^18.0.8",
|
"@types/react-dom": "^18.0.8",
|
||||||
"@vitejs/plugin-react": "^2.2.0",
|
"@vitejs/plugin-react": "^2.2.0",
|
||||||
"eslint": "^8.27.0",
|
|
||||||
"eslint-config-react-app": "^7.0.1",
|
|
||||||
"jest": "^29.3.1",
|
|
||||||
"jest-environment-jsdom": "^29.3.1",
|
|
||||||
"prettier": "^2.7.1",
|
|
||||||
"sass": "^1.56.1",
|
|
||||||
"ts-jest": "^29.0.3",
|
|
||||||
"typescript": "^4.6.4",
|
"typescript": "^4.6.4",
|
||||||
"vite": "^3.2.3"
|
"vite": "^3.2.3"
|
||||||
},
|
|
||||||
"eslintConfig": {
|
|
||||||
"extends": [
|
|
||||||
"react-app",
|
|
||||||
"react-app/jest"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,34 @@
|
||||||
import Authentication from "./pages/Authentication/Authentication";
|
import { useState } from 'react'
|
||||||
import "./App.css";
|
import reactLogo from './assets/react.svg'
|
||||||
|
import './App.css'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const [count, setCount] = useState(0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<Authentication />
|
<div>
|
||||||
|
<a href="https://vitejs.dev" target="_blank">
|
||||||
|
<img src="/vite.svg" className="logo" alt="Vite logo" />
|
||||||
|
</a>
|
||||||
|
<a href="https://reactjs.org" target="_blank">
|
||||||
|
<img src={reactLogo} className="logo react" alt="React logo" />
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
<h1>Vite + React</h1>
|
||||||
|
<div className="card">
|
||||||
|
<button onClick={() => setCount((count) => count + 1)}>
|
||||||
|
count is {count}
|
||||||
|
</button>
|
||||||
|
<p>
|
||||||
|
Edit <code>src/App.tsx</code> and save to test HMR
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className="read-the-docs">
|
||||||
|
Click on the Vite and React logos to learn more
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
/**
|
|
||||||
* @jest-environment jsdom
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { render, screen } from "@testing-library/react";
|
|
||||||
import "@testing-library/jest-dom";
|
|
||||||
import Authentication from "./Authentication";
|
|
||||||
|
|
||||||
test("prompts the user for the invitation URL", () => {
|
|
||||||
render(<Authentication />);
|
|
||||||
|
|
||||||
expect(screen.getByText("Please enter your invitation URL")).toBeDefined();
|
|
||||||
});
|
|
|
@ -1,10 +0,0 @@
|
||||||
import "./Authentication.scss";
|
|
||||||
|
|
||||||
const Authentication = () => (
|
|
||||||
<div>
|
|
||||||
<p>Please enter your invitation URL</p>
|
|
||||||
<input type="text" placeholder="Invitation URL" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Authentication;
|
|
Loading…
Reference in New Issue