diff options
author | Santo Cariotti <santo@dcariotti.me> | 2024-08-21 13:25:55 +0200 |
---|---|---|
committer | Santo Cariotti <santo@dcariotti.me> | 2024-08-21 13:25:55 +0200 |
commit | a92fb07d23fb2268a6f4e650c5cbd00ad993e760 (patch) | |
tree | 149f2d084b8fec0b5a2bc5364f5d9e3487c94971 /src | |
parent | 24388ba81515c57e812994fdb9147e6de7f3a5b6 (diff) |
Add login
Fields sent are
```
{
"query": "mutation Login($input: LoginCredentials!) { login(input: $input) { accessToken tokenType } }",
"variables": {
"input": {
"email": "....",
"password": "..."
}
}
}
```
Diffstat (limited to 'src')
-rw-r--r-- | src/graphql/mod.rs | 1 | ||||
-rw-r--r-- | src/graphql/mutation.rs | 35 | ||||
-rw-r--r-- | src/graphql/routes.rs | 7 | ||||
-rw-r--r-- | src/graphql/types/jwt.rs | 79 | ||||
-rw-r--r-- | src/graphql/types/mod.rs | 1 | ||||
-rw-r--r-- | src/main.rs | 12 |
6 files changed, 128 insertions, 7 deletions
diff --git a/src/graphql/mod.rs b/src/graphql/mod.rs index b394fc1..425faca 100644 --- a/src/graphql/mod.rs +++ b/src/graphql/mod.rs @@ -1,3 +1,4 @@ +pub mod mutation; pub mod query; pub mod routes; pub mod types; diff --git a/src/graphql/mutation.rs b/src/graphql/mutation.rs new file mode 100644 index 0000000..9321653 --- /dev/null +++ b/src/graphql/mutation.rs @@ -0,0 +1,35 @@ +use crate::graphql::types::jwt; +use crate::state::AppState; +use async_graphql::{Context, Error, FieldResult, Object}; + +pub struct Mutation; + +#[Object] +impl Mutation { + async fn login<'ctx>( + &self, + ctx: &Context<'ctx>, + input: jwt::LoginCredentials, + ) -> FieldResult<jwt::AuthBody> { + let state = ctx.data::<AppState>().expect("Can't connect to db"); + let client = &*state.client; + + let password = sha256::digest(input.password); + let rows = client + .query( + "SELECT id FROM users WHERE email = $1 AND password = $2", + &[&input.email, &password], + ) + .await + .unwrap(); + + let id: Vec<i32> = rows.iter().map(|row| row.get(0)).collect(); + if id.len() == 1 { + let claims = jwt::Claims::new(id[0]); + let token = claims.get_token().unwrap(); + Ok(jwt::AuthBody::new(token)) + } else { + Err(Error::new("Invalid email or password")) + } + } +} diff --git a/src/graphql/routes.rs b/src/graphql/routes.rs index 2380760..e15267e 100644 --- a/src/graphql/routes.rs +++ b/src/graphql/routes.rs @@ -1,10 +1,11 @@ -use crate::graphql::query::*; -use async_graphql::{EmptyMutation, EmptySubscription, Schema}; +use crate::graphql::mutation::Mutation; +use crate::graphql::query::Query; +use async_graphql::{EmptySubscription, Schema}; use async_graphql_axum::{GraphQLRequest, GraphQLResponse}; use std::sync::Arc; pub async fn graphql_handler( - schema: Arc<Schema<Query, EmptyMutation, EmptySubscription>>, + schema: Arc<Schema<Query, Mutation, EmptySubscription>>, req: GraphQLRequest, ) -> GraphQLResponse { schema.execute(req.into_inner()).await.into() diff --git a/src/graphql/types/jwt.rs b/src/graphql/types/jwt.rs new file mode 100644 index 0000000..932f7fd --- /dev/null +++ b/src/graphql/types/jwt.rs @@ -0,0 +1,79 @@ +use crate::errors::AppError; +use async_graphql::{InputObject, SimpleObject}; +use chrono::{Duration, Local}; +use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; + +struct Keys { + encoding: EncodingKey, + _decoding: DecodingKey, +} + +static KEYS: Lazy<Keys> = Lazy::new(|| { + let secret = &crate::config::CONFIG.jwt_secret; + Keys::new(secret.as_bytes()) +}); + +impl Keys { + fn new(secret: &[u8]) -> Self { + Self { + encoding: EncodingKey::from_secret(secret), + _decoding: DecodingKey::from_secret(secret), + } + } +} + +/// Claims struct +#[derive(Serialize, Deserialize)] +pub struct Claims { + /// ID from the user model + pub user_id: i32, + /// Expiration timestamp + exp: usize, +} + +impl Claims { + /// Create a new Claim using the `user_id` and the current timestamp + 2 days + pub fn new(user_id: i32) -> Self { + let expiration = Local::now() + Duration::days(2); + + Self { + user_id, + exp: expiration.timestamp() as usize, + } + } + + /// Returns the token as a string. If a token is not encoded, raises an + /// `AppError::TokenCreation` + pub fn get_token(&self) -> Result<String, AppError> { + let token = encode(&Header::default(), &self, &KEYS.encoding) + .map_err(|_| AppError::TokenCreation)?; + + Ok(token) + } +} + +#[derive(InputObject, Debug)] +pub struct LoginCredentials { + pub email: String, + pub password: String, +} + +/// Body used as response to login +#[derive(Serialize, SimpleObject)] +pub struct AuthBody { + /// Access token string + access_token: String, + /// "Bearer" string + token_type: String, +} + +impl AuthBody { + pub fn new(access_token: String) -> Self { + Self { + access_token, + token_type: "Bearer".to_string(), + } + } +} diff --git a/src/graphql/types/mod.rs b/src/graphql/types/mod.rs index 22d12a3..d7cdece 100644 --- a/src/graphql/types/mod.rs +++ b/src/graphql/types/mod.rs @@ -1 +1,2 @@ +pub mod jwt; pub mod user; diff --git a/src/main.rs b/src/main.rs index e87a7c4..04e2564 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ mod state; use std::{net::SocketAddr, sync::Arc, time::Duration}; use crate::config::CONFIG; -use async_graphql::{EmptyMutation, EmptySubscription, Schema}; +use async_graphql::{EmptySubscription, Schema}; use axum::{ http::{header, Request}, routing::post, @@ -30,9 +30,13 @@ async fn create_app() -> Router { client: Arc::new(dbclient), }; - let schema = Schema::build(graphql::query::Query, EmptyMutation, EmptySubscription) - .data(state.clone()) - .finish(); + let schema = Schema::build( + graphql::query::Query, + graphql::mutation::Mutation, + EmptySubscription, + ) + .data(state.clone()) + .finish(); Router::new() .route( "/graphql", |