]> Untitled Git - lemmy.git/blob - server/src/db/user.rs
Merge branch 'dev' into federation
[lemmy.git] / server / src / db / user.rs
1 use super::*;
2 use crate::schema::user_;
3 use crate::schema::user_::dsl::*;
4 use crate::{is_email_regex, naive_now, Settings};
5 use bcrypt::{hash, DEFAULT_COST};
6 use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
7
8 #[derive(Queryable, Identifiable, PartialEq, Debug)]
9 #[table_name = "user_"]
10 pub struct User_ {
11   pub id: i32,
12   pub name: String,
13   pub preferred_username: Option<String>,
14   pub password_encrypted: String,
15   pub email: Option<String>,
16   pub avatar: Option<String>,
17   pub admin: bool,
18   pub banned: bool,
19   pub published: chrono::NaiveDateTime,
20   pub updated: Option<chrono::NaiveDateTime>,
21   pub show_nsfw: bool,
22   pub theme: String,
23   pub default_sort_type: i16,
24   pub default_listing_type: i16,
25   pub lang: String,
26   pub show_avatars: bool,
27   pub send_notifications_to_email: bool,
28   pub matrix_user_id: Option<String>,
29   pub actor_id: String,
30   pub bio: Option<String>,
31   pub local: bool,
32   pub private_key: Option<String>,
33   pub public_key: Option<String>,
34   pub last_refreshed_at: chrono::NaiveDateTime,
35 }
36
37 #[derive(Insertable, AsChangeset, Clone, Debug)]
38 #[table_name = "user_"]
39 pub struct UserForm {
40   pub name: String,
41   pub preferred_username: Option<String>,
42   pub password_encrypted: String,
43   pub admin: bool,
44   pub banned: bool,
45   pub email: Option<String>,
46   pub avatar: Option<String>,
47   pub updated: Option<chrono::NaiveDateTime>,
48   pub show_nsfw: bool,
49   pub theme: String,
50   pub default_sort_type: i16,
51   pub default_listing_type: i16,
52   pub lang: String,
53   pub show_avatars: bool,
54   pub send_notifications_to_email: bool,
55   pub matrix_user_id: Option<String>,
56   pub actor_id: String,
57   pub bio: Option<String>,
58   pub local: bool,
59   pub private_key: Option<String>,
60   pub public_key: Option<String>,
61   pub last_refreshed_at: Option<chrono::NaiveDateTime>,
62 }
63
64 impl Crud<UserForm> for User_ {
65   fn read(conn: &PgConnection, user_id: i32) -> Result<Self, Error> {
66     user_.find(user_id).first::<Self>(conn)
67   }
68   fn delete(conn: &PgConnection, user_id: i32) -> Result<usize, Error> {
69     diesel::delete(user_.find(user_id)).execute(conn)
70   }
71   fn create(conn: &PgConnection, form: &UserForm) -> Result<Self, Error> {
72     insert_into(user_).values(form).get_result::<Self>(conn)
73   }
74   fn update(conn: &PgConnection, user_id: i32, form: &UserForm) -> Result<Self, Error> {
75     diesel::update(user_.find(user_id))
76       .set(form)
77       .get_result::<Self>(conn)
78   }
79 }
80
81 impl User_ {
82   pub fn register(conn: &PgConnection, form: &UserForm) -> Result<Self, Error> {
83     let mut edited_user = form.clone();
84     let password_hash =
85       hash(&form.password_encrypted, DEFAULT_COST).expect("Couldn't hash password");
86     edited_user.password_encrypted = password_hash;
87
88     Self::create(&conn, &edited_user)
89   }
90
91   // TODO do more individual updates like these
92   pub fn update_password(
93     conn: &PgConnection,
94     user_id: i32,
95     new_password: &str,
96   ) -> Result<Self, Error> {
97     let password_hash = hash(new_password, DEFAULT_COST).expect("Couldn't hash password");
98
99     diesel::update(user_.find(user_id))
100       .set((
101         password_encrypted.eq(password_hash),
102         updated.eq(naive_now()),
103       ))
104       .get_result::<Self>(conn)
105   }
106
107   pub fn read_from_name(conn: &PgConnection, from_user_name: String) -> Result<Self, Error> {
108     user_.filter(name.eq(from_user_name)).first::<Self>(conn)
109   }
110
111   pub fn add_admin(conn: &PgConnection, user_id: i32, added: bool) -> Result<Self, Error> {
112     diesel::update(user_.find(user_id))
113       .set(admin.eq(added))
114       .get_result::<Self>(conn)
115   }
116
117   pub fn ban_user(conn: &PgConnection, user_id: i32, ban: bool) -> Result<Self, Error> {
118     diesel::update(user_.find(user_id))
119       .set(banned.eq(ban))
120       .get_result::<Self>(conn)
121   }
122
123   pub fn read_from_apub_id(conn: &PgConnection, object_id: &str) -> Result<Self, Error> {
124     use crate::schema::user_::dsl::*;
125     user_.filter(actor_id.eq(object_id)).first::<Self>(conn)
126   }
127 }
128
129 #[derive(Debug, Serialize, Deserialize)]
130 pub struct Claims {
131   pub id: i32,
132   pub username: String,
133   pub iss: String,
134   pub show_nsfw: bool,
135   pub theme: String,
136   pub default_sort_type: i16,
137   pub default_listing_type: i16,
138   pub lang: String,
139   pub avatar: Option<String>,
140   pub show_avatars: bool,
141 }
142
143 impl Claims {
144   pub fn decode(jwt: &str) -> Result<TokenData<Claims>, jsonwebtoken::errors::Error> {
145     let v = Validation {
146       validate_exp: false,
147       ..Validation::default()
148     };
149     decode::<Claims>(
150       &jwt,
151       &DecodingKey::from_secret(Settings::get().jwt_secret.as_ref()),
152       &v,
153     )
154   }
155 }
156
157 type Jwt = String;
158 impl User_ {
159   pub fn jwt(&self) -> Jwt {
160     let my_claims = Claims {
161       id: self.id,
162       username: self.name.to_owned(),
163       iss: Settings::get().hostname.to_owned(),
164       show_nsfw: self.show_nsfw,
165       theme: self.theme.to_owned(),
166       default_sort_type: self.default_sort_type,
167       default_listing_type: self.default_listing_type,
168       lang: self.lang.to_owned(),
169       avatar: self.avatar.to_owned(),
170       show_avatars: self.show_avatars.to_owned(),
171     };
172     encode(
173       &Header::default(),
174       &my_claims,
175       &EncodingKey::from_secret(Settings::get().jwt_secret.as_ref()),
176     )
177     .unwrap()
178   }
179
180   pub fn find_by_username(conn: &PgConnection, username: &str) -> Result<Self, Error> {
181     user_.filter(name.eq(username)).first::<User_>(conn)
182   }
183
184   pub fn find_by_email(conn: &PgConnection, from_email: &str) -> Result<Self, Error> {
185     user_.filter(email.eq(from_email)).first::<User_>(conn)
186   }
187
188   pub fn find_by_email_or_username(
189     conn: &PgConnection,
190     username_or_email: &str,
191   ) -> Result<Self, Error> {
192     if is_email_regex(username_or_email) {
193       User_::find_by_email(conn, username_or_email)
194     } else {
195       User_::find_by_username(conn, username_or_email)
196     }
197   }
198
199   pub fn get_profile_url(&self) -> String {
200     format!("https://{}/u/{}", Settings::get().hostname, self.name)
201   }
202
203   pub fn find_by_jwt(conn: &PgConnection, jwt: &str) -> Result<Self, Error> {
204     let claims: Claims = Claims::decode(&jwt).expect("Invalid token").claims;
205     Self::read(&conn, claims.id)
206   }
207 }
208
209 #[cfg(test)]
210 mod tests {
211   use super::User_;
212   use super::*;
213
214   #[test]
215   fn test_crud() {
216     let conn = establish_unpooled_connection();
217
218     let new_user = UserForm {
219       name: "thommy".into(),
220       preferred_username: None,
221       password_encrypted: "nope".into(),
222       email: None,
223       matrix_user_id: None,
224       avatar: None,
225       admin: false,
226       banned: false,
227       updated: None,
228       show_nsfw: false,
229       theme: "darkly".into(),
230       default_sort_type: SortType::Hot as i16,
231       default_listing_type: ListingType::Subscribed as i16,
232       lang: "browser".into(),
233       show_avatars: true,
234       send_notifications_to_email: false,
235       actor_id: "changeme".into(),
236       bio: None,
237       local: true,
238       private_key: None,
239       public_key: None,
240       last_refreshed_at: None,
241     };
242
243     let inserted_user = User_::create(&conn, &new_user).unwrap();
244
245     let expected_user = User_ {
246       id: inserted_user.id,
247       name: "thommy".into(),
248       preferred_username: None,
249       password_encrypted: "nope".into(),
250       email: None,
251       matrix_user_id: None,
252       avatar: None,
253       admin: false,
254       banned: false,
255       published: inserted_user.published,
256       updated: None,
257       show_nsfw: false,
258       theme: "darkly".into(),
259       default_sort_type: SortType::Hot as i16,
260       default_listing_type: ListingType::Subscribed as i16,
261       lang: "browser".into(),
262       show_avatars: true,
263       send_notifications_to_email: false,
264       actor_id: "changeme".into(),
265       bio: None,
266       local: true,
267       private_key: None,
268       public_key: None,
269       last_refreshed_at: inserted_user.published,
270     };
271
272     let read_user = User_::read(&conn, inserted_user.id).unwrap();
273     let updated_user = User_::update(&conn, inserted_user.id, &new_user).unwrap();
274     let num_deleted = User_::delete(&conn, inserted_user.id).unwrap();
275
276     assert_eq!(expected_user, read_user);
277     assert_eq!(expected_user, inserted_user);
278     assert_eq!(expected_user, updated_user);
279     assert_eq!(1, num_deleted);
280   }
281 }