feat(api): implemented /account/auth
This commit is contained in:
parent
9d01fc2a54
commit
eb6aa532ff
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
- /account
|
- /account
|
||||||
- [X] `POST` /register
|
- [X] `POST` /register
|
||||||
- [ ] `POST` /auth
|
- [X] `POST` /auth
|
||||||
- [ ] `POST` /delete
|
- [ ] `POST` /delete
|
||||||
- [ ] `GET` /blob
|
- [ ] `GET` /blob
|
||||||
- [ ] `POST` /blob
|
- [ ] `POST` /blob
|
||||||
|
|
|
@ -4,6 +4,8 @@ use crate::backend::Backend;
|
||||||
use actix_web::{post, web, HttpResponse, Responder};
|
use actix_web::{post, web, HttpResponse, Responder};
|
||||||
use log::error;
|
use log::error;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct RegisterRequest {
|
struct RegisterRequest {
|
||||||
|
@ -13,7 +15,7 @@ struct RegisterRequest {
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct RegisterResponse {
|
struct RegisterResponse {
|
||||||
uuid: String,
|
userid: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/account/register")]
|
#[post("/account/register")]
|
||||||
|
@ -30,8 +32,39 @@ pub async fn register(
|
||||||
Ok(res) => match res {
|
Ok(res) => match res {
|
||||||
Err(e) => e.into(),
|
Err(e) => e.into(),
|
||||||
Ok(uuid) => HttpResponse::Ok().json(RegisterResponse {
|
Ok(uuid) => HttpResponse::Ok().json(RegisterResponse {
|
||||||
uuid: uuid.to_string(),
|
userid: uuid.to_string(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AuthRequest {
|
||||||
|
userid: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct AuthResponse {
|
||||||
|
token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/account/auth")]
|
||||||
|
pub async fn auth(backend: web::Data<Backend>, body: web::Json<AuthRequest>) -> impl Responder {
|
||||||
|
let body = body.into_inner();
|
||||||
|
let userid = match Uuid::from_str(&body.userid) {
|
||||||
|
Ok(userid) => userid,
|
||||||
|
Err(_) => return HttpResponse::BadRequest().finish(),
|
||||||
|
};
|
||||||
|
|
||||||
|
match backend.authenticate(userid, body.password).await {
|
||||||
|
Err(e) => {
|
||||||
|
error!("{e}");
|
||||||
|
HttpResponse::InternalServerError().finish()
|
||||||
|
}
|
||||||
|
Ok(res) => match res {
|
||||||
|
Err(e) => e.into(),
|
||||||
|
Ok(token) => HttpResponse::Ok().json(AuthResponse { token }),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ pub async fn start(config: &Config, backend: Backend) -> Result<()> {
|
||||||
App::new()
|
App::new()
|
||||||
.app_data(web::Data::new(backend.clone()))
|
.app_data(web::Data::new(backend.clone()))
|
||||||
.service(endpoints::account::register)
|
.service(endpoints::account::register)
|
||||||
|
.service(endpoints::account::auth)
|
||||||
.service(endpoints::account::invite::new)
|
.service(endpoints::account::invite::new)
|
||||||
})
|
})
|
||||||
.bind((config.addr.as_str(), config.port))?;
|
.bind((config.addr.as_str(), config.port))?;
|
||||||
|
|
|
@ -4,15 +4,18 @@ use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Debug, Error, Serialize, Copy, Clone)]
|
#[derive(Debug, Error, Serialize, Copy, Clone)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
#[error("Failed to authenticate with the given credentials")]
|
||||||
|
AuthenticationFailure,
|
||||||
|
|
||||||
#[error("The given token is invalid")]
|
#[error("The given token is invalid")]
|
||||||
InvalidToken,
|
InvalidToken,
|
||||||
|
|
||||||
#[error("The given token is expired")]
|
|
||||||
TokenExpired,
|
|
||||||
|
|
||||||
#[error("Permission denied: {0}")]
|
#[error("Permission denied: {0}")]
|
||||||
PermissionDenied(&'static str),
|
PermissionDenied(&'static str),
|
||||||
|
|
||||||
|
#[error("The given token is expired")]
|
||||||
|
TokenExpired,
|
||||||
|
|
||||||
#[error("The given user cannot be found")]
|
#[error("The given user cannot be found")]
|
||||||
UserNotFound,
|
UserNotFound,
|
||||||
}
|
}
|
||||||
|
@ -31,9 +34,10 @@ impl Into<HttpResponse> for Error {
|
||||||
};
|
};
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
|
Error::AuthenticationFailure => HttpResponse::Unauthorized().json(body),
|
||||||
Error::InvalidToken => HttpResponse::Unauthorized().json(body),
|
Error::InvalidToken => HttpResponse::Unauthorized().json(body),
|
||||||
Error::TokenExpired => HttpResponse::Gone().json(body),
|
|
||||||
Error::PermissionDenied(_) => HttpResponse::Forbidden().json(body),
|
Error::PermissionDenied(_) => HttpResponse::Forbidden().json(body),
|
||||||
|
Error::TokenExpired => HttpResponse::Gone().json(body),
|
||||||
Error::UserNotFound => HttpResponse::NotFound().json(body),
|
Error::UserNotFound => HttpResponse::NotFound().json(body),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ mod user;
|
||||||
|
|
||||||
use crate::backend::{error::Error, permissions::Permission, user::IntoUser};
|
use crate::backend::{error::Error, permissions::Permission, user::IntoUser};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use anyhow::Result;
|
use anyhow::{bail, Result};
|
||||||
use argon2::{
|
use argon2::{
|
||||||
password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
|
password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
|
||||||
Argon2,
|
Argon2,
|
||||||
|
@ -99,7 +99,7 @@ impl Backend {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check whether an invite-token is valid or not.
|
/// Check whether an invite-token is valid or not.
|
||||||
async fn check_activation_token(&self, token: &str) -> Result<Result<(), Error>> {
|
async fn check_invite_token(&self, token: &str) -> Result<Result<(), Error>> {
|
||||||
match sqlx::query_as!(
|
match sqlx::query_as!(
|
||||||
InviteTokensRow,
|
InviteTokensRow,
|
||||||
r#"SELECT * FROM InviteTokens WHERE token = ?;"#,
|
r#"SELECT * FROM InviteTokens WHERE token = ?;"#,
|
||||||
|
@ -149,7 +149,7 @@ impl Backend {
|
||||||
token: String,
|
token: String,
|
||||||
password: String,
|
password: String,
|
||||||
) -> Result<Result<Uuid, Error>> {
|
) -> Result<Result<Uuid, Error>> {
|
||||||
if let Err(e) = self.check_activation_token(&token).await? {
|
if let Err(e) = self.check_invite_token(&token).await? {
|
||||||
return Ok(Err(e));
|
return Ok(Err(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,6 +173,43 @@ impl Backend {
|
||||||
Ok(Ok(userid))
|
Ok(Ok(userid))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generates an auth token for a user, given their UUID and password.
|
||||||
|
pub async fn authenticate(
|
||||||
|
&self,
|
||||||
|
userid: Uuid,
|
||||||
|
password: String,
|
||||||
|
) -> Result<Result<String, Error>> {
|
||||||
|
let user = match userid.into_user(&self).await? {
|
||||||
|
Ok(user) => user,
|
||||||
|
Err(e) => return Ok(Err(e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !user.verify_password(&password)? {
|
||||||
|
return Ok(Err(Error::AuthenticationFailure));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut token = rand::distributions::Alphanumeric.sample_string(&mut OsRng, 48);
|
||||||
|
// just for the case, that there's some duplication
|
||||||
|
loop {
|
||||||
|
match self.resolve_auth_token(&token).await? {
|
||||||
|
Ok(_) => token = rand::distributions::Alphanumeric.sample_string(&mut OsRng, 48),
|
||||||
|
Err(Error::InvalidToken) | Err(Error::TokenExpired) => break,
|
||||||
|
Err(e) => bail!("!THIS ERROR SHOULDN'T BE HERE! -> {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
r#"INSERT INTO AuthTokens VALUES (?, ?, ?);"#,
|
||||||
|
token,
|
||||||
|
userid.as_bytes().as_slice(),
|
||||||
|
Utc::now().naive_utc().checked_add_days(Days::new(14))
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Ok(token))
|
||||||
|
}
|
||||||
|
|
||||||
/// Generates a new invite token, if the user identified by the UUID has the permission to do so.
|
/// Generates a new invite token, if the user identified by the UUID has the permission to do so.
|
||||||
pub async fn create_invite(&self, user: impl IntoUser) -> Result<Result<String, Error>> {
|
pub async fn create_invite(&self, user: impl IntoUser) -> Result<Result<String, Error>> {
|
||||||
let user = match user.into_user(&self).await? {
|
let user = match user.into_user(&self).await? {
|
||||||
|
@ -188,11 +225,10 @@ impl Backend {
|
||||||
|
|
||||||
let token = rand::distributions::Alphanumeric.sample_string(&mut OsRng, 48);
|
let token = rand::distributions::Alphanumeric.sample_string(&mut OsRng, 48);
|
||||||
|
|
||||||
// TODO: change days to 7
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"INSERT INTO InviteTokens VALUES (?, ?);"#,
|
r#"INSERT INTO InviteTokens VALUES (?, ?);"#,
|
||||||
token,
|
token,
|
||||||
Utc::now().naive_utc().checked_add_days(Days::new(14))
|
Utc::now().naive_utc().checked_add_days(Days::new(7))
|
||||||
)
|
)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
@ -2,6 +2,7 @@ use crate::backend::db_structures::UsersRow;
|
||||||
use crate::backend::error::Error;
|
use crate::backend::error::Error;
|
||||||
use crate::backend::{permissions::Permission, Backend};
|
use crate::backend::{permissions::Permission, Backend};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use argon2::PasswordVerifier;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
@ -27,6 +28,20 @@ impl User {
|
||||||
(self.permissions & permission.as_u16()) > 0
|
(self.permissions & permission.as_u16()) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn verify_password(&self, password: &str) -> Result<bool> {
|
||||||
|
let hash = argon2::PasswordHash::new(self.password.as_str()).map_err(|_| {
|
||||||
|
anyhow::Error::msg(format!(
|
||||||
|
"Failed to parse the password hash of user: {}",
|
||||||
|
self.uuid
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
match argon2::Argon2::default().verify_password(password.as_bytes(), &hash) {
|
||||||
|
Err(_) => Ok(false),
|
||||||
|
Ok(_) => Ok(true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn flush(&self, backend: &Backend) -> Result<()> {
|
async fn flush(&self, backend: &Backend) -> Result<()> {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"UPDATE Users SET password = ?, permissions = ? WHERE userid = ?;"#,
|
r#"UPDATE Users SET password = ?, permissions = ? WHERE userid = ?;"#,
|
||||||
|
|
Loading…
Reference in New Issue