Skip to content

Commit c7bbf69

Browse files
authored
feat(orm): varchar(n) support (#64)
1 parent 0874171 commit c7bbf69

File tree

9 files changed

+258
-20
lines changed

9 files changed

+258
-20
lines changed

.github/workflows/rust.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ jobs:
5050
run: cargo +${{ matrix.rust }} build
5151

5252
- name: Test
53-
run: cargo +${{ matrix.rust }} nextest run
53+
run: cargo +${{ matrix.rust }} nextest run --all-features
5454

5555
# Nextest does not support doc tests as in stable Rust
5656
# they are not exposed in the same way as normal tests.

flareon/Cargo.toml

+5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ bytes.workspace = true
1616
chrono.workspace = true
1717
derive_builder.workspace = true
1818
derive_more.workspace = true
19+
fake = { workspace = true, optional = true }
1920
flareon_macros.workspace = true
2021
form_urlencoded.workspace = true
2122
futures-core.workspace = true
@@ -30,6 +31,7 @@ mime_guess.workspace = true
3031
mockall.workspace = true
3132
password-auth.workspace = true
3233
pin-project-lite.workspace = true
34+
rand = { workspace = true, optional = true }
3335
regex.workspace = true
3436
sea-query-binder.workspace = true
3537
sea-query.workspace = true
@@ -59,3 +61,6 @@ ignored = [
5961
# time requires version 0.3.35 to work with the latest versions of Rust, but we don't use it directly
6062
"time",
6163
]
64+
65+
[features]
66+
fake = ["dep:fake", "dep:rand"]

flareon/src/auth.rs

+10-2
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,12 @@ impl PasswordHash {
285285
/// Returns an error if the password hash is invalid.
286286
pub fn new<T: Into<String>>(hash: T) -> Result<Self> {
287287
let hash = hash.into();
288+
289+
if hash.len() > MAX_PASSWORD_HASH_LENGTH as usize {
290+
return Err(AuthError::PasswordHashInvalid);
291+
}
288292
password_auth::is_hash_obsolete(&hash).map_err(|_| AuthError::PasswordHashInvalid)?;
293+
289294
Ok(Self(hash))
290295
}
291296

@@ -303,6 +308,8 @@ impl PasswordHash {
303308
#[must_use]
304309
pub fn from_password(password: &Password) -> Self {
305310
let hash = password_auth::generate_hash(password.as_str());
311+
312+
assert!(hash.len() <= MAX_PASSWORD_HASH_LENGTH as usize);
306313
Self(hash)
307314
}
308315

@@ -393,9 +400,10 @@ impl Debug for PasswordHash {
393400
}
394401
}
395402

403+
const MAX_PASSWORD_HASH_LENGTH: u32 = 128;
404+
396405
impl DatabaseField for PasswordHash {
397-
// TODO change to length-limiting type
398-
const TYPE: ColumnType = ColumnType::Text;
406+
const TYPE: ColumnType = ColumnType::String(MAX_PASSWORD_HASH_LENGTH);
399407
}
400408

401409
impl FromDbValue for PasswordHash {

flareon/src/auth/db.rs

+49-10
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use async_trait::async_trait;
99
use flareon_macros::model;
1010
use hmac::{Hmac, KeyInit, Mac};
1111
use sha2::Sha512;
12+
use thiserror::Error;
1213

1314
use crate::admin::{AdminModel, AdminModelManager, DefaultAdminModelManager};
1415
use crate::auth::{
@@ -17,24 +18,33 @@ use crate::auth::{
1718
};
1819
use crate::config::SecretKey;
1920
use crate::db::migrations::DynMigration;
20-
use crate::db::{query, DatabaseBackend, Model};
21+
use crate::db::{query, DatabaseBackend, LimitedString, Model};
2122
use crate::request::{Request, RequestExt};
2223
use crate::FlareonApp;
2324

2425
pub mod migrations;
2526

27+
pub(crate) const MAX_USERNAME_LENGTH: u32 = 255;
28+
2629
/// A user stored in the database.
2730
#[derive(Debug, Clone)]
2831
#[model]
2932
pub struct DatabaseUser {
3033
id: i64,
31-
username: String,
34+
username: LimitedString<MAX_USERNAME_LENGTH>,
3235
password: PasswordHash,
3336
}
3437

38+
#[derive(Debug, Clone, Error)]
39+
#[non_exhaustive]
40+
pub enum CreateUserError {
41+
#[error("username is too long (max {MAX_USERNAME_LENGTH} characters, got {0})")]
42+
UsernameTooLong(usize),
43+
}
44+
3545
impl DatabaseUser {
3646
#[must_use]
37-
pub fn new(id: i64, username: String, password: &Password) -> Self {
47+
pub fn new(id: i64, username: LimitedString<MAX_USERNAME_LENGTH>, password: &Password) -> Self {
3848
Self {
3949
id,
4050
username,
@@ -88,7 +98,13 @@ impl DatabaseUser {
8898
username: T,
8999
password: U,
90100
) -> Result<Self> {
91-
let mut user = Self::new(0, username.into(), &password.into());
101+
let username = username.into();
102+
let username_length = username.len();
103+
let username = LimitedString::<MAX_USERNAME_LENGTH>::new(username).map_err(|_| {
104+
AuthError::backend_error(CreateUserError::UsernameTooLong(username_length))
105+
})?;
106+
107+
let mut user = Self::new(0, username, &password.into());
92108
user.save(db).await.map_err(AuthError::backend_error)?;
93109

94110
Ok(user)
@@ -109,7 +125,10 @@ impl DatabaseUser {
109125
db: &DB,
110126
credentials: &DatabaseUserCredentials,
111127
) -> Result<Option<Self>> {
112-
let user = query!(DatabaseUser, $username == credentials.username())
128+
let username_limited =
129+
LimitedString::<MAX_USERNAME_LENGTH>::new(credentials.username().to_string())
130+
.map_err(|_| AuthError::backend_error(CreateUserError::UsernameTooLong(0)))?;
131+
let user = query!(DatabaseUser, $username == username_limited)
113132
.get(db)
114133
.await
115134
.map_err(AuthError::backend_error)?;
@@ -339,7 +358,11 @@ mod tests {
339358

340359
#[test]
341360
fn session_auth_hash() {
342-
let user = DatabaseUser::new(1, "testuser".to_string(), &Password::new("password123"));
361+
let user = DatabaseUser::new(
362+
1,
363+
LimitedString::new("testuser").unwrap(),
364+
&Password::new("password123"),
365+
);
343366
let secret_key = SecretKey::new(b"supersecretkey");
344367

345368
let hash = user.session_auth_hash(&secret_key);
@@ -348,7 +371,11 @@ mod tests {
348371

349372
#[test]
350373
fn database_user_traits() {
351-
let user = DatabaseUser::new(1, "testuser".to_string(), &Password::new("password123"));
374+
let user = DatabaseUser::new(
375+
1,
376+
LimitedString::new("testuser").unwrap(),
377+
&Password::new("password123"),
378+
);
352379
let user_ref: &dyn User = &user;
353380
assert_eq!(user_ref.id(), Some(UserId::Int(1)));
354381
assert_eq!(user_ref.username(), Some("testuser"));
@@ -378,7 +405,11 @@ mod tests {
378405
#[tokio::test]
379406
async fn get_by_id() {
380407
let mut mock_db = MockDatabaseBackend::new();
381-
let user = DatabaseUser::new(1, "testuser".to_string(), &Password::new("password123"));
408+
let user = DatabaseUser::new(
409+
1,
410+
LimitedString::new("testuser").unwrap(),
411+
&Password::new("password123"),
412+
);
382413

383414
mock_db
384415
.expect_get::<DatabaseUser>()
@@ -394,7 +425,11 @@ mod tests {
394425
#[tokio::test]
395426
async fn authenticate() {
396427
let mut mock_db = MockDatabaseBackend::new();
397-
let user = DatabaseUser::new(1, "testuser".to_string(), &Password::new("password123"));
428+
let user = DatabaseUser::new(
429+
1,
430+
LimitedString::new("testuser").unwrap(),
431+
&Password::new("password123"),
432+
);
398433

399434
mock_db
400435
.expect_get::<DatabaseUser>()
@@ -428,7 +463,11 @@ mod tests {
428463
#[tokio::test]
429464
async fn authenticate_invalid_password() {
430465
let mut mock_db = MockDatabaseBackend::new();
431-
let user = DatabaseUser::new(1, "testuser".to_string(), &Password::new("password123"));
466+
let user = DatabaseUser::new(
467+
1,
468+
LimitedString::new("testuser").unwrap(),
469+
&Password::new("password123"),
470+
);
432471

433472
mock_db
434473
.expect_get::<DatabaseUser>()

flareon/src/auth/db/migrations/m_0001_initial.rs

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
//! Generated by flareon CLI 0.1.0 on 2024-11-06 02:29:53+00:00
1+
//! Generated by flareon CLI 0.1.0 on 2024-11-07 11:30:40+00:00
22
3+
use crate::auth::db::MAX_USERNAME_LENGTH;
34
use crate::auth::PasswordHash;
5+
use crate::db::LimitedString;
46

57
#[derive(Debug, Copy, Clone)]
68
pub(super) struct Migration;
@@ -20,9 +22,11 @@ impl ::flareon::db::migrations::Migration for Migration {
2022
.set_null(<i64 as ::flareon::db::DatabaseField>::NULLABLE),
2123
::flareon::db::migrations::Field::new(
2224
::flareon::db::Identifier::new("username"),
23-
<String as ::flareon::db::DatabaseField>::TYPE,
25+
<LimitedString<MAX_USERNAME_LENGTH> as ::flareon::db::DatabaseField>::TYPE,
2426
)
25-
.set_null(<String as ::flareon::db::DatabaseField>::NULLABLE),
27+
.set_null(
28+
<LimitedString<MAX_USERNAME_LENGTH> as ::flareon::db::DatabaseField>::NULLABLE,
29+
),
2630
::flareon::db::migrations::Field::new(
2731
::flareon::db::Identifier::new("password"),
2832
<PasswordHash as ::flareon::db::DatabaseField>::TYPE,
@@ -36,6 +40,6 @@ impl ::flareon::db::migrations::Migration for Migration {
3640
#[::flareon::db::model(model_type = "migration")]
3741
struct _DatabaseUser {
3842
id: i64,
39-
username: String,
43+
username: LimitedString<MAX_USERNAME_LENGTH>,
4044
password: PasswordHash,
4145
}

0 commit comments

Comments
 (0)