A very performant and light (2mb in memory) link shortener and tracker. Written in Rust and React and uses Postgres/SQLite.
at master 27 kB view raw
1use crate::auth::AuthenticatedUser; 2use crate::{ 3 error::AppError, 4 models::{ 5 AuthResponse, Claims, ClickStats, CreateLink, DatabasePool, Link, LoginRequest, 6 RegisterRequest, SourceStats, User, UserResponse, 7 }, 8 AppState, 9}; 10use actix_web::{web, HttpRequest, HttpResponse, Responder}; 11use argon2::{ 12 password_hash::{rand_core::OsRng, SaltString}, 13 PasswordVerifier, 14}; 15use argon2::{Argon2, PasswordHash, PasswordHasher}; 16use jsonwebtoken::{encode, EncodingKey, Header}; 17use lazy_static::lazy_static; 18use regex::Regex; 19use serde_json::json; 20use sqlx::{Postgres, Sqlite}; 21 22lazy_static! { 23 static ref VALID_CODE_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9_-]{1,32}$").unwrap(); 24} 25 26pub async fn create_short_url( 27 state: web::Data<AppState>, 28 user: AuthenticatedUser, 29 payload: web::Json<CreateLink>, 30) -> Result<impl Responder, AppError> { 31 tracing::debug!("Creating short URL with user_id: {}", user.user_id); 32 validate_url(&payload.url)?; 33 34 let short_code = if let Some(ref custom_code) = payload.custom_code { 35 validate_custom_code(custom_code)?; 36 37 // Check if code exists using match on pool type 38 let exists = match &state.db { 39 DatabasePool::Postgres(pool) => { 40 sqlx::query_as::<_, Link>("SELECT * FROM links WHERE short_code = $1") 41 .bind(custom_code) 42 .fetch_optional(pool) 43 .await? 44 } 45 DatabasePool::Sqlite(pool) => { 46 sqlx::query_as::<_, Link>("SELECT * FROM links WHERE short_code = ?1") 47 .bind(custom_code) 48 .fetch_optional(pool) 49 .await? 50 } 51 }; 52 53 if exists.is_some() { 54 return Err(AppError::InvalidInput( 55 "Custom code already taken".to_string(), 56 )); 57 } 58 custom_code.clone() 59 } else { 60 generate_short_code() 61 }; 62 63 // Start transaction based on pool type 64 let result = match &state.db { 65 DatabasePool::Postgres(pool) => { 66 let mut tx = pool.begin().await?; 67 68 let link = sqlx::query_as::<_, Link>( 69 "INSERT INTO links (original_url, short_code, user_id) VALUES ($1, $2, $3) RETURNING *" 70 ) 71 .bind(&payload.url) 72 .bind(&short_code) 73 .bind(user.user_id) 74 .fetch_one(&mut *tx) 75 .await?; 76 77 if let Some(ref source) = payload.source { 78 sqlx::query("INSERT INTO clicks (link_id, source) VALUES ($1, $2)") 79 .bind(link.id) 80 .bind(source) 81 .execute(&mut *tx) 82 .await?; 83 } 84 85 tx.commit().await?; 86 link 87 } 88 DatabasePool::Sqlite(pool) => { 89 let mut tx = pool.begin().await?; 90 91 let link = sqlx::query_as::<_, Link>( 92 "INSERT INTO links (original_url, short_code, user_id) VALUES (?1, ?2, ?3) RETURNING *" 93 ) 94 .bind(&payload.url) 95 .bind(&short_code) 96 .bind(user.user_id) 97 .fetch_one(&mut *tx) 98 .await?; 99 100 if let Some(ref source) = payload.source { 101 sqlx::query("INSERT INTO clicks (link_id, source) VALUES (?1, ?2)") 102 .bind(link.id) 103 .bind(source) 104 .execute(&mut *tx) 105 .await?; 106 } 107 108 tx.commit().await?; 109 link 110 } 111 }; 112 113 Ok(HttpResponse::Created().json(result)) 114} 115 116fn validate_custom_code(code: &str) -> Result<(), AppError> { 117 if !VALID_CODE_REGEX.is_match(code) { 118 return Err(AppError::InvalidInput( 119 "Custom code must be 1-32 characters long and contain only letters, numbers, underscores, and hyphens".to_string() 120 )); 121 } 122 123 // Add reserved words check 124 let reserved_words = ["api", "health", "admin", "static", "assets"]; 125 if reserved_words.contains(&code.to_lowercase().as_str()) { 126 return Err(AppError::InvalidInput( 127 "This code is reserved and cannot be used".to_string(), 128 )); 129 } 130 131 Ok(()) 132} 133 134fn validate_url(url: &str) -> Result<(), AppError> { 135 if url.is_empty() { 136 return Err(AppError::InvalidInput("URL cannot be empty".to_string())); 137 } 138 if !url.starts_with("http://") && !url.starts_with("https://") { 139 return Err(AppError::InvalidInput( 140 "URL must start with http:// or https://".to_string(), 141 )); 142 } 143 Ok(()) 144} 145 146pub async fn redirect_to_url( 147 state: web::Data<AppState>, 148 path: web::Path<String>, 149 req: HttpRequest, 150) -> Result<impl Responder, AppError> { 151 let short_code = path.into_inner(); 152 153 // Extract query source if present 154 let query_source = req 155 .uri() 156 .query() 157 .and_then(|q| web::Query::<std::collections::HashMap<String, String>>::from_query(q).ok()) 158 .and_then(|params| params.get("source").cloned()); 159 160 let link = match &state.db { 161 DatabasePool::Postgres(pool) => { 162 let mut tx = pool.begin().await?; 163 let link = sqlx::query_as::<_, Link>( 164 "UPDATE links SET clicks = clicks + 1 WHERE short_code = $1 RETURNING *", 165 ) 166 .bind(&short_code) 167 .fetch_optional(&mut *tx) 168 .await?; 169 tx.commit().await?; 170 link 171 } 172 DatabasePool::Sqlite(pool) => { 173 let mut tx = pool.begin().await?; 174 let link = sqlx::query_as::<_, Link>( 175 "UPDATE links SET clicks = clicks + 1 WHERE short_code = ?1 RETURNING *", 176 ) 177 .bind(&short_code) 178 .fetch_optional(&mut *tx) 179 .await?; 180 tx.commit().await?; 181 link 182 } 183 }; 184 185 match link { 186 Some(link) => { 187 // Handle click recording based on database type 188 match &state.db { 189 DatabasePool::Postgres(pool) => { 190 let mut tx = pool.begin().await?; 191 let user_agent = req 192 .headers() 193 .get("user-agent") 194 .and_then(|h| h.to_str().ok()) 195 .unwrap_or("unknown") 196 .to_string(); 197 198 sqlx::query( 199 "INSERT INTO clicks (link_id, source, query_source) VALUES ($1, $2, $3)", 200 ) 201 .bind(link.id) 202 .bind(user_agent) 203 .bind(query_source) 204 .execute(&mut *tx) 205 .await?; 206 207 tx.commit().await?; 208 } 209 DatabasePool::Sqlite(pool) => { 210 let mut tx = pool.begin().await?; 211 let user_agent = req 212 .headers() 213 .get("user-agent") 214 .and_then(|h| h.to_str().ok()) 215 .unwrap_or("unknown") 216 .to_string(); 217 218 sqlx::query( 219 "INSERT INTO clicks (link_id, source, query_source) VALUES (?1, ?2, ?3)", 220 ) 221 .bind(link.id) 222 .bind(user_agent) 223 .bind(query_source) 224 .execute(&mut *tx) 225 .await?; 226 227 tx.commit().await?; 228 } 229 }; 230 231 Ok(HttpResponse::TemporaryRedirect() 232 .append_header(("Location", link.original_url)) 233 .finish()) 234 } 235 None => Err(AppError::NotFound), 236 } 237} 238 239pub async fn get_all_links( 240 state: web::Data<AppState>, 241 user: AuthenticatedUser, 242) -> Result<impl Responder, AppError> { 243 let links = match &state.db { 244 DatabasePool::Postgres(pool) => { 245 sqlx::query_as::<_, Link>( 246 "SELECT * FROM links WHERE user_id = $1 ORDER BY created_at DESC", 247 ) 248 .bind(user.user_id) 249 .fetch_all(pool) 250 .await? 251 } 252 DatabasePool::Sqlite(pool) => { 253 sqlx::query_as::<_, Link>( 254 "SELECT * FROM links WHERE user_id = ?1 ORDER BY created_at DESC", 255 ) 256 .bind(user.user_id) 257 .fetch_all(pool) 258 .await? 259 } 260 }; 261 262 Ok(HttpResponse::Ok().json(links)) 263} 264 265pub async fn health_check(state: web::Data<AppState>) -> impl Responder { 266 let is_healthy = match &state.db { 267 DatabasePool::Postgres(pool) => sqlx::query("SELECT 1").execute(pool).await.is_ok(), 268 DatabasePool::Sqlite(pool) => sqlx::query("SELECT 1").execute(pool).await.is_ok(), 269 }; 270 271 if is_healthy { 272 HttpResponse::Ok().json("Healthy") 273 } else { 274 HttpResponse::ServiceUnavailable().json("Database unavailable") 275 } 276} 277 278fn generate_short_code() -> String { 279 use base62::encode; 280 use uuid::Uuid; 281 282 let uuid = Uuid::new_v4(); 283 encode(uuid.as_u128() as u64).chars().take(32).collect() 284} 285 286pub async fn register( 287 state: web::Data<AppState>, 288 payload: web::Json<RegisterRequest>, 289) -> Result<impl Responder, AppError> { 290 // Check if any users exist 291 let user_count = match &state.db { 292 DatabasePool::Postgres(pool) => { 293 let mut tx = pool.begin().await?; 294 let count = sqlx::query_as::<Postgres, (i64,)>("SELECT COUNT(*)::bigint FROM users") 295 .fetch_one(&mut *tx) 296 .await? 297 .0; 298 tx.commit().await?; 299 count 300 } 301 DatabasePool::Sqlite(pool) => { 302 let mut tx = pool.begin().await?; 303 let count = sqlx::query_as::<Sqlite, (i64,)>("SELECT COUNT(*) FROM users") 304 .fetch_one(&mut *tx) 305 .await? 306 .0; 307 tx.commit().await?; 308 count 309 } 310 }; 311 312 // If users exist, registration is closed - no exceptions 313 if user_count > 0 { 314 return Err(AppError::Auth("Registration is closed".to_string())); 315 } 316 317 // Verify admin token for first user 318 match (&state.admin_token, &payload.admin_token) { 319 (Some(stored_token), Some(provided_token)) if stored_token == provided_token => { 320 // Token matches, proceed with registration 321 } 322 _ => return Err(AppError::Auth("Invalid admin setup token".to_string())), 323 } 324 325 // Check if email already exists 326 let exists = match &state.db { 327 DatabasePool::Postgres(pool) => { 328 let mut tx = pool.begin().await?; 329 let exists = 330 sqlx::query_as::<Postgres, (i32,)>("SELECT id FROM users WHERE email = $1") 331 .bind(&payload.email) 332 .fetch_optional(&mut *tx) 333 .await?; 334 tx.commit().await?; 335 exists 336 } 337 DatabasePool::Sqlite(pool) => { 338 let mut tx = pool.begin().await?; 339 let exists = sqlx::query_as::<Sqlite, (i32,)>("SELECT id FROM users WHERE email = ?") 340 .bind(&payload.email) 341 .fetch_optional(&mut *tx) 342 .await?; 343 tx.commit().await?; 344 exists 345 } 346 }; 347 348 if exists.is_some() { 349 return Err(AppError::Auth("Email already registered".to_string())); 350 } 351 352 let salt = SaltString::generate(&mut OsRng); 353 let argon2 = Argon2::default(); 354 let password_hash = argon2 355 .hash_password(payload.password.as_bytes(), &salt) 356 .map_err(|e| AppError::Auth(e.to_string()))? 357 .to_string(); 358 359 // Insert new user 360 let user = match &state.db { 361 DatabasePool::Postgres(pool) => { 362 let mut tx = pool.begin().await?; 363 let user = sqlx::query_as::<Postgres, User>( 364 "INSERT INTO users (email, password_hash) VALUES ($1, $2) RETURNING *", 365 ) 366 .bind(&payload.email) 367 .bind(&password_hash) 368 .fetch_one(&mut *tx) 369 .await?; 370 tx.commit().await?; 371 user 372 } 373 DatabasePool::Sqlite(pool) => { 374 let mut tx = pool.begin().await?; 375 let user = sqlx::query_as::<Sqlite, User>( 376 "INSERT INTO users (email, password_hash) VALUES (?, ?) RETURNING *", 377 ) 378 .bind(&payload.email) 379 .bind(&password_hash) 380 .fetch_one(&mut *tx) 381 .await?; 382 tx.commit().await?; 383 user 384 } 385 }; 386 387 let claims = Claims::new(user.id); 388 let secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| "default_secret".to_string()); 389 let token = encode( 390 &Header::default(), 391 &claims, 392 &EncodingKey::from_secret(secret.as_bytes()), 393 ) 394 .map_err(|e| AppError::Auth(e.to_string()))?; 395 396 Ok(HttpResponse::Ok().json(AuthResponse { 397 token, 398 user: UserResponse { 399 id: user.id, 400 email: user.email, 401 }, 402 })) 403} 404 405pub async fn login( 406 state: web::Data<AppState>, 407 payload: web::Json<LoginRequest>, 408) -> Result<impl Responder, AppError> { 409 let user = match &state.db { 410 DatabasePool::Postgres(pool) => { 411 let mut tx = pool.begin().await?; 412 let user = sqlx::query_as::<Postgres, User>("SELECT * FROM users WHERE email = $1") 413 .bind(&payload.email) 414 .fetch_optional(&mut *tx) 415 .await?; 416 tx.commit().await?; 417 user 418 } 419 DatabasePool::Sqlite(pool) => { 420 let mut tx = pool.begin().await?; 421 let user = sqlx::query_as::<Sqlite, User>("SELECT * FROM users WHERE email = ?") 422 .bind(&payload.email) 423 .fetch_optional(&mut *tx) 424 .await?; 425 tx.commit().await?; 426 user 427 } 428 } 429 .ok_or_else(|| AppError::Auth("Invalid credentials".to_string()))?; 430 431 let argon2 = Argon2::default(); 432 let parsed_hash = 433 PasswordHash::new(&user.password_hash).map_err(|e| AppError::Auth(e.to_string()))?; 434 435 if argon2 436 .verify_password(payload.password.as_bytes(), &parsed_hash) 437 .is_err() 438 { 439 return Err(AppError::Auth("Invalid credentials".to_string())); 440 } 441 442 let claims = Claims::new(user.id); 443 let secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| "default_secret".to_string()); 444 let token = encode( 445 &Header::default(), 446 &claims, 447 &EncodingKey::from_secret(secret.as_bytes()), 448 ) 449 .map_err(|e| AppError::Auth(e.to_string()))?; 450 451 Ok(HttpResponse::Ok().json(AuthResponse { 452 token, 453 user: UserResponse { 454 id: user.id, 455 email: user.email, 456 }, 457 })) 458} 459 460pub async fn edit_link( 461 state: web::Data<AppState>, 462 user: AuthenticatedUser, 463 path: web::Path<i32>, 464 payload: web::Json<CreateLink>, 465) -> Result<impl Responder, AppError> { 466 let link_id: i32 = path.into_inner(); 467 468 // Validate the new URL if provided 469 validate_url(&payload.url)?; 470 471 // Validate custom code if provided 472 if let Some(ref custom_code) = payload.custom_code { 473 validate_custom_code(custom_code)?; 474 475 // Check if the custom code is already taken by another link 476 let existing_link = match &state.db { 477 DatabasePool::Postgres(pool) => { 478 sqlx::query_as::<_, Link>("SELECT * FROM links WHERE short_code = $1 AND id != $2") 479 .bind(custom_code) 480 .bind(link_id) 481 .fetch_optional(pool) 482 .await? 483 } 484 DatabasePool::Sqlite(pool) => { 485 sqlx::query_as::<_, Link>("SELECT * FROM links WHERE short_code = ?1 AND id != ?2") 486 .bind(custom_code) 487 .bind(link_id) 488 .fetch_optional(pool) 489 .await? 490 } 491 }; 492 493 if existing_link.is_some() { 494 return Err(AppError::InvalidInput( 495 "Custom code already taken".to_string(), 496 )); 497 } 498 } 499 500 // Update the link 501 let updated_link = match &state.db { 502 DatabasePool::Postgres(pool) => { 503 let mut tx = pool.begin().await?; 504 505 // First verify the link belongs to the user 506 let link = 507 sqlx::query_as::<_, Link>("SELECT * FROM links WHERE id = $1 AND user_id = $2") 508 .bind(link_id) 509 .bind(user.user_id) 510 .fetch_optional(&mut *tx) 511 .await?; 512 513 if link.is_none() { 514 return Err(AppError::NotFound); 515 } 516 517 // Update the link 518 let updated = sqlx::query_as::<_, Link>( 519 r#" 520 UPDATE links 521 SET 522 original_url = $1, 523 short_code = COALESCE($2, short_code) 524 WHERE id = $3 AND user_id = $4 525 RETURNING * 526 "#, 527 ) 528 .bind(&payload.url) 529 .bind(&payload.custom_code) 530 .bind(link_id) 531 .bind(user.user_id) 532 .fetch_one(&mut *tx) 533 .await?; 534 535 // If source is provided, add a click record 536 if let Some(ref source) = payload.source { 537 sqlx::query("INSERT INTO clicks (link_id, source) VALUES ($1, $2)") 538 .bind(link_id) 539 .bind(source) 540 .execute(&mut *tx) 541 .await?; 542 } 543 544 tx.commit().await?; 545 updated 546 } 547 DatabasePool::Sqlite(pool) => { 548 let mut tx = pool.begin().await?; 549 550 // First verify the link belongs to the user 551 let link = 552 sqlx::query_as::<_, Link>("SELECT * FROM links WHERE id = ?1 AND user_id = ?2") 553 .bind(link_id) 554 .bind(user.user_id) 555 .fetch_optional(&mut *tx) 556 .await?; 557 558 if link.is_none() { 559 return Err(AppError::NotFound); 560 } 561 562 // Update the link 563 let updated = sqlx::query_as::<_, Link>( 564 r#" 565 UPDATE links 566 SET 567 original_url = ?1, 568 short_code = COALESCE(?2, short_code) 569 WHERE id = ?3 AND user_id = ?4 570 RETURNING * 571 "#, 572 ) 573 .bind(&payload.url) 574 .bind(&payload.custom_code) 575 .bind(link_id) 576 .bind(user.user_id) 577 .fetch_one(&mut *tx) 578 .await?; 579 580 // If source is provided, add a click record 581 if let Some(ref source) = payload.source { 582 sqlx::query("INSERT INTO clicks (link_id, source) VALUES (?1, ?2)") 583 .bind(link_id) 584 .bind(source) 585 .execute(&mut *tx) 586 .await?; 587 } 588 589 tx.commit().await?; 590 updated 591 } 592 }; 593 594 Ok(HttpResponse::Ok().json(updated_link)) 595} 596 597pub async fn delete_link( 598 state: web::Data<AppState>, 599 user: AuthenticatedUser, 600 path: web::Path<i32>, 601) -> Result<impl Responder, AppError> { 602 let link_id: i32 = path.into_inner(); 603 604 match &state.db { 605 DatabasePool::Postgres(pool) => { 606 let mut tx = pool.begin().await?; 607 608 // Verify the link belongs to the user 609 let link = sqlx::query_as::<Postgres, (i32,)>( 610 "SELECT id FROM links WHERE id = $1 AND user_id = $2", 611 ) 612 .bind(link_id) 613 .bind(user.user_id) 614 .fetch_optional(&mut *tx) 615 .await?; 616 617 if link.is_none() { 618 return Err(AppError::NotFound); 619 } 620 621 // Delete associated clicks first due to foreign key constraint 622 sqlx::query("DELETE FROM clicks WHERE link_id = $1") 623 .bind(link_id) 624 .execute(&mut *tx) 625 .await?; 626 627 // Delete the link 628 sqlx::query("DELETE FROM links WHERE id = $1") 629 .bind(link_id) 630 .execute(&mut *tx) 631 .await?; 632 633 tx.commit().await?; 634 } 635 DatabasePool::Sqlite(pool) => { 636 let mut tx = pool.begin().await?; 637 638 // Verify the link belongs to the user 639 let link = sqlx::query_as::<Sqlite, (i32,)>( 640 "SELECT id FROM links WHERE id = ? AND user_id = ?", 641 ) 642 .bind(link_id) 643 .bind(user.user_id) 644 .fetch_optional(&mut *tx) 645 .await?; 646 647 if link.is_none() { 648 return Err(AppError::NotFound); 649 } 650 651 // Delete associated clicks first due to foreign key constraint 652 sqlx::query("DELETE FROM clicks WHERE link_id = ?") 653 .bind(link_id) 654 .execute(&mut *tx) 655 .await?; 656 657 // Delete the link 658 sqlx::query("DELETE FROM links WHERE id = ?") 659 .bind(link_id) 660 .execute(&mut *tx) 661 .await?; 662 663 tx.commit().await?; 664 } 665 } 666 667 Ok(HttpResponse::NoContent().finish()) 668} 669 670pub async fn get_link_clicks( 671 state: web::Data<AppState>, 672 user: AuthenticatedUser, 673 path: web::Path<i32>, 674) -> Result<impl Responder, AppError> { 675 let link_id = path.into_inner(); 676 677 // First verify the link belongs to the user 678 let link = match &state.db { 679 DatabasePool::Postgres(pool) => { 680 sqlx::query_as::<_, (i32,)>("SELECT id FROM links WHERE id = $1 AND user_id = $2") 681 .bind(link_id) 682 .bind(user.user_id) 683 .fetch_optional(pool) 684 .await? 685 } 686 DatabasePool::Sqlite(pool) => { 687 sqlx::query_as::<_, (i32,)>("SELECT id FROM links WHERE id = ? AND user_id = ?") 688 .bind(link_id) 689 .bind(user.user_id) 690 .fetch_optional(pool) 691 .await? 692 } 693 }; 694 695 if link.is_none() { 696 return Err(AppError::NotFound); 697 } 698 699 let clicks = match &state.db { 700 DatabasePool::Postgres(pool) => { 701 sqlx::query_as::<_, ClickStats>( 702 r#" 703 SELECT 704 DATE(created_at)::text as date, 705 COUNT(*)::bigint as clicks 706 FROM clicks 707 WHERE link_id = $1 708 GROUP BY DATE(created_at) 709 ORDER BY DATE(created_at) ASC 710 "#, 711 ) 712 .bind(link_id) 713 .fetch_all(pool) 714 .await? 715 } 716 DatabasePool::Sqlite(pool) => { 717 sqlx::query_as::<_, ClickStats>( 718 r#" 719 SELECT 720 DATE(created_at) as date, 721 COUNT(*) as clicks 722 FROM clicks 723 WHERE link_id = ? 724 GROUP BY DATE(created_at) 725 ORDER BY DATE(created_at) ASC 726 "#, 727 ) 728 .bind(link_id) 729 .fetch_all(pool) 730 .await? 731 } 732 }; 733 734 Ok(HttpResponse::Ok().json(clicks)) 735} 736 737pub async fn get_link_sources( 738 state: web::Data<AppState>, 739 user: AuthenticatedUser, 740 path: web::Path<i32>, 741) -> Result<impl Responder, AppError> { 742 let link_id = path.into_inner(); 743 744 // Verify the link belongs to the user 745 let link = match &state.db { 746 DatabasePool::Postgres(pool) => { 747 let mut tx = pool.begin().await?; 748 let link = sqlx::query_as::<Postgres, (i32,)>( 749 "SELECT id FROM links WHERE id = $1 AND user_id = $2", 750 ) 751 .bind(link_id) 752 .bind(user.user_id) 753 .fetch_optional(&mut *tx) 754 .await?; 755 tx.commit().await?; 756 link 757 } 758 DatabasePool::Sqlite(pool) => { 759 let mut tx = pool.begin().await?; 760 let link = sqlx::query_as::<Sqlite, (i32,)>( 761 "SELECT id FROM links WHERE id = ? AND user_id = ?", 762 ) 763 .bind(link_id) 764 .bind(user.user_id) 765 .fetch_optional(&mut *tx) 766 .await?; 767 tx.commit().await?; 768 link 769 } 770 }; 771 772 if link.is_none() { 773 return Err(AppError::NotFound); 774 } 775 776 let sources = match &state.db { 777 DatabasePool::Postgres(pool) => { 778 sqlx::query_as::<_, SourceStats>( 779 r#" 780 SELECT 781 DATE(created_at)::text as date, 782 query_source as source, 783 COUNT(*)::bigint as count 784 FROM clicks 785 WHERE link_id = $1 786 AND query_source IS NOT NULL 787 AND query_source != '' 788 GROUP BY DATE(created_at), query_source 789 ORDER BY DATE(created_at) ASC, COUNT(*) DESC 790 "#, 791 ) 792 .bind(link_id) 793 .fetch_all(pool) 794 .await? 795 } 796 DatabasePool::Sqlite(pool) => { 797 sqlx::query_as::<_, SourceStats>( 798 r#" 799 SELECT 800 DATE(created_at) as date, 801 query_source as source, 802 COUNT(*) as count 803 FROM clicks 804 WHERE link_id = ? 805 AND query_source IS NOT NULL 806 AND query_source != '' 807 GROUP BY DATE(created_at), query_source 808 ORDER BY DATE(created_at) ASC, COUNT(*) DESC 809 "#, 810 ) 811 .bind(link_id) 812 .fetch_all(pool) 813 .await? 814 } 815 }; 816 817 Ok(HttpResponse::Ok().json(sources)) 818} 819 820pub async fn check_first_user(state: web::Data<AppState>) -> Result<impl Responder, AppError> { 821 let user_count = match &state.db { 822 DatabasePool::Postgres(pool) => { 823 sqlx::query_as::<Postgres, (i64,)>("SELECT COUNT(*)::bigint FROM users") 824 .fetch_one(pool) 825 .await? 826 .0 827 } 828 DatabasePool::Sqlite(pool) => { 829 sqlx::query_as::<Sqlite, (i64,)>("SELECT COUNT(*) FROM users") 830 .fetch_one(pool) 831 .await? 832 .0 833 } 834 }; 835 836 Ok(HttpResponse::Ok().json(json!({ 837 "isFirstUser": user_count == 0 838 }))) 839}