feat: add repo importing

This commit is contained in:
2026-01-17 19:32:37 -08:00
parent 831259a6a6
commit fb62081ca8
28 changed files with 1823 additions and 116 deletions

544
api/Cargo.lock generated
View File

@@ -244,6 +244,8 @@ name = "api"
version = "0.1.0"
dependencies = [
"actix-web",
"aws-config",
"aws-sdk-dynamodb",
"chrono",
"jsonwebtoken",
"reqwest 0.13.1",
@@ -278,6 +280,48 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "aws-config"
version = "1.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96571e6996817bf3d58f6b569e4b9fd2e9d2fcf9f7424eed07b2ce9bb87535e5"
dependencies = [
"aws-credential-types",
"aws-runtime",
"aws-sdk-sso",
"aws-sdk-ssooidc",
"aws-sdk-sts",
"aws-smithy-async",
"aws-smithy-http",
"aws-smithy-json",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-types",
"aws-types",
"bytes",
"fastrand",
"hex",
"http 1.4.0",
"ring",
"time",
"tokio",
"tracing",
"url",
"zeroize",
]
[[package]]
name = "aws-credential-types"
version = "1.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cd362783681b15d136480ad555a099e82ecd8e2d10a841e14dfd0078d67fee3"
dependencies = [
"aws-smithy-async",
"aws-smithy-runtime-api",
"aws-smithy-types",
"zeroize",
]
[[package]]
name = "aws-lc-rs"
version = "1.15.2"
@@ -300,6 +344,325 @@ dependencies = [
"fs_extra",
]
[[package]]
name = "aws-runtime"
version = "1.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "959dab27ce613e6c9658eb3621064d0e2027e5f2acb65bc526a43577facea557"
dependencies = [
"aws-credential-types",
"aws-sigv4",
"aws-smithy-async",
"aws-smithy-http",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-types",
"aws-types",
"bytes",
"fastrand",
"http 0.2.12",
"http-body 0.4.6",
"percent-encoding",
"pin-project-lite",
"tracing",
"uuid",
]
[[package]]
name = "aws-sdk-dynamodb"
version = "1.102.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5f7e6a53cf5ee8b7041c73106d9a93480b47f8b955466262b043aab0b5bf489"
dependencies = [
"aws-credential-types",
"aws-runtime",
"aws-smithy-async",
"aws-smithy-http",
"aws-smithy-json",
"aws-smithy-observability",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-types",
"aws-types",
"bytes",
"fastrand",
"http 0.2.12",
"regex-lite",
"tracing",
]
[[package]]
name = "aws-sdk-sso"
version = "1.92.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7d63bd2bdeeb49aa3f9b00c15e18583503b778b2e792fc06284d54e7d5b6566"
dependencies = [
"aws-credential-types",
"aws-runtime",
"aws-smithy-async",
"aws-smithy-http",
"aws-smithy-json",
"aws-smithy-observability",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-types",
"aws-types",
"bytes",
"fastrand",
"http 0.2.12",
"regex-lite",
"tracing",
]
[[package]]
name = "aws-sdk-ssooidc"
version = "1.94.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "532d93574bf731f311bafb761366f9ece345a0416dbcc273d81d6d1a1205239b"
dependencies = [
"aws-credential-types",
"aws-runtime",
"aws-smithy-async",
"aws-smithy-http",
"aws-smithy-json",
"aws-smithy-observability",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-types",
"aws-types",
"bytes",
"fastrand",
"http 0.2.12",
"regex-lite",
"tracing",
]
[[package]]
name = "aws-sdk-sts"
version = "1.96.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357e9a029c7524db6a0099cd77fbd5da165540339e7296cca603531bc783b56c"
dependencies = [
"aws-credential-types",
"aws-runtime",
"aws-smithy-async",
"aws-smithy-http",
"aws-smithy-json",
"aws-smithy-observability",
"aws-smithy-query",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-types",
"aws-smithy-xml",
"aws-types",
"fastrand",
"http 0.2.12",
"regex-lite",
"tracing",
]
[[package]]
name = "aws-sigv4"
version = "1.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69e523e1c4e8e7e8ff219d732988e22bfeae8a1cafdbe6d9eca1546fa080be7c"
dependencies = [
"aws-credential-types",
"aws-smithy-http",
"aws-smithy-runtime-api",
"aws-smithy-types",
"bytes",
"form_urlencoded",
"hex",
"hmac",
"http 0.2.12",
"http 1.4.0",
"percent-encoding",
"sha2",
"time",
"tracing",
]
[[package]]
name = "aws-smithy-async"
version = "1.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ee19095c7c4dda59f1697d028ce704c24b2d33c6718790c7f1d5a3015b4107c"
dependencies = [
"futures-util",
"pin-project-lite",
"tokio",
]
[[package]]
name = "aws-smithy-http"
version = "0.62.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "826141069295752372f8203c17f28e30c464d22899a43a0c9fd9c458d469c88b"
dependencies = [
"aws-smithy-runtime-api",
"aws-smithy-types",
"bytes",
"bytes-utils",
"futures-core",
"futures-util",
"http 0.2.12",
"http 1.4.0",
"http-body 0.4.6",
"percent-encoding",
"pin-project-lite",
"pin-utils",
"tracing",
]
[[package]]
name = "aws-smithy-http-client"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59e62db736db19c488966c8d787f52e6270be565727236fd5579eaa301e7bc4a"
dependencies = [
"aws-smithy-async",
"aws-smithy-runtime-api",
"aws-smithy-types",
"h2 0.3.27",
"h2 0.4.13",
"http 0.2.12",
"http 1.4.0",
"http-body 0.4.6",
"hyper 0.14.32",
"hyper 1.8.1",
"hyper-rustls 0.24.2",
"hyper-rustls 0.27.7",
"hyper-util",
"pin-project-lite",
"rustls 0.21.12",
"rustls 0.23.36",
"rustls-native-certs",
"rustls-pki-types",
"tokio",
"tokio-rustls 0.26.4",
"tower",
"tracing",
]
[[package]]
name = "aws-smithy-json"
version = "0.61.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49fa1213db31ac95288d981476f78d05d9cbb0353d22cdf3472cc05bb02f6551"
dependencies = [
"aws-smithy-types",
]
[[package]]
name = "aws-smithy-observability"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef1fcbefc7ece1d70dcce29e490f269695dfca2d2bacdeaf9e5c3f799e4e6a42"
dependencies = [
"aws-smithy-runtime-api",
]
[[package]]
name = "aws-smithy-query"
version = "0.60.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae5d689cf437eae90460e944a58b5668530d433b4ff85789e69d2f2a556e057d"
dependencies = [
"aws-smithy-types",
"urlencoding",
]
[[package]]
name = "aws-smithy-runtime"
version = "1.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb5b6167fcdf47399024e81ac08e795180c576a20e4d4ce67949f9a88ae37dc1"
dependencies = [
"aws-smithy-async",
"aws-smithy-http",
"aws-smithy-http-client",
"aws-smithy-observability",
"aws-smithy-runtime-api",
"aws-smithy-types",
"bytes",
"fastrand",
"http 0.2.12",
"http 1.4.0",
"http-body 0.4.6",
"http-body 1.0.1",
"pin-project-lite",
"pin-utils",
"tokio",
"tracing",
]
[[package]]
name = "aws-smithy-runtime-api"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efce7aaaf59ad53c5412f14fc19b2d5c6ab2c3ec688d272fd31f76ec12f44fb0"
dependencies = [
"aws-smithy-async",
"aws-smithy-types",
"bytes",
"http 0.2.12",
"http 1.4.0",
"pin-project-lite",
"tokio",
"tracing",
"zeroize",
]
[[package]]
name = "aws-smithy-types"
version = "1.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65f172bcb02424eb94425db8aed1b6d583b5104d4d5ddddf22402c661a320048"
dependencies = [
"base64-simd",
"bytes",
"bytes-utils",
"futures-core",
"http 0.2.12",
"http 1.4.0",
"http-body 0.4.6",
"http-body 1.0.1",
"http-body-util",
"itoa",
"num-integer",
"pin-project-lite",
"pin-utils",
"ryu",
"serde",
"time",
"tokio",
"tokio-util",
]
[[package]]
name = "aws-smithy-xml"
version = "0.60.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11b2f670422ff42bf7065031e72b45bc52a3508bd089f743ea90731ca2b6ea57"
dependencies = [
"xmlparser",
]
[[package]]
name = "aws-types"
version = "1.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d980627d2dd7bfc32a3c025685a033eeab8d365cc840c631ef59d1b8f428164"
dependencies = [
"aws-credential-types",
"aws-smithy-async",
"aws-smithy-runtime-api",
"aws-smithy-types",
"rustc_version",
"tracing",
]
[[package]]
name = "backtrace"
version = "0.3.76"
@@ -327,6 +690,16 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64-simd"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195"
dependencies = [
"outref",
"vsimd",
]
[[package]]
name = "base64ct"
version = "1.8.3"
@@ -399,6 +772,16 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
[[package]]
name = "bytes-utils"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35"
dependencies = [
"bytes",
"either",
]
[[package]]
name = "bytestring"
version = "1.5.0"
@@ -1207,6 +1590,17 @@ dependencies = [
"itoa",
]
[[package]]
name = "http-body"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
dependencies = [
"bytes",
"http 0.2.12",
"pin-project-lite",
]
[[package]]
name = "http-body"
version = "1.0.1"
@@ -1226,7 +1620,7 @@ dependencies = [
"bytes",
"futures-core",
"http 1.4.0",
"http-body",
"http-body 1.0.1",
"pin-project-lite",
]
@@ -1242,6 +1636,30 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "0.14.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
dependencies = [
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"h2 0.3.27",
"http 0.2.12",
"http-body 0.4.6",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"socket2 0.5.10",
"tokio",
"tower-service",
"tracing",
"want",
]
[[package]]
name = "hyper"
version = "1.8.1"
@@ -1254,7 +1672,7 @@ dependencies = [
"futures-core",
"h2 0.4.13",
"http 1.4.0",
"http-body",
"http-body 1.0.1",
"httparse",
"itoa",
"pin-project-lite",
@@ -1264,6 +1682,21 @@ dependencies = [
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [
"futures-util",
"http 0.2.12",
"hyper 0.14.32",
"log",
"rustls 0.21.12",
"tokio",
"tokio-rustls 0.24.1",
]
[[package]]
name = "hyper-rustls"
version = "0.27.7"
@@ -1271,12 +1704,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
dependencies = [
"http 1.4.0",
"hyper",
"hyper 1.8.1",
"hyper-util",
"rustls",
"rustls 0.23.36",
"rustls-native-certs",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tokio-rustls 0.26.4",
"tower-service",
]
@@ -1288,7 +1722,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper",
"hyper 1.8.1",
"hyper-util",
"native-tls",
"tokio",
@@ -1308,8 +1742,8 @@ dependencies = [
"futures-core",
"futures-util",
"http 1.4.0",
"http-body",
"hyper",
"http-body 1.0.1",
"hyper 1.8.1",
"ipnet",
"libc",
"percent-encoding",
@@ -2048,6 +2482,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "outref"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
[[package]]
name = "p256"
version = "0.13.2"
@@ -2239,7 +2679,7 @@ dependencies = [
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls",
"rustls 0.23.36",
"socket2 0.6.1",
"thiserror 2.0.17",
"tokio",
@@ -2260,7 +2700,7 @@ dependencies = [
"rand 0.9.2",
"ring",
"rustc-hash",
"rustls",
"rustls 0.23.36",
"rustls-pki-types",
"slab",
"thiserror 2.0.17",
@@ -2422,9 +2862,9 @@ dependencies = [
"futures-core",
"futures-util",
"http 1.4.0",
"http-body",
"http-body 1.0.1",
"http-body-util",
"hyper",
"hyper 1.8.1",
"hyper-tls",
"hyper-util",
"js-sys",
@@ -2460,10 +2900,10 @@ dependencies = [
"futures-core",
"h2 0.4.13",
"http 1.4.0",
"http-body",
"http-body 1.0.1",
"http-body-util",
"hyper",
"hyper-rustls",
"hyper 1.8.1",
"hyper-rustls 0.27.7",
"hyper-util",
"js-sys",
"log",
@@ -2471,7 +2911,7 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls",
"rustls 0.23.36",
"rustls-pki-types",
"rustls-platform-verifier",
"serde",
@@ -2479,7 +2919,7 @@ dependencies = [
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tokio-rustls",
"tokio-rustls 0.26.4",
"tower",
"tower-http",
"tower-service",
@@ -2567,6 +3007,18 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.21.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
dependencies = [
"log",
"ring",
"rustls-webpki 0.101.7",
"sct",
]
[[package]]
name = "rustls"
version = "0.23.36"
@@ -2576,7 +3028,7 @@ dependencies = [
"aws-lc-rs",
"once_cell",
"rustls-pki-types",
"rustls-webpki",
"rustls-webpki 0.103.8",
"subtle",
"zeroize",
]
@@ -2614,10 +3066,10 @@ dependencies = [
"jni",
"log",
"once_cell",
"rustls",
"rustls 0.23.36",
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki",
"rustls-webpki 0.103.8",
"security-framework 3.5.1",
"security-framework-sys",
"webpki-root-certs",
@@ -2630,6 +3082,16 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.101.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "rustls-webpki"
version = "0.103.8"
@@ -2678,6 +3140,16 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sct"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "sec1"
version = "0.7.3"
@@ -3447,13 +3919,23 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
dependencies = [
"rustls 0.21.12",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
"rustls",
"rustls 0.23.36",
"tokio",
]
@@ -3506,7 +3988,7 @@ dependencies = [
"bytes",
"futures-util",
"http 1.4.0",
"http-body",
"http-body 1.0.1",
"iri-string",
"pin-project-lite",
"tower",
@@ -3709,6 +4191,12 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "utf-8"
version = "0.7.6"
@@ -3751,6 +4239,12 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "vsimd"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
[[package]]
name = "walkdir"
version = "2.5.0"
@@ -4289,6 +4783,12 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]]
name = "xmlparser"
version = "0.13.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4"
[[package]]
name = "yoke"
version = "0.8.1"

View File

@@ -5,6 +5,8 @@ edition = "2024"
[dependencies]
actix-web = "4.12.1"
aws-config = "1.8.12"
aws-sdk-dynamodb = "1.102.0"
chrono = { version = "0.4.43", features = ["serde"] }
jsonwebtoken = { version = "10.2.0", features = ["rust_crypto"] }
reqwest = { version = "0.13.1", features = ["json", "query"] }

View File

@@ -1,21 +0,0 @@
use crate::error::Result;
use sqlx::{PgPool, query_scalar};
pub struct AccountRepository<'a> {
pub pool: &'a PgPool,
}
impl<'a> AccountRepository<'a> {
pub fn new(pool: &'a PgPool) -> Self {
Self { pool }
}
pub async fn get_access_token(&self, user_id: &str) -> Result<String> {
query_scalar("SELECT access_token FROM account WHERE user_id = $1")
.bind(user_id)
.fetch_one(self.pool)
.await
.map_err(|_| crate::error::Error::AccessToken)
}
}

View File

@@ -0,0 +1,19 @@
use crate::{auth::User, error::Result, user::RepositorySchema};
use actix_web::{HttpResponse, web};
use serde::Serialize;
use crate::AppState;
#[derive(Serialize)]
struct AddResponse {
id: String,
}
pub async fn add_repo(
app_state: web::Data<AppState>,
user: web::ReqData<User>,
payload: web::Json<RepositorySchema>,
) -> Result<HttpResponse> {
let repo = payload.into_inner();
app_state.user.add_repository(&user.id, repo).await
}

View File

@@ -1,8 +1,10 @@
use actix_web::{HttpRequest, HttpResponse, web};
use std::collections::HashSet;
use actix_web::{HttpResponse, web};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::{AppState, account::AccountRepository, auth::User, error::Result};
use crate::{AppState, auth::User, error::Result};
#[derive(Debug, Serialize, Deserialize)]
pub struct Repository {
@@ -15,6 +17,9 @@ pub struct Repository {
pub stars: Option<usize>,
pub updated_at: DateTime<Utc>,
pub private: bool,
pub default_branch: String,
#[serde(default)]
pub added: bool,
}
pub async fn get_repos(
@@ -22,8 +27,7 @@ pub async fn get_repos(
req: web::ReqData<User>,
) -> Result<HttpResponse> {
let user = req.into_inner();
let query = AccountRepository::new(&app_state.pool);
let token = query.get_access_token(&user.id).await?;
let token = app_state.user.get_access_token(&user.id).await?;
let response = app_state
.reqwest_client
@@ -32,11 +36,24 @@ pub async fn get_repos(
.send()
.await?;
response.error_for_status_ref()?;
let added_ids = app_state
.user
.get_repositories(&user.id)
.await?
.into_iter()
.map(|r| r.id.clone())
.collect::<HashSet<String>>();
tracing::debug!(added_response = ?added_ids);
let data = response
.json::<Vec<Repository>>()
.await?
.into_iter()
.filter(|r| r.private == false)
.filter_map(|mut r| {
(!r.private).then(|| {
r.added = added_ids.contains(&r.id.to_string());
r
})
})
.collect::<Vec<Repository>>();
Ok(HttpResponse::Ok().json(data))

View File

@@ -1,2 +1,3 @@
pub mod add_repo;
pub mod get_repos;
pub mod search_repos;

View File

@@ -1,6 +1,4 @@
use crate::{
account::AccountRepository, auth::User, endpoints::get_repos::Repository, error::Result,
};
use crate::{auth::User, endpoints::get_repos::Repository, error::Result};
use actix_web::{
HttpRequest, HttpResponse,
web::{self, ReqData},
@@ -25,9 +23,7 @@ pub async fn search_repos(
req: ReqData<User>,
) -> Result<HttpResponse> {
let user = req.into_inner();
let token = AccountRepository::new(&app_state.pool)
.get_access_token(&user.id)
.await?;
let token = app_state.user.get_access_token(&user.id).await?;
let search_query = format!("user:{} {} fork:true", user.name, query.q);
let response = app_state

View File

@@ -1,4 +1,5 @@
use actix_web::{HttpResponse, ResponseError, http::StatusCode};
use aws_sdk_dynamodb::error::SdkError;
use serde::Serialize;
use thiserror::Error;
@@ -18,6 +19,16 @@ pub enum Error {
Jwx(#[from] jsonwebtoken::errors::Error),
#[error("token expired")]
TokenExpired,
#[error("dynamodb error: {0}")]
DynamoDB(String),
#[error("item already exists")]
AlreadyExists,
}
impl<E: std::fmt::Debug> From<SdkError<E>> for Error {
fn from(err: SdkError<E>) -> Self {
Error::DynamoDB(format!("{:?}", err))
}
}
#[derive(Serialize)]
@@ -33,6 +44,9 @@ impl ResponseError for Error {
Error::TokenExpired => HttpResponse::Unauthorized().json(ErrorResponse {
error: "token expired".to_string(),
}),
Error::AlreadyExists => HttpResponse::BadRequest().json(ErrorResponse {
error: "item already exists".to_string(),
}),
_ => HttpResponse::InternalServerError().finish(),
}
}

View File

@@ -1,8 +1,8 @@
mod account;
mod auth;
mod endpoints;
mod error;
mod middleware;
mod user;
use std::env;
@@ -10,20 +10,24 @@ use actix_web::{
App, HttpServer,
middleware::from_fn,
rt::System,
web::{self, route},
web::{self},
};
use aws_config::BehaviorVersion;
use sqlx::PgPool;
use tracing::level_filters::LevelFilter;
use tracing_subscriber::{
EnvFilter, fmt::format::FmtSpan, layer::SubscriberExt, util::SubscriberInitExt,
};
use crate::auth::{Auth, JWT};
use crate::{
auth::{Auth, JWT},
user::UserRepository,
};
struct AppState {
reqwest_client: reqwest::Client,
pool: PgPool,
auth: Auth,
user: UserRepository,
}
async fn run() -> std::io::Result<()> {
@@ -35,14 +39,20 @@ async fn run() -> std::io::Result<()> {
))
.build()
.expect("failed to create reqwest client");
let config = aws_config::load_defaults(BehaviorVersion::v2026_01_12()).await;
let dynamodb_client = aws_sdk_dynamodb::Client::new(&config);
let app_data = web::Data::new(AppState {
reqwest_client: reqwest_client.clone(),
pool: PgPool::connect(
&env::var("DATABASE_URL").expect("DATABASE_URL environment variable must be set"),
)
.await
.expect("error connecting to db"),
auth: Auth::JWT(JWT::new(reqwest_client.clone())),
user: UserRepository::new(
PgPool::connect(
&env::var("DATABASE_URL").expect("DATABASE_URL environment variable must be set"),
)
.await
.expect("error connecting to db"),
dynamodb_client,
),
});
HttpServer::new(move || {
@@ -70,6 +80,8 @@ async fn run() -> std::io::Result<()> {
"/repos/search",
web::get().to(endpoints::search_repos::search_repos),
)
.wrap(from_fn(middleware::protected))
.route("/repo/add", web::post().to(endpoints::add_repo::add_repo))
.wrap(from_fn(middleware::protected)),
),
),
@@ -90,7 +102,8 @@ fn main() -> std::io::Result<()> {
.add_directive("reqwest=info".parse().unwrap())
.add_directive("hyper=info".parse().unwrap())
.add_directive("h2=info".parse().unwrap())
.add_directive("rustls=info".parse().unwrap());
.add_directive("rustls=info".parse().unwrap())
.add_directive("aws=info".parse().unwrap());
tracing_subscriber::registry()
.with(tracing_env_filter)

104
api/src/user.rs Normal file
View File

@@ -0,0 +1,104 @@
use std::env;
use crate::error::Result;
use actix_web::HttpResponse;
use aws_sdk_dynamodb::types::AttributeValue;
use chrono::Utc;
use serde::{Deserialize, Serialize};
use sqlx::{PgPool, query_scalar};
pub struct UserRepository {
pool: PgPool,
dynamodb_client: aws_sdk_dynamodb::Client,
table_name: String,
}
#[derive(Serialize, Deserialize)]
pub struct RepositorySchema {
pub id: String,
pub name: String,
}
impl UserRepository {
pub fn new(pool: PgPool, dynamodb_client: aws_sdk_dynamodb::Client) -> Self {
Self {
pool,
dynamodb_client,
table_name: env::var("REPOS_TABLE_NAME")
.expect("environment variable REPOS_TABLE_NAME must be set"),
}
}
pub async fn get_access_token(&self, user_id: &str) -> Result<String> {
query_scalar("SELECT access_token FROM account WHERE user_id = $1")
.bind(user_id)
.fetch_one(&self.pool)
.await
.map_err(|_| crate::error::Error::AccessToken)
}
pub async fn get_repositories(&self, user_id: &str) -> Result<Vec<RepositorySchema>> {
let response = self
.dynamodb_client
.query()
.table_name(&self.table_name)
.key_condition_expression("pk = :pk AND begins_with(sk, :sk)")
.expression_attribute_values(":pk", AttributeValue::S(format!("USER#{user_id}")))
.expression_attribute_values(":sk", AttributeValue::S("REPO#".into()))
.send()
.await?
.items()
.iter()
.filter_map(|item| {
Some(RepositorySchema {
id: item
.get("sk")?
.as_s()
.ok()?
.strip_prefix("REPO#")?
.to_string(),
name: item.get("full_name")?.as_s().ok()?.to_string(),
})
})
.collect::<Vec<RepositorySchema>>();
Ok(response)
}
pub async fn add_repository(
&self,
user_id: &str,
repo: RepositorySchema,
) -> Result<HttpResponse> {
let now = Utc::now().to_rfc3339();
let response = self
.dynamodb_client
.put_item()
.table_name(&self.table_name)
.condition_expression("attribute_not_exists(sk)")
.item("pk", AttributeValue::S(format!("USER#{user_id}")))
.item("sk", AttributeValue::S(format!("REPO#{}", repo.id)))
.item("full_name", AttributeValue::S(repo.name.to_string()))
.item("gsi1pk", AttributeValue::S("REPOS".into()))
.item("gsi1sk", AttributeValue::S(now.clone()))
.item("imported_at", AttributeValue::S(now))
.send()
.await;
match response {
Ok(_) => Ok(HttpResponse::Ok().json(repo)),
Err(err) => {
if err
.as_service_error()
.map(|e| e.is_conditional_check_failed_exception())
.unwrap_or(false)
{
Err(crate::error::Error::AlreadyExists)
} else {
Err(err.into())
}
}
}
}
}