1
Fork 0

database: Create auth db model

This commit is contained in:
Steffo 2025-02-17 18:08:25 +01:00
parent 7b7fea9666
commit 64da830ba1
Signed by: steffo
GPG key ID: 6B8E18743E7E1F86
7 changed files with 480 additions and 57 deletions
acrate_database
Cargo.toml
migrations
2025-02-15-104632_auth
2025-02-15-104632_users
src

View file

@ -10,7 +10,7 @@ keywords = ["fediverse", "diesel", "database", "postgresql", "database-migration
categories = ["database"]
[dependencies]
diesel = { version = "2.2.4", features = ["postgres", "uuid"] }
diesel = { version = "2.2.4", features = ["chrono", "postgres", "serde_json", "uuid"] }
diesel-async = { version = "0.5.1", features = ["postgres"] }
diesel_migrations = { version = "2.2.0", optional = true }
log = { version = "0.4.22", features = ["std", "max_level_trace", "release_max_level_debug"] }
@ -20,6 +20,10 @@ mime = "0.3.17"
pretty_env_logger = { version = "0.5.0", optional = true }
uuid = "1.11.0"
tokio = { version = "1.42.0", optional = true }
chrono = "0.4.39"
webauthn-rs = { version = "0.5.1", features = ["danger-allow-state-serialisation"] }
serde = "1.0.217"
serde_json = "1.0.138"
[features]
default = ["connect"]

View file

@ -0,0 +1,4 @@
DROP TABLE IF EXISTS auth_authentication CASCADE;
DROP TABLE IF EXISTS auth_registration CASCADE;
DROP TABLE IF EXISTS auth_passkeys CASCADE;
DROP TABLE IF EXISTS auth_users CASCADE;

View file

@ -1,4 +1,4 @@
CREATE TABLE users (
CREATE TABLE auth_users (
id UUID DEFAULT gen_random_uuid() NOT NULL,
username VARCHAR NOT NULL,
display_name VARCHAR NOT NULL,
@ -7,18 +7,18 @@ CREATE TABLE users (
PRIMARY KEY (id)
);
CREATE TABLE users_passkeys (
user_id UUID REFERENCES users (id) NOT NULL,
CREATE TABLE auth_passkeys (
user_id UUID REFERENCES auth_users (id) NOT NULL,
id UUID NOT NULL,
passkey BYTEA NOT NULL,
passkey JSONB NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE users_passkey_registration_state (
user_id UUID REFERENCES users (id) NOT NULL,
state BYTEA NOT NULL,
CREATE TABLE auth_registration (
user_id UUID REFERENCES auth_users (id) NOT NULL,
state JSONB NOT NULL,
id UUID DEFAULT gen_random_uuid() NOT NULL,
created TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
@ -26,13 +26,12 @@ CREATE TABLE users_passkey_registration_state (
PRIMARY KEY (id)
);
CREATE TABLE users_passkey_authentication_state (
user_id UUID REFERENCES users (id) NOT NULL,
state BYTEA NOT NULL,
CREATE TABLE auth_authentication (
user_id UUID REFERENCES auth_users (id) NOT NULL,
state JSONB NOT NULL,
id UUID DEFAULT gen_random_uuid() NOT NULL,
created TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
PRIMARY KEY (id)
);

View file

@ -1,4 +0,0 @@
DROP TABLE IF EXISTS users_passkey_authentication_state CASCADE;
DROP TABLE IF EXISTS users_passkey_registration_state CASCADE;
DROP TABLE IF EXISTS users_passkeys CASCADE;
DROP TABLE IF EXISTS users CASCADE;

419
acrate_database/src/auth.rs Normal file
View file

@ -0,0 +1,419 @@
use chrono::{DateTime, Local};
use diesel::deserialize::FromSql;
use diesel::{AsExpression, FromSqlRow, Identifiable, Insertable, Queryable, QueryableByName, Selectable, ExpressionMethods, PgConnection, QueryResult, SelectableHelper, OptionalExtension};
use diesel::pg::{Pg};
use diesel::serialize::{Output, ToSql};
use diesel_async::{AsyncPgConnection};
use uuid::Uuid;
use webauthn_rs::prelude::{AuthenticationState, Passkey, PasskeyRegistration};
use crate::schema;
use crate::impl_to_insert;
/// Wrapper to use [`Passkey`] with [`diesel`].
#[derive(Debug, Clone, FromSqlRow, AsExpression)]
#[diesel(sql_type = diesel::pg::sql_types::Jsonb)]
pub struct PasskeyDatabase(serde_json::Value);
/// Wrapper to use [`PasskeyRegistration`] with [`diesel`].
#[derive(Debug, Clone, FromSqlRow, AsExpression)]
#[diesel(sql_type = diesel::pg::sql_types::Jsonb)]
pub struct PasskeyRegistrationDatabase(serde_json::Value);
/// Wrapper to use [`AuthenticationState`] with [`diesel`].
#[derive(Debug, Clone, FromSqlRow, AsExpression)]
#[diesel(sql_type = diesel::pg::sql_types::Jsonb)]
pub struct AuthenticationStateDatabase(serde_json::Value);
/// An user.
#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable)]
#[diesel(table_name = schema::auth_users)]
#[diesel(check_for_backend(Pg))]
pub struct AuthUser {
/// The identity column of the record.
pub id: Uuid,
/// The user's username, which should be hard to change.
pub username: String,
/// The user's display name, which can be changed at any time.
pub display_name: String,
}
/// An [`Insertable`] version of [`AuthUser`].
#[derive(Debug, Insertable)]
#[diesel(table_name = schema::auth_users)]
#[diesel(check_for_backend(Pg))]
pub struct AuthUserInsert {
/// The user's username, which should be hard to change.
pub username: String,
/// The user's display name, which can be changed at any time.
pub display_name: String,
}
/// An user's passkey.
#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable)]
#[diesel(table_name = schema::auth_passkeys)]
#[diesel(check_for_backend(Pg))]
pub struct AuthPasskey {
/// The identity column of the record.
pub id: Uuid,
/// The [`AuthUser::id`] this record refers to.
pub user_id: Uuid,
/// The data of the passkey itself.
pub passkey: PasskeyDatabase,
}
/// An [`Insertable`] version of [`AuthPasskey`].
#[derive(Debug, Insertable)]
#[diesel(table_name = schema::auth_passkeys)]
#[diesel(check_for_backend(Pg))]
pub struct AuthPasskeyInsert {
/// The [`AuthUser::id`] this record refers to.
pub user_id: Uuid,
/// The data of the passkey itself.
pub passkey: PasskeyDatabase,
}
/// An in-progress passkey registration.
#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable)]
#[diesel(belongs_to(AuthUser))]
#[diesel(table_name = schema::auth_registration)]
#[diesel(check_for_backend(Pg))]
pub struct AuthRegistration {
/// The [`AuthUser::id`] this record refers to.
pub user_id: Uuid,
/// The binary data of the in-progress registration.
pub state: PasskeyRegistrationDatabase,
/// The identity column of the record.
pub id: Uuid,
/// The moment when this procedure was started.
pub created: DateTime<Local>
}
/// An [`Insertable`] version of [`AuthRegistration`].
#[derive(Debug, Insertable)]
#[diesel(table_name = schema::auth_registration)]
#[diesel(check_for_backend(Pg))]
pub struct AuthRegistrationInsert {
/// The [`AuthUser::id`] this record refers to.
pub user_id: Uuid,
/// The binary data of the in-progress registration.
pub state: PasskeyRegistrationDatabase,
}
/// An in-progress passkey authentication.
#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable)]
#[diesel(belongs_to(AuthUser))]
#[diesel(table_name = schema::auth_authentication)]
#[diesel(check_for_backend(Pg))]
pub struct AuthAuthentication {
/// The [`AuthUser::id`] this record refers to.
pub user_id: Uuid,
/// The binary data of the in-progress authentication.
pub state: AuthenticationStateDatabase,
/// The identity column of the record.
pub id: Uuid,
/// The moment when this procedure was started.
pub created: DateTime<Local>
}
/// An [`Insertable`] version of [`AuthAuthentication`].
#[derive(Debug, Insertable)]
#[diesel(table_name = schema::auth_authentication)]
#[diesel(check_for_backend(Pg))]
pub struct AuthAuthenticationInsert {
/// The [`AuthUser::id`] this record refers to.
pub user_id: Uuid,
/// The binary data of the in-progress authentication.
pub state: AuthenticationStateDatabase,
}
impl TryFrom<Passkey> for PasskeyDatabase {
type Error = serde_json::Error;
fn try_from(value: Passkey) -> Result<Self, Self::Error> {
Ok(Self(
serde_json::to_value(value)?
))
}
}
impl TryFrom<PasskeyDatabase> for Passkey {
type Error = serde_json::Error;
fn try_from(value: PasskeyDatabase) -> Result<Self, Self::Error> {
serde_json::from_value(value.0)
}
}
/// Allow [`diesel::pg::sql_types::Jsonb`] values to be parsed as [`PasskeyDatabase`].
impl FromSql<diesel::pg::sql_types::Jsonb, Pg> for PasskeyDatabase
where
serde_json::Value: FromSql<diesel::pg::sql_types::Jsonb, Pg>,
{
fn from_sql(bytes: <Pg as diesel::backend::Backend>::RawValue<'_>) -> diesel::deserialize::Result<Self> {
let v = <serde_json::Value as FromSql<diesel::pg::sql_types::Jsonb, Pg>>::from_sql(bytes)?;
Ok(Self(v))
}
}
/// Allow [`diesel::pg::sql_types::Jsonb`] values to be written to with [`PasskeyDatabase`].
impl ToSql<diesel::pg::sql_types::Jsonb, Pg> for PasskeyDatabase
where
serde_json::Value: ToSql<diesel::pg::sql_types::Jsonb, Pg>,
{
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> diesel::serialize::Result {
<serde_json::Value as ToSql<diesel::pg::sql_types::Jsonb, Pg>>::to_sql(&self.0, out)
}
}
impl TryFrom<PasskeyRegistration> for PasskeyRegistrationDatabase {
type Error = serde_json::Error;
fn try_from(value: PasskeyRegistration) -> Result<Self, Self::Error> {
Ok(Self(
serde_json::to_value(value)?
))
}
}
impl TryFrom<PasskeyRegistrationDatabase> for PasskeyRegistration {
type Error = serde_json::Error;
fn try_from(value: PasskeyRegistrationDatabase) -> Result<Self, Self::Error> {
serde_json::from_value(value.0)
}
}
/// Allow [`diesel::pg::sql_types::Jsonb`] values to be parsed as [`PasskeyRegistrationDatabase`].
impl FromSql<diesel::pg::sql_types::Jsonb, Pg> for PasskeyRegistrationDatabase
where
serde_json::Value: FromSql<diesel::pg::sql_types::Jsonb, Pg>,
{
fn from_sql(bytes: <Pg as diesel::backend::Backend>::RawValue<'_>) -> diesel::deserialize::Result<Self> {
let v = <serde_json::Value as FromSql<diesel::pg::sql_types::Jsonb, Pg>>::from_sql(bytes)?;
Ok(Self(v))
}
}
/// Allow [`diesel::pg::sql_types::Jsonb`] values to be written to with [`PasskeyRegistrationDatabase`].
impl ToSql<diesel::pg::sql_types::Jsonb, Pg> for PasskeyRegistrationDatabase
where
serde_json::Value: ToSql<diesel::pg::sql_types::Jsonb, Pg>,
{
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> diesel::serialize::Result {
<serde_json::Value as ToSql<diesel::pg::sql_types::Jsonb, Pg>>::to_sql(&self.0, out)
}
}
impl TryFrom<AuthenticationState> for AuthenticationStateDatabase {
type Error = serde_json::Error;
fn try_from(value: AuthenticationState) -> Result<Self, Self::Error> {
Ok(Self(
serde_json::to_value(value)?
))
}
}
impl TryFrom<AuthenticationStateDatabase> for AuthenticationState {
type Error = serde_json::Error;
fn try_from(value: AuthenticationStateDatabase) -> Result<Self, Self::Error> {
serde_json::from_value(value.0)
}
}
/// Allow [`diesel::pg::sql_types::Jsonb`] values to be parsed as [`AuthenticationStateDatabase`].
impl FromSql<diesel::pg::sql_types::Jsonb, Pg> for AuthenticationStateDatabase
where
serde_json::Value: FromSql<diesel::pg::sql_types::Jsonb, Pg>,
{
fn from_sql(bytes: <Pg as diesel::backend::Backend>::RawValue<'_>) -> diesel::deserialize::Result<Self> {
let v = <serde_json::Value as FromSql<diesel::pg::sql_types::Jsonb, Pg>>::from_sql(bytes)?;
Ok(Self(v))
}
}
/// Allow [`diesel::pg::sql_types::Jsonb`] values to be written to with [`AuthenticationStateDatabase`].
impl ToSql<diesel::pg::sql_types::Jsonb, Pg> for AuthenticationStateDatabase
where
serde_json::Value: ToSql<diesel::pg::sql_types::Jsonb, Pg>,
{
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> diesel::serialize::Result {
<serde_json::Value as ToSql<diesel::pg::sql_types::Jsonb, Pg>>::to_sql(&self.0, out)
}
}
impl AuthUser {
/// Synchronously get the user with the specified id.
pub fn query_id(conn: &mut PgConnection, user_id: Uuid) -> QueryResult<Option<Self>> {
use diesel::QueryDsl;
use diesel::RunQueryDsl;
use schema::auth_users::dsl::*;
auth_users
.find(user_id)
.select(Self::as_select())
.get_result(conn)
.optional()
}
/// Asynchronously get the user with the specified id.
pub async fn aquery_id(conn: &mut AsyncPgConnection, user_id: Uuid) -> QueryResult<Option<Self>> {
use diesel::QueryDsl;
use diesel_async::RunQueryDsl;
use schema::auth_users::dsl::*;
auth_users
.find(user_id)
.select(Self::as_select())
.get_result(conn)
.await
.optional()
}
/// Synchronously get the user with the specified username.
pub fn query_username(conn: &mut PgConnection, user_name: &str) -> QueryResult<Option<Self>> {
use diesel::QueryDsl;
use diesel::RunQueryDsl;
use schema::auth_users::dsl::*;
let matching_username = username.eq(user_name);
auth_users
.filter(matching_username)
.select(Self::as_select())
.get_result(conn)
.optional()
}
/// Asynchronously get the user with the specified username.
pub async fn aquery_username(conn: &mut AsyncPgConnection, user_name: &str) -> QueryResult<Option<Self>> {
use diesel::QueryDsl;
use diesel_async::RunQueryDsl;
use schema::auth_users::dsl::*;
let matching_username = username.eq(user_name);
auth_users
.filter(matching_username)
.select(Self::as_select())
.get_result(conn)
.await
.optional()
}
}
impl AuthPasskey {
/// Synchronously get the passkey with the specified id.
pub fn query_id(conn: &mut PgConnection, passkey_id: Uuid) -> QueryResult<Option<Self>> {
use diesel::QueryDsl;
use diesel::RunQueryDsl;
use schema::auth_passkeys::dsl::*;
auth_passkeys
.find(passkey_id)
.select(Self::as_select())
.get_result(conn)
.optional()
}
/// Asynchronously get the passkey with the specified id.
pub async fn aquery_id(conn: &mut AsyncPgConnection, passkey_id: Uuid) -> QueryResult<Option<Self>> {
use diesel::QueryDsl;
use diesel_async::RunQueryDsl;
use schema::auth_passkeys::dsl::*;
auth_passkeys
.find(passkey_id)
.select(Self::as_select())
.get_result(conn)
.await
.optional()
}
}
impl AuthRegistration {
/// Synchronously get the registration with the specified id.
pub fn query_id(conn: &mut PgConnection, registration_id: Uuid) -> QueryResult<Option<Self>> {
use diesel::QueryDsl;
use diesel::RunQueryDsl;
use schema::auth_registration::dsl::*;
auth_registration
.find(registration_id)
.select(Self::as_select())
.get_result(conn)
.optional()
}
/// Asynchronously get the registration with the specified id.
pub async fn aquery_id(conn: &mut AsyncPgConnection, registration_id: Uuid) -> QueryResult<Option<Self>> {
use diesel::QueryDsl;
use diesel_async::RunQueryDsl;
use schema::auth_registration::dsl::*;
auth_registration
.find(registration_id)
.select(Self::as_select())
.get_result(conn)
.await
.optional()
}
}
impl AuthAuthentication {
/// Synchronously get the authentication with the specified id.
pub fn query_id(conn: &mut PgConnection, authentication_id: Uuid) -> QueryResult<Option<Self>> {
use diesel::QueryDsl;
use diesel::RunQueryDsl;
use schema::auth_authentication::dsl::*;
auth_authentication
.find(authentication_id)
.select(Self::as_select())
.get_result(conn)
.optional()
}
/// Asynchronously get the authentication with the specified id.
pub async fn aquery_id(conn: &mut AsyncPgConnection, authentication_id: Uuid) -> QueryResult<Option<Self>> {
use diesel::QueryDsl;
use diesel_async::RunQueryDsl;
use schema::auth_authentication::dsl::*;
auth_authentication
.find(authentication_id)
.select(Self::as_select())
.get_result(conn)
.await
.optional()
}
}
impl_to_insert!(AuthUserInsert => AuthUser: schema::auth_users::table);
impl_to_insert!(AuthPasskeyInsert => AuthPasskey: schema::auth_passkeys::table);
impl_to_insert!(AuthRegistrationInsert => AuthRegistration: schema::auth_registration::table);
impl_to_insert!(AuthAuthenticationInsert => AuthAuthentication: schema::auth_authentication::table);

View file

@ -8,6 +8,7 @@
mod schema;
pub mod meta;
pub mod auth;
mod macros;

View file

@ -1,5 +1,39 @@
// @generated automatically by Diesel CLI.
diesel::table! {
auth_authentication (id) {
user_id -> Uuid,
state -> Jsonb,
id -> Uuid,
created -> Timestamptz,
}
}
diesel::table! {
auth_passkeys (id) {
user_id -> Uuid,
id -> Uuid,
passkey -> Jsonb,
}
}
diesel::table! {
auth_registration (id) {
user_id -> Uuid,
state -> Jsonb,
id -> Uuid,
created -> Timestamptz,
}
}
diesel::table! {
auth_users (id) {
id -> Uuid,
username -> Varchar,
display_name -> Varchar,
}
}
diesel::table! {
meta_aliases (id) {
id -> Uuid,
@ -60,55 +94,21 @@ diesel::table! {
}
}
diesel::table! {
users (id) {
id -> Uuid,
username -> Varchar,
display_name -> Varchar,
}
}
diesel::table! {
users_passkey_authentication_state (id) {
user_id -> Uuid,
state -> Bytea,
id -> Uuid,
created -> Timestamptz,
}
}
diesel::table! {
users_passkey_registration_state (id) {
user_id -> Uuid,
state -> Bytea,
id -> Uuid,
created -> Timestamptz,
}
}
diesel::table! {
users_passkeys (id) {
user_id -> Uuid,
id -> Uuid,
passkey -> Bytea,
}
}
diesel::joinable!(auth_authentication -> auth_users (user_id));
diesel::joinable!(auth_passkeys -> auth_users (user_id));
diesel::joinable!(auth_registration -> auth_users (user_id));
diesel::joinable!(meta_link_properties -> meta_links (meta_link_id));
diesel::joinable!(meta_link_titles -> meta_links (meta_link_id));
diesel::joinable!(users_passkey_authentication_state -> users (user_id));
diesel::joinable!(users_passkey_registration_state -> users (user_id));
diesel::joinable!(users_passkeys -> users (user_id));
diesel::allow_tables_to_appear_in_same_query!(
auth_authentication,
auth_passkeys,
auth_registration,
auth_users,
meta_aliases,
meta_link_properties,
meta_link_titles,
meta_links,
meta_properties,
meta_subjects,
users,
users_passkey_authentication_state,
users_passkey_registration_state,
users_passkeys,
);