From 5898e5ab4359945ef52dc7460bb2937a85603fc2 Mon Sep 17 00:00:00 2001 From: niliara-edu Date: Sun, 26 Jan 2025 12:25:13 +0100 Subject: authentication done --- .ignore | 2 +- curlie_guide | 8 ++- scripts/create_db.sql | 9 +++ src/api/album.rs | 2 +- src/api/artist.rs | 2 +- src/api/search_results.rs | 2 +- src/api/song.rs | 2 +- src/auth.rs | 129 ++++++++++++++++++++++++++++------------- src/database/album.rs | 13 +++-- src/database/artist.rs | 9 ++- src/database/mod.rs | 4 +- src/database/search_results.rs | 6 +- src/database/song.rs | 12 ++-- src/database/user.rs | 84 +++++++++++++++++++++++++++ src/main.rs | 80 ++++++++++++------------- 15 files changed, 260 insertions(+), 104 deletions(-) create mode 100644 src/database/user.rs diff --git a/.ignore b/.ignore index 7f63ba7..60343c2 100644 --- a/.ignore +++ b/.ignore @@ -1,2 +1,2 @@ scripts/populate/ -scripts/ +ascripts/ diff --git a/curlie_guide b/curlie_guide index 462ed48..7db110d 100644 --- a/curlie_guide +++ b/curlie_guide @@ -3,8 +3,12 @@ curlie -f [POST/PUT/DELETE] (leave empty for GET) (:8000/api/[song/album/artist] || :8000/auth/[encode_token/decode_token/protected]) argument=value (ex: id=2) -header:value (for tokens: Accept:application/json Authorization:token_here) - +header:value (Authorization:token_here) == decode-auth == curlie [-f POST] :8000/auth/decode-token token="token_here" + + + + +(saving just in case: Accept:application/json) diff --git a/scripts/create_db.sql b/scripts/create_db.sql index 405fa96..56423f5 100644 --- a/scripts/create_db.sql +++ b/scripts/create_db.sql @@ -34,9 +34,18 @@ CREATE TABLE song ( FOREIGN KEY (album_id) REFERENCES album(id) ); +CREATE OR REPLACE TABLE user ( + id int auto_increment, + name varchar(255) unique, + password varchar(510), + primary key(id) +); + ALTER TABLE song CONVERT TO CHARACTER SET utf8; ALTER TABLE album CONVERT TO CHARACTER SET utf8; ALTER TABLE artist CONVERT TO CHARACTER SET utf8; +ALTER TABLE user CONVERT TO CHARACTER SET utf8; GRANT ALL PRIVILEGES ON balalaika.* TO 'balalaika_user'@'%' WITH GRANT OPTION; FLUSH PRIVILEGES; + diff --git a/src/api/album.rs b/src/api/album.rs index 2c6dfbb..9094495 100644 --- a/src/api/album.rs +++ b/src/api/album.rs @@ -1,6 +1,6 @@ use crate::api::{get_response_from_query, Response}; -use crate::database::Delete; use crate::database::album::{Album, AlbumPost, AlbumPut}; +use crate::database::Delete; use crate::extractors::auth_token::AuthenticationToken; use crate::AppState; use actix_web::{delete, get, post, put, web, HttpResponse}; diff --git a/src/api/artist.rs b/src/api/artist.rs index e8e5009..d36ccf4 100644 --- a/src/api/artist.rs +++ b/src/api/artist.rs @@ -1,6 +1,6 @@ use crate::api::{get_response_from_query, Response}; -use crate::database::Delete; use crate::database::artist::{Artist, ArtistPost, ArtistPut}; +use crate::database::Delete; use crate::extractors::auth_token::AuthenticationToken; use crate::AppState; use actix_web::{delete, get, post, put, web, HttpResponse}; diff --git a/src/api/search_results.rs b/src/api/search_results.rs index 9ccb013..f53bce8 100644 --- a/src/api/search_results.rs +++ b/src/api/search_results.rs @@ -29,7 +29,7 @@ pub async fn search_results( _ => ( Err(sqlx::Error::RowNotFound), Err(sqlx::Error::RowNotFound), - Err(sqlx::Error::RowNotFound) + Err(sqlx::Error::RowNotFound), ), }; diff --git a/src/api/song.rs b/src/api/song.rs index e046d22..1fec41c 100644 --- a/src/api/song.rs +++ b/src/api/song.rs @@ -1,6 +1,6 @@ use crate::api::{get_response_from_query, Response}; -use crate::database::Delete; use crate::database::song::{Song, SongPost, SongPut}; +use crate::database::Delete; use crate::extractors::auth_token::AuthenticationToken; use crate::AppState; use actix_web::{delete, get, post, put, web, HttpResponse}; diff --git a/src/auth.rs b/src/auth.rs index e0c8ae9..5fdf079 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,19 +1,15 @@ -use actix_web::{web, HttpResponse, Scope}; +use crate::database::user::{User, UserForm}; +use crate::AppState; +use actix_web::{delete, post, web, HttpResponse, Scope}; use chrono::{Duration, Utc}; -use jsonwebtoken::{ - decode, encode, errors::Error as JwtError, Algorithm, DecodingKey, EncodingKey, Header, - TokenData, Validation, -}; +use jsonwebtoken::{encode, EncodingKey, Header}; use serde::{Deserialize, Serialize}; -use crate::AppState; -use crate::extractors::auth_token::AuthenticationToken; - pub fn auth_scope() -> Scope { web::scope("/auth") - .route("/encode-token/{id}", web::get().to(encode_token)) - .route("/decode-token", web::post().to(decode_token)) - .route("/protected", web::get().to(protected)) + .service(register) + .service(login) + .service(delete_user) } #[derive(Serialize, Deserialize)] @@ -44,44 +40,97 @@ struct DecodeBody { token: String, } -async fn encode_token(path: web::Path, data: web::Data) -> HttpResponse { - let id: usize = path.into_inner(); +#[post("/register")] +pub async fn register( + app_state: web::Data, + request_data: web::Json, +) -> HttpResponse { + let query = app_state.database.register(request_data.into_inner()).await; + match query { + Ok(_) => HttpResponse::Ok().json(Response { + message: "Registration executed with no errors".to_owned(), + }), + Err(e) => HttpResponse::BadRequest().json(Response { + message: format!("There was an issue in the request: {}", e).to_owned(), + }), + } +} + +#[post("/login")] +pub async fn login( + app_state: web::Data, + request_data: web::Json, +) -> HttpResponse { + let query = app_state.database.login(request_data.into_inner()).await; + + let result = match query { + Ok(res) => res, + Err(e) => { + return HttpResponse::BadRequest().json(Response { + message: format!("There was an issue in the request: {}", e).to_owned(), + }) + } + }; + + let user: User = match result { + Some(user) => user, + None => { + return HttpResponse::BadRequest().json(Response { + message: "Username/Password incorrect!".to_owned(), + }) + } + }; + + let id: usize = match user.id { + Some(res) => res as usize, + None => { + return HttpResponse::BadRequest().json(Response { + message: "Internal error: user id not found".to_owned(), + }) + } + }; + + return match encode_token(id, &app_state.secret).await { + Ok(token) => HttpResponse::Ok().json(EncodeResponse { + message: format!("Successfully logged in as {}", user.name.unwrap()).to_owned(), + token: token.to_owned(), + }), + Err(response) => response, + }; +} + +async fn encode_token(id: usize, secret: &String) -> Result { let exp: usize = (Utc::now() + Duration::days(365)).timestamp() as usize; let claims: Claims = Claims { id, exp }; - let token: String = match encode( + match encode( &Header::default(), &claims, - &EncodingKey::from_secret(data.secret.as_str().as_ref()), + &EncodingKey::from_secret(secret.as_str().as_ref()), ) { - Ok(res) => res, - Err(_) => return HttpResponse::Ok().body("Token encoding didn't work\n"), + Ok(token) => return Ok(token), + Err(_) => return Err(HttpResponse::Ok().body("Token encoding didn't work\n")), }; - - HttpResponse::Ok().json(EncodeResponse { - message: "success".to_owned(), - token: token.to_owned(), - }) } -async fn decode_token(body: web::Json, data: web::Data) -> HttpResponse { - let decoded: Result, JwtError> = decode::( - &body.token, - &DecodingKey::from_secret(data.secret.as_str().as_ref()), - &Validation::new(Algorithm::HS256), - ); +// todo! tell if the user has been deleted or not +#[delete("/user")] +pub async fn delete_user( + app_state: web::Data, + request_data: web::Json, +) -> HttpResponse { + let query = app_state + .database + .delete_user(request_data.into_inner()) + .await; - match decoded { - Ok(token) => HttpResponse::Ok().json(DecodeResponse { - message: "Authorized".to_string(), - id: token.claims.id, - }), - Err(e) => HttpResponse::BadRequest().json(Response { - message: e.to_string(), + match query { + Ok(_) => HttpResponse::Ok().json(Response { + message: "Deletion executed with no errors".to_owned(), }), + Err(e) => { + return HttpResponse::BadRequest().json(Response { + message: format!("There was an issue in the request: {}", e).to_owned(), + }) + } } } - -async fn protected(auth_token: AuthenticationToken) -> HttpResponse { - println!("{:#?}", auth_token); - HttpResponse::Ok().json(Response { message: "Authorized".to_owned() }) -} diff --git a/src/database/album.rs b/src/database/album.rs index 87054f1..d7ffae0 100644 --- a/src/database/album.rs +++ b/src/database/album.rs @@ -114,18 +114,22 @@ impl DatabaseWrapper { } pub async fn edit_album(&self, data: AlbumPut) -> Result { - if data.id.is_none() { return Err(sqlx::Error::RowNotFound); } + if data.id.is_none() { + return Err(sqlx::Error::RowNotFound); + } let og_album: Album = match self.select_album_by_id(data.id.as_ref().unwrap()).await { Ok(res) => match res.is_some() { true => res.unwrap(), false => return Err(sqlx::Error::RowNotFound), - } + }, Err(_) => return Err(sqlx::Error::RowNotFound), }; sqlx::query!( "UPDATE album SET name=?, cover=? WHERE id=?", - data.name.unwrap_or(og_album.name.unwrap_or(String::default())), - data.cover.unwrap_or(og_album.cover.unwrap_or(String::default())), + data.name + .unwrap_or(og_album.name.unwrap_or(String::default())), + data.cover + .unwrap_or(og_album.cover.unwrap_or(String::default())), data.id, ) .execute(&self.db_pool) @@ -149,5 +153,4 @@ impl DatabaseWrapper { .execute(&self.db_pool) .await } - } diff --git a/src/database/artist.rs b/src/database/artist.rs index 5dda7a8..1d4e1cd 100644 --- a/src/database/artist.rs +++ b/src/database/artist.rs @@ -72,17 +72,20 @@ impl DatabaseWrapper { } pub async fn edit_artist(&self, data: ArtistPut) -> Result { - if data.id.is_none() { return Err(sqlx::Error::RowNotFound); } + if data.id.is_none() { + return Err(sqlx::Error::RowNotFound); + } let og_artist: Artist = match self.select_artist_by_id(data.id.as_ref().unwrap()).await { Ok(res) => match res.is_some() { true => res.unwrap(), false => return Err(sqlx::Error::RowNotFound), - } + }, Err(_) => return Err(sqlx::Error::RowNotFound), }; sqlx::query!( "UPDATE artist SET name=? WHERE id=?", - data.name.unwrap_or(og_artist.name.unwrap_or(String::default())), + data.name + .unwrap_or(og_artist.name.unwrap_or(String::default())), data.id, ) .execute(&self.db_pool) diff --git a/src/database/mod.rs b/src/database/mod.rs index d66b72b..884a597 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,7 +1,8 @@ -pub mod song; pub mod album; pub mod artist; pub mod search_results; +pub mod song; +pub mod user; use serde::{Deserialize, Serialize}; use sqlx::mysql::{MySqlPool, MySqlPoolOptions}; @@ -31,4 +32,3 @@ impl DatabaseWrapper { Ok(DatabaseWrapper { db_pool: pool }) } } - diff --git a/src/database/search_results.rs b/src/database/search_results.rs index 027d160..3f0bf01 100644 --- a/src/database/search_results.rs +++ b/src/database/search_results.rs @@ -1,7 +1,7 @@ -use crate::database::DatabaseWrapper; -use crate::database::song::Song; -use crate::database::artist::Artist; use crate::database::album::Album; +use crate::database::artist::Artist; +use crate::database::song::Song; +use crate::database::DatabaseWrapper; impl DatabaseWrapper { pub async fn search_results_by_name( diff --git a/src/database/song.rs b/src/database/song.rs index 45c217b..95b05ba 100644 --- a/src/database/song.rs +++ b/src/database/song.rs @@ -140,18 +140,22 @@ impl DatabaseWrapper { } pub async fn edit_song(&self, data: SongPut) -> Result { - if data.id.is_none() { return Err(sqlx::Error::RowNotFound); } + if data.id.is_none() { + return Err(sqlx::Error::RowNotFound); + } let og_song: Song = match self.select_song_by_id(data.id.as_ref().unwrap()).await { Ok(res) => match res.is_some() { true => res.unwrap(), false => return Err(sqlx::Error::RowNotFound), - } + }, Err(_) => return Err(sqlx::Error::RowNotFound), }; sqlx::query!( "UPDATE song SET name=?, lyrics=? WHERE id=?", - data.name.unwrap_or(og_song.name.unwrap_or(String::default())), - data.lyrics.unwrap_or(og_song.lyrics.unwrap_or(String::default())), + data.name + .unwrap_or(og_song.name.unwrap_or(String::default())), + data.lyrics + .unwrap_or(og_song.lyrics.unwrap_or(String::default())), data.id, ) .execute(&self.db_pool) diff --git a/src/database/user.rs b/src/database/user.rs new file mode 100644 index 0000000..ff7b1ab --- /dev/null +++ b/src/database/user.rs @@ -0,0 +1,84 @@ +use crate::database::DatabaseWrapper; +use serde::{Deserialize, Serialize}; +use sqlx::mysql::MySqlQueryResult; + +#[derive(Deserialize, Serialize)] +pub struct User { + pub id: Option, + pub name: Option, + password: Option, +} + +#[derive(Deserialize, Serialize)] +pub struct UserForm { + name: Option, + password: Option, +} + +impl DatabaseWrapper { + pub async fn register(&self, data: UserForm) -> Result { + if data.name.is_none() || data.password.is_none() { + return Err(sqlx::Error::RowNotFound); + } + + sqlx::query!( + "INSERT INTO user (name, password) + VALUE (?, PASSWORD(?))", + data.name, + data.password, + ) + .execute(&self.db_pool) + .await + } + + pub async fn login(&self, data: UserForm) -> Result, sqlx::Error> { + if data.name.is_none() || data.password.is_none() { + return Err(sqlx::Error::RowNotFound); + } + + sqlx::query_as!( + User, + "SELECT * FROM user + WHERE ? = name + AND password(?) = password + ", + data.name, + data.password, + ) + .fetch_optional(&self.db_pool) + .await + } + + pub async fn delete_user(&self, data: UserForm) -> Result { + if data.name.is_none() || data.password.is_none() { + return Err(sqlx::Error::RowNotFound); + } + + match sqlx::query_as!( + User, + "SELECT * FROM user + WHERE name = ? + AND password = password(?) + ", + data.name, + data.password, + ) + .fetch_one(&self.db_pool) + .await + { + Ok(_) => (), + Err(_) => return Err(sqlx::Error::RowNotFound), + }; + + sqlx::query!( + "DELETE FROM user + WHERE name = ? + AND password = password(?) + ", + data.name, + data.password, + ) + .execute(&self.db_pool) + .await + } +} diff --git a/src/main.rs b/src/main.rs index 595723a..941df3a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,13 +8,13 @@ use dotenv::dotenv; use std::env; use std::sync::Arc; -use utoipa::{ - openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme}, - Modify, OpenApi, ToSchema, -}; - -use utoipa_swagger_ui::SwaggerUi; -use database::artist::Artist; +// use utoipa::{ +// openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme}, +// Modify, OpenApi, +// }; +// +// use utoipa_swagger_ui::SwaggerUi; +// use database::artist::Artist; #[derive(Clone)] struct AppState { @@ -48,47 +48,47 @@ async fn main() -> std::io::Result<()> { secret: jwt_secret, }; - /* utoipa setup */ - #[derive(OpenApi)] - #[openapi( - paths( - ), - components( - schemas( - Artist - ) - ), - modifiers(&SecurityAddon) - )] - struct ApiDoc; + // /* utoipa setup */ + // #[derive(OpenApi)] + // #[openapi( + // paths( + // ), + // components( + // schemas( + // Artist + // ) + // ), + // modifiers(&SecurityAddon) + // )] + // struct ApiDoc; - struct SecurityAddon; - impl Modify for SecurityAddon { - fn modify(&self, openapi : &mut utoipa::openapi::OpenApi) { - let components = openapi.components.as_mut().unwrap(); - components.add_security_scheme( - "bearer_auth", - SecurityScheme::Http( - HttpBuilder::new() - .scheme(HttpAuthScheme::Bearer) - .bearer_format("JWT") - .build() - ), - ); - } - } + // struct SecurityAddon; + // impl Modify for SecurityAddon { + // fn modify(&self, openapi : &mut utoipa::openapi::OpenApi) { + // let components = openapi.components.as_mut().unwrap(); + // components.add_security_scheme( + // "bearer_auth", + // SecurityScheme::Http( + // HttpBuilder::new() + // .scheme(HttpAuthScheme::Bearer) + // .bearer_format("JWT") + // .build() + // ), + // ); + // } + // } - let openapi = ApiDoc::openapi(); + // let openapi = ApiDoc::openapi(); /* main server setup */ HttpServer::new(move || { App::new() .app_data(web::Data::new(app_state.clone())) .route("/", web::get().to(root)) - .service(SwaggerUi::new("/docs/{_:.*}").url( - "/docs/openapi.json", - openapi.clone(), - )) + // .service(SwaggerUi::new("/docs/{_:.*}").url( + // "/docs/openapi.json", + // openapi.clone(), + // )) .service(api::api_scope()) .service(auth::auth_scope()) }) -- cgit v1.2.3