Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
5db8f45
Add user passwords migration
Jul 9, 2025
7120691
Add users password to database when logging in
Jul 9, 2025
ba1e424
Send info to tmc that users password is stored in db
Jul 10, 2025
23d2578
Url and payload change to set user password
Jul 11, 2025
e261563
User authentication when logging in
Jul 14, 2025
ed671f1
Changed function name
Jul 15, 2025
3396bc9
Endpoint for changing password from tmc and little fixes
Jul 15, 2025
facc79a
Update password to database upon sign up
Jul 15, 2025
0c5f2d5
Small fixes
Jul 16, 2025
2e7bfc8
Try to auth user from database when logging in
Jul 17, 2025
a4e2f4c
Added test for creating an account and logging in
Jul 17, 2025
1ec83cd
Refactored log in function
Jul 17, 2025
47e770d
Auth and password change uses user_id instead of email
Jul 23, 2025
08ee405
User can change password with reset link
Jul 25, 2025
83a218b
Reset email in different languages and added authentication to emai d…
Aug 13, 2025
f9c95df
Merge remote-tracking branch 'origin/master' into move-passwords-from…
Aug 21, 2025
aa3ad19
Merge remote-tracking branch 'origin/master' into move-passwords-from…
Aug 21, 2025
2ab91f8
Check email from tmc if not stored in courses mooc
Aug 21, 2025
33f858b
Changed expires_at to 1 hour
Aug 21, 2025
b602c56
Addes reset password emails to seed
Aug 21, 2025
04a5bf9
Little changes and fixes
Aug 22, 2025
7c84a09
Little improvements
Aug 25, 2025
e2b5c9a
Merge remote-tracking branch 'origin/master' into move-passwords-from…
Aug 25, 2025
8d647bd
Redirect to login after inserting new password
Aug 25, 2025
5e4a6f3
Test for resetting password
Aug 25, 2025
1a683e0
Mock-up secrets
Aug 26, 2025
c4d5d1a
Refactoring
Aug 26, 2025
10f0fa6
Create account here, don't fetch details from mooc.fi
Redande Aug 27, 2025
e5cfdc4
Remove unused import and parameter
Redande Aug 27, 2025
486e919
Chatbot bug fix (#1531)
HeljaeRaeisaenen Aug 26, 2025
1631329
Restored package-lock files
Aug 27, 2025
7aa3a9b
Delete account form
Aug 28, 2025
95034eb
Backend for deleting user
Sep 2, 2025
4b2ad4e
Save upstream id from tmc during signup
Redande Sep 9, 2025
2c62428
Added single-use-code when deleting account
Sep 10, 2025
2806570
Refactored to work with multiple email placeholders
Sep 10, 2025
ba0a090
Merge branch 'move-passwords-from-tmc' of github.com:rage/secret-proj…
Sep 10, 2025
e26b541
Removed &mut
Sep 10, 2025
47d9001
Merge remote-tracking branch 'origin/master' into move-passwords-from…
Sep 11, 2025
5e75d21
Little test fix
Sep 12, 2025
4f664a2
Merge remote-tracking branch 'origin/master' into move-passwords-from…
Sep 12, 2025
f7f6136
Added delete account email template to seed
Sep 15, 2025
690357b
Merge remote-tracking branch 'origin/master' into move-passwords-from…
Sep 15, 2025
17bd8b7
Delete account from TMC
Sep 15, 2025
1210b07
Minor improvements and timer for resend button
Sep 15, 2025
c153fb7
Improvements to VerifyOneTimeCodeForm
Sep 16, 2025
b64c495
Old password optional and updated screenshots
Sep 22, 2025
3a95ec0
Merge remote-tracking branch 'origin/master' into move-passwords-from…
Oct 8, 2025
df88247
User can change password in user settings -page
Oct 13, 2025
2771ba7
Refactor to fetch user data from TMC not mooc.fi
Redande Oct 28, 2025
ead3008
Use SecretString for access tokens
Redande Oct 28, 2025
9f7c00e
Borrow the SecretStrings while passing them to functions
Redande Oct 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions services/headless-lms/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE user_passwords;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
CREATE TABLE user_passwords (
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
password_hash TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE
);
CREATE TRIGGER set_timestamp BEFORE
UPDATE ON user_passwords FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp();

COMMENT ON TABLE user_passwords IS 'This table is used to keep a record of the users password';
COMMENT ON COLUMN user_passwords.user_id IS 'References the unique identifier of the user.';
COMMENT ON COLUMN user_passwords.password_hash IS 'Hashed password of the user';
;
COMMENT ON COLUMN user_passwords.created_at IS 'Timestamp of when the record was created.';
COMMENT ON COLUMN user_passwords.updated_at IS 'Timestamp when the record was last updated. The field is updated automatically by the set_timestamp trigger.';
COMMENT ON COLUMN user_passwords.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.';

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions services/headless-lms/models/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ lettre = "0.11.17"
rand = "0.9.1"
# Flexible concrete Error type built on std::error::Error
anyhow = "1.0.98"
# Secret value wrapper to avoid leaking secrets in logs and memory
secrecy = "0.10.3"
# Password hashing using Argon2id
argon2 = "0.5.3"

[dev-dependencies]
# Overwrite `assert_eq!` and `assert_ne!` with drop-in replacements, adding colorful diffs.
Expand Down
1 change: 1 addition & 0 deletions services/headless-lms/models/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ pub mod user_details;
pub mod user_exercise_slide_states;
pub mod user_exercise_states;
pub mod user_exercise_task_states;
pub mod user_passwords;
pub mod user_research_consents;
pub mod users;

Expand Down
96 changes: 96 additions & 0 deletions services/headless-lms/models/src/user_passwords.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use crate::prelude::*;
use argon2::password_hash::{SaltString, rand_core::OsRng};
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use secrecy::{ExposeSecret, SecretString};

pub struct UserPassword {
pub user_id: Uuid,
pub password_hash: SecretString,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub deleted_at: Option<DateTime<Utc>>,
}

pub async fn upsert_user_password(
conn: &mut PgConnection,
user_id: Uuid,
password_hash: SecretString,
) -> ModelResult<bool> {
let result = sqlx::query!(
r#"
INSERT INTO user_passwords (user_id, password_hash, created_at, updated_at)
VALUES ($1, $2, NOW(), NOW()) ON CONFLICT (user_id) DO
UPDATE
SET password_hash = EXCLUDED.password_hash,
updated_at = NOW()
"#,
user_id,
password_hash.expose_secret()
)
.execute(conn)
.await?;

Ok(result.rows_affected() > 0)
}

pub fn hash_password(
password: &SecretString,
) -> Result<SecretString, argon2::password_hash::Error> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();

let password_hash = argon2.hash_password(password.expose_secret().as_bytes(), &salt)?;
Ok(SecretString::new(password_hash.to_string().into()))
}

