commit
9ae6d88c21
24 changed files with 7589 additions and 0 deletions
275
src/auth.rs
Normal file
275
src/auth.rs
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
use crate::config::Config;
|
||||
use crate::entities::user;
|
||||
use anyhow::{Context, Result};
|
||||
use axum::{
|
||||
extract::{FromRef, FromRequestParts},
|
||||
http::{header, request::Parts},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use jsonwebtoken::{DecodingKey, Validation, decode, decode_header};
|
||||
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[derive(Clone, FromRef)]
|
||||
pub struct AppState {
|
||||
pub db: DatabaseConnection,
|
||||
pub oidc: OidcClient,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct Jwk {
|
||||
pub kid: String,
|
||||
pub n: String,
|
||||
pub e: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
struct Jwks {
|
||||
keys: Vec<Jwk>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct JwksCache {
|
||||
keys: Arc<RwLock<Jwks>>,
|
||||
jwks_url: String,
|
||||
}
|
||||
|
||||
impl JwksCache {
|
||||
pub async fn new(jwks_url: String) -> Result<Self> {
|
||||
let cache = Self {
|
||||
keys: Arc::new(RwLock::new(Jwks { keys: vec![] })),
|
||||
jwks_url,
|
||||
};
|
||||
cache.refresh().await?;
|
||||
Ok(cache)
|
||||
}
|
||||
|
||||
pub async fn refresh(&self) -> Result<()> {
|
||||
let jwks: Jwks = reqwest::get(&self.jwks_url)
|
||||
.await?
|
||||
.json()
|
||||
.await
|
||||
.context("Failed to fetch JWKS")?;
|
||||
|
||||
let mut keys = self.keys.write().await;
|
||||
*keys = jwks;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_key(&self, kid: &str) -> Option<Jwk> {
|
||||
let keys = self.keys.read().await;
|
||||
keys.keys.iter().find(|k| k.kid == kid).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OidcClient {
|
||||
pub client_id: String,
|
||||
pub client_secret: String,
|
||||
pub issuer: String,
|
||||
pub auth_url: String,
|
||||
pub token_url: String,
|
||||
pub redirect_uri: String,
|
||||
pub jwks_cache: JwksCache,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct DiscoveryConfig {
|
||||
authorization_endpoint: String,
|
||||
token_endpoint: String,
|
||||
jwks_uri: String,
|
||||
}
|
||||
|
||||
impl OidcClient {
|
||||
pub async fn new(config: &Config) -> Result<Self> {
|
||||
let issuer = config.oidc_issuer_url.trim_end_matches('/');
|
||||
let discovery_url = format!("{}/.well-known/openid-configuration", issuer);
|
||||
|
||||
// Manual discovery using reqwest
|
||||
let discovery: DiscoveryConfig = reqwest::get(&discovery_url)
|
||||
.await?
|
||||
.json()
|
||||
.await
|
||||
.context("Failed to fetch OIDC discovery document")?;
|
||||
|
||||
let jwks_cache = JwksCache::new(discovery.jwks_uri).await?;
|
||||
|
||||
Ok(Self {
|
||||
client_id: config.oidc_client_id.clone(),
|
||||
client_secret: config.oidc_client_secret.clone(),
|
||||
issuer: config.oidc_issuer_url.clone(),
|
||||
auth_url: discovery.authorization_endpoint,
|
||||
token_url: discovery.token_endpoint,
|
||||
redirect_uri: config.oidc_redirect_uri.clone(),
|
||||
jwks_cache,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn auth_url(&self) -> (String, String, String) {
|
||||
let state = uuid::Uuid::new_v4().to_string();
|
||||
let nonce = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
let url = format!(
|
||||
"{}?response_type=code&client_id={}&redirect_uri={}&scope=openid%20profile%20email%20offline_access&state={}&nonce={}",
|
||||
self.auth_url,
|
||||
urlencoding::encode(&self.client_id),
|
||||
urlencoding::encode(&self.redirect_uri),
|
||||
state,
|
||||
nonce
|
||||
);
|
||||
|
||||
(url, state, nonce)
|
||||
}
|
||||
|
||||
pub async fn exchange_code(&self, code: String) -> Result<TokenResponse> {
|
||||
let client = reqwest::Client::new();
|
||||
let res = client
|
||||
.post(&self.token_url)
|
||||
.form(&[
|
||||
("grant_type", "authorization_code"),
|
||||
("code", &code),
|
||||
("client_id", &self.client_id),
|
||||
("client_secret", &self.client_secret),
|
||||
("redirect_uri", &self.redirect_uri),
|
||||
])
|
||||
.send()
|
||||
.await?
|
||||
.json::<serde_json::Value>()
|
||||
.await?;
|
||||
|
||||
let id_token = res["id_token"]
|
||||
.as_str()
|
||||
.context("No ID token in response")?
|
||||
.to_string();
|
||||
let refresh_token = res["refresh_token"].as_str().map(|s| s.to_string());
|
||||
|
||||
Ok(TokenResponse {
|
||||
id_token,
|
||||
refresh_token,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn refresh_token(&self, refresh_token: String) -> Result<TokenResponse> {
|
||||
let client = reqwest::Client::new();
|
||||
let res = client
|
||||
.post(&self.token_url)
|
||||
.form(&[
|
||||
("grant_type", "refresh_token"),
|
||||
("refresh_token", &refresh_token),
|
||||
("client_id", &self.client_id),
|
||||
("client_secret", &self.client_secret),
|
||||
])
|
||||
.send()
|
||||
.await?
|
||||
.json::<serde_json::Value>()
|
||||
.await?;
|
||||
|
||||
let id_token = res["id_token"]
|
||||
.as_str()
|
||||
.context("No ID token in refresh response")?
|
||||
.to_string();
|
||||
let refresh_token = res["refresh_token"]
|
||||
.as_str()
|
||||
.map(|s| s.to_string())
|
||||
.or(Some(refresh_token)); // Keep old one if new one not provided
|
||||
|
||||
Ok(TokenResponse {
|
||||
id_token,
|
||||
refresh_token,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn validate_token(&self, token: &str) -> Result<Claims> {
|
||||
let header = decode_header(token).context("Failed to decode token header")?;
|
||||
let kid = header.kid.context("No kid in token header")?;
|
||||
|
||||
let jwk = self
|
||||
.jwks_cache
|
||||
.get_key(&kid)
|
||||
.await
|
||||
.context("Key not found in JWKS")?;
|
||||
|
||||
let decoding_key = DecodingKey::from_rsa_components(&jwk.n, &jwk.e)
|
||||
.context("Failed to create decoding key")?;
|
||||
|
||||
let mut validation = Validation::new(header.alg);
|
||||
validation.validate_exp = true;
|
||||
validation.required_spec_claims.insert("sub".to_string());
|
||||
validation.validate_aud = false;
|
||||
|
||||
let token_data = decode::<Claims>(token, &decoding_key, &validation)
|
||||
.context("Failed to validate token")?;
|
||||
|
||||
Ok(token_data.claims)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct TokenResponse {
|
||||
pub id_token: String,
|
||||
pub refresh_token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Claims {
|
||||
pub sub: String,
|
||||
pub email: Option<String>,
|
||||
pub name: Option<String>,
|
||||
pub exp: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct CurrentUser {
|
||||
pub id: i32,
|
||||
pub sub: String,
|
||||
pub email: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl<S> FromRequestParts<S> for CurrentUser
|
||||
where
|
||||
AppState: FromRef<S>,
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = Response;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let state = AppState::from_ref(state);
|
||||
let db = state.db;
|
||||
let oidc = state.oidc;
|
||||
|
||||
let auth_header = parts
|
||||
.headers
|
||||
.get(header::AUTHORIZATION)
|
||||
.and_then(|h| h.to_str().ok());
|
||||
|
||||
if let Some(auth_header) = auth_header {
|
||||
if auth_header.starts_with("Bearer ") {
|
||||
let token = &auth_header[7..];
|
||||
if let Ok(claims) = oidc.validate_token(token).await {
|
||||
let user = user::Entity::find()
|
||||
.filter(user::Column::Sub.eq(&claims.sub))
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
|
||||
.into_response()
|
||||
})?;
|
||||
|
||||
if let Some(user) = user {
|
||||
return Ok(CurrentUser {
|
||||
id: user.id,
|
||||
sub: user.sub,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err((axum::http::StatusCode::UNAUTHORIZED, "Unauthorized").into_response())
|
||||
}
|
||||
}
|
||||
39
src/config.rs
Normal file
39
src/config.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
use dotenvy::dotenv;
|
||||
use std::env;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Config {
|
||||
pub database_url: String,
|
||||
pub port: u16,
|
||||
pub oidc_client_id: String,
|
||||
pub oidc_client_secret: String,
|
||||
pub oidc_issuer_url: String,
|
||||
pub oidc_redirect_uri: String,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn from_env() -> Self {
|
||||
dotenv().ok();
|
||||
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||
let port = env::var("PORT")
|
||||
.unwrap_or_else(|_| "3000".to_string())
|
||||
.parse::<u16>()
|
||||
.expect("PORT must be a valid u16");
|
||||
|
||||
let oidc_client_id = env::var("OIDC_CLIENT_ID").expect("OIDC_CLIENT_ID must be set");
|
||||
let oidc_client_secret =
|
||||
env::var("OIDC_CLIENT_SECRET").expect("OIDC_CLIENT_SECRET must be set");
|
||||
let oidc_issuer_url = env::var("OIDC_ISSUER_URL").expect("OIDC_ISSUER_URL must be set");
|
||||
let oidc_redirect_uri =
|
||||
env::var("OIDC_REDIRECT_URI").expect("OIDC_REDIRECT_URI must be set");
|
||||
|
||||
Config {
|
||||
database_url,
|
||||
port,
|
||||
oidc_client_id,
|
||||
oidc_client_secret,
|
||||
oidc_issuer_url,
|
||||
oidc_redirect_uri,
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/db.rs
Normal file
7
src/db.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
use sea_orm::{Database, DatabaseConnection, DbErr};
|
||||
use crate::config::Config;
|
||||
|
||||
pub async fn connect(config: &Config) -> Result<DatabaseConnection, DbErr> {
|
||||
let db = Database::connect(&config.database_url).await?;
|
||||
Ok(db)
|
||||
}
|
||||
31
src/entities/event.rs
Normal file
31
src/entities/event.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "event")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i64,
|
||||
pub user_id: Option<i32>,
|
||||
pub name: String,
|
||||
pub from: DateTime,
|
||||
pub to: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user::Entity",
|
||||
from = "Column::UserId",
|
||||
to = "super::user::Column::Id"
|
||||
)]
|
||||
User,
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
2
src/entities/mod.rs
Normal file
2
src/entities/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod event;
|
||||
pub mod user;
|
||||
27
src/entities/user.rs
Normal file
27
src/entities/user.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "user")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
#[sea_orm(unique)]
|
||||
pub sub: String,
|
||||
pub email: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::event::Entity")]
|
||||
Event,
|
||||
}
|
||||
|
||||
impl Related<super::event::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Event.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
119
src/handlers/auth.rs
Normal file
119
src/handlers/auth.rs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
use crate::auth::AppState;
|
||||
use crate::entities::user;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Query, State},
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AuthCallback {
|
||||
pub code: String,
|
||||
#[allow(dead_code)]
|
||||
pub state: String,
|
||||
}
|
||||
|
||||
pub async fn login(State(state): State<AppState>) -> Response {
|
||||
let oidc = state.oidc;
|
||||
let (auth_url, _csrf_token, _nonce) = oidc.auth_url();
|
||||
Redirect::to(&auth_url).into_response()
|
||||
}
|
||||
|
||||
pub async fn callback(
|
||||
State(state): State<AppState>,
|
||||
Query(params): Query<AuthCallback>,
|
||||
) -> Response {
|
||||
let oidc = state.oidc;
|
||||
let db = state.db;
|
||||
|
||||
match oidc.exchange_code(params.code).await {
|
||||
Ok(tokens) => match oidc.validate_token(&tokens.id_token).await {
|
||||
Ok(claims) => {
|
||||
let sub = claims.sub;
|
||||
let email = claims.email.unwrap_or_default();
|
||||
let name = claims.name.unwrap_or_default();
|
||||
|
||||
let existing_user = user::Entity::find()
|
||||
.filter(user::Column::Sub.eq(&sub))
|
||||
.one(&db)
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
|
||||
if existing_user.is_none() {
|
||||
let new_user = user::ActiveModel {
|
||||
sub: Set(sub),
|
||||
email: Set(email),
|
||||
name: Set(name),
|
||||
..Default::default()
|
||||
};
|
||||
new_user.insert(&db).await.unwrap();
|
||||
};
|
||||
|
||||
let mut redirect_url = format!("/#access_token={}", tokens.id_token);
|
||||
if let Some(refresh_token) = tokens.refresh_token {
|
||||
redirect_url.push_str(&format!("&refresh_token={}", refresh_token));
|
||||
}
|
||||
|
||||
Redirect::to(&redirect_url).into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Token validation error: {:?}", e);
|
||||
(
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Invalid token",
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("OIDC callback error: {:?}", e);
|
||||
(
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Authentication failed",
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn refresh(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<RefreshRequest>,
|
||||
) -> Response {
|
||||
let oidc = state.oidc;
|
||||
|
||||
match oidc.refresh_token(payload.refresh_token).await {
|
||||
Ok(tokens) => Json(tokens).into_response(),
|
||||
Err(e) => {
|
||||
tracing::error!("Token refresh error: {:?}", e);
|
||||
(axum::http::StatusCode::UNAUTHORIZED, "Refresh failed").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RefreshRequest {
|
||||
pub refresh_token: String,
|
||||
}
|
||||
|
||||
pub async fn logout() -> Response {
|
||||
Redirect::to("/").into_response()
|
||||
}
|
||||
|
||||
pub async fn me(
|
||||
crate::auth::CurrentUser {
|
||||
id,
|
||||
sub,
|
||||
email,
|
||||
name,
|
||||
}: crate::auth::CurrentUser,
|
||||
) -> Json<crate::auth::CurrentUser> {
|
||||
Json(crate::auth::CurrentUser {
|
||||
id,
|
||||
sub,
|
||||
email,
|
||||
name,
|
||||
})
|
||||
}
|
||||
148
src/handlers/event.rs
Normal file
148
src/handlers/event.rs
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
use crate::auth::{AppState, CurrentUser};
|
||||
use crate::entities::{event, event::Entity as Event};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
};
|
||||
use sea_orm::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct CreateEventRequest {
|
||||
pub name: String,
|
||||
pub from: String,
|
||||
pub to: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ListEventsQuery {
|
||||
pub upcoming: Option<bool>,
|
||||
}
|
||||
|
||||
fn parse_datetime(dt_str: &str) -> Result<chrono::NaiveDateTime, StatusCode> {
|
||||
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(dt_str, "%Y-%m-%dT%H:%M") {
|
||||
return Ok(dt);
|
||||
}
|
||||
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(dt_str, "%Y-%m-%dT%H:%M:%S") {
|
||||
return Ok(dt);
|
||||
}
|
||||
dt_str.parse().map_err(|_| StatusCode::BAD_REQUEST)
|
||||
}
|
||||
|
||||
pub async fn create_event(
|
||||
State(state): State<AppState>,
|
||||
CurrentUser { id, .. }: CurrentUser,
|
||||
Json(payload): Json<CreateEventRequest>,
|
||||
) -> Result<Json<event::Model>, StatusCode> {
|
||||
let db = state.db;
|
||||
let from = parse_datetime(&payload.from)?;
|
||||
let to = parse_datetime(&payload.to)?;
|
||||
|
||||
let new_event = event::ActiveModel {
|
||||
name: Set(payload.name),
|
||||
from: Set(from),
|
||||
to: Set(to),
|
||||
user_id: Set(Some(id)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = new_event.insert(&db).await.map_err(|e| {
|
||||
tracing::error!("Failed to create event: {:?}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
tracing::info!("Created event {} for user {}", result.id, id);
|
||||
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
pub async fn list_events(
|
||||
State(state): State<AppState>,
|
||||
CurrentUser { id, .. }: CurrentUser,
|
||||
Query(query): Query<ListEventsQuery>,
|
||||
) -> Result<Json<Vec<event::Model>>, StatusCode> {
|
||||
let db = state.db;
|
||||
let mut find = Event::find().filter(event::Column::UserId.eq(id));
|
||||
|
||||
if let Some(true) = query.upcoming {
|
||||
let now = chrono::Utc::now().naive_utc();
|
||||
find = find.filter(event::Column::From.gte(now));
|
||||
}
|
||||
|
||||
let events = find
|
||||
.all(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(Json(events))
|
||||
}
|
||||
|
||||
pub async fn get_event(
|
||||
State(state): State<AppState>,
|
||||
CurrentUser { id, .. }: CurrentUser,
|
||||
Path(event_id): Path<i32>,
|
||||
) -> Result<Json<event::Model>, StatusCode> {
|
||||
let db = state.db;
|
||||
let event = Event::find_by_id(event_id)
|
||||
.filter(event::Column::UserId.eq(id))
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
.ok_or(StatusCode::NOT_FOUND)?;
|
||||
|
||||
Ok(Json(event))
|
||||
}
|
||||
|
||||
pub async fn update_event(
|
||||
State(state): State<AppState>,
|
||||
CurrentUser { id, .. }: CurrentUser,
|
||||
Path(event_id): Path<i32>,
|
||||
Json(payload): Json<CreateEventRequest>,
|
||||
) -> Result<Json<event::Model>, StatusCode> {
|
||||
let db = state.db;
|
||||
let from = parse_datetime(&payload.from)?;
|
||||
let to = parse_datetime(&payload.to)?;
|
||||
|
||||
let event = Event::find_by_id(event_id)
|
||||
.filter(event::Column::UserId.eq(id))
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
.ok_or(StatusCode::NOT_FOUND)?;
|
||||
|
||||
let mut event: event::ActiveModel = event.into();
|
||||
event.name = Set(payload.name);
|
||||
event.from = Set(from);
|
||||
event.to = Set(to);
|
||||
|
||||
let result = event.update(&db).await.map_err(|e| {
|
||||
tracing::error!("Failed to update event {}: {:?}", event_id, e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
tracing::info!("Updated event {} for user {}", event_id, id);
|
||||
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
pub async fn delete_event(
|
||||
State(state): State<AppState>,
|
||||
CurrentUser { id, .. }: CurrentUser,
|
||||
Path(event_id): Path<i32>,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
let db = state.db;
|
||||
let event = Event::find_by_id(event_id)
|
||||
.filter(event::Column::UserId.eq(id))
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
.ok_or(StatusCode::NOT_FOUND)?;
|
||||
|
||||
event
|
||||
.delete(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
2
src/handlers/mod.rs
Normal file
2
src/handlers/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod auth;
|
||||
pub mod event;
|
||||
58
src/main.rs
Normal file
58
src/main.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
mod auth;
|
||||
mod config;
|
||||
mod db;
|
||||
mod entities;
|
||||
mod handlers;
|
||||
mod routes;
|
||||
|
||||
use crate::auth::{AppState, OidcClient};
|
||||
use crate::config::Config;
|
||||
use crate::routes::create_router;
|
||||
use migration::MigratorTrait;
|
||||
use std::net::SocketAddr;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Initialize tracing
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
|
||||
// Load configuration
|
||||
let config = Config::from_env();
|
||||
|
||||
// Connect to database
|
||||
let db = db::connect(&config)
|
||||
.await
|
||||
.expect("Failed to connect to database");
|
||||
tracing::info!("Connected to database");
|
||||
|
||||
// Run migrations
|
||||
migration::Migrator::up(&db, None)
|
||||
.await
|
||||
.expect("Failed to run migrations");
|
||||
tracing::info!("Migrations completed");
|
||||
|
||||
// Initialize OIDC client
|
||||
let oidc = OidcClient::new(&config)
|
||||
.await
|
||||
.expect("Failed to initialize OIDC client");
|
||||
|
||||
// Create AppState
|
||||
let state = AppState {
|
||||
db: db.clone(),
|
||||
oidc,
|
||||
};
|
||||
|
||||
// Create router
|
||||
let app = create_router(state)
|
||||
.layer(tower_http::cors::CorsLayer::permissive())
|
||||
.fallback_service(tower_http::services::ServeDir::new("static"));
|
||||
|
||||
// Start server
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], config.port));
|
||||
tracing::info!("listening on {}", addr);
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
||||
29
src/routes.rs
Normal file
29
src/routes.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
use crate::auth::AppState;
|
||||
use crate::handlers::{auth, event};
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{get, post},
|
||||
};
|
||||
|
||||
pub fn create_router(state: AppState) -> Router {
|
||||
// Explicitly set the router state type to AppState
|
||||
Router::<AppState>::new()
|
||||
.route("/health", get(health_check))
|
||||
.route("/auth/login", get(auth::login))
|
||||
.route("/auth/callback", get(auth::callback))
|
||||
.route("/auth/logout", get(auth::logout))
|
||||
.route("/auth/me", get(auth::me))
|
||||
.route("/auth/refresh", post(auth::refresh))
|
||||
.route("/events", post(event::create_event).get(event::list_events))
|
||||
.route(
|
||||
"/events/{id}",
|
||||
get(event::get_event)
|
||||
.put(event::update_event)
|
||||
.delete(event::delete_event),
|
||||
)
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
async fn health_check() -> &'static str {
|
||||
"OK"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue