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]
|
||||
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,10 +1,52 @@
|
||||
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]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
HttpServer::new(|| App::new())
|
||||
let reqwest_client = reqwest::Client::new();
|
||||
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())),
|
||||
});
|
||||
|
||||
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,
|
||||
"tag": "0000_many_joseph",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1768455615596,
|
||||
"tag": "0001_abnormal_swordsman",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -10,9 +10,7 @@
|
||||
const query = createQuery(() => ({
|
||||
queryKey: ['github-repositories'],
|
||||
queryFn: async () => {
|
||||
const response = await fetch('https://api.github.com/user/repos?affiliation=owner', {
|
||||
headers: {}
|
||||
});
|
||||
const response = await fetch(`/api/${$session.data?.user.id}/repos`);
|
||||
return await response.json();
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { jwt } from 'better-auth/plugins';
|
||||
import { createAuthClient } from 'better-auth/svelte';
|
||||
|
||||
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 { db } from './db/drizzle';
|
||||
import * as authSchema from './db/auth-schema';
|
||||
import { jwt } from 'better-auth/plugins';
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
provider: 'pg',
|
||||
schema: { ...authSchema }
|
||||
}),
|
||||
plugins: [jwt()],
|
||||
socialProviders: {
|
||||
github: {
|
||||
clientId: process.env.GH_CLIENT_ID!,
|
||||
|
||||
@@ -73,6 +73,14 @@ export const verification = pgTable(
|
||||
(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 }) => ({
|
||||
sessions: many(session),
|
||||
accounts: many(account),
|
||||
|
||||
@@ -2,4 +2,14 @@ import tailwindcss from '@tailwindcss/vite';
|
||||
import { sveltekit } from '@sveltejs/kit/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