pub async fn verify_user_password(
conn: &mut PgConnection,
user_id: Uuid,
password: &SecretString,
) -> ModelResult<bool> {
let user_password = match sqlx::query!(
r#"
SELECT password_hash
FROM user_passwords
WHERE user_id = $1
AND deleted_at IS NULL
"#,
user_id
)
.fetch_optional(conn)
.await?
{
Some(p) => p,
None => return Ok(false),
};

let parsed_hash = match PasswordHash::new(&user_password.password_hash) {
Ok(hash) => hash,
Err(_) => return Ok(false),
};

let is_valid = Argon2::default()
.verify_password(password.expose_secret().as_bytes(), &parsed_hash)
.is_ok();

Ok(is_valid)
}

pub async fn check_if_users_password_is_stored(
conn: &mut PgConnection,
user_id: Uuid,
) -> ModelResult<bool> {
let result = sqlx::query!(
r#"
SELECT *
FROM user_passwords
WHERE user_id = $1
AND deleted_at IS NULL
"#,
user_id
)
.fetch_optional(conn)
.await?;

Ok(result.is_some())
}
4 changes: 4 additions & 0 deletions services/headless-lms/server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ async-trait = "0.1.88"
tokio-util = "0.7.15"
# Asynchronous streams using async & await notation
async-stream = "0.3.6"
# Secret value wrapper to avoid leaking secrets in logs and memory
secrecy = { version = "0.10.3", features = ["serde"] }
# Password hashing using Argon2id
argon2 = "0.5.3"

[dev-dependencies]
# Overwrite `assert_eq!` and `assert_ne!` with drop-in replacements, adding colorful diffs.
Expand Down
Loading
Loading