This commit is contained in:
Miguel da Mota 2024-01-01 03:18:33 +01:00
parent 8a19a733d6
commit 52ef924f12
28 changed files with 577 additions and 170 deletions

View file

@ -1,13 +1,18 @@
use crate::utils::get_jwt_secret;
use jwt::VerifyWithKey;
use std::collections::BTreeMap;
use crate::routes::auth::JWTClaims;
use jsonwebtoken::errors::Error;
use jsonwebtoken::{decode, DecodingKey, EncodingKey, TokenData, Validation};
pub fn get_token(token: &str) -> Result<BTreeMap<String, String>, &str> {
let secret = get_jwt_secret().unwrap();
let claims = token.verify_with_key(&secret);
const JWT_SECRET: &str = "secret";
match claims {
Ok(claims) => Ok(claims),
Err(_e) => return Err("Error parsing token"),
}
pub fn get_token(token: &str) -> Result<TokenData<JWTClaims>, Error> {
let token = decode::<JWTClaims>(&token, &get_decoding_key(), &Validation::default())?;
Ok(token)
}
pub fn get_encoding_key() -> EncodingKey {
EncodingKey::from_secret(JWT_SECRET.as_ref())
}
pub fn get_decoding_key() -> DecodingKey {
DecodingKey::from_secret(JWT_SECRET.as_ref())
}

View file

@ -1,9 +1,10 @@
extern crate core;
mod helpers;
mod middlewares;
mod models;
mod routes;
mod schema;
mod utils;
use crate::helpers::db;
use actix_web::{web, App, HttpServer};
@ -20,7 +21,7 @@ async fn main() -> std::io::Result<()> {
.service(web::scope("/playlists").service(routes::playlists::get_playlist))
.service(routes::auth::login)
.service(routes::me::routes())
.service(web::scope("/users").service(routes::users::get_user))
.service(routes::users::routes())
})
.bind(("127.0.0.1", 9000))?
.run()

View file

@ -9,18 +9,24 @@ pub fn get_user(req: HttpRequest) -> Result<Users, ErrorResponse> {
match authorization {
Some(header) => {
let claims = get_token(header.to_str().unwrap());
let token_data = get_token(header.to_str().unwrap());
match claims {
Ok(claims) => {
let user = Users::find(claims["user_id"].as_str())?;
match token_data {
Ok(token_data) => {
let user = Users::find(token_data.claims.user_id.as_str())?;
Ok(user)
}
Err(e) => {
return Err(ErrorResponse {
message: e.to_string(),
status: StatusCode::INTERNAL_SERVER_ERROR,
return Err(match e.kind() {
jsonwebtoken::errors::ErrorKind::ExpiredSignature => ErrorResponse {
message: "Not Authorized".to_string(),
status: StatusCode::UNAUTHORIZED,
},
_ => ErrorResponse {
message: e.to_string(),
status: StatusCode::INTERNAL_SERVER_ERROR,
},
})
}
}

79
src/models/artists.rs Normal file
View file

@ -0,0 +1,79 @@
use crate::helpers::db;
use crate::schema::{artists, artists_tracks};
use chrono::NaiveDateTime;
use diesel::result::Error;
use diesel::{
AsChangeset, ExpressionMethods, Insertable, QueryDsl, Queryable, RunQueryDsl, Selectable,
};
use serde::{Deserialize, Serialize};
#[derive(AsChangeset, Insertable, Queryable, Selectable, Deserialize, Serialize)]
#[diesel(table_name = crate::schema::artists)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct Artist {
pub id: String,
pub name: String,
created_at: NaiveDateTime,
updated_at: Option<NaiveDateTime>,
pub spotify_id: Option<String>,
pub tidal_id: Option<String>,
}
#[derive(Debug, Deserialize, Queryable, Serialize)]
pub struct Artists {
pub id: String,
pub title: String,
created_at: NaiveDateTime,
updated_at: Option<NaiveDateTime>,
pub spotify_id: Option<String>,
pub tidal_id: Option<String>,
}
impl Artists {
pub fn find(id: String) -> Result<Self, Error> {
let conn = &mut db::connection()?;
let playlist = artists::table.filter(artists::id.eq(id)).first(conn)?;
Ok(playlist)
}
pub fn create(artist: Artist) -> Result<Self, Error> {
let conn = &mut db::connection()?;
let playlist = diesel::insert_into(artists::table)
.values(Artist::from(artist))
.get_result(conn)?;
Ok(playlist)
}
pub fn find_by_track(track_id: &str) -> Result<Vec<Artists>, Error> {
let conn = &mut db::connection()?;
let tracks: Vec<(String, String)> = artists_tracks::table
.filter(artists_tracks::track_id.eq(track_id))
.get_results::<(String, String)>(conn)?;
let artists = tracks
.into_iter()
.map(|(artist_id, _)| Artists::find(artist_id).unwrap())
.collect::<Vec<Artists>>();
Ok(artists)
}
}
impl Artist {
fn from(artist: Artist) -> Artist {
Artist {
id: artist.id,
name: artist.name,
created_at: artist.created_at,
updated_at: artist.updated_at,
spotify_id: artist.spotify_id,
tidal_id: artist.tidal_id,
}
}
}

View file

@ -1,3 +1,4 @@
pub mod playlist;
pub mod artists;
pub mod playlists;
pub mod tracks;
pub mod user;

View file

@ -1,11 +1,11 @@
use crate::helpers::db;
use crate::models::tracks::Tracks;
use crate::models::tracks::{Track, Tracks, TracksWithArtists};
use crate::models::user::Users;
use crate::schema::playlists;
use chrono::NaiveDateTime;
use diesel::result::Error;
use diesel::{
AsChangeset, EqAll, ExpressionMethods, Insertable, QueryDsl, Queryable, RunQueryDsl, Selectable,
AsChangeset, ExpressionMethods, Insertable, QueryDsl, Queryable, RunQueryDsl, Selectable,
};
use serde::{Deserialize, Serialize};
@ -22,6 +22,7 @@ pub struct PlaylistCreator {
pub struct Playlist {
pub id: String,
pub name: String,
pub public: bool,
pub creator_id: String,
pub created_at: NaiveDateTime,
@ -32,6 +33,7 @@ pub struct Playlist {
pub struct Playlists {
pub id: String,
pub name: String,
pub public: bool,
pub creator_id: String,
pub created_at: NaiveDateTime,
@ -45,24 +47,36 @@ impl Playlists {
Ok(playlist)
}
pub fn create(playlist: Playlist) -> Result<Self, Error> {
pub fn create(playlist: NewPlaylist) -> Result<Self, Error> {
let conn = &mut db::connection()?;
let playlist = diesel::insert_into(playlists::table)
.values(Playlist::from(playlist))
.values(playlist)
.get_result(conn)?;
Ok(playlist)
}
pub fn find_for_user(user_id: &str) -> Result<Vec<Playlists>, Error> {
pub fn find_for_user(user_id: &str, filter_public: bool) -> Result<Vec<Playlists>, Error> {
let conn = &mut db::connection()?;
let playlists = playlists::table
let mut playlists = playlists::table
.filter(playlists::creator_id.eq(user_id))
.get_results(conn)?;
.into_boxed();
if filter_public {
playlists = playlists.filter(playlists::public.eq(true));
}
let playlists = playlists.get_results(conn)?;
Ok(playlists)
}
pub fn get_tracks(&self) -> Result<Vec<Tracks>, Error> {
pub fn get_tracks(&self) -> Result<Vec<TracksWithArtists>, Error> {
let tracks = Tracks::find_by_playlist(&self.id)?;
let tracks: Vec<TracksWithArtists> = tracks
.into_iter()
.map(Track::with_artists)
.collect::<Result<Vec<TracksWithArtists>, _>>()?;
Ok(tracks)
}
@ -72,11 +86,28 @@ impl Playlists {
}
}
#[derive(Debug, Insertable, Deserialize)]
#[diesel(table_name = playlists)]
pub struct NewPlaylist {
pub name: String,
pub public: bool,
pub creator_id: Option<String>,
}
impl Playlist {
pub fn create(name: &str, public: bool, creator_id: &str) -> NewPlaylist {
NewPlaylist {
name: name.to_string(),
public,
creator_id: Some(creator_id.to_string()),
}
}
fn from(playlist: Playlist) -> Playlist {
Playlist {
id: playlist.id,
name: playlist.name,
public: playlist.public,
creator_id: playlist.creator_id,
created_at: playlist.created_at,

View file

@ -1,4 +1,5 @@
use crate::helpers::db;
use crate::models::artists::Artists;
use crate::schema::tracks;
use chrono::NaiveDateTime;
use diesel::result::Error;
@ -69,11 +70,10 @@ impl Tracks {
Ok(tracks)
}
// pub fn get_artist(&self) -> Result<Artist, Error> {
// let conn = &mut db::connection();
// let artist = ;
// Ok(artist)
// }
pub fn get_artists(&self) -> Result<Vec<Artists>, Error> {
let artists = Artists::find_by_track(&self.id)?;
Ok(artists)
}
}
impl Track {
@ -90,4 +90,31 @@ impl Track {
tidal_id: track.tidal_id,
}
}
pub fn with_artists(track: Tracks) -> Result<TracksWithArtists, Error> {
let artists = track.get_artists()?;
Ok(TracksWithArtists {
id: track.id,
title: track.title,
duration_ms: track.duration_ms,
artists,
spotify_id: track.spotify_id,
tidal_id: track.tidal_id,
})
}
}
#[derive(Deserialize, Serialize)]
pub struct TracksWithArtists {
pub id: String,
pub title: String,
pub duration_ms: i32,
pub artists: Vec<Artists>,
pub spotify_id: Option<String>,
pub tidal_id: Option<String>,
}

View file

@ -1,5 +1,6 @@
use crate::helpers::db;
use crate::schema::users;
use bcrypt::BcryptError;
use chrono::NaiveDateTime;
use diesel::result::Error;
use diesel::{
@ -54,8 +55,8 @@ impl Users {
Ok(user)
}
pub fn verify_password(password: &str, user: &Users) -> bool {
bcrypt::verify(password, &user.password).unwrap()
pub fn verify_password(password: &str, hash: &str) -> Result<bool, BcryptError> {
bcrypt::verify(password, hash)
}
}

View file

@ -1,40 +1,53 @@
use crate::helpers::jwt::get_encoding_key;
use crate::middlewares::error::ErrorResponse;
use crate::models::user::Users;
use crate::utils::get_jwt_secret;
use actix_web::http::StatusCode;
use actix_web::{post, web, HttpResponse};
use jwt::SignWithKey;
use chrono::{Days, Utc};
use jsonwebtoken::{encode, Header};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Deserialize, Serialize)]
pub struct JWTClaims {
pub user_id: String,
pub exp: usize,
}
#[post("/auth/login")]
async fn login(body: web::Json<LoginBody>) -> Result<HttpResponse, ErrorResponse> {
#[derive(Deserialize, Serialize)]
struct Response {
access_token: String,
exp: usize,
}
let user = Users::find_by_email(&body.email);
match user {
Ok(user) => {
let password = Users::verify_password(&body.password, &user);
Ok(user) => match Users::verify_password(&body.password, &user.password) {
Ok(_res) => {
let exp = Utc::now()
.checked_add_days(Days::new(30))
.expect("valid timestamp")
.timestamp();
if password == false {
let claims = JWTClaims {
user_id: user.id.to_string(),
exp: exp as usize,
};
let token = encode(&Header::default(), &claims, &get_encoding_key()).unwrap();
Ok(HttpResponse::Ok().json(Response {
access_token: token,
exp: exp as usize,
}))
}
Err(_e) => {
return Err(ErrorResponse {
message: "Invalid credentials.".to_string(),
status: StatusCode::BAD_REQUEST,
});
})
}
let key = get_jwt_secret().unwrap();
let mut claims = BTreeMap::new();
claims.insert("user_id", &user.id);
let token_str = claims.sign_with_key(&key).unwrap();
Ok(HttpResponse::Ok().json(Response {
access_token: token_str,
}))
}
},
Err(_err) => {
return Err(ErrorResponse {
message: "Invalid credentials.".to_string(),

View file

@ -1,11 +1,14 @@
use crate::middlewares::error::ErrorResponse;
use crate::middlewares::user::get_user;
use crate::models::playlist::Playlists;
use actix_web::{get, web, HttpRequest, HttpResponse, Scope};
use crate::models::playlists::{NewPlaylist, Playlists};
use actix_web::{get, post, web, HttpRequest, HttpResponse, Scope};
use serde::Serialize;
pub fn routes() -> Scope {
web::scope("/me").service(me).service(me_playlists)
web::scope("/me")
.service(me)
.service(me_playlists)
.service(create_playlist)
}
#[derive(Serialize)]
@ -36,6 +39,20 @@ async fn me_playlists(req: HttpRequest) -> Result<HttpResponse, ErrorResponse> {
}
Ok(HttpResponse::Ok().json(Response {
playlists: Playlists::find_for_user(&user.id)?,
playlists: Playlists::find_for_user(&user.id, false)?,
}))
}
#[post("/playlists")]
async fn create_playlist(
req: HttpRequest,
mut playlist: web::Json<NewPlaylist>,
) -> Result<HttpResponse, ErrorResponse> {
let user = get_user(req)?;
playlist.creator_id = Option::from(user.id);
Playlists::create(playlist.into_inner())?;
Ok(HttpResponse::Accepted().finish())
}

View file

@ -1,6 +1,6 @@
use crate::middlewares::error::ErrorResponse;
use crate::models::playlist::{PlaylistCreator, Playlists};
use crate::models::tracks::Tracks;
use crate::models::playlists::{PlaylistCreator, Playlists};
use crate::models::tracks::TracksWithArtists;
use actix_web::{get, web, HttpResponse};
use serde::{Deserialize, Serialize};
@ -9,7 +9,9 @@ struct GetPlaylistResponse {
pub id: String,
pub name: String,
pub creator: PlaylistCreator,
pub tracks: Vec<Tracks>,
pub tracks: Vec<TracksWithArtists>,
pub tracks_count: usize,
pub duration: usize,
}
#[get("/{playlist_id}")]
@ -19,6 +21,14 @@ pub async fn get_playlist(path: web::Path<String>) -> Result<HttpResponse, Error
let creator = playlist.get_creator()?;
let tracks = playlist.get_tracks()?;
let tracks_count = tracks.len();
let duration = tracks
.iter()
.map(|track| track.duration_ms)
.reduce(|a, b| a + b)
.unwrap() as usize;
Ok(HttpResponse::Ok().json(GetPlaylistResponse {
id: playlist.id.to_string(),
name: playlist.name.to_string(),
@ -26,6 +36,8 @@ pub async fn get_playlist(path: web::Path<String>) -> Result<HttpResponse, Error
id: creator.id,
name: creator.name,
},
tracks: playlist.get_tracks()?,
tracks,
tracks_count,
duration,
}))
}

View file

@ -1,27 +1,29 @@
use crate::middlewares::error::ErrorResponse;
use crate::models::playlists::Playlists;
use crate::models::user::Users;
use actix_web::http::StatusCode;
use actix_web::{get, web, HttpResponse, Result};
use actix_web::{get, web, HttpResponse, Result, Scope};
use diesel::result::Error as DBError;
use serde::Serialize;
pub fn routes() -> Scope {
web::scope("/users")
.service(get_user)
.service(get_user_playlists)
}
#[derive(Serialize)]
struct GetUserResponse {
id: String,
name: String,
cover: String,
}
#[get("/{user_id}")]
async fn get_user(path: web::Path<String>) -> Result<HttpResponse, ErrorResponse> {
let user_id = path.into_inner();
let user = Users::find(user_id.as_str());
fn get_a_user(user_id: &str) -> Result<Users, ErrorResponse> {
let user = Users::find(user_id);
match user {
Ok(user) => Ok(HttpResponse::Ok().json(GetUserResponse {
id: user.id,
name: user.name,
})),
Ok(user) => Ok(user),
Err(DBError::NotFound) => {
return Err(ErrorResponse {
message: "User not found".to_string(),
@ -36,3 +38,24 @@ async fn get_user(path: web::Path<String>) -> Result<HttpResponse, ErrorResponse
}
}
}
#[get("/{user_id}")]
async fn get_user(path: web::Path<String>) -> Result<HttpResponse, ErrorResponse> {
let user = get_a_user(&path.into_inner())?;
let user_id = &user.id;
Ok(HttpResponse::Ok().json(GetUserResponse {
id: user_id.to_string(),
name: user.name,
cover: format!("https://assets.vybr.net/users/{}.png", user_id),
}))
}
#[get("/{user_id}/playlists")]
async fn get_user_playlists(path: web::Path<String>) -> Result<HttpResponse, ErrorResponse> {
let user = get_a_user(&path.into_inner())?;
let playlists = Playlists::find_for_user(&user.id, true)?;
Ok(HttpResponse::Ok().json(playlists))
}

View file

@ -1,11 +1,36 @@
// @generated automatically by Diesel CLI.
diesel::table! {
artists (id) {
#[max_length = 24]
id -> Varchar,
#[max_length = 255]
name -> Varchar,
created_at -> Timestamp,
updated_at -> Nullable<Timestamp>,
#[max_length = 21]
spotify_id -> Nullable<Varchar>,
#[max_length = 10]
tidal_id -> Nullable<Varchar>,
}
}
diesel::table! {
artists_tracks (artist_id, track_id) {
#[max_length = 24]
artist_id -> Varchar,
#[max_length = 24]
track_id -> Varchar,
}
}
diesel::table! {
playlists (id) {
#[max_length = 24]
id -> Varchar,
#[max_length = 255]
name -> Varchar,
public -> Bool,
#[max_length = 24]
creator_id -> Varchar,
created_at -> Timestamp,
@ -52,11 +77,15 @@ diesel::table! {
}
}
diesel::joinable!(artists_tracks -> artists (artist_id));
diesel::joinable!(artists_tracks -> tracks (track_id));
diesel::joinable!(playlists -> users (creator_id));
diesel::joinable!(playlists_tracks -> playlists (playlist_id));
diesel::joinable!(playlists_tracks -> tracks (track_id));
diesel::allow_tables_to_appear_in_same_query!(
artists,
artists_tracks,
playlists,
playlists_tracks,
tracks,

View file

@ -1,8 +0,0 @@
use hmac::{Hmac, Mac};
use sha2::Sha256;
use std::error::Error;
pub fn get_jwt_secret() -> Result<Hmac<Sha256>, Box<dyn Error>> {
let key: Hmac<Sha256> = Hmac::new_from_slice(b"secret")?;
Ok(key)
}