diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 84ea2bb..6107b35 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -187,6 +187,7 @@ dependencies = [ "sha2", "sqlx", "tokio", + "totp-rs", ] [[package]] @@ -204,6 +205,12 @@ dependencies = [ "windows-link 0.2.0", ] +[[package]] +name = "base32" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" + [[package]] name = "base64" version = "0.21.7" @@ -289,6 +296,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.10.1" @@ -363,6 +376,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "cookie" version = "0.18.1" @@ -420,6 +439,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -628,6 +656,15 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "figment" version = "0.10.19" @@ -660,6 +697,16 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "flume" version = "0.11.1" @@ -1340,6 +1387,18 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "image" +version = "0.25.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "num-traits", + "png", +] + [[package]] name = "indexmap" version = "2.11.4" @@ -1590,6 +1649,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -2033,6 +2093,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polyval" version = "0.6.2" @@ -2091,6 +2164,23 @@ dependencies = [ "yansi", ] +[[package]] +name = "qrcodegen" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142" + +[[package]] +name = "qrcodegen-image" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "221b7eace1aef8c95d65dbe09fb7a1a43d006045394a89afba6997721fcb7708" +dependencies = [ + "base64 0.22.1", + "image", + "qrcodegen", +] + [[package]] name = "quote" version = "1.0.41" @@ -2708,6 +2798,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "siphasher" version = "1.0.1" @@ -3326,6 +3422,23 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "totp-rs" +version = "5.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f124352108f58ef88299e909f6e9470f1cdc8d2a1397963901b4a6366206bf72" +dependencies = [ + "base32", + "constant_time_eq", + "hmac", + "qrcodegen-image", + "rand 0.9.2", + "sha1", + "sha2", + "url", + "urlencoding", +] + [[package]] name = "tower" version = "0.5.2" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index d87fbeb..149814a 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -17,3 +17,4 @@ serde = { version = "1.0.228", features = ["derive"] } sha2 = "0.10.9" sqlx = { version = "0.7.4", features = ["macros", "time"] } tokio = { version = "1.47.1", features = ["full"] } +totp-rs = { version = "5.7.0", features = ["gen_secret", "qr", "rand"] } diff --git a/backend/migrations/20251009091103_auth-v2.sql b/backend/migrations/20251009091103_auth-v2.sql new file mode 100644 index 0000000..69cb4e9 --- /dev/null +++ b/backend/migrations/20251009091103_auth-v2.sql @@ -0,0 +1,15 @@ +-- Add migration script here +DROP TABLE users; + +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + username VARCHAR(50) UNIQUE NOT NULL, + display_name VARCHAR(50), + + email VARCHAR(100) NOT NULL, + password VARCHAR(100) NOT NULL, + totp_secret VARCHAR(100), + + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); diff --git a/backend/src/auth.rs b/backend/src/auth.rs index 11f4e3a..f699632 100644 --- a/backend/src/auth.rs +++ b/backend/src/auth.rs @@ -3,6 +3,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use rand::Rng; use rocket::{ Request, + fs::NamedFile, http::{CookieJar, Status}, outcome::Outcome, post, @@ -17,6 +18,7 @@ use rocket_dyn_templates::{Template, context}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use sqlx::postgres::PgQueryResult; +use totp_rs::{Algorithm, Secret, TOTP}; use crate::db::DbConn; @@ -91,6 +93,29 @@ pub async fn login( Err("login failed".to_string()) } +#[get("/totp")] +pub async fn mfa_page(session: Session) -> Template { + Template::render("2fa", context!()) +} + +#[get("/api/totp.jpg")] +pub async fn gen_totp(s: Session) -> Option { + let totp = TOTP::new( + Algorithm::SHA1, + 6, + 1, + 30, + Secret::generate_secret().to_bytes().unwrap(), + Some("Github".to_string()), + format!("{}", s.user_id), + ) + .unwrap(); + + let qr = totp.get_qr_base64().unwrap(); + + Some(QrCodeImage(qr.into())) +} + #[derive(Debug)] pub struct Session { pub token: String, @@ -153,3 +178,18 @@ impl<'r> FromRequest<'r> for Session { } } } + +use rocket::http::ContentType; +use rocket::response::{self, Responder, Response}; +use std::io::Cursor; + +pub struct QrCodeImage(Vec); + +impl<'r> Responder<'r, 'static> for QrCodeImage { + fn respond_to(self, _: &'r rocket::Request<'_>) -> response::Result<'static> { + Response::build() + .header(ContentType::PNG) + .sized_body(self.0.len(), Cursor::new(self.0)) + .ok() + } +} diff --git a/backend/templates/2fa.html.tera b/backend/templates/2fa.html.tera new file mode 100644 index 0000000..a75d859 --- /dev/null +++ b/backend/templates/2fa.html.tera @@ -0,0 +1,56 @@ + + + + + + Discord Clone - Group Chat + + + +
+ + +
+
+ +

Chat title

+
+
+ + +
+
+
+ Map +
+
+
+ Location +
+ +
Live Location
+
+ +
+
+
+
+ + + +
+ + +
+
+ + +
+
+
+ + diff --git a/migrations/20251001212022_prototype-v1.sql b/migrations/20251001212022_prototype-v1.sql deleted file mode 100644 index 9a2956b..0000000 --- a/migrations/20251001212022_prototype-v1.sql +++ /dev/null @@ -1,47 +0,0 @@ --- Add migration script here -CREATE TABLE users { - id SERIAL PRIMARY KEY, - username VARCHAR(50) UNIQUE NOT NULL, - password VARCHAR(50) NOT NULL, - display_name VARCHAR(50), - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP -} - -CREATE TABLE messages { - id SERIAL PRIMARY KEY, - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - content TEXT NOT NULL, - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - is_edited BOOLEAN DEFAULT FALSE -} - -create table attachments { - id SERIAL PRIMARY KEY, - message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE, - path TEXT NOT NULL -} - -CREATE INDEX idx_users_username ON users(username) -CREATE INDEX idx_new_messages ON messages(created_at DESC) - --- Create a function to update the updated_at timestamp -CREATE OR REPLACE FUNCTION update_updated_at_column() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = CURRENT_TIMESTAMP; - RETURN NEW; -END; -$$ language 'plpgsql'; - --- Create trigger for users table -CREATE TRIGGER update_users_updated_at - BEFORE UPDATE ON users - FOR EACH ROW - EXECUTE FUNCTION update_updated_at_column(); - --- Create trigger for messages table -CREATE TRIGGER update_messages_updated_at - BEFORE UPDATE ON messages - FOR EACH ROW - EXECUTE FUNCTION update_updated_at_column();