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:
|
||||
cd servilo && make
|
||||
test:
|
||||
cd server && make test-oneshot
|
||||
cd v-client && make test
|
||||
|
||||
server-dev:
|
||||
cd server && make dev
|
||||
|
||||
server-test:
|
||||
cd server && make test
|
||||
|
||||
client-dev:
|
||||
cd v-client && make dev
|
||||
|
||||
client-test:
|
||||
cd v-client && make test
|
||||
|
|
|
@ -2,15 +2,44 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_yaml",
|
||||
"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]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.34"
|
||||
|
@ -30,10 +59,16 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.132"
|
||||
name = "ryu"
|
||||
version = "1.0.11"
|
||||
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]]
|
||||
name = "serde_derive"
|
||||
|
@ -46,6 +81,19 @@ dependencies = [
|
|||
"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]]
|
||||
name = "syn"
|
||||
version = "1.0.84"
|
||||
|
@ -82,3 +130,9 @@ name = "unicode-xid"
|
|||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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:
|
||||
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": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1.2.0",
|
||||
|
@ -15,10 +16,27 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@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-dom": "^18.0.8",
|
||||
"@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",
|
||||
"vite": "^3.2.3"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,34 +1,12 @@
|
|||
import { useState } from 'react'
|
||||
import reactLogo from './assets/react.svg'
|
||||
import './App.css'
|
||||
import Authentication from "./pages/Authentication/Authentication";
|
||||
import "./App.css";
|
||||
|
||||
function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<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>
|
||||
<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>
|
||||
<Authentication />
|
||||
</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