feat!: start implementing repo listing & auth middleware
This commit is contained in:
1265
api/Cargo.lock
generated
1265
api/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -5,4 +5,10 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = "4.12.1"
|
actix-web = "4.12.1"
|
||||||
reqwest = "0.13.1"
|
chrono = { version = "0.4.43", features = ["serde"] }
|
||||||
|
jsonwebtoken = { version = "10.2.0", features = ["rust_crypto"] }
|
||||||
|
reqwest = { version = "0.13.1", features = ["json"] }
|
||||||
|
serde = "1.0.228"
|
||||||
|
sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio", "tls-native-tls"] }
|
||||||
|
thiserror = "2.0.17"
|
||||||
|
tokio = "1.49.0"
|
||||||
|
|||||||
21
api/src/account.rs
Normal file
21
api/src/account.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
74
api/src/auth.rs
Normal file
74
api/src/auth.rs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
use std::{env, sync::Arc};
|
||||||
|
|
||||||
|
use crate::error::Result;
|
||||||
|
use jsonwebtoken::{
|
||||||
|
Algorithm, DecodingKey, Validation, decode, decode_header, errors::ErrorKind, jwk::JwkSet,
|
||||||
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
pub struct UserId(pub String);
|
||||||
|
|
||||||
|
pub struct JWT {
|
||||||
|
reqwest_client: reqwest::Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait AuthImpl {
|
||||||
|
async fn for_protected(&self, token: &str) -> Result<UserId>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Auth {
|
||||||
|
JWT(JWT),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Claims {
|
||||||
|
sub: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JWT {
|
||||||
|
pub fn new(reqwest_client: reqwest::Client) -> Self {
|
||||||
|
Self { reqwest_client }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthImpl for JWT {
|
||||||
|
async fn for_protected(&self, token: &str) -> Result<UserId> {
|
||||||
|
let frontend_url =
|
||||||
|
env::var("FRONTEND_BASE_URL").unwrap_or_else(|_| "http://localhost:5173".to_string());
|
||||||
|
|
||||||
|
let jwks = get_jwks(
|
||||||
|
&self.reqwest_client,
|
||||||
|
&format!("{frontend_url}/api/auth/jwks"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let header = decode_header(token)?;
|
||||||
|
let kid = header.kid.ok_or(crate::error::Error::Unauthorized)?;
|
||||||
|
|
||||||
|
let jwk = jwks.find(&kid).ok_or(crate::error::Error::Unauthorized)?;
|
||||||
|
let decoding_key = DecodingKey::from_jwk(jwk)?;
|
||||||
|
|
||||||
|
let mut validation = Validation::new(Algorithm::EdDSA);
|
||||||
|
validation.set_issuer(&[&frontend_url]);
|
||||||
|
validation.set_audience(&[&frontend_url]);
|
||||||
|
|
||||||
|
let token_data =
|
||||||
|
decode::<Claims>(token, &decoding_key, &validation).map_err(|e| match e.kind() {
|
||||||
|
ErrorKind::ExpiredSignature => crate::error::Error::TokenExpired,
|
||||||
|
_ => crate::error::Error::Unauthorized,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(UserId(token_data.claims.sub))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthImpl for Auth {
|
||||||
|
async fn for_protected(&self, token: &str) -> Result<UserId> {
|
||||||
|
match self {
|
||||||
|
Auth::JWT(jwt) => jwt.for_protected(token).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_jwks(client: &reqwest::Client, jwks_url: &str) -> Result<JwkSet> {
|
||||||
|
Ok(client.get(jwks_url).send().await?.json::<JwkSet>().await?)
|
||||||
|
}
|
||||||
35
api/src/endpoints/get_repos.rs
Normal file
35
api/src/endpoints/get_repos.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
use actix_web::{HttpRequest, HttpResponse, web};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{AppState, account::AccountRepository, error::Result};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct Repository {
|
||||||
|
name: String,
|
||||||
|
full_name: String,
|
||||||
|
description: String,
|
||||||
|
language: String,
|
||||||
|
stars: usize,
|
||||||
|
updated_at: DateTime<Utc>,
|
||||||
|
private: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_repos(
|
||||||
|
app_state: web::Data<AppState>,
|
||||||
|
path: web::Path<String>,
|
||||||
|
) -> Result<HttpResponse> {
|
||||||
|
let user_id = path.into_inner();
|
||||||
|
let query = AccountRepository::new(&app_state.pool);
|
||||||
|
let token = query.get_access_token(&user_id).await?;
|
||||||
|
|
||||||
|
let response = app_state
|
||||||
|
.reqwest_client
|
||||||
|
.get("https://api.github.com/user/repos")
|
||||||
|
.bearer_auth(token)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
response.error_for_status_ref()?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(response.json::<Vec<Repository>>().await?))
|
||||||
|
}
|
||||||
1
api/src/endpoints/mod.rs
Normal file
1
api/src/endpoints/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod get_repos;
|
||||||
39
api/src/error.rs
Normal file
39
api/src/error.rs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
use actix_web::{HttpResponse, ResponseError, http::StatusCode};
|
||||||
|
use serde::Serialize;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
pub type Result<T> = core::result::Result<T, Error>;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("db error: {0}")]
|
||||||
|
DB(#[from] sqlx::Error),
|
||||||
|
#[error("http error: {0}")]
|
||||||
|
Http(#[from] reqwest::Error),
|
||||||
|
#[error("error getting access token")]
|
||||||
|
AccessToken,
|
||||||
|
#[error("unauthorized")]
|
||||||
|
Unauthorized,
|
||||||
|
#[error("jwx error")]
|
||||||
|
Jwx(#[from] jsonwebtoken::errors::Error),
|
||||||
|
#[error("token expired")]
|
||||||
|
TokenExpired,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ErrorResponse {
|
||||||
|
error: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResponseError for Error {
|
||||||
|
fn error_response(&self) -> actix_web::HttpResponse<actix_web::body::BoxBody> {
|
||||||
|
match self {
|
||||||
|
Error::AccessToken => HttpResponse::Unauthorized().finish(),
|
||||||
|
Error::Unauthorized => HttpResponse::Unauthorized().finish(),
|
||||||
|
Error::TokenExpired => HttpResponse::Unauthorized().json(ErrorResponse {
|
||||||
|
error: "token expired".to_string(),
|
||||||
|
}),
|
||||||
|
_ => HttpResponse::InternalServerError().finish(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,53 @@
|
|||||||
use actix_web::{App, HttpServer};
|
mod account;
|
||||||
|
mod auth;
|
||||||
|
mod endpoints;
|
||||||
|
mod error;
|
||||||
|
mod middleware;
|
||||||
|
|
||||||
struct AppState {}
|
use std::env;
|
||||||
|
|
||||||
|
use actix_web::{App, HttpServer, middleware::from_fn, web};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
use crate::auth::{Auth, JWT};
|
||||||
|
|
||||||
|
struct AppState {
|
||||||
|
reqwest_client: reqwest::Client,
|
||||||
|
pool: PgPool,
|
||||||
|
auth: Auth,
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
HttpServer::new(|| App::new())
|
let reqwest_client = reqwest::Client::new();
|
||||||
.bind(("127.0.0.1", 8080))?
|
let app_data = web::Data::new(AppState {
|
||||||
.run()
|
reqwest_client: reqwest_client.clone(),
|
||||||
|
pool: PgPool::connect(
|
||||||
|
&env::var("DATABASE_URL").expect("DATABASE_URL environment variable must be set"),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
|
.expect("error connecting to db"),
|
||||||
|
auth: Auth::JWT(JWT::new(reqwest_client.clone())),
|
||||||
|
});
|
||||||
|
|
||||||
|
HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
.app_data(app_data.clone())
|
||||||
|
.route(
|
||||||
|
"/",
|
||||||
|
web::get()
|
||||||
|
.to(async || concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"))),
|
||||||
|
)
|
||||||
|
.service(
|
||||||
|
web::scope("/api")
|
||||||
|
.route(
|
||||||
|
"/{user_id}/repos",
|
||||||
|
web::get().to(endpoints::get_repos::get_repos),
|
||||||
|
)
|
||||||
|
.wrap(from_fn(middleware::protected)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.bind(("127.0.0.1", 8080))?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|||||||
3
api/src/middleware/mod.rs
Normal file
3
api/src/middleware/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
mod protected;
|
||||||
|
|
||||||
|
pub use protected::protected;
|
||||||
44
api/src/middleware/protected.rs
Normal file
44
api/src/middleware/protected.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
use actix_web::{
|
||||||
|
Error, HttpMessage, HttpRequest, HttpResponse,
|
||||||
|
body::MessageBody,
|
||||||
|
dev::{ServiceRequest, ServiceResponse},
|
||||||
|
http::header::AUTHORIZATION,
|
||||||
|
middleware::Next,
|
||||||
|
web,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{AppState, auth::AuthImpl};
|
||||||
|
|
||||||
|
pub async fn protected(
|
||||||
|
app_state: web::Data<AppState>,
|
||||||
|
req: ServiceRequest,
|
||||||
|
next: Next<impl MessageBody + 'static>,
|
||||||
|
) -> Result<ServiceResponse<impl MessageBody>, Error> {
|
||||||
|
let Some(token) = token_from_request(&req) else {
|
||||||
|
return Ok(req
|
||||||
|
.into_response(HttpResponse::Unauthorized().finish())
|
||||||
|
.map_into_right_body());
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id = app_state.auth.for_protected(&token).await?;
|
||||||
|
req.extensions_mut().insert(user_id);
|
||||||
|
|
||||||
|
next.call(req)
|
||||||
|
.await
|
||||||
|
.map(ServiceResponse::map_into_left_body)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn token_from_request(req: &ServiceRequest) -> Option<String> {
|
||||||
|
let token = req
|
||||||
|
.headers()
|
||||||
|
.get(AUTHORIZATION)
|
||||||
|
.and_then(|s| s.to_str().ok())?;
|
||||||
|
|
||||||
|
let token = if token.to_lowercase().starts_with("bearer ") {
|
||||||
|
token[7..].to_string()
|
||||||
|
} else {
|
||||||
|
token.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(token)
|
||||||
|
}
|
||||||
7
drizzle/0001_abnormal_swordsman.sql
Normal file
7
drizzle/0001_abnormal_swordsman.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
CREATE TABLE "jwks" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"public_key" text NOT NULL,
|
||||||
|
"private_key" text NOT NULL,
|
||||||
|
"created_at" timestamp NOT NULL,
|
||||||
|
"expires_at" timestamp
|
||||||
|
);
|
||||||
417
drizzle/meta/0001_snapshot.json
Normal file
417
drizzle/meta/0001_snapshot.json
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
{
|
||||||
|
"id": "2d1e972c-8275-4278-a835-e8bc5d477168",
|
||||||
|
"prevId": "2cbcf685-9d9c-420f-9c34-16cea513e9b8",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.account": {
|
||||||
|
"name": "account",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"account_id": {
|
||||||
|
"name": "account_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"provider_id": {
|
||||||
|
"name": "provider_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"access_token": {
|
||||||
|
"name": "access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"name": "refresh_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"id_token": {
|
||||||
|
"name": "id_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"access_token_expires_at": {
|
||||||
|
"name": "access_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token_expires_at": {
|
||||||
|
"name": "refresh_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"name": "scope",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"account_userId_idx": {
|
||||||
|
"name": "account_userId_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "user_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"account_user_id_user_id_fk": {
|
||||||
|
"name": "account_user_id_user_id_fk",
|
||||||
|
"tableFrom": "account",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.jwks": {
|
||||||
|
"name": "jwks",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"public_key": {
|
||||||
|
"name": "public_key",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"private_key": {
|
||||||
|
"name": "private_key",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.session": {
|
||||||
|
"name": "session",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"name": "token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"ip_address": {
|
||||||
|
"name": "ip_address",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_agent": {
|
||||||
|
"name": "user_agent",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"session_userId_idx": {
|
||||||
|
"name": "session_userId_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "user_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"session_user_id_user_id_fk": {
|
||||||
|
"name": "session_user_id_user_id_fk",
|
||||||
|
"tableFrom": "session",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"session_token_unique": {
|
||||||
|
"name": "session_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"token"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email_verified": {
|
||||||
|
"name": "email_verified",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"name": "image",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"user_email_unique": {
|
||||||
|
"name": "user_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.verification": {
|
||||||
|
"name": "verification",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"name": "identifier",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"verification_identifier_idx": {
|
||||||
|
"name": "verification_identifier_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "identifier",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,13 @@
|
|||||||
"when": 1768364902417,
|
"when": 1768364902417,
|
||||||
"tag": "0000_many_joseph",
|
"tag": "0000_many_joseph",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1768455615596,
|
||||||
|
"tag": "0001_abnormal_swordsman",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -10,9 +10,7 @@
|
|||||||
const query = createQuery(() => ({
|
const query = createQuery(() => ({
|
||||||
queryKey: ['github-repositories'],
|
queryKey: ['github-repositories'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await fetch('https://api.github.com/user/repos?affiliation=owner', {
|
const response = await fetch(`/api/${$session.data?.user.id}/repos`);
|
||||||
headers: {}
|
|
||||||
});
|
|
||||||
return await response.json();
|
return await response.json();
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import { jwt } from 'better-auth/plugins';
|
||||||
import { createAuthClient } from 'better-auth/svelte';
|
import { createAuthClient } from 'better-auth/svelte';
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
export const authClient = createAuthClient({
|
||||||
baseURL: 'http://localhost:5173'
|
baseURL: 'http://localhost:5173',
|
||||||
|
plugins: [jwt()]
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import { betterAuth } from 'better-auth';
|
|||||||
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
|
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
|
||||||
import { db } from './db/drizzle';
|
import { db } from './db/drizzle';
|
||||||
import * as authSchema from './db/auth-schema';
|
import * as authSchema from './db/auth-schema';
|
||||||
|
import { jwt } from 'better-auth/plugins';
|
||||||
|
|
||||||
export const auth = betterAuth({
|
export const auth = betterAuth({
|
||||||
database: drizzleAdapter(db, {
|
database: drizzleAdapter(db, {
|
||||||
provider: 'pg',
|
provider: 'pg',
|
||||||
schema: { ...authSchema }
|
schema: { ...authSchema }
|
||||||
}),
|
}),
|
||||||
|
plugins: [jwt()],
|
||||||
socialProviders: {
|
socialProviders: {
|
||||||
github: {
|
github: {
|
||||||
clientId: process.env.GH_CLIENT_ID!,
|
clientId: process.env.GH_CLIENT_ID!,
|
||||||
|
|||||||
@@ -73,6 +73,14 @@ export const verification = pgTable(
|
|||||||
(table) => [index("verification_identifier_idx").on(table.identifier)],
|
(table) => [index("verification_identifier_idx").on(table.identifier)],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const jwks = pgTable("jwks", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
publicKey: text("public_key").notNull(),
|
||||||
|
privateKey: text("private_key").notNull(),
|
||||||
|
createdAt: timestamp("created_at").notNull(),
|
||||||
|
expiresAt: timestamp("expires_at"),
|
||||||
|
});
|
||||||
|
|
||||||
export const userRelations = relations(user, ({ many }) => ({
|
export const userRelations = relations(user, ({ many }) => ({
|
||||||
sessions: many(session),
|
sessions: many(session),
|
||||||
accounts: many(account),
|
accounts: many(account),
|
||||||
|
|||||||
@@ -2,4 +2,14 @@ import tailwindcss from '@tailwindcss/vite';
|
|||||||
import { sveltekit } from '@sveltejs/kit/vite';
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
export default defineConfig({ plugins: [tailwindcss(), sveltekit()] });
|
export default defineConfig({
|
||||||
|
plugins: [tailwindcss(), sveltekit()],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
// '/api': {
|
||||||
|
// target: 'http://localhost:8080',
|
||||||
|
// changeOrigin: true
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user