rework DB Models
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Patrick Michl 2022-04-30 23:59:21 +02:00
parent 341c516fcb
commit 3b8c529183
20 changed files with 342 additions and 184 deletions

10
Cargo.lock generated
View File

@ -612,6 +612,7 @@ dependencies = [
"tower-http", "tower-http",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"uuid",
] ]
[[package]] [[package]]
@ -1445,6 +1446,15 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "uuid"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cfcd319456c4d6ea10087ed423473267e1a071f3bc0aa89f80d60997843c6f0"
dependencies = [
"getrandom",
]
[[package]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.0" version = "0.1.0"

View File

@ -17,3 +17,4 @@ anyhow = "1.0"
thiserror = "1.0" thiserror = "1.0"
argon2 = { version = "0.4", features = ["std"] } argon2 = { version = "0.4", features = ["std"] }
rand_core = { version = "0.6", features = ["std"] } rand_core = { version = "0.6", features = ["std"] }
uuid = { version = "1.0", features = ["v4"] }

View File

@ -1,6 +1,6 @@
-- Add migration script here -- Add migration script here
CREATE TABLE users( CREATE TABLE users(
id INTEGER PRIMARY KEY NOT NULL, uuid TEXT PRIMARY KEY NOT NULL,
user_id CHAR(255) NOT NULL, user_id CHAR(255) NOT NULL,
display_name TEXT NOT NULL, display_name TEXT NOT NULL,
password TEXT NOT NULL password TEXT NOT NULL

View File

@ -1,11 +1,11 @@
-- Add migration script here -- Add migration script here
CREATE TABLE devices( CREATE TABLE devices(
id INTEGER PRIMARY KEY NOT NULL, uuid TEXT PRIMARY KEY NOT NULL,
user_id INT NOT NULL, user_uuid INT NOT NULL,
device_id TEXT NOT NULL, device_id TEXT NOT NULL,
display_name TEXT NOT NULL, display_name TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id) FOREIGN KEY(user_uuid) REFERENCES users(uuid)
); );
CREATE INDEX device_id_index ON devices (device_id); CREATE INDEX device_id_index ON devices (device_id);

View File

@ -1,10 +1,10 @@
-- Add migration script here -- Add migration script here
CREATE TABLE sessions( CREATE TABLE sessions(
id INTEGER PRIMARY KEY NOT NULL, uuid TEXT PRIMARY KEY NOT NULL,
device_id INT NOT NULL, device_uuid INT NOT NULL,
value TEXT NOT NULL, key TEXT NOT NULL,
FOREIGN KEY(device_id) REFERENCES devices(id) FOREIGN KEY(device_uuid) REFERENCES devices(uuid)
); );
CREATE INDEX value_index ON sessions (value); CREATE INDEX key_index ON sessions (key);

View File

@ -1,15 +1,81 @@
{ {
"db": "SQLite", "db": "SQLite",
"22e18063d81e86afceca0e0c74c9070a8b21a406cba7cf7c01a966a869a9dad8": { "112b5723b62084ee14191a2f8773d2493814d70bfc4d4ce046655958d2ae472b": {
"describe": { "describe": {
"columns": [ "columns": [
{ {
"name": "id", "name": "uuid: Uuid",
"ordinal": 0, "ordinal": 0,
"type_info": "Text"
},
{
"name": "user_uuid: Uuid",
"ordinal": 1,
"type_info": "Int64" "type_info": "Int64"
}, },
{ {
"name": "user_id", "name": "device_id",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "display_name",
"ordinal": 3,
"type_info": "Text"
}
],
"nullable": [
false,
false,
false,
false
],
"parameters": {
"Right": 4
}
},
"query": "insert into devices(uuid, user_uuid, device_id, display_name)\n values(?, ?, ?, ?)\n returning uuid as 'uuid: Uuid', user_uuid as 'user_uuid: Uuid', device_id, display_name"
},
"221d0935dff8911fe58ac047d39e11b0472d2180d7c297291a5dc440e00efb80": {
"describe": {
"columns": [
{
"name": "uuid: Uuid",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "device_uuid: Uuid",
"ordinal": 1,
"type_info": "Int64"
},
{
"name": "key",
"ordinal": 2,
"type_info": "Text"
}
],
"nullable": [
false,
false,
false
],
"parameters": {
"Right": 3
}
},
"query": "insert into sessions(uuid, device_uuid, key)\n values(?, ?, ?)\n returning uuid as 'uuid: Uuid', device_uuid as 'device_uuid: Uuid', key"
},
"3fead3dac0e110757bc30be40bb0c6c2bc02127b6d9b6145bfc40fa5fe22ad06": {
"describe": {
"columns": [
{
"name": "uuid: Uuid",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "user_id: UserId",
"ordinal": 1, "ordinal": 1,
"type_info": "Text" "type_info": "Text"
}, },
@ -31,10 +97,10 @@
false false
], ],
"parameters": { "parameters": {
"Right": 1 "Right": 4
} }
}, },
"query": "select id, user_id, display_name, password from users where user_id = ?" "query": "insert into users(uuid, user_id, display_name, password)\n values (?, ?, ?, ?)\n returning uuid as 'uuid: Uuid', user_id as 'user_id: UserId', display_name, password"
}, },
"58d27b1d424297504f1da2e3b9b4020121251c1155fbf5dc870dafbef97659f3": { "58d27b1d424297504f1da2e3b9b4020121251c1155fbf5dc870dafbef97659f3": {
"describe": { "describe": {
@ -54,88 +120,16 @@
}, },
"query": "select user_id from users where user_id = ?" "query": "select user_id from users where user_id = ?"
}, },
"87395ffa7fe0382080056bdf67805044fbe89770ef366b4553d9797059034e44": { "778b7f0a1c66f00812f0232a5904b7b6b295720ebb75d1c2720afeeda4f66936": {
"describe": { "describe": {
"columns": [ "columns": [
{ {
"name": "id", "name": "uuid: Uuid",
"ordinal": 0, "ordinal": 0,
"type_info": "Int64"
},
{
"name": "user_id",
"ordinal": 1,
"type_info": "Int64"
},
{
"name": "device_id",
"ordinal": 2,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "display_name", "name": "user_id: UserId",
"ordinal": 3,
"type_info": "Text"
}
],
"nullable": [
false,
false,
false,
false
],
"parameters": {
"Right": 3
}
},
"query": "insert into devices(user_id, device_id, display_name) values(?, ?, ?) returning id, user_id, device_id, display_name"
},
"9a092e024bfe2854631e0572880f761125a2261a973b46d721db2e1401ce9aec": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "user_id",
"ordinal": 1,
"type_info": "Int64"
},
{
"name": "device_id",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "display_name",
"ordinal": 3,
"type_info": "Text"
}
],
"nullable": [
false,
false,
false,
false
],
"parameters": {
"Right": 1
}
},
"query": "select id, user_id, device_id, display_name from devices where user_id = ?"
},
"bb4e2386cf2987aa86f2c4dcac1159687b7220c770542bf52485e7e16f9ce987": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "user_id",
"ordinal": 1, "ordinal": 1,
"type_info": "Text" "type_info": "Text"
}, },
@ -157,39 +151,45 @@
false false
], ],
"parameters": { "parameters": {
"Right": 3 "Right": 5
} }
}, },
"query": "insert into users(user_id, display_name, password) values (?, ?, ?) returning id, user_id, display_name, password" "query": "update users set uuid = ?, user_id = ?, display_name = ?, password = ?\n where uuid = ?\n returning uuid as 'uuid: Uuid', user_id as 'user_id: UserId', display_name, password"
}, },
"cd7c9d3ae02f9553d4b7c4e8d98f63c6f556ac230a339683fe25025592812033": { "f57c49e5390c81f971851ff9ab35242a472b9efbb1ffa658de9b102188769750": {
"describe": { "describe": {
"columns": [ "columns": [
{ {
"name": "id", "name": "uuid: Uuid",
"ordinal": 0, "ordinal": 0,
"type_info": "Int64" "type_info": "Text"
}, },
{ {
"name": "device_id", "name": "user_id: UserId",
"ordinal": 1, "ordinal": 1,
"type_info": "Int64" "type_info": "Text"
}, },
{ {
"name": "value", "name": "display_name",
"ordinal": 2, "ordinal": 2,
"type_info": "Text" "type_info": "Text"
},
{
"name": "password",
"ordinal": 3,
"type_info": "Text"
} }
], ],
"nullable": [ "nullable": [
false,
false, false,
false, false,
false false
], ],
"parameters": { "parameters": {
"Right": 2 "Right": 1
} }
}, },
"query": "insert into sessions(device_id, value) values(?, ?) returning id, device_id, value" "query": "select uuid as 'uuid: Uuid', user_id as 'user_id: UserId', display_name, password\n from users where user_id = ?"
} }
} }

View File

@ -10,7 +10,14 @@ use axum::{
use rand_core::OsRng; use rand_core::OsRng;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::{responses::{registration::RegistrationResponse, authentication::{AuthenticationResponse, AuthenticationSuccess}}, api::client_server::errors::authentication_error::AuthenticationError}; use crate::{
api::client_server::errors::authentication_error::AuthenticationError,
models::sessions::Session,
responses::{
authentication::{AuthenticationResponse, AuthenticationSuccess},
registration::RegistrationResponse,
},
};
use crate::{ use crate::{
models::devices::Device, models::devices::Device,
responses::{flow::Flows, registration::RegistrationSuccess}, responses::{flow::Flows, registration::RegistrationSuccess},
@ -41,10 +48,11 @@ async fn get_login() -> Result<Json<Flows>, ApiError> {
async fn post_login( async fn post_login(
Extension(config): Extension<Arc<Config>>, Extension(config): Extension<Arc<Config>>,
Extension(db): Extension<SqlitePool>, Extension(db): Extension<SqlitePool>,
Json(body): Json<AuthenticationData> Json(body): Json<AuthenticationData>,
) -> Result<Json<AuthenticationResponse>, ApiError> { ) -> Result<Json<AuthenticationResponse>, ApiError> {
let user = UserId::new("name", "server_name").ok().ok_or(AuthenticationError::InvalidUserId)?; let user = UserId::new("name", "server_name")
todo!("Flesh this out more"); .ok()
.ok_or(AuthenticationError::InvalidUserId)?;
let resp = AuthenticationSuccess::new("", "", &user); let resp = AuthenticationSuccess::new("", "", &user);
Ok(Json(AuthenticationResponse::Success(resp))) Ok(Json(AuthenticationResponse::Success(resp)))
@ -92,22 +100,29 @@ async fn post_register(
None => "Random displayname", None => "Random displayname",
}; };
let user = let user = User::new(&user_id, &user_id.to_string(), auth_data.password())?
User::create(&db, &user_id, &user_id.to_string(), auth_data.password()).await?; .create(&db)
let device = Device::create(&db, &user, "test", display_name).await?; .await?;
let device = Device::new(&user, "test", display_name)?
.create(&db)
.await?;
(user, device) (user, device)
} }
}; };
if body.inhibit_login().unwrap_or(false) { if body.inhibit_login().unwrap_or(false) {
let resp = RegistrationSuccess::new(None, device.device_id(), user.user_id()); let resp = RegistrationSuccess::new(None, device.device_id(), &user.user_id().to_string());
Ok(Json(RegistrationResponse::Success(resp))) Ok(Json(RegistrationResponse::Success(resp)))
} else { } else {
let session = device.create_session(&db).await?; let session = Session::new(&device)?.create(&db).await?;
let resp = let resp = RegistrationSuccess::new(
RegistrationSuccess::new(Some(session.value()), device.device_id(), user.user_id()); Some(session.key()),
device.device_id(),
&user.user_id().to_string(),
);
Ok(Json(RegistrationResponse::Success(resp))) Ok(Json(RegistrationResponse::Success(resp)))
} }

View File

@ -39,7 +39,8 @@ impl From<anyhow::Error> for ApiError {
fn from(err: anyhow::Error) -> Self { fn from(err: anyhow::Error) -> Self {
map_err!(err, map_err!(err,
sqlx::Error => ApiError::DBError, sqlx::Error => ApiError::DBError,
RegistrationError => ApiError::RegistrationError RegistrationError => ApiError::RegistrationError,
AuthenticationError => ApiError::AuthenticationError
); );
ApiError::Generic(err) ApiError::Generic(err)

View File

@ -30,12 +30,22 @@ impl IntoResponse for AuthenticationError {
.into_response(), .into_response(),
Self::Forbidden => ( Self::Forbidden => (
StatusCode::FORBIDDEN, StatusCode::FORBIDDEN,
Json(ErrorResponse::new(ErrorCode::Forbidden, &self.to_string(), None)), Json(ErrorResponse::new(
).into_response(), ErrorCode::Forbidden,
&self.to_string(),
None,
)),
)
.into_response(),
Self::UserDeactivated => ( Self::UserDeactivated => (
StatusCode::FORBIDDEN, StatusCode::FORBIDDEN,
Json(ErrorResponse::new(ErrorCode::UserDeactivated, &self.to_string(), None)), Json(ErrorResponse::new(
).into_response(), ErrorCode::UserDeactivated,
&self.to_string(),
None,
)),
)
.into_response(),
} }
} }
} }

View File

@ -1,50 +1,50 @@
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::types::uuid::Uuid;
use super::{sessions::Session, users::User}; use super::{sessions::Session, users::User};
pub struct Device { pub struct Device {
id: i64, uuid: Uuid,
user_id: i64, user_uuid: Uuid,
device_id: String, device_id: String,
display_name: String, display_name: String,
} }
impl Device { impl Device {
pub async fn create( pub fn new(user: &User, device_id: &str, display_name: &str) -> anyhow::Result<Self> {
conn: &SqlitePool, Ok(Self {
user: &User, uuid: uuid::Uuid::new_v4().into(),
device_id: &str, user_uuid: user.uuid().clone(),
display_name: &str, device_id: device_id.to_owned(),
) -> anyhow::Result<Self> { display_name: display_name.to_owned(),
let user_id = user.id(); })
Ok(sqlx::query_as!(Self, "insert into devices(user_id, device_id, display_name) values(?, ?, ?) returning id, user_id, device_id, display_name", user_id, device_id, display_name).fetch_one(conn).await?)
} }
pub async fn by_user(conn: &SqlitePool, user: &User) -> anyhow::Result<Self> { pub async fn create(&self, conn: &SqlitePool) -> anyhow::Result<Self> {
let user_id = user.id();
Ok(sqlx::query_as!( Ok(sqlx::query_as!(
Self, Self,
"select id, user_id, device_id, display_name from devices where user_id = ?", "insert into devices(uuid, user_uuid, device_id, display_name)
user_id values(?, ?, ?, ?)
returning uuid as 'uuid: Uuid', user_uuid as 'user_uuid: Uuid', device_id, display_name",
self.uuid,
self.user_uuid,
self.device_id,
self.display_name)
.fetch_one(conn).await?
) )
.fetch_one(conn)
.await?)
}
pub async fn create_session(&self, conn: &SqlitePool) -> anyhow::Result<Session> {
Ok(Session::create(conn, self, "random_session_id").await?)
} }
/// Get the device's id. /// Get the device's id.
#[must_use] #[must_use]
pub fn id(&self) -> i64 { pub fn uuid(&self) -> &Uuid {
self.id &self.uuid
} }
/// Get the device's user id. /// Get the device's user id.
#[must_use] #[must_use]
pub fn user_id(&self) -> i64 { pub fn user_uuid(&self) -> &Uuid {
self.user_id &self.user_uuid
} }
/// Get a reference to the device's device id. /// Get a reference to the device's device id.

View File

@ -1,21 +1,33 @@
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::types::uuid::Uuid;
use super::devices::Device; use super::devices::Device;
pub struct Session { pub struct Session {
id: i64, uuid: Uuid,
device_id: i64, device_uuid: Uuid,
value: String, key: String,
} }
impl Session { impl Session {
pub async fn create(conn: &SqlitePool, device: &Device, value: &str) -> anyhow::Result<Self> { pub fn new(device: &Device) -> anyhow::Result<Self> {
let device_id = device.id(); Ok(Self {
uuid: uuid::Uuid::new_v4().into(),
device_uuid: device.uuid().clone(),
key: String::new(),
})
}
pub async fn create(&self, conn: &SqlitePool) -> anyhow::Result<Self> {
Ok(sqlx::query_as!( Ok(sqlx::query_as!(
Self, Self,
"insert into sessions(device_id, value) values(?, ?) returning id, device_id, value", "insert into sessions(uuid, device_uuid, key)
device_id, values(?, ?, ?)
value returning uuid as 'uuid: Uuid', device_uuid as 'device_uuid: Uuid', key",
self.uuid,
self.device_uuid,
self.key
) )
.fetch_one(conn) .fetch_one(conn)
.await?) .await?)
@ -23,19 +35,19 @@ impl Session {
/// Get the session's id. /// Get the session's id.
#[must_use] #[must_use]
pub fn id(&self) -> i64 { pub fn uuid(&self) -> &Uuid {
self.id &self.uuid
} }
/// Get the session's device id. /// Get the session's device id.
#[must_use] #[must_use]
pub fn device_id(&self) -> i64 { pub fn device_uuid(&self) -> &Uuid {
self.device_id &self.device_uuid
} }
/// Get a reference to the session's value. /// Get a reference to the session's value.
#[must_use] #[must_use]
pub fn value(&self) -> &str { pub fn key(&self) -> &str {
self.value.as_ref() self.key.as_ref()
} }
} }

View File

@ -1,17 +1,33 @@
use crate::types::uuid::Uuid;
use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
use rand_core::OsRng; use rand_core::OsRng;
use sqlx::SqlitePool; use sqlx::{encode::IsNull, sqlite::SqliteTypeInfo, FromRow, Sqlite, SqlitePool};
use crate::types::user_id::UserId; use crate::types::user_id::UserId;
pub struct User { pub struct User {
id: i64, uuid: Uuid,
user_id: String, user_id: UserId,
display_name: String, display_name: String,
password: String, password: String,
} }
impl User { impl User {
pub fn new(user_id: &UserId, display_name: &str, password: &str) -> anyhow::Result<Self> {
let argon2 = Argon2::default();
let salt = SaltString::generate(OsRng);
let password = argon2
.hash_password(password.as_bytes(), &salt)?
.to_string();
Ok(Self {
uuid: uuid::Uuid::new_v4().into(),
user_id: user_id.clone(),
display_name: display_name.to_owned(),
password,
})
}
pub async fn exists(conn: &SqlitePool, user_id: &UserId) -> anyhow::Result<bool> { pub async fn exists(conn: &SqlitePool, user_id: &UserId) -> anyhow::Result<bool> {
Ok( Ok(
sqlx::query!("select user_id from users where user_id = ?", user_id) sqlx::query!("select user_id from users where user_id = ?", user_id)
@ -21,25 +37,42 @@ impl User {
) )
} }
pub async fn create( pub async fn create(&self, conn: &SqlitePool) -> anyhow::Result<Self> {
conn: &SqlitePool, Ok(sqlx::query_as!(
user_id: &UserId, Self,
display_name: &str, "insert into users(uuid, user_id, display_name, password)
password: &str, values (?, ?, ?, ?)
) -> anyhow::Result<Self> { returning uuid as 'uuid: Uuid', user_id as 'user_id: UserId', display_name, password",
let salt = SaltString::generate(OsRng); self.uuid,
let argon2 = Argon2::default(); self.user_id,
let pw_hash = argon2 self.display_name,
.hash_password(password.as_bytes(), &salt)? self.password
.to_string(); )
.fetch_one(conn)
.await?)
}
Ok(sqlx::query_as!(Self, "insert into users(user_id, display_name, password) values (?, ?, ?) returning id, user_id, display_name, password", user_id, display_name, pw_hash).fetch_one(conn).await?) pub async fn update(&self, conn: &SqlitePool) -> anyhow::Result<Self> {
Ok(sqlx::query_as!(
Self,
"update users set uuid = ?, user_id = ?, display_name = ?, password = ?
where uuid = ?
returning uuid as 'uuid: Uuid', user_id as 'user_id: UserId', display_name, password",
self.uuid,
self.user_id,
self.display_name,
self.password,
self.uuid
)
.fetch_one(conn)
.await?)
} }
pub async fn by_user_id(conn: &SqlitePool, user_id: &UserId) -> anyhow::Result<Self> { pub async fn by_user_id(conn: &SqlitePool, user_id: &UserId) -> anyhow::Result<Self> {
Ok(sqlx::query_as!( Ok(sqlx::query_as!(
Self, Self,
"select id, user_id, display_name, password from users where user_id = ?", "select uuid as 'uuid: Uuid', user_id as 'user_id: UserId', display_name, password
from users where user_id = ?",
user_id user_id
) )
.fetch_one(conn) .fetch_one(conn)
@ -48,14 +81,14 @@ impl User {
/// Get the user's id. /// Get the user's id.
#[must_use] #[must_use]
pub fn id(&self) -> i64 { pub fn uuid(&self) -> &Uuid {
self.id &self.uuid
} }
/// Get a reference to the user's user id. /// Get a reference to the user's user id.
#[must_use] #[must_use]
pub fn user_id(&self) -> &str { pub fn user_id(&self) -> &UserId {
self.user_id.as_ref() &self.user_id
} }
/// Get a reference to the user's password. /// Get a reference to the user's password.

View File

@ -5,7 +5,7 @@ use crate::types::user_id::UserId;
#[derive(Debug, serde::Serialize)] #[derive(Debug, serde::Serialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum AuthenticationResponse { pub enum AuthenticationResponse {
Success(AuthenticationSuccess) Success(AuthenticationSuccess),
} }
#[derive(Debug, serde::Serialize)] #[derive(Debug, serde::Serialize)]

View File

@ -1,5 +1,5 @@
pub mod authentication;
pub mod flow; pub mod flow;
pub mod registration; pub mod registration;
pub mod username_available; pub mod username_available;
pub mod versions; pub mod versions;
pub mod authentication;

View File

@ -12,7 +12,7 @@ pub enum ErrorCode {
UserInUse, UserInUse,
InvalidUsername, InvalidUsername,
Exclusive, Exclusive,
UserDeactivated UserDeactivated,
} }
impl serde::Serialize for ErrorCode { impl serde::Serialize for ErrorCode {

View File

@ -5,3 +5,4 @@ pub mod identifier;
pub mod identifier_type; pub mod identifier_type;
pub mod user_id; pub mod user_id;
pub mod user_interactive_authorization; pub mod user_interactive_authorization;
pub mod uuid;

View File

@ -1,10 +1,40 @@
use std::fmt::Display; use std::fmt::Display;
#[derive(sqlx::Type)] use sqlx::{encode::IsNull, Sqlite};
#[sqlx(transparent)]
#[derive(Clone)]
#[repr(transparent)] #[repr(transparent)]
pub struct UserId(String); pub struct UserId(String);
impl sqlx::Type<Sqlite> for UserId {
fn type_info() -> <Sqlite as sqlx::Database>::TypeInfo {
<&str as sqlx::Type<Sqlite>>::type_info()
}
}
impl<'e> sqlx::Encode<'e, Sqlite> for UserId {
fn encode_by_ref(
&self,
buf: &mut <Sqlite as sqlx::database::HasArguments<'e>>::ArgumentBuffer,
) -> sqlx::encode::IsNull {
buf.push(sqlx::sqlite::SqliteArgumentValue::Text(
self.0.to_string().into(),
));
IsNull::No
}
}
impl<'d> sqlx::Decode<'d, Sqlite> for UserId {
fn decode(
value: <Sqlite as sqlx::database::HasValueRef<'d>>::ValueRef,
) -> Result<Self, sqlx::error::BoxDynError> {
let value = <String as sqlx::Decode<Sqlite>>::decode(value)?;
Ok(UserId(value))
}
}
impl UserId { impl UserId {
pub fn new(name: &str, server_name: &str) -> anyhow::Result<Self> { pub fn new(name: &str, server_name: &str) -> anyhow::Result<Self> {
let user_id = Self(format!("@{name}:{server_name}")); let user_id = Self(format!("@{name}:{server_name}"));

45
src/types/uuid.rs Normal file
View File

@ -0,0 +1,45 @@
use sqlx::{encode::IsNull, Sqlite, Type};
#[derive(Debug, Clone)]
pub struct Uuid(pub uuid::Uuid);
impl Uuid {
pub fn new_v4() -> Self {
Uuid(uuid::Uuid::new_v4())
}
}
impl Type<Sqlite> for Uuid {
fn type_info() -> <Sqlite as sqlx::Database>::TypeInfo {
<&str as Type<Sqlite>>::type_info()
}
}
impl<'e> sqlx::Encode<'e, Sqlite> for Uuid {
fn encode_by_ref(
&self,
buf: &mut <Sqlite as sqlx::database::HasArguments<'e>>::ArgumentBuffer,
) -> sqlx::encode::IsNull {
buf.push(sqlx::sqlite::SqliteArgumentValue::Text(
self.0.to_string().into(),
));
IsNull::No
}
}
impl<'d> sqlx::Decode<'d, Sqlite> for Uuid {
fn decode(
value: <Sqlite as sqlx::database::HasValueRef<'d>>::ValueRef,
) -> Result<Self, sqlx::error::BoxDynError> {
let value = <&str as sqlx::Decode<Sqlite>>::decode(value)?;
Ok(Uuid(uuid::Uuid::parse_str(value)?))
}
}
impl From<uuid::Uuid> for Uuid {
fn from(uuid: uuid::Uuid) -> Self {
Uuid(uuid)
}
}