feat(api): implemented /account/invite/new
This commit is contained in:
parent
4bf8f83ba8
commit
00279afbaa
|
@ -476,13 +476,15 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.34"
|
version = "0.4.35"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b"
|
checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-tzdata",
|
"android-tzdata",
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"wasm-bindgen",
|
||||||
"windows-targets 0.52.4",
|
"windows-targets 0.52.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1027,10 +1029,12 @@ dependencies = [
|
||||||
"actix-web-httpauth",
|
"actix-web-httpauth",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
|
"chrono",
|
||||||
"compile-time-run",
|
"compile-time-run",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"log",
|
"log",
|
||||||
|
"rand",
|
||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
|
14
Cargo.toml
14
Cargo.toml
|
@ -9,14 +9,16 @@ license = "MIT"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.81"
|
anyhow = "1.0.81"
|
||||||
log = "0.4.21"
|
|
||||||
env_logger = "0.11.3"
|
|
||||||
dotenvy = "0.15.7"
|
|
||||||
compile-time-run = "0.2.12"
|
|
||||||
uuid = { version = "1.7.0", features = ["v4"] }
|
|
||||||
thiserror = "1.0.58"
|
|
||||||
argon2 = "0.5.3"
|
argon2 = "0.5.3"
|
||||||
|
chrono = "0.4.35"
|
||||||
|
compile-time-run = "0.2.12"
|
||||||
|
dotenvy = "0.15.7"
|
||||||
|
env_logger = "0.11.3"
|
||||||
|
log = "0.4.21"
|
||||||
|
rand = "0.8.5"
|
||||||
serde = { version = "1.0.197", features = ["default"] }
|
serde = { version = "1.0.197", features = ["default"] }
|
||||||
|
thiserror = "1.0.58"
|
||||||
|
uuid = { version = "1.7.0", features = ["v4"] }
|
||||||
|
|
||||||
actix-web = "4.5.1"
|
actix-web = "4.5.1"
|
||||||
actix-web-httpauth = "0.8.1"
|
actix-web-httpauth = "0.8.1"
|
||||||
|
|
|
@ -1,64 +0,0 @@
|
||||||
use crate::backend::error::AccountRegisterError;
|
|
||||||
use crate::backend::Backend;
|
|
||||||
use actix_web::{post, web, HttpResponse, Responder};
|
|
||||||
use log::error;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct RegisterRequest {
|
|
||||||
token: String,
|
|
||||||
password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
struct RegisterResponse {
|
|
||||||
uuid: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/account/register")]
|
|
||||||
pub async fn register(
|
|
||||||
backend: web::Data<Backend>,
|
|
||||||
body: web::Json<RegisterRequest>,
|
|
||||||
) -> impl Responder {
|
|
||||||
let body = body.into_inner();
|
|
||||||
match backend.account_register(body.token, body.password).await {
|
|
||||||
Err(e) => {
|
|
||||||
error!("{e}");
|
|
||||||
HttpResponse::InternalServerError().finish()
|
|
||||||
}
|
|
||||||
Ok(res) => match res {
|
|
||||||
Err(e) => match e {
|
|
||||||
AccountRegisterError::InvalidToken => HttpResponse::Unauthorized().finish(),
|
|
||||||
AccountRegisterError::SqlError(e) => {
|
|
||||||
error!("{e}");
|
|
||||||
HttpResponse::InternalServerError().finish()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Ok(uuid) => HttpResponse::Ok().json(RegisterResponse {
|
|
||||||
uuid: uuid.to_string(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/account/new_token")]
|
|
||||||
pub async fn new_token(backend: web::Data<Backend>) -> impl Responder {
|
|
||||||
match backend.account_register(body.token, body.password).await {
|
|
||||||
Err(e) => {
|
|
||||||
error!("{e}");
|
|
||||||
HttpResponse::InternalServerError().finish()
|
|
||||||
}
|
|
||||||
Ok(res) => match res {
|
|
||||||
Err(e) => match e {
|
|
||||||
AccountRegisterError::InvalidToken => HttpResponse::Unauthorized().finish(),
|
|
||||||
AccountRegisterError::SqlError(e) => {
|
|
||||||
error!("{e}");
|
|
||||||
HttpResponse::InternalServerError().finish()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Ok(uuid) => HttpResponse::Ok().json(RegisterResponse {
|
|
||||||
uuid: uuid.to_string(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
use crate::backend::error::Error;
|
||||||
|
use crate::backend::Backend;
|
||||||
|
use actix_web::{post, web, HttpResponse, Responder};
|
||||||
|
use actix_web_httpauth::extractors::bearer::BearerAuth;
|
||||||
|
use log::error;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct NewResponse {
|
||||||
|
token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/account/invite/new")]
|
||||||
|
pub async fn new(backend: web::Data<Backend>, auth: BearerAuth) -> impl Responder {
|
||||||
|
match backend.create_invite(auth.token()).await {
|
||||||
|
Err(e) => {
|
||||||
|
error!("{e}");
|
||||||
|
HttpResponse::InternalServerError().finish()
|
||||||
|
}
|
||||||
|
Ok(res) => match res {
|
||||||
|
Err(e) => match e {
|
||||||
|
Error::PermissionDenied => HttpResponse::Unauthorized().finish(),
|
||||||
|
e => {
|
||||||
|
error!("!!! Error unknown to this context!!! -> {e}");
|
||||||
|
HttpResponse::InternalServerError().finish()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Ok(uuid) => HttpResponse::Ok().json(NewResponse {
|
||||||
|
token: uuid.to_string(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
pub mod invite;
|
||||||
|
|
||||||
|
use crate::backend::error::Error;
|
||||||
|
use crate::backend::Backend;
|
||||||
|
use actix_web::{post, web, HttpResponse, Responder};
|
||||||
|
use log::error;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct RegisterRequest {
|
||||||
|
token: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct RegisterResponse {
|
||||||
|
uuid: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/account/register")]
|
||||||
|
pub async fn register(
|
||||||
|
backend: web::Data<Backend>,
|
||||||
|
body: web::Json<RegisterRequest>,
|
||||||
|
) -> impl Responder {
|
||||||
|
let body = body.into_inner();
|
||||||
|
match backend.account_register(body.token, body.password).await {
|
||||||
|
Err(e) => {
|
||||||
|
error!("{e}");
|
||||||
|
HttpResponse::InternalServerError().finish()
|
||||||
|
}
|
||||||
|
Ok(res) => match res {
|
||||||
|
Err(e) => match e {
|
||||||
|
Error::InvalidToken => HttpResponse::Unauthorized().finish(),
|
||||||
|
e => {
|
||||||
|
error!("!!! Error unknown to this context!!! -> {e}");
|
||||||
|
HttpResponse::InternalServerError().finish()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Ok(uuid) => HttpResponse::Ok().json(RegisterResponse {
|
||||||
|
uuid: uuid.to_string(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -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::invite::new)
|
||||||
})
|
})
|
||||||
.bind((config.addr.as_str(), config.port))?;
|
.bind((config.addr.as_str(), config.port))?;
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
use sqlx::types::chrono::NaiveDateTime;
|
use sqlx::types::chrono::NaiveDateTime;
|
||||||
|
|
||||||
pub struct ActivationTokensRow {
|
pub struct InviteTokensRow {
|
||||||
pub token: String,
|
pub token: String,
|
||||||
pub expire: NaiveDateTime,
|
pub expire: NaiveDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct AuthTokensRow {
|
||||||
|
pub token: String,
|
||||||
|
pub userid: String,
|
||||||
|
pub expire: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct UsersRow {
|
pub struct UsersRow {
|
||||||
pub userid: String,
|
pub userid: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum AccountRegisterError {
|
pub enum Error {
|
||||||
#[error("The given token is invalid")]
|
#[error("The given token is invalid")]
|
||||||
InvalidToken,
|
InvalidToken,
|
||||||
|
|
||||||
#[error("SQL Error: {0}")]
|
#[error("The given token is expired")]
|
||||||
SqlError(sqlx::Error),
|
TokenExpired,
|
||||||
|
|
||||||
|
#[error("Permission denied")]
|
||||||
|
PermissionDenied,
|
||||||
|
|
||||||
|
#[error("The given user cannot be found")]
|
||||||
|
UserNotFound,
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,16 +3,19 @@ pub mod error;
|
||||||
mod permissions;
|
mod permissions;
|
||||||
mod user;
|
mod user;
|
||||||
|
|
||||||
use crate::backend::error::AccountRegisterError;
|
use crate::backend::{error::Error, permissions::Permission, user::IntoUser};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use argon2::{
|
use argon2::{
|
||||||
password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
|
password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
|
||||||
Argon2,
|
Argon2,
|
||||||
};
|
};
|
||||||
use db_structures::{ActivationTokensRow, UsersRow};
|
use chrono::Days;
|
||||||
|
use db_structures::{AuthTokensRow, InviteTokensRow, UsersRow};
|
||||||
use log::info;
|
use log::info;
|
||||||
|
use rand::distributions::DistString;
|
||||||
use sqlx::{types::chrono::Utc, MySqlPool};
|
use sqlx::{types::chrono::Utc, MySqlPool};
|
||||||
|
use std::str::FromStr;
|
||||||
use user::User;
|
use user::User;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
@ -45,7 +48,7 @@ impl Backend {
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"CREATE TABLE IF NOT EXISTS ActivationTokens (
|
r#"CREATE TABLE IF NOT EXISTS InviteTokens (
|
||||||
token CHAR(48) NOT NULL PRIMARY KEY,
|
token CHAR(48) NOT NULL PRIMARY KEY,
|
||||||
expire DATETIME NOT NULL
|
expire DATETIME NOT NULL
|
||||||
);"#
|
);"#
|
||||||
|
@ -57,7 +60,7 @@ impl Backend {
|
||||||
r#"CREATE TABLE IF NOT EXISTS AuthTokens (
|
r#"CREATE TABLE IF NOT EXISTS AuthTokens (
|
||||||
token CHAR(48) NOT NULL PRIMARY KEY,
|
token CHAR(48) NOT NULL PRIMARY KEY,
|
||||||
userid UUID NOT NULL,
|
userid UUID NOT NULL,
|
||||||
exipre DATETIME NOT NULL
|
expire DATETIME NOT NULL
|
||||||
);"#
|
);"#
|
||||||
)
|
)
|
||||||
.execute(&pool)
|
.execute(&pool)
|
||||||
|
@ -68,33 +71,62 @@ impl Backend {
|
||||||
Ok(Self { pool })
|
Ok(Self { pool })
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn check_activation_token(&self, token: &str) -> Result<bool, sqlx::Error> {
|
/// Returns the UUID of the user who owns the auth token.
|
||||||
|
pub async fn resolve_auth_token(&self, token: &str) -> Result<Result<Uuid, Error>> {
|
||||||
match sqlx::query_as!(
|
match sqlx::query_as!(
|
||||||
ActivationTokensRow,
|
AuthTokensRow,
|
||||||
r#"SELECT * FROM ActivationTokens WHERE token = ?;"#,
|
r#"SELECT * FROM AuthTokens WHERE token = ?;"#,
|
||||||
token
|
token
|
||||||
)
|
)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Err(e) => match e {
|
Err(e) => match e {
|
||||||
sqlx::Error::RowNotFound => Ok(false),
|
sqlx::Error::RowNotFound => Ok(Err(Error::InvalidToken)),
|
||||||
_ => Err(e),
|
_ => Err(e.into()),
|
||||||
},
|
},
|
||||||
Ok(row) => {
|
Ok(row) => {
|
||||||
sqlx::query!(r#"DELETE FROM ActivationTokens WHERE token = ?;"#, token)
|
|
||||||
.execute(&self.pool)
|
|
||||||
.await?;
|
|
||||||
if row.expire > Utc::now().naive_utc() {
|
if row.expire > Utc::now().naive_utc() {
|
||||||
Ok(true)
|
Ok(Ok(Uuid::from_str(&row.userid)?))
|
||||||
} else {
|
} else {
|
||||||
Ok(false)
|
sqlx::query!(r#"DELETE FROM AuthTokens WHERE token = ?;"#, token)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(Err(Error::TokenExpired))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_user(&self, userid: Uuid) -> Result<Option<User>> {
|
/// Check whether an invite-token is valid or not.
|
||||||
|
async fn check_activation_token(&self, token: &str) -> Result<Result<(), Error>> {
|
||||||
|
match sqlx::query_as!(
|
||||||
|
InviteTokensRow,
|
||||||
|
r#"SELECT * FROM InviteTokens WHERE token = ?;"#,
|
||||||
|
token
|
||||||
|
)
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Err(e) => match e {
|
||||||
|
sqlx::Error::RowNotFound => Ok(Err(Error::InvalidToken)),
|
||||||
|
_ => Err(e.into()),
|
||||||
|
},
|
||||||
|
Ok(row) => {
|
||||||
|
sqlx::query!(r#"DELETE FROM InviteTokens WHERE token = ?;"#, token)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
if row.expire > Utc::now().naive_utc() {
|
||||||
|
Ok(Ok(()))
|
||||||
|
} else {
|
||||||
|
Ok(Err(Error::TokenExpired))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns detailed information about the user identified by the UUID.
|
||||||
|
async fn get_user(&self, userid: Uuid) -> Result<Result<User, Error>> {
|
||||||
match sqlx::query_as!(
|
match sqlx::query_as!(
|
||||||
UsersRow,
|
UsersRow,
|
||||||
r#"SELECT * FROM Users WHERE userid = ?;"#,
|
r#"SELECT * FROM Users WHERE userid = ?;"#,
|
||||||
|
@ -104,20 +136,21 @@ impl Backend {
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Err(e) => match e {
|
Err(e) => match e {
|
||||||
sqlx::Error::RowNotFound => Ok(None),
|
sqlx::Error::RowNotFound => Ok(Err(Error::UserNotFound)),
|
||||||
_ => Err(e.into()),
|
_ => Err(e.into()),
|
||||||
},
|
},
|
||||||
Ok(row) => Ok(Some(row.try_into()?)),
|
Ok(row) => Ok(Ok(row.try_into()?)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a new account and returns its UUID.
|
||||||
pub async fn account_register(
|
pub async fn account_register(
|
||||||
&self,
|
&self,
|
||||||
token: String,
|
token: String,
|
||||||
password: String,
|
password: String,
|
||||||
) -> Result<Result<Uuid, error::AccountRegisterError>> {
|
) -> Result<Result<Uuid, Error>> {
|
||||||
if !self.check_activation_token(&token).await? {
|
if let Err(e) = self.check_activation_token(&token).await? {
|
||||||
return Ok(Err(AccountRegisterError::InvalidToken));
|
return Ok(Err(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
let salt = SaltString::generate(&mut OsRng);
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
|
@ -140,32 +173,28 @@ impl Backend {
|
||||||
Ok(Ok(userid))
|
Ok(Ok(userid))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_activation_token(
|
/// Generates a new invite token, if the user identified by the UUID has the permission to do so.
|
||||||
&self,
|
pub async fn create_invite(&self, user: impl IntoUser) -> Result<Result<String, Error>> {
|
||||||
token: String,
|
let user = match user.into_user(&self).await? {
|
||||||
password: String,
|
Ok(user) => user,
|
||||||
) -> Result<Result<Uuid, error::AccountRegisterError>> {
|
Err(e) => return Ok(Err(e)),
|
||||||
if !self.check_activation_token(&token).await? {
|
};
|
||||||
return Ok(Err(AccountRegisterError::InvalidToken));
|
|
||||||
|
if !user.has_permission(Permission::GenerateInviteTokens) {
|
||||||
|
return Ok(Err(Error::PermissionDenied));
|
||||||
}
|
}
|
||||||
|
|
||||||
let salt = SaltString::generate(&mut OsRng);
|
let token = rand::distributions::Alphanumeric.sample_string(&mut OsRng, 48);
|
||||||
|
|
||||||
let hash = Argon2::default()
|
|
||||||
.hash_password(password.as_bytes(), &salt)
|
|
||||||
.map_err(|_| anyhow::Error::msg("Failed to hash the password"))?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let userid = Uuid::new_v4();
|
|
||||||
|
|
||||||
|
// TODO: change days to 7
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"INSERT INTO Users VALUES (?, ?, 0);"#,
|
r#"INSERT INTO InviteTokens VALUES (?, ?);"#,
|
||||||
userid.as_bytes().as_slice(),
|
token,
|
||||||
hash
|
Utc::now().naive_utc().checked_add_days(Days::new(14))
|
||||||
)
|
)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Ok(userid))
|
Ok(Ok(token))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::backend::db_structures::UsersRow;
|
use crate::backend::db_structures::UsersRow;
|
||||||
|
use crate::backend::error::Error;
|
||||||
use crate::backend::{permissions::Permission, Backend};
|
use crate::backend::{permissions::Permission, Backend};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
@ -38,3 +39,29 @@ impl User {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait IntoUser {
|
||||||
|
async fn into_user(self, backend: &Backend) -> Result<Result<User, Error>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoUser for Uuid {
|
||||||
|
async fn into_user(self, backend: &Backend) -> Result<Result<User, Error>> {
|
||||||
|
backend.get_user(self).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoUser for &str {
|
||||||
|
async fn into_user(self, backend: &Backend) -> Result<Result<User, Error>> {
|
||||||
|
let userid = match backend.resolve_auth_token(self).await? {
|
||||||
|
Ok(userid) => userid,
|
||||||
|
Err(e) => return Ok(Err(e)),
|
||||||
|
};
|
||||||
|
userid.into_user(backend).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoUser for User {
|
||||||
|
async fn into_user(self, _backend: &Backend) -> Result<Result<User, Error>> {
|
||||||
|
Ok(Ok(self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue