1use rusqlite::{Connection, params};
4
5use crate::auth::{self, Role};
6
7#[derive(Debug, Clone)]
12pub struct UserRow {
13 pub id: i64,
14 pub username: String,
15 pub password_hash: String,
16 pub role: Role,
17 pub created_at: Option<String>,
18}
19
20#[derive(Debug, Clone)]
21pub struct RefreshTokenRow {
22 pub id: String,
23 pub user_id: i64,
24 pub expires_at: i64,
25 pub revoked: bool,
26 pub created_at: Option<String>,
27 pub grant: Option<OAuthGrant>,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct OAuthGrant {
34 pub id: String,
35 pub client_name: String,
36}
37
38const TOKEN_COLUMNS: &str = "id, user_id, expires_at, revoked, created_at, grant_id, client_name";
39
40fn token_row(row: &rusqlite::Row) -> rusqlite::Result<RefreshTokenRow> {
41 let grant_id: Option<String> = row.get(5)?;
42 let client_name: Option<String> = row.get(6)?;
43 Ok(RefreshTokenRow {
44 id: row.get(0)?,
45 user_id: row.get(1)?,
46 expires_at: row.get(2)?,
47 revoked: row.get::<_, i32>(3)? != 0,
48 created_at: row.get(4)?,
49 grant: grant_id.map(|id| OAuthGrant {
50 id,
51 client_name: client_name.unwrap_or_default(),
52 }),
53 })
54}
55
56pub const LOCAL_USER: i64 = 0;
68
69pub fn first_admin(conn: &Connection) -> Result<Option<i64>, rusqlite::Error> {
71 conn.prepare_cached("SELECT MIN(id) FROM users WHERE role = 'admin'")?
72 .query_row([], |r| r.get(0))
73}
74
75pub fn resolve_user(conn: &Connection, user: i64) -> Result<i64, rusqlite::Error> {
78 if user != LOCAL_USER {
79 return Ok(user);
80 }
81 Ok(first_admin(conn)?.unwrap_or(LOCAL_USER))
82}
83
84pub fn is_local_user(conn: &Connection, user: i64) -> Result<bool, rusqlite::Error> {
87 Ok(resolve_user(conn, user)? == resolve_user(conn, LOCAL_USER)?)
88}
89
90pub fn adopt_local_rows(conn: &Connection) -> Result<(), rusqlite::Error> {
94 let Some(admin) = first_admin(conn)? else {
95 return Ok(());
96 };
97 for table in [
98 "favourites",
99 "favourite_albums",
100 "favourite_artists",
101 "play_history",
102 "playlists",
103 "shares",
104 ] {
105 let pending: bool = conn.query_row(
108 &format!("SELECT EXISTS(SELECT 1 FROM {table} WHERE user_id = ?1)"),
109 params![LOCAL_USER],
110 |r| r.get(0),
111 )?;
112 if !pending {
113 continue;
114 }
115 conn.execute(
116 &format!("UPDATE OR IGNORE {table} SET user_id = ?1 WHERE user_id = ?2"),
117 params![admin, LOCAL_USER],
118 )?;
119 conn.execute(
120 &format!("DELETE FROM {table} WHERE user_id = ?1"),
121 params![LOCAL_USER],
122 )?;
123 }
124 Ok(())
125}
126
127pub fn set_sealed_password(
133 conn: &Connection,
134 username: &str,
135 sealed: &[u8],
136) -> Result<(), rusqlite::Error> {
137 conn.execute(
138 "UPDATE users SET sealed_password = ?2 WHERE username = ?1",
139 params![username, sealed],
140 )?;
141 Ok(())
142}
143
144pub fn sealed_password(
145 conn: &Connection,
146 username: &str,
147) -> Result<Option<Vec<u8>>, rusqlite::Error> {
148 use rusqlite::OptionalExtension;
149 Ok(conn
150 .query_row(
151 "SELECT sealed_password FROM users WHERE username = ?1",
152 params![username],
153 |r| r.get::<_, Option<Vec<u8>>>(0),
154 )
155 .optional()?
156 .flatten())
157}
158
159pub fn remember_password(
162 conn: &Connection,
163 username: &str,
164 password: &str,
165) -> Result<(), Box<dyn std::error::Error>> {
166 let key = auth::subsonic_key()?;
167 set_sealed_password(
168 conn,
169 username,
170 &auth::seal_password(&key, username, password)?,
171 )?;
172 Ok(())
173}
174
175pub fn create_user(
177 conn: &Connection,
178 username: &str,
179 password: &str,
180 role: Role,
181) -> Result<i64, rusqlite::Error> {
182 let hash = auth::hash_password(password)
183 .map_err(|e| rusqlite::Error::ToSqlConversionFailure(e.into()))?;
184 conn.execute(
185 "INSERT INTO users (username, password_hash, role) VALUES (?1, ?2, ?3)",
186 params![username, hash, role.as_str()],
187 )?;
188 let id = conn.last_insert_rowid();
189 adopt_local_rows(conn)?;
190 Ok(id)
191}
192
193pub fn get_user_by_username(
195 conn: &Connection,
196 username: &str,
197) -> Result<Option<UserRow>, rusqlite::Error> {
198 let mut stmt = conn.prepare_cached(
199 "SELECT id, username, password_hash, role, created_at FROM users WHERE username = ?1",
200 )?;
201 let mut rows = stmt.query_map(params![username], |row| {
202 let role_str: String = row.get(3)?;
203 Ok(UserRow {
204 id: row.get(0)?,
205 username: row.get(1)?,
206 password_hash: row.get(2)?,
207 role: role_str.parse().unwrap_or(Role::Readonly),
208 created_at: row.get(4)?,
209 })
210 })?;
211 match rows.next() {
212 Some(Ok(user)) => Ok(Some(user)),
213 Some(Err(e)) => Err(e),
214 None => Ok(None),
215 }
216}
217
218pub fn get_user_by_id(conn: &Connection, user_id: i64) -> Result<Option<UserRow>, rusqlite::Error> {
220 let mut stmt = conn
221 .prepare("SELECT id, username, password_hash, role, created_at FROM users WHERE id = ?1")?;
222 let mut rows = stmt.query_map(params![user_id], |row| {
223 let role_str: String = row.get(3)?;
224 Ok(UserRow {
225 id: row.get(0)?,
226 username: row.get(1)?,
227 password_hash: row.get(2)?,
228 role: role_str.parse().unwrap_or(Role::Readonly),
229 created_at: row.get(4)?,
230 })
231 })?;
232 match rows.next() {
233 Some(Ok(user)) => Ok(Some(user)),
234 Some(Err(e)) => Err(e),
235 None => Ok(None),
236 }
237}
238
239pub fn list_users(conn: &Connection) -> Result<Vec<UserRow>, rusqlite::Error> {
241 let mut stmt = conn
242 .prepare("SELECT id, username, password_hash, role, created_at FROM users ORDER BY id")?;
243 let rows = stmt.query_map([], |row| {
244 let role_str: String = row.get(3)?;
245 Ok(UserRow {
246 id: row.get(0)?,
247 username: row.get(1)?,
248 password_hash: row.get(2)?,
249 role: role_str.parse().unwrap_or(Role::Readonly),
250 created_at: row.get(4)?,
251 })
252 })?;
253 rows.collect()
254}
255
256pub fn delete_user(conn: &Connection, user_id: i64) -> Result<bool, rusqlite::Error> {
258 let count = conn.execute("DELETE FROM users WHERE id = ?1", params![user_id])?;
259 Ok(count > 0)
260}
261
262pub fn update_password(
266 conn: &Connection,
267 username: &str,
268 new_password: &str,
269) -> Result<bool, Box<dyn std::error::Error>> {
270 let hash = crate::auth::hash_password(new_password)?;
271 let updated = conn.execute(
272 "UPDATE users SET password_hash = ?1 WHERE username = ?2",
273 params![hash, username],
274 )?;
275 if updated > 0 {
276 if let Some(user) = get_user_by_username(conn, username)? {
278 revoke_all_user_tokens(conn, user.id)?;
279 super::api_keys::revoke_user_api_keys(conn, user.id)?;
280 }
281 }
282 Ok(updated > 0)
283}
284
285pub fn update_role(
287 conn: &Connection,
288 username: &str,
289 role: crate::auth::Role,
290) -> Result<bool, rusqlite::Error> {
291 let updated = conn.execute(
292 "UPDATE users SET role = ?1 WHERE username = ?2",
293 params![role.as_str(), username],
294 )?;
295 adopt_local_rows(conn)?;
296 Ok(updated > 0)
297}
298
299pub fn has_users(conn: &Connection) -> Result<bool, rusqlite::Error> {
301 let count: i64 = conn.query_row("SELECT COUNT(*) FROM users", [], |row| row.get(0))?;
302 Ok(count > 0)
303}
304
305pub fn admin_count(conn: &Connection) -> Result<i64, rusqlite::Error> {
307 conn.query_row(
308 "SELECT COUNT(*) FROM users WHERE role = 'admin'",
309 [],
310 |row| row.get(0),
311 )
312}
313
314pub fn store_refresh_token(
321 conn: &Connection,
322 token_id: &str,
323 user_id: i64,
324 expires_at: i64,
325) -> Result<(), rusqlite::Error> {
326 store_grant_token(conn, token_id, user_id, expires_at, None)
327}
328
329pub fn store_grant_token(
331 conn: &Connection,
332 token_id: &str,
333 user_id: i64,
334 expires_at: i64,
335 grant: Option<&OAuthGrant>,
336) -> Result<(), rusqlite::Error> {
337 conn.execute(
338 "INSERT INTO refresh_tokens (id, user_id, expires_at, grant_id, client_name)
339 VALUES (?1, ?2, ?3, ?4, ?5)",
340 params![
341 auth::sha256_hex(token_id),
342 user_id,
343 expires_at,
344 grant.map(|g| &g.id),
345 grant.map(|g| &g.client_name),
346 ],
347 )?;
348 Ok(())
349}
350
351pub fn get_valid_refresh_token(
353 conn: &Connection,
354 token_id: &str,
355) -> Result<Option<RefreshTokenRow>, rusqlite::Error> {
356 let now = auth::now_unix() as i64;
357 let mut stmt = conn.prepare_cached(&format!(
358 "SELECT {TOKEN_COLUMNS} FROM refresh_tokens
359 WHERE id = ?1 AND revoked = 0 AND expires_at > ?2"
360 ))?;
361 let mut rows = stmt.query_map(params![auth::sha256_hex(token_id), now], token_row)?;
362 match rows.next() {
363 Some(Ok(token)) => Ok(Some(token)),
364 Some(Err(e)) => Err(e),
365 None => Ok(None),
366 }
367}
368
369pub fn consume_refresh_token(
373 conn: &Connection,
374 token_id: &str,
375) -> Result<Option<RefreshTokenRow>, rusqlite::Error> {
376 let now = auth::now_unix() as i64;
377 let mut stmt = conn.prepare(&format!(
378 "UPDATE refresh_tokens SET revoked = 1, used_at = ?2
379 WHERE id = ?1 AND revoked = 0 AND expires_at > ?2
380 RETURNING {TOKEN_COLUMNS}"
381 ))?;
382 let mut rows = stmt.query_map(params![auth::sha256_hex(token_id), now], token_row)?;
383 match rows.next() {
384 Some(Ok(token)) => Ok(Some(token)),
385 Some(Err(e)) => Err(e),
386 None => Ok(None),
387 }
388}
389
390pub fn revoke_refresh_token(conn: &Connection, token_id: &str) -> Result<bool, rusqlite::Error> {
392 let count = conn.execute(
393 "UPDATE refresh_tokens SET revoked = 1 WHERE id = ?1",
394 params![auth::sha256_hex(token_id)],
395 )?;
396 Ok(count > 0)
397}
398
399pub fn revoke_all_user_tokens(conn: &Connection, user_id: i64) -> Result<usize, rusqlite::Error> {
401 let count = conn.execute(
402 "UPDATE refresh_tokens SET revoked = 1 WHERE user_id = ?1 AND revoked = 0",
403 params![user_id],
404 )?;
405 Ok(count)
406}
407
408pub fn revoke_replayed_grant(
416 conn: &Connection,
417 token_id: &str,
418 grace_secs: i64,
419) -> Result<usize, rusqlite::Error> {
420 let cutoff = auth::now_unix() as i64 - grace_secs;
421 conn.execute(
422 "UPDATE refresh_tokens SET revoked = 1
423 WHERE revoked = 0 AND grant_id = (
424 SELECT grant_id FROM refresh_tokens
425 WHERE id = ?1 AND revoked = 1 AND grant_id IS NOT NULL AND used_at < ?2)",
426 params![auth::sha256_hex(token_id), cutoff],
427 )
428}
429
430pub fn revoke_grant(conn: &Connection, grant_id: &str) -> Result<usize, rusqlite::Error> {
432 conn.execute(
433 "UPDATE refresh_tokens SET revoked = 1 WHERE grant_id = ?1 AND revoked = 0",
434 params![grant_id],
435 )
436}
437
438pub fn cleanup_expired_tokens(conn: &Connection) -> Result<usize, rusqlite::Error> {
442 let now = auth::now_unix() as i64;
443 let count = conn.execute(
444 "DELETE FROM refresh_tokens
445 WHERE expires_at <= ?1 OR (revoked = 1 AND (grant_id IS NULL OR used_at IS NULL))",
446 params![now],
447 )?;
448 Ok(count)
449}
450
451#[cfg(test)]
456mod tests {
457 use super::*;
458 use crate::db::connection::Database;
459 use tempfile::TempDir;
460
461 fn test_db() -> (Database, TempDir) {
462 let tmp = TempDir::new().unwrap();
463 let db_path = tmp.path().join("test.db");
464 let db = Database::open(&db_path).unwrap();
465 (db, tmp)
466 }
467
468 #[test]
469 fn create_and_get_user() {
470 let (db, _tmp) = test_db();
471 let id = create_user(&db.conn, "alice", "password123", Role::Admin).unwrap();
472 assert!(id > 0);
473
474 let user = get_user_by_username(&db.conn, "alice").unwrap().unwrap();
475 assert_eq!(user.username, "alice");
476 assert_eq!(user.role, Role::Admin);
477 assert!(user.password_hash.starts_with("$argon2"));
478 }
479
480 #[test]
481 fn duplicate_username_rejected() {
482 let (db, _tmp) = test_db();
483 create_user(&db.conn, "bob", "pass1", Role::User).unwrap();
484 let result = create_user(&db.conn, "bob", "pass2", Role::User);
485 assert!(result.is_err());
486 }
487
488 #[test]
489 fn list_and_delete_users() {
490 let (db, _tmp) = test_db();
491 let id1 = create_user(&db.conn, "user1", "pass", Role::Admin).unwrap();
492 create_user(&db.conn, "user2", "pass", Role::User).unwrap();
493
494 let users = list_users(&db.conn).unwrap();
495 assert_eq!(users.len(), 2);
496
497 assert!(delete_user(&db.conn, id1).unwrap());
498 let users = list_users(&db.conn).unwrap();
499 assert_eq!(users.len(), 1);
500 assert_eq!(users[0].username, "user2");
501 }
502
503 #[test]
504 fn has_users_empty_and_populated() {
505 let (db, _tmp) = test_db();
506 assert!(!has_users(&db.conn).unwrap());
507 create_user(&db.conn, "first", "pass", Role::Admin).unwrap();
508 assert!(has_users(&db.conn).unwrap());
509 }
510
511 #[test]
512 fn refresh_token_lifecycle() {
513 let (db, _tmp) = test_db();
514 let uid = create_user(&db.conn, "user", "pass", Role::User).unwrap();
515
516 let future_ts = auth::now_unix() as i64 + 86400;
517 store_refresh_token(&db.conn, "tok-123", uid, future_ts).unwrap();
518
519 let tok = get_valid_refresh_token(&db.conn, "tok-123")
521 .unwrap()
522 .unwrap();
523 assert_eq!(tok.user_id, uid);
524
525 assert!(revoke_refresh_token(&db.conn, "tok-123").unwrap());
527 assert!(
528 get_valid_refresh_token(&db.conn, "tok-123")
529 .unwrap()
530 .is_none()
531 );
532 }
533
534 #[test]
535 fn expired_token_not_returned() {
536 let (db, _tmp) = test_db();
537 let uid = create_user(&db.conn, "user", "pass", Role::User).unwrap();
538
539 store_refresh_token(&db.conn, "tok-old", uid, 0).unwrap();
541 assert!(
542 get_valid_refresh_token(&db.conn, "tok-old")
543 .unwrap()
544 .is_none()
545 );
546 }
547
548 #[test]
549 fn a_spent_grant_token_coming_back_revokes_the_grant() {
550 let (db, _tmp) = test_db();
551 let uid = create_user(&db.conn, "user", "pass", Role::User).unwrap();
552 let future = auth::now_unix() as i64 + 86400;
553 let grant = OAuthGrant {
554 id: "g1".into(),
555 client_name: "Claude".into(),
556 };
557 store_grant_token(&db.conn, "first", uid, future, Some(&grant)).unwrap();
558 let spent = consume_refresh_token(&db.conn, "first").unwrap().unwrap();
559 assert_eq!(spent.grant.as_ref(), Some(&grant));
560 store_grant_token(&db.conn, "second", uid, future, Some(&grant)).unwrap();
561 store_refresh_token(&db.conn, "app", uid, future).unwrap();
563 consume_refresh_token(&db.conn, "app").unwrap().unwrap();
564
565 assert_eq!(revoke_replayed_grant(&db.conn, "first", 30).unwrap(), 0);
567 assert_eq!(revoke_replayed_grant(&db.conn, "app", -1).unwrap(), 0);
568 cleanup_expired_tokens(&db.conn).unwrap();
570 assert_eq!(revoke_replayed_grant(&db.conn, "first", -1).unwrap(), 1);
571 assert!(
572 get_valid_refresh_token(&db.conn, "second")
573 .unwrap()
574 .is_none()
575 );
576 }
577
578 #[test]
579 fn cleanup_removes_expired_and_revoked() {
580 let (db, _tmp) = test_db();
581 let uid = create_user(&db.conn, "user", "pass", Role::User).unwrap();
582
583 let future = auth::now_unix() as i64 + 86400;
584 store_refresh_token(&db.conn, "active", uid, future).unwrap();
585 store_refresh_token(&db.conn, "expired", uid, 0).unwrap();
586 store_refresh_token(&db.conn, "revoked", uid, future).unwrap();
587 revoke_refresh_token(&db.conn, "revoked").unwrap();
588
589 let cleaned = cleanup_expired_tokens(&db.conn).unwrap();
590 assert_eq!(cleaned, 2);
591
592 assert!(
594 get_valid_refresh_token(&db.conn, "active")
595 .unwrap()
596 .is_some()
597 );
598 }
599
600 use crate::db::queries::{self, sample_meta, upsert_track};
603 use std::path::Path;
604
605 fn count(db: &Database, sql: &str) -> i64 {
606 db.conn.query_row(sql, [], |r| r.get(0)).unwrap()
607 }
608
609 #[test]
610 fn two_users_star_the_same_track_independently() {
611 let (db, _tmp) = test_db();
612 let admin = create_user(&db.conn, "owner", "pw", Role::Admin).unwrap();
613 let mate = create_user(&db.conn, "mate", "pw", Role::User).unwrap();
614 let path = Path::new("/music/a.flac");
615
616 queries::add_favourite(&db.conn, admin, path).unwrap();
617 queries::add_favourite(&db.conn, mate, path).unwrap();
618 queries::remove_favourite(&db.conn, admin, path).unwrap();
619
620 assert!(
621 queries::load_favourites(&db.conn, admin)
622 .unwrap()
623 .is_empty()
624 );
625 assert!(
626 queries::load_favourites(&db.conn, mate)
627 .unwrap()
628 .contains(path)
629 );
630 assert!(queries::toggle_favourite_album(&db.conn, mate, "Coil", "Scatology").unwrap());
631 assert!(queries::toggle_favourite_album(&db.conn, admin, "Coil", "Scatology").unwrap());
632 assert_eq!(count(&db, "SELECT COUNT(*) FROM favourite_albums"), 2);
633 }
634
635 #[test]
636 fn the_local_user_is_the_first_admin_once_there_is_one() {
637 let (db, _tmp) = test_db();
638 let path = Path::new("/music/a.flac");
639 let track = upsert_track(&db.conn, &sample_meta("T", "A", "B")).unwrap();
640 queries::add_favourite(&db.conn, LOCAL_USER, path).unwrap();
641 queries::record_play(&db.conn, LOCAL_USER, track, None).unwrap();
642 let list = queries::create_playlist(&db.conn, LOCAL_USER, "Mine", None).unwrap();
643 assert_eq!(resolve_user(&db.conn, LOCAL_USER).unwrap(), LOCAL_USER);
644
645 create_user(&db.conn, "mate", "pw", Role::User).unwrap();
646 assert_eq!(resolve_user(&db.conn, LOCAL_USER).unwrap(), LOCAL_USER);
647 let admin = create_user(&db.conn, "owner", "pw", Role::Admin).unwrap();
648
649 assert_eq!(resolve_user(&db.conn, LOCAL_USER).unwrap(), admin);
650 assert!(
651 queries::load_favourites(&db.conn, admin)
652 .unwrap()
653 .contains(path)
654 );
655 assert_eq!(queries::play_count(&db.conn, admin, track).unwrap(), 1);
656 assert_eq!(
657 queries::get_playlist(&db.conn, list)
658 .unwrap()
659 .unwrap()
660 .user_id,
661 admin
662 );
663 assert_eq!(
664 count(&db, "SELECT COUNT(*) FROM favourites WHERE user_id = 0"),
665 0
666 );
667 }
668
669 #[test]
670 fn playlists_are_the_owners_plus_everyones_public_ones() {
671 let (db, _tmp) = test_db();
672 let admin = create_user(&db.conn, "owner", "pw", Role::Admin).unwrap();
673 let mate = create_user(&db.conn, "mate", "pw", Role::User).unwrap();
674 let private = queries::create_playlist(&db.conn, admin, "Private", None).unwrap();
675 let public = queries::create_playlist(&db.conn, admin, "Public", None).unwrap();
676 db.conn
677 .execute("UPDATE playlists SET public = 1 WHERE id = ?1", [public])
678 .unwrap();
679 let own = queries::create_playlist(&db.conn, mate, "Mate's", None).unwrap();
680
681 let ids = |user| -> Vec<i64> {
682 let mut ids: Vec<i64> = queries::list_playlists(&db.conn, user)
683 .unwrap()
684 .into_iter()
685 .map(|p| p.id)
686 .collect();
687 ids.sort_unstable();
688 ids
689 };
690 assert_eq!(ids(mate), vec![public, own]);
691 assert_eq!(ids(admin), vec![private, public]);
692 assert_eq!(ids(LOCAL_USER), vec![private, public]);
694
695 let row = queries::get_playlist(&db.conn, public).unwrap().unwrap();
696 assert!(row.readable_by(mate) && !row.editable_by(mate));
697 assert_eq!(row.owner.as_deref(), Some("owner"));
698 let row = queries::get_playlist(&db.conn, private).unwrap().unwrap();
699 assert!(!row.readable_by(mate));
700 }
701
702 #[test]
703 fn deleting_an_account_takes_its_data_with_it() {
704 let (db, _tmp) = test_db();
705 let admin = create_user(&db.conn, "owner", "pw", Role::Admin).unwrap();
706 let mate = create_user(&db.conn, "mate", "pw", Role::User).unwrap();
707 let track = upsert_track(&db.conn, &sample_meta("T", "A", "B")).unwrap();
708 for user in [admin, mate] {
709 queries::add_favourite(&db.conn, user, Path::new("/music/a.flac")).unwrap();
710 queries::set_favourite_album(&db.conn, user, "A", "B", true).unwrap();
711 queries::set_favourite_artist(&db.conn, user, "A", true).unwrap();
712 queries::record_play(&db.conn, user, track, None).unwrap();
713 queries::create_playlist(&db.conn, user, "List", None).unwrap();
714 queries::shares::create_share(
715 &db.conn,
716 user,
717 queries::shares::Slice::TRACKS,
718 &[track],
719 None,
720 0,
721 None,
722 )
723 .unwrap();
724 }
725
726 assert!(delete_user(&db.conn, mate).unwrap());
727
728 for table in [
729 "favourites",
730 "favourite_albums",
731 "favourite_artists",
732 "play_history",
733 "playlists",
734 "shares",
735 ] {
736 assert_eq!(
737 count(
738 &db,
739 &format!("SELECT COUNT(*) FROM {table} WHERE user_id = {mate}")
740 ),
741 0,
742 "{table} kept the deleted account's rows"
743 );
744 assert_eq!(
745 count(
746 &db,
747 &format!("SELECT COUNT(*) FROM {table} WHERE user_id = {admin}")
748 ),
749 1,
750 "{table} lost another account's rows"
751 );
752 }
753 }
754}