use crate::{errors::AppError, state::AppState};
use async_graphql::{Context, Error, FieldResult, InputObject, SimpleObject};
use axum::{async_trait, extract::FromRequestParts, http::request::Parts};
use axum_extra::{
headers::{authorization::Bearer, Authorization},
typed_header::TypedHeader,
};
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),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Claims {
pub user_id: i32,
exp: usize,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum Authentication {
Logged(Claims),
NotLogged,
}
impl Claims {
pub fn new(user_id: i32) -> Self {
let expiration = Local::now() + Duration::days(2);
Self {
user_id,
exp: expiration.timestamp() as usize,
}
}
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,
}
#[derive(Serialize, SimpleObject)]
pub struct AuthBody {
access_token: String,
token_type: String,
user_id: i32,
}
impl AuthBody {
pub fn new(access_token: String, user_id: i32) -> Self {
Self {
access_token,
token_type: "Bearer".to_string(),
user_id,
}
}
}
#[async_trait]
impl<S> FromRequestParts<S> for Authentication
where
S: Send + Sync,
{
type Rejection = AppError;
async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
match TypedHeader::<Authorization<Bearer>>::from_request_parts(parts, &()).await {
Ok(TypedHeader(Authorization(bearer))) => {
let token_data =
decode::<Claims>(bearer.token(), &KEYS.decoding, &Validation::default())
.map_err(|err| match err.kind() {
jsonwebtoken::errors::ErrorKind::ExpiredSignature => {
AppError::InvalidToken
}
_ => {
tracing::error!("{err:?}");
AppError::Unauthorized
}
})?;
Ok(Self::Logged(token_data.claims))
}
Err(_) => Ok(Self::NotLogged),
}
}
}
pub mod mutations {
use super::*;
pub async fn login<'ctx>(
ctx: &Context<'ctx>,
input: LoginCredentials,
) -> FieldResult<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?;
let id: Vec<i32> = rows.iter().map(|row| row.get(0)).collect();
if id.len() == 1 {
let claims = Claims::new(id[0]);
let token = claims.get_token()?;
Ok(AuthBody::new(token, id[0]))
} else {
Err(Error::new("Invalid email or password"))
}
}
}