Compare commits
8 Commits
ab3ae24b56
...
977059c90d
Author | SHA1 | Date |
---|---|---|
Savanni D'Gerinel | 977059c90d | |
Savanni D'Gerinel | 23b28144a1 | |
Savanni D'Gerinel | 6d32a641b1 | |
Savanni D'Gerinel | 6e1e4d7811 | |
Savanni D'Gerinel | 57b438c6e6 | |
Savanni D'Gerinel | 5aa50feb5b | |
Savanni D'Gerinel | a3345b0a4f | |
Savanni D'Gerinel | 6210b433c7 |
16
Makefile
16
Makefile
|
@ -1,8 +1,18 @@
|
||||||
|
|
||||||
.PHONY: server client-dev
|
.PHONY: server-dev server-test client-dev client-test
|
||||||
|
|
||||||
server:
|
test:
|
||||||
cd servilo && make
|
cd server && make test-oneshot
|
||||||
|
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,15 +2,44 @@
|
||||||
# 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"
|
||||||
|
@ -30,10 +59,16 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "ryu"
|
||||||
version = "1.0.132"
|
version = "1.0.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b9875c23cf305cd1fd7eb77234cbb705f21ea6a72c637a5c6db5fe4b8e7f008"
|
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.147"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
|
@ -46,6 +81,19 @@ 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"
|
||||||
|
@ -82,3 +130,9 @@ 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
|
@ -0,0 +1,21 @@
|
||||||
|
[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" }
|
|
@ -0,0 +1,11 @@
|
||||||
|
|
||||||
|
.PHONY: dev
|
||||||
|
|
||||||
|
dev:
|
||||||
|
cargo watch -x run
|
||||||
|
|
||||||
|
test:
|
||||||
|
cargo watch -x test
|
||||||
|
|
||||||
|
test-oneshot:
|
||||||
|
cargo test
|
|
@ -0,0 +1,286 @@
|
||||||
|
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 {}
|
|
@ -0,0 +1,3 @@
|
||||||
|
fn main() {
|
||||||
|
println!("There is a tool");
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
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")));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
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(())
|
||||||
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
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,3 +3,6 @@
|
||||||
|
|
||||||
dev:
|
dev:
|
||||||
npx tauri dev
|
npx tauri dev
|
||||||
|
|
||||||
|
test:
|
||||||
|
npm run test
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
};
|
File diff suppressed because it is too large
Load Diff
|
@ -6,7 +6,8 @@
|
||||||
"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",
|
||||||
|
@ -15,10 +16,27 @@
|
||||||
},
|
},
|
||||||
"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,34 +1,12 @@
|
||||||
import { useState } from 'react'
|
import Authentication from "./pages/Authentication/Authentication";
|
||||||
import reactLogo from './assets/react.svg'
|
import "./App.css";
|
||||||
import './App.css'
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [count, setCount] = useState(0)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<div>
|
<Authentication />
|
||||||
<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>
|
|
||||||
<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>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default App;
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
/**
|
||||||
|
* @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();
|
||||||
|
});
|
|
@ -0,0 +1,10 @@
|
||||||
|
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