add initial chat server stuff
This commit is contained in:
parent
3025f1380c
commit
0e71e9dc5f
18 changed files with 722 additions and 40 deletions
|
@ -9,9 +9,7 @@ edition = "2021"
|
|||
foxchat = { path = "../foxchat" }
|
||||
axum = { version = "0.7.4", features = ["macros", "query", "tracing", "ws"] }
|
||||
clap = { version = "4.4.16", features = ["env", "derive"] }
|
||||
dotenvy = "0.15.7"
|
||||
sqlx = { version = "0.7.3", features = ["runtime-tokio", "tls-rustls", "postgres", "migrate", "uuid", "chrono", "json"] }
|
||||
uuid = { version = "1.6.1", features = ["v7"] }
|
||||
sqlx = { version = "0.7.3", features = ["runtime-tokio", "tls-rustls", "postgres", "migrate", "chrono", "json"] }
|
||||
serde = { version = "1.0.195", features = ["derive"] }
|
||||
serde_json = "1.0.111"
|
||||
ulid = { version = "1.1.0", features = ["serde"] }
|
||||
|
@ -25,3 +23,6 @@ tracing-subscriber = "0.3.18"
|
|||
tracing = "0.1.40"
|
||||
tower-http = { version = "0.5.1", features = ["trace"] }
|
||||
bcrypt = "0.15.0"
|
||||
base64 = "0.21.7"
|
||||
sha256 = "1.5.0"
|
||||
reqwest = { version = "0.11.23", features = ["json", "gzip", "brotli", "multipart"] }
|
||||
|
|
4
identity/migrations/20240116172715_tokens.sql
Normal file
4
identity/migrations/20240116172715_tokens.sql
Normal file
|
@ -0,0 +1,4 @@
|
|||
create table tokens (
|
||||
token text primary key,
|
||||
account_id text not null references accounts (id) on delete cascade
|
||||
);
|
|
@ -12,6 +12,9 @@ pub struct Config {
|
|||
pub port: u16,
|
||||
pub auto_migrate: Option<bool>,
|
||||
pub log_level: Option<String>,
|
||||
/// Whether to try HTTP if a server is not served over HTTPS.
|
||||
/// This should only be set to `true` in development.
|
||||
pub insecure_requests: Option<bool>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
@ -40,6 +43,11 @@ impl Config {
|
|||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
/// If true, the server will try to identify remote servers over HTTP if they are not available over HTTPS
|
||||
pub fn should_try_insecure(&self) -> bool {
|
||||
self.insecure_requests.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
|
@ -49,6 +57,7 @@ impl Default for Config {
|
|||
port: 3000,
|
||||
auto_migrate: None,
|
||||
log_level: None,
|
||||
insecure_requests: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
83
identity/src/db/account.rs
Normal file
83
identity/src/db/account.rs
Normal file
|
@ -0,0 +1,83 @@
|
|||
use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD};
|
||||
use eyre::{Context, Report, Result};
|
||||
use foxchat::QueryError;
|
||||
use rand::RngCore;
|
||||
use sqlx::{PgExecutor, Pool, Postgres};
|
||||
|
||||
use crate::model::account::{Account, Role};
|
||||
|
||||
pub async fn get_user_by_username_and_password(
|
||||
pool: Pool<Postgres>,
|
||||
username: String,
|
||||
password: String,
|
||||
) -> Result<Account> {
|
||||
let account = sqlx::query_as!(
|
||||
Account,
|
||||
r#"select
|
||||
id, username, email, password, role as "role: Role", avatar
|
||||
from accounts where username = $1"#,
|
||||
username
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| -> Report {
|
||||
match e {
|
||||
sqlx::Error::RowNotFound => QueryError::NotFound.into(),
|
||||
_ => e.into(),
|
||||
}
|
||||
})?;
|
||||
|
||||
if bcrypt::verify(password, &account.password).map_err(|_| QueryError::NotFound)? {
|
||||
return Ok(account);
|
||||
}
|
||||
|
||||
Err(QueryError::NotFound.into())
|
||||
}
|
||||
|
||||
pub async fn check_token(pool: &Pool<Postgres>, token: String) -> Result<Account> {
|
||||
let hash = sha256::digest(token);
|
||||
|
||||
let account = sqlx::query_as!(
|
||||
Account,
|
||||
r#"select
|
||||
a.id, a.username, a.email, a.password, a.role as "role: Role", a.avatar
|
||||
from accounts a
|
||||
join tokens t on t.account_id = a.id
|
||||
where t.account_id = $1"#,
|
||||
hash
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
match account {
|
||||
Some(a) => Ok(a),
|
||||
None => Err(QueryError::NotFound.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_token(executor: impl PgExecutor<'_>, user_id: &String) -> Result<String> {
|
||||
let token = generate_token_string();
|
||||
let hash = sha256::digest(&token);
|
||||
sqlx::query!(
|
||||
"insert into tokens (token, account_id) values ($1, $2)",
|
||||
hash,
|
||||
user_id
|
||||
)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
fn generate_token_string() -> String {
|
||||
let mut data = [0u8; 32];
|
||||
rand::thread_rng().fill_bytes(&mut data);
|
||||
|
||||
BASE64_URL_SAFE_NO_PAD.encode(data)
|
||||
}
|
||||
|
||||
const PASSWORD_COST: u32 = 12;
|
||||
|
||||
pub fn hash_password(password: String) -> Result<String> {
|
||||
bcrypt::hash(password, PASSWORD_COST).wrap_err("failed to hash password")
|
||||
}
|
|
@ -1,5 +1,9 @@
|
|||
pub mod account;
|
||||
|
||||
pub use account::{check_token, create_token, get_user_by_username_and_password};
|
||||
|
||||
use eyre::{OptionExt, Result};
|
||||
use rsa::pkcs1::{LineEnding, EncodeRsaPublicKey, EncodeRsaPrivateKey};
|
||||
use rsa::pkcs1::{EncodeRsaPrivateKey, EncodeRsaPublicKey, LineEnding};
|
||||
use rsa::{RsaPrivateKey, RsaPublicKey};
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
|
|
@ -1,31 +1,34 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use axum::{Extension, Json};
|
||||
use eyre::{Context, Result};
|
||||
use eyre::Result;
|
||||
use foxchat::http::ApiError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::app_state::AppState;
|
||||
|
||||
const PASSWORD_COST: u32 = 12;
|
||||
use crate::{app_state::AppState, db::{create_token, account::hash_password}};
|
||||
|
||||
pub async fn create_user(
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(data): Json<CreateUserRequest>,
|
||||
) -> Result<Json<CreateUserResponse>, ApiError> {
|
||||
let password_hash =
|
||||
bcrypt::hash(data.password, PASSWORD_COST).wrap_err("failed to hash password")?;
|
||||
let mut tx = state.pool.begin().await?;
|
||||
|
||||
let password_hash = hash_password(data.password)?;
|
||||
|
||||
let account = sqlx::query!(
|
||||
"insert into accounts (id, username, email, password) values ($1, $2, $3, $4) returning id, username",
|
||||
Ulid::new().to_string(), data.username, data.email, password_hash)
|
||||
.fetch_one(&state.pool)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
let token = create_token(&mut *tx, &account.id).await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(Json(CreateUserResponse {
|
||||
id: account.id,
|
||||
username: account.username,
|
||||
token,
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -40,4 +43,5 @@ pub struct CreateUserRequest {
|
|||
pub struct CreateUserResponse {
|
||||
pub id: String,
|
||||
pub username: String,
|
||||
pub token: String,
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
|
||||
pub struct Account {
|
||||
pub id: Ulid,
|
||||
pub id: String,
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use eyre::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::app_state::AppState;
|
||||
|
||||
pub struct ChatInstance {
|
||||
pub id: Ulid,
|
||||
pub id: String,
|
||||
pub domain: String,
|
||||
pub base_url: String,
|
||||
pub public_key: String,
|
||||
|
@ -16,3 +20,25 @@ pub enum InstanceStatus {
|
|||
Active,
|
||||
Suspended,
|
||||
}
|
||||
|
||||
impl ChatInstance {
|
||||
pub async fn get(state: Arc<AppState>, domain: String) -> Result<Self> {
|
||||
if let Some(instance) = sqlx::query_as!(
|
||||
Self,
|
||||
r#"select id, domain, base_url, public_key,
|
||||
status as "status: InstanceStatus", reason
|
||||
from chat_instances where domain = $1"#,
|
||||
domain
|
||||
)
|
||||
.fetch_optional(&state.pool)
|
||||
.await?
|
||||
{
|
||||
return Ok(instance);
|
||||
}
|
||||
|
||||
// TODO: identify server process
|
||||
// only try HTTP if `state.config.should_try_insecure()`
|
||||
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@ pub struct Instance {
|
|||
}
|
||||
|
||||
impl Instance {
|
||||
/// Gets the instance's configuration.
|
||||
/// This is a singleton row that is always present.
|
||||
pub async fn get(pool: &Pool<Postgres>) -> Result<Self> {
|
||||
let instance = sqlx::query!("SELECT * FROM instance WHERE id = 1")
|
||||
.fetch_one(pool)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue