feat!: add repo searching, only show most relevant repos first
This commit is contained in:
1
api/Cargo.lock
generated
1
api/Cargo.lock
generated
@@ -2476,6 +2476,7 @@ dependencies = [
|
||||
"rustls-platform-verifier",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
|
||||
@@ -7,7 +7,7 @@ edition = "2024"
|
||||
actix-web = "4.12.1"
|
||||
chrono = { version = "0.4.43", features = ["serde"] }
|
||||
jsonwebtoken = { version = "10.2.0", features = ["rust_crypto"] }
|
||||
reqwest = { version = "0.13.1", features = ["json"] }
|
||||
reqwest = { version = "0.13.1", features = ["json", "query"] }
|
||||
sentry = { version = "0.46.1", features = ["actix", "tracing"] }
|
||||
serde = "1.0.228"
|
||||
sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio", "tls-native-tls"] }
|
||||
|
||||
@@ -6,14 +6,18 @@ use jsonwebtoken::{
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
pub struct UserId(pub String);
|
||||
#[derive(Clone)]
|
||||
pub struct User {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
pub struct JWT {
|
||||
reqwest_client: reqwest::Client,
|
||||
}
|
||||
|
||||
pub trait AuthImpl {
|
||||
async fn for_protected(&self, token: &str) -> Result<UserId>;
|
||||
async fn for_protected(&self, token: &str) -> Result<User>;
|
||||
}
|
||||
|
||||
pub enum Auth {
|
||||
@@ -23,6 +27,7 @@ pub enum Auth {
|
||||
#[derive(Deserialize)]
|
||||
struct Claims {
|
||||
sub: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl JWT {
|
||||
@@ -32,7 +37,7 @@ impl JWT {
|
||||
}
|
||||
|
||||
impl AuthImpl for JWT {
|
||||
async fn for_protected(&self, token: &str) -> Result<UserId> {
|
||||
async fn for_protected(&self, token: &str) -> Result<User> {
|
||||
let frontend_url =
|
||||
env::var("FRONTEND_BASE_URL").unwrap_or_else(|_| "http://localhost:5173".to_string());
|
||||
|
||||
@@ -57,12 +62,15 @@ impl AuthImpl for JWT {
|
||||
_ => crate::error::Error::Unauthorized,
|
||||
})?;
|
||||
|
||||
Ok(UserId(token_data.claims.sub))
|
||||
Ok(User {
|
||||
id: token_data.claims.sub,
|
||||
name: token_data.claims.name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthImpl for Auth {
|
||||
async fn for_protected(&self, token: &str) -> Result<UserId> {
|
||||
async fn for_protected(&self, token: &str) -> Result<User> {
|
||||
match self {
|
||||
Auth::JWT(jwt) => jwt.for_protected(token).await,
|
||||
}
|
||||
|
||||
@@ -2,38 +2,42 @@ use actix_web::{HttpRequest, HttpResponse, web};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{AppState, account::AccountRepository, error::Result};
|
||||
use crate::{AppState, account::AccountRepository, auth::User, error::Result};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Repository {
|
||||
id: u64,
|
||||
name: String,
|
||||
full_name: String,
|
||||
description: Option<String>,
|
||||
language: Option<String>,
|
||||
pub struct Repository {
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
pub full_name: String,
|
||||
pub description: Option<String>,
|
||||
pub language: Option<String>,
|
||||
#[serde(alias = "stargazers_count")]
|
||||
stars: Option<usize>,
|
||||
updated_at: DateTime<Utc>,
|
||||
private: bool,
|
||||
pub stars: Option<usize>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub private: bool,
|
||||
}
|
||||
|
||||
pub async fn get_repos(
|
||||
app_state: web::Data<AppState>,
|
||||
path: web::Path<String>,
|
||||
req: web::ReqData<User>,
|
||||
) -> Result<HttpResponse> {
|
||||
let user_id = path.into_inner();
|
||||
let user = req.into_inner();
|
||||
let query = AccountRepository::new(&app_state.pool);
|
||||
let token = query.get_access_token(&user_id).await?;
|
||||
let token = query.get_access_token(&user.id).await?;
|
||||
|
||||
let response = app_state
|
||||
.reqwest_client
|
||||
.get("https://api.github.com/user/repos?affiliation=owner")
|
||||
.get("https://api.github.com/user/repos?affiliation=owner&sort=updated&direction=desc&per_page=5")
|
||||
.bearer_auth(token)
|
||||
.send()
|
||||
.await?;
|
||||
response.error_for_status_ref()?;
|
||||
let data = response.json::<Vec<Repository>>().await?;
|
||||
tracing::debug!(github_response = ?data.iter().filter(|r| r.private == true).collect::<Vec<&Repository>>(), "received repos");
|
||||
let data = response
|
||||
.json::<Vec<Repository>>()
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|r| r.private == false)
|
||||
.collect::<Vec<Repository>>();
|
||||
|
||||
Ok(HttpResponse::Ok().json(data))
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod get_repos;
|
||||
pub mod search_repos;
|
||||
|
||||
50
api/src/endpoints/search_repos.rs
Normal file
50
api/src/endpoints/search_repos.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use crate::{
|
||||
account::AccountRepository, auth::User, endpoints::get_repos::Repository, error::Result,
|
||||
};
|
||||
use actix_web::{
|
||||
HttpRequest, HttpResponse,
|
||||
web::{self, ReqData},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(crate) struct SearchQuery {
|
||||
q: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SearchResponse {
|
||||
items: Vec<Repository>,
|
||||
}
|
||||
|
||||
pub async fn search_repos(
|
||||
app_state: web::Data<AppState>,
|
||||
query: web::Query<SearchQuery>,
|
||||
req: ReqData<User>,
|
||||
) -> Result<HttpResponse> {
|
||||
let user = req.into_inner();
|
||||
let token = AccountRepository::new(&app_state.pool)
|
||||
.get_access_token(&user.id)
|
||||
.await?;
|
||||
|
||||
let search_query = format!("user:{} {} fork:true", user.name, query.q);
|
||||
let response = app_state
|
||||
.reqwest_client
|
||||
.get("https://api.github.com/search/repositories")
|
||||
.query(&[("q", &search_query)])
|
||||
.bearer_auth(&token)
|
||||
.send()
|
||||
.await?
|
||||
.json::<SearchResponse>()
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(
|
||||
response
|
||||
.items
|
||||
.into_iter()
|
||||
.filter(|r| r.private == false)
|
||||
.collect::<Vec<Repository>>(),
|
||||
))
|
||||
}
|
||||
@@ -6,7 +6,12 @@ mod middleware;
|
||||
|
||||
use std::env;
|
||||
|
||||
use actix_web::{App, HttpServer, middleware::from_fn, rt::System, web};
|
||||
use actix_web::{
|
||||
App, HttpServer,
|
||||
middleware::from_fn,
|
||||
rt::System,
|
||||
web::{self, route},
|
||||
};
|
||||
use sqlx::PgPool;
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_subscriber::{
|
||||
@@ -59,9 +64,11 @@ async fn run() -> std::io::Result<()> {
|
||||
web::scope("/api").service(
|
||||
web::scope("/v0").service(
|
||||
web::scope("/user")
|
||||
.route("/repos", web::get().to(endpoints::get_repos::get_repos))
|
||||
.wrap(from_fn(middleware::protected))
|
||||
.route(
|
||||
"/{user_id}/repos",
|
||||
web::get().to(endpoints::get_repos::get_repos),
|
||||
"/repos/search",
|
||||
web::get().to(endpoints::search_repos::search_repos),
|
||||
)
|
||||
.wrap(from_fn(middleware::protected)),
|
||||
),
|
||||
|
||||
@@ -20,8 +20,8 @@ pub async fn protected(
|
||||
.map_into_right_body());
|
||||
};
|
||||
|
||||
let user_id = app_state.auth.for_protected(&token).await?;
|
||||
req.extensions_mut().insert(user_id);
|
||||
let user = app_state.auth.for_protected(&token).await?;
|
||||
req.extensions_mut().insert(user);
|
||||
|
||||
next.call(req)
|
||||
.await
|
||||
|
||||
Reference in New Issue
Block a user