Skip to content

Commit b97ece3

Browse files
metalwhalesontdhust
authored andcommitted
Create "client" tables. Add a route for authentication and a middleware for authorization.
1 parent 22a547f commit b97ece3

File tree

13 files changed

+560
-14
lines changed

13 files changed

+560
-14
lines changed

chloria-backend/Cargo.lock

Lines changed: 331 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

chloria-backend/chloria-api/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,13 @@ version = "0.1.0"
44
edition = "2021"
55

66
[dependencies]
7+
anyhow = "1.0.96"
8+
axum = "0.8.1"
9+
axum-extra = { version = "0.10.0", features = ["typed-header"] }
710
diesel = { version = "2.2.7", features = ["postgres"] }
11+
jsonwebtoken = "9.3.1"
12+
serde = "1.0.218"
13+
serde_json = "1.0.140"
14+
serde_with = "3.12.0"
15+
tokio = { version = "1.43.0", features = ["rt-multi-thread"] }
16+
tower = "0.5.2"
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
use std::time::{SystemTime, UNIX_EPOCH};
2+
3+
use anyhow::Result;
4+
use axum::{
5+
extract::{Request, State},
6+
http::StatusCode,
7+
middleware::Next,
8+
response::Response,
9+
Json, RequestExt,
10+
};
11+
use axum_extra::{
12+
headers::{authorization::Bearer, Authorization},
13+
TypedHeader,
14+
};
15+
use jsonwebtoken::{decode, encode, Header, Validation};
16+
use serde::{Deserialize, Serialize};
17+
18+
use super::{super::state::RouterState, ErrorResponse};
19+
20+
#[derive(Deserialize)]
21+
pub(in super::super) struct AuthenticateRequest {}
22+
23+
#[derive(Serialize)]
24+
pub(in super::super) struct AuthenticateResponse {
25+
token: String,
26+
}
27+
28+
#[derive(Deserialize, Serialize)]
29+
struct Claim {
30+
exp: u64,
31+
}
32+
33+
pub(in super::super) async fn authenticate(
34+
State(state): State<RouterState>,
35+
Json(request): Json<AuthenticateRequest>,
36+
) -> Result<Json<AuthenticateResponse>, (StatusCode, Json<ErrorResponse>)> {
37+
let claim = Claim {
38+
exp: SystemTime::now()
39+
.duration_since(UNIX_EPOCH)
40+
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(e.to_string().into())))?
41+
.as_secs()
42+
+ state.jwt.lifetime,
43+
};
44+
let token = encode(&Header::default(), &claim, &state.jwt.encoding_key)
45+
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(e.to_string().into())))?;
46+
Ok(Json(AuthenticateResponse { token }))
47+
}
48+
49+
pub(in super::super) async fn authorize(
50+
State(state): State<RouterState>,
51+
mut request: Request,
52+
next: Next,
53+
) -> Result<Response, (StatusCode, Json<ErrorResponse>)> {
54+
let TypedHeader(Authorization(bearer)) = request
55+
.extract_parts::<TypedHeader<Authorization<Bearer>>>()
56+
.await
57+
.map_err(|r| (StatusCode::BAD_REQUEST, Json(r.to_string().into())))?;
58+
decode::<Claim>(bearer.token(), &state.jwt.decoding_key, &Validation::default()).map_err(|error| {
59+
(
60+
StatusCode::UNAUTHORIZED,
61+
Json(ErrorResponse {
62+
code: Some(error.to_string()),
63+
..Default::default()
64+
}),
65+
)
66+
})?;
67+
let response = next.run(request).await;
68+
Ok(response)
69+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
pub(super) mod auth;
2+
3+
use serde::Serialize;
4+
use serde_json::Value;
5+
6+
#[serde_with::skip_serializing_none]
7+
#[derive(Default, Serialize)]
8+
pub(super) struct ErrorResponse {
9+
code: Option<String>,
10+
reason: Option<String>,
11+
extra: Option<Value>,
12+
}
13+
14+
impl From<String> for ErrorResponse {
15+
fn from(value: String) -> Self {
16+
Self {
17+
reason: Some(value),
18+
..Default::default()
19+
}
20+
}
21+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
mod adapters;
2+
pub(crate) mod router;
3+
mod state;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
use axum::{
2+
middleware,
3+
routing::{get, post},
4+
Router,
5+
};
6+
use jsonwebtoken::{DecodingKey, EncodingKey};
7+
use tower::ServiceBuilder;
8+
9+
use super::{
10+
adapters::auth::{authenticate, authorize},
11+
state::{RouterState, RouterStateJwt},
12+
};
13+
14+
pub(crate) struct RouterConfig {
15+
pub(crate) jwt_key: String,
16+
pub(crate) jwt_lifetime: u64,
17+
}
18+
19+
pub(crate) fn new(config: RouterConfig) -> Router {
20+
let state = RouterState {
21+
jwt: RouterStateJwt {
22+
decoding_key: DecodingKey::from_secret(config.jwt_key.as_bytes()),
23+
encoding_key: EncodingKey::from_secret(config.jwt_key.as_bytes()),
24+
lifetime: config.jwt_lifetime,
25+
},
26+
};
27+
let public_router = Router::new()
28+
.route("/authenticate", post(authenticate))
29+
.with_state(state.clone());
30+
let authorized_router = Router::new()
31+
.route("/news", get(read_news))
32+
.route_layer(ServiceBuilder::new().layer(middleware::from_fn_with_state(state, authorize)));
33+
let router = Router::new().merge(public_router).merge(authorized_router);
34+
router
35+
}
36+
37+
async fn read_news() {}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
use jsonwebtoken::{DecodingKey, EncodingKey};
2+
3+
#[derive(Clone)]
4+
pub(super) struct RouterState {
5+
pub(super) jwt: RouterStateJwt,
6+
}
7+
8+
#[derive(Clone)]
9+
pub(super) struct RouterStateJwt {
10+
pub(super) decoding_key: DecodingKey,
11+
pub(super) encoding_key: EncodingKey,
12+
pub(super) lifetime: u64,
13+
}
Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
1+
mod interface;
12
mod schema;
23

3-
fn main() {
4-
println!("Hello, world!");
4+
use std::env;
5+
6+
use anyhow::Result;
7+
use tokio::net::TcpListener;
8+
9+
use crate::interface::router::{self, RouterConfig};
10+
11+
#[tokio::main]
12+
async fn main() -> Result<()> {
13+
// Read env vars
14+
let chloria_jwt_key = env::var("CHLORIA_JWT_KEY")?;
15+
let chloria_jwt_lifetime = env::var("CHLORIA_JWT_LIFETIME")?.parse()?; // In seconds
16+
let chloria_api_port: i32 = env::var("CHLORIA_API_PORT")?.parse()?;
17+
// Initialize interface
18+
let router = router::new(RouterConfig {
19+
jwt_key: chloria_jwt_key,
20+
jwt_lifetime: chloria_jwt_lifetime,
21+
});
22+
let listener = TcpListener::bind(format!("0.0.0.0:{}", chloria_api_port)).await?;
23+
axum::serve(listener, router).await?;
24+
Ok(())
525
}

chloria-backend/chloria-api/src/schema.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
// @generated automatically by Diesel CLI.
22

3+
diesel::table! {
4+
client_credentials (id) {
5+
id -> Int4,
6+
api_key -> Text,
7+
api_secret -> Text,
8+
created_at -> Timestamptz,
9+
updated_at -> Timestamptz,
10+
}
11+
}
12+
13+
diesel::table! {
14+
clients (id) {
15+
id -> Int4,
16+
authentication_method -> Text,
17+
authentication_registry -> Text,
18+
created_at -> Timestamptz,
19+
updated_at -> Timestamptz,
20+
}
21+
}
22+
323
diesel::table! {
424
news (id) {
525
id -> Int4,
@@ -15,3 +35,7 @@ diesel::table! {
1535
updated_at -> Timestamptz,
1636
}
1737
}
38+
39+
diesel::joinable!(client_credentials -> clients (id));
40+
41+
diesel::allow_tables_to_appear_in_same_query!(client_credentials, clients, news,);
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- This file should undo anything in `up.sql`
2+
3+
DROP TABLE client_credentials;
4+
DROP TABLE clients;

0 commit comments

Comments
 (0)