Signed by hanna SSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
crates/store/src/lib.rs +2 −0 crates/store/src/sessions.rs +127 −0 crates/store/src/tests.rs +56 −0 crates/store/src/users.rs +2 −2 crates/store/src/lib.rs +2 −0 29 mod invites; 29 mod invites; 30 mod keys; 30 mod keys; 31 mod repos; 31 mod repos; 32 mod sessions; 32 mod tokens; 33 mod tokens; 33 mod users; 34 mod users; 34 35 43 pub use crate :: invites:: NewInvite ; 44 pub use crate :: invites:: NewInvite ; 44 pub use crate :: keys:: NewKey ; 45 pub use crate :: keys:: NewKey ; 45 pub use crate :: repos:: NewRepo ; 46 pub use crate :: repos:: NewRepo ; 47 pub use crate :: sessions:: NewSession ; 46 pub use crate :: tokens:: NewToken ; 48 pub use crate :: tokens:: NewToken ; 47 pub use crate :: users:: NewUser ; 49 pub use crate :: users:: NewUser ; 48 50
crates/store/src/sessions.rs +127 −0 1 2 3 4 5 6 7 8 9 10 11 use model:: User ; 12 use sqlx:: Row ; 13 14 use crate :: users:: USER_COLUMNS ; 15 use crate :: { Store , StoreError , now_ms} ; 16 17 18 # [ derive ( Debug , Clone ) ] 19 pub struct NewSession { 20 21 pub id: String , 22 23 pub user_id: String , 24 25 pub user_agent: Option < String > , 26 27 pub ip: Option < String > , 28 29 pub expires_at: i64 , 30 } 31 32 impl Store { 33 34 35 36 37 38 pub async fn create_session ( & self , new : NewSession ) -> Result < ( ) , StoreError > { 39 let now = now_ms ( ) ; 40 sqlx:: query ( 41 "INSERT INTO sessions (id, user_id, user_agent, ip, created_at, last_seen_at, expires_at) \ 42 VALUES ($1, $2, $3, $4, $5, $5, $6)" , 43 ) 44 . bind ( new. id) 45 . bind ( new. user_id) 46 . bind ( new. user_agent) 47 . bind ( new. ip) 48 . bind ( now) 49 . bind ( new. expires_at) 50 . execute ( & self . pool) 51 . await ?; 52 Ok ( ( ) ) 53 } 54 55 56 57 58 59 60 61 62 63 pub async fn user_for_session ( & self , session_id : & str ) -> Result < Option < User > , StoreError > { 64 let sql = format ! ( 65 "SELECT {cols} FROM users u JOIN sessions s ON s.user_id = u.id \ 66 WHERE s.id = $1 AND s.expires_at > $2 AND u.disabled_at IS NULL" , 67 cols = USER_COLUMNS 68 . split ( ", " ) 69 . map ( |c| format ! ( "u.{c}" ) ) 70 . collect :: < Vec < _ > > ( ) 71 . join ( ", " ) , 72 ) ; 73 let row = sqlx:: query ( sqlx:: AssertSqlSafe ( sql) ) 74 . bind ( session_id) 75 . bind ( now_ms ( ) ) 76 . fetch_optional ( & self . pool) 77 . await ?; 78 Ok ( row. as_ref ( ) . map ( |r| self . map_user ( r) ) . transpose ( ) ?) 79 } 80 81 82 83 84 85 86 pub async fn touch_session ( & self , session_id : & str ) -> Result < ( ) , StoreError > { 87 let now = now_ms ( ) ; 88 sqlx:: query ( 89 "UPDATE sessions SET last_seen_at = $1 \ 90 WHERE id = $2 AND last_seen_at < $3" , 91 ) 92 . bind ( now) 93 . bind ( session_id) 94 . bind ( now - 60_000 ) 95 . execute ( & self . pool) 96 . await ?; 97 Ok ( ( ) ) 98 } 99 100 101 102 103 104 105 pub async fn delete_session ( & self , session_id : & str ) -> Result < bool , StoreError > { 106 let deleted = sqlx:: query ( "DELETE FROM sessions WHERE id = $1" ) 107 . bind ( session_id) 108 . execute ( & self . pool) 109 . await ? 110 . rows_affected ( ) ; 111 Ok ( deleted > 0 ) 112 } 113 114 115 116 117 118 119 pub async fn delete_expired_sessions ( & self ) -> Result < u64 , StoreError > { 120 let deleted = sqlx:: query ( "DELETE FROM sessions WHERE expires_at <= $1" ) 121 . bind ( now_ms ( ) ) 122 . execute ( & self . pool) 123 . await ? 124 . rows_affected ( ) ; 125 Ok ( deleted) 126 } 127 }
crates/store/src/tests.rs +56 −0 733 } 733 } 734 734 735 # [ tokio :: test ] 735 # [ tokio :: test ] 736 async fn sessions_resolve_the_user_and_respect_expiry ( ) { 737 use super :: NewSession ; 738 739 for fx in sqlite_fixtures ( ) . await { 740 let user = fx 741 . store 742 . create_user ( sample_user ( "s" , "s@example.com" ) ) 743 . await 744 . unwrap ( ) ; 745 746 fx. store 747 . create_session ( NewSession { 748 id: "sess-live" . to_string ( ) , 749 user_id: user. id. clone ( ) , 750 user_agent: Some ( "curl" . to_string ( ) ) , 751 ip: None , 752 expires_at: now_plus_a_day ( ) , 753 } ) 754 . await 755 . unwrap ( ) ; 756 let resolved = fx. store. user_for_session ( "sess-live" ) . await . unwrap ( ) ; 757 assert_eq ! ( resolved. map ( |u| u. id) , Some ( user. id. clone ( ) ) ) ; 758 759 760 fx. store 761 . create_session ( NewSession { 762 id: "sess-dead" . to_string ( ) , 763 user_id: user. id. clone ( ) , 764 user_agent: None , 765 ip: None , 766 expires_at: 1 , 767 } ) 768 . await 769 . unwrap ( ) ; 770 assert ! ( 771 fx. store 772 . user_for_session ( "sess-dead" ) 773 . await 774 . unwrap ( ) 775 . is_none ( ) 776 ) ; 777 assert_eq ! ( fx. store. delete_expired_sessions ( ) . await . unwrap ( ) , 1 ) ; 778 779 780 assert ! ( fx. store. delete_session ( "sess-live" ) . await . unwrap ( ) ) ; 781 assert ! ( 782 fx. store 783 . user_for_session ( "sess-live" ) 784 . await 785 . unwrap ( ) 786 . is_none ( ) 787 ) ; 788 } 789 } 790 791 # [ tokio :: test ] 736 async fn invites_round_trip_and_are_single_use ( ) { 792 async fn invites_round_trip_and_are_single_use ( ) { 737 use super :: NewInvite ; 793 use super :: NewInvite ; 738 794
crates/store/src/users.rs +2 −2 12 12 13 13 14 14 15 const USER_COLUMNS : & str = "id, username, email, display_name, password_hash, \ 15 pub ( crate ) const USER_COLUMNS : & str = "id, username, email, display_name, password_hash, \ 16 must_change_password, is_admin, disabled_at, created_at, updated_at" ; 16 must_change_password, is_admin, disabled_at, created_at, updated_at" ; 17 17 18 18 221 } 221 } 222 222 223 223 224 fn map_user ( & self , row : & AnyRow ) -> Result < User , sqlx:: Error > { 224 pub ( crate ) fn map_user ( & self , row : & AnyRow ) -> Result < User , sqlx:: Error > { 225 Ok ( User { 225 Ok ( User { 226 id: row. try_get ( "id" ) ?, 226 id: row. try_get ( "id" ) ?, 227 username: row. try_get ( "username" ) ?, 227 username: row. try_get ( "username" ) ?,