initial commit
This commit is contained in:
commit
8a19a733d6
35 changed files with 2489 additions and 0 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
/target
|
||||||
|
|
||||||
|
.env
|
||||||
|
|
||||||
|
.vscode
|
||||||
|
.idea
|
1684
Cargo.lock
generated
Normal file
1684
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
22
Cargo.toml
Normal file
22
Cargo.toml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
[package]
|
||||||
|
name = "vybr-api"
|
||||||
|
version = "0.0.1"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix-rt = "2"
|
||||||
|
actix-web = "4"
|
||||||
|
async-trait = "0.1"
|
||||||
|
bcrypt = "0.15"
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
diesel = { version = "2", features = ["r2d2", "postgres", "chrono"] }
|
||||||
|
diesel_migrations = "2"
|
||||||
|
dotenvy = "*"
|
||||||
|
hmac = "0.12"
|
||||||
|
sha2 = "0.10"
|
||||||
|
lazy_static = "1"
|
||||||
|
jwt = "0.16"
|
||||||
|
serde = "1"
|
||||||
|
serde_json = "1"
|
2
diesel.toml
Normal file
2
diesel.toml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[print_schema]
|
||||||
|
file = "src/schema.rs"
|
0
migrations/.keep
Normal file
0
migrations/.keep
Normal file
6
migrations/00000000000000_diesel_initial_setup/down.sql
Normal file
6
migrations/00000000000000_diesel_initial_setup/down.sql
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
-- This file was automatically created by Diesel to setup helper functions
|
||||||
|
-- and other internal bookkeeping. This file is safe to edit, any future
|
||||||
|
-- changes will be added to existing projects as new migrations.
|
||||||
|
|
||||||
|
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
|
||||||
|
DROP FUNCTION IF EXISTS diesel_set_updated_at();
|
36
migrations/00000000000000_diesel_initial_setup/up.sql
Normal file
36
migrations/00000000000000_diesel_initial_setup/up.sql
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
-- This file was automatically created by Diesel to setup helper functions
|
||||||
|
-- and other internal bookkeeping. This file is safe to edit, any future
|
||||||
|
-- changes will be added to existing projects as new migrations.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- Sets up a trigger for the given table to automatically set a column called
|
||||||
|
-- `updated_at` whenever the row is modified (unless `updated_at` was included
|
||||||
|
-- in the modified columns)
|
||||||
|
--
|
||||||
|
-- # Example
|
||||||
|
--
|
||||||
|
-- ```sql
|
||||||
|
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
|
||||||
|
--
|
||||||
|
-- SELECT diesel_manage_updated_at('users');
|
||||||
|
-- ```
|
||||||
|
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
|
||||||
|
BEGIN
|
||||||
|
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
|
||||||
|
BEGIN
|
||||||
|
IF (
|
||||||
|
NEW IS DISTINCT FROM OLD AND
|
||||||
|
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
|
||||||
|
) THEN
|
||||||
|
NEW.updated_at := current_timestamp;
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
2
migrations/2023-12-29-191547_users/down.sql
Normal file
2
migrations/2023-12-29-191547_users/down.sql
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
DROP TABLE users;
|
13
migrations/2023-12-29-191547_users/up.sql
Normal file
13
migrations/2023-12-29-191547_users/up.sql
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
-- Your SQL goes here
|
||||||
|
CREATE TABLE IF NOT EXISTS users
|
||||||
|
(
|
||||||
|
id VARCHAR(24) DEFAULT nanoid(24),
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
email VARCHAR(255) NOT NULL,
|
||||||
|
password TEXT NOT NULL,
|
||||||
|
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
created_at TIMESTAMP DEFAULT now() NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
2
migrations/2023-12-30-162032_playlists/down.sql
Normal file
2
migrations/2023-12-30-162032_playlists/down.sql
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
DROP TABLE playlists;
|
16
migrations/2023-12-30-162032_playlists/up.sql
Normal file
16
migrations/2023-12-30-162032_playlists/up.sql
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS playlists
|
||||||
|
(
|
||||||
|
id VARCHAR(24) DEFAULT nanoid(24),
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
|
||||||
|
creator_id VARCHAR(24) NOT NULL,
|
||||||
|
|
||||||
|
created_at TIMESTAMP DEFAULT now() NOT NULL,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
FOREIGN KEY (creator_id)
|
||||||
|
REFERENCES users (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX ON playlists (creator_id);
|
2
migrations/2023-12-30-192105_tracks/down.sql
Normal file
2
migrations/2023-12-30-192105_tracks/down.sql
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
DROP TABLE tracks;
|
16
migrations/2023-12-30-192105_tracks/up.sql
Normal file
16
migrations/2023-12-30-192105_tracks/up.sql
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
-- Your SQL goes here
|
||||||
|
CREATE TABLE IF NOT EXISTS tracks
|
||||||
|
(
|
||||||
|
id VARCHAR(24) DEFAULT nanoid(24),
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
duration_ms INT NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
created_at TIMESTAMP DEFAULT now() NOT NULL,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
|
||||||
|
-- music services
|
||||||
|
spotify_id VARCHAR(21) UNIQUE,
|
||||||
|
tidal_id VARCHAR(10) UNIQUE,
|
||||||
|
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
2
migrations/2023-12-30-214938_playlists_tracks/down.sql
Normal file
2
migrations/2023-12-30-214938_playlists_tracks/down.sql
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
DROP TABLE playlists_tracks;
|
5
migrations/2023-12-30-214938_playlists_tracks/up.sql
Normal file
5
migrations/2023-12-30-214938_playlists_tracks/up.sql
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS playlists_tracks (
|
||||||
|
playlist_id VARCHAR(24) REFERENCES playlists(id),
|
||||||
|
track_id VARCHAR(24) REFERENCES tracks(id),
|
||||||
|
PRIMARY KEY (playlist_id, track_id)
|
||||||
|
);
|
8
src/errors/db.rs
Normal file
8
src/errors/db.rs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DBError {}
|
||||||
|
|
||||||
|
impl From<diesel::result::Error> for DBError {
|
||||||
|
fn from(err: diesel::result::Error) {
|
||||||
|
DBError {}
|
||||||
|
}
|
||||||
|
}
|
1
src/errors/mod.rs
Normal file
1
src/errors/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod db;
|
26
src/helpers/db.rs
Normal file
26
src/helpers/db.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
use diesel::r2d2::ConnectionManager;
|
||||||
|
use diesel::result::Error;
|
||||||
|
use diesel::{pg::PgConnection, r2d2};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
|
type Pool = r2d2::Pool<ConnectionManager<PgConnection>>;
|
||||||
|
pub type DbConnection = r2d2::PooledConnection<ConnectionManager<PgConnection>>;
|
||||||
|
|
||||||
|
const DATABASE_URL: &str = "postgres://postgres:root@127.0.0.1/vybr";
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref POOL: Pool = {
|
||||||
|
let manager = ConnectionManager::<PgConnection>::new(DATABASE_URL);
|
||||||
|
Pool::new(manager).expect("Failed to create db pool.")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init() {
|
||||||
|
lazy_static::initialize(&POOL);
|
||||||
|
let _conn = connection().expect("Failed to get db connection.");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn connection() -> Result<DbConnection, Error> {
|
||||||
|
POOL.get()
|
||||||
|
.map_err(|e| panic!("Database connection error: {}", e))
|
||||||
|
}
|
13
src/helpers/jwt.rs
Normal file
13
src/helpers/jwt.rs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
use crate::utils::get_jwt_secret;
|
||||||
|
use jwt::VerifyWithKey;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
pub fn get_token(token: &str) -> Result<BTreeMap<String, String>, &str> {
|
||||||
|
let secret = get_jwt_secret().unwrap();
|
||||||
|
let claims = token.verify_with_key(&secret);
|
||||||
|
|
||||||
|
match claims {
|
||||||
|
Ok(claims) => Ok(claims),
|
||||||
|
Err(_e) => return Err("Error parsing token"),
|
||||||
|
}
|
||||||
|
}
|
2
src/helpers/mod.rs
Normal file
2
src/helpers/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod db;
|
||||||
|
pub mod jwt;
|
28
src/main.rs
Normal file
28
src/main.rs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
mod helpers;
|
||||||
|
mod middlewares;
|
||||||
|
mod models;
|
||||||
|
mod routes;
|
||||||
|
mod schema;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
use crate::helpers::db;
|
||||||
|
use actix_web::{web, App, HttpServer};
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
dotenvy::dotenv().expect("No .env file found");
|
||||||
|
std::env::set_var("RUST_LOG", "debug");
|
||||||
|
|
||||||
|
db::init();
|
||||||
|
|
||||||
|
HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
.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))
|
||||||
|
})
|
||||||
|
.bind(("127.0.0.1", 9000))?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
68
src/middlewares/error.rs
Normal file
68
src/middlewares/error.rs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
use actix_web::{http::StatusCode, HttpResponse, ResponseError};
|
||||||
|
use diesel::result::Error;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::fmt::{Debug, Display, Formatter, Result};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ErrorResponse {
|
||||||
|
pub message: String,
|
||||||
|
pub status: StatusCode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ErrorResponse {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
|
write!(f, "{}: {}", self.status, self.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<actix_web::Error> for ErrorResponse {
|
||||||
|
fn from(err: actix_web::Error) -> Self {
|
||||||
|
ErrorResponse {
|
||||||
|
status: err.error_response().status(),
|
||||||
|
message: err.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Error> for ErrorResponse {
|
||||||
|
fn from(err: Error) -> Self {
|
||||||
|
match err {
|
||||||
|
Error::NotFound => ErrorResponse {
|
||||||
|
status: StatusCode::NOT_FOUND,
|
||||||
|
message: err.to_string(),
|
||||||
|
},
|
||||||
|
_ => ErrorResponse {
|
||||||
|
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
message: err.to_string(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResponseError for ErrorResponse {
|
||||||
|
fn status_code(&self) -> StatusCode {
|
||||||
|
self.status
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error_response(&self) -> HttpResponse {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Response {
|
||||||
|
error: ErrorResponse,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ErrorResponse {
|
||||||
|
message: String,
|
||||||
|
status: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponse::build(self.status_code())
|
||||||
|
.content_type("application/json")
|
||||||
|
.json(Response {
|
||||||
|
error: ErrorResponse {
|
||||||
|
message: self.message.to_string(),
|
||||||
|
status: self.status_code().as_u16(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
2
src/middlewares/mod.rs
Normal file
2
src/middlewares/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod error;
|
||||||
|
pub mod user;
|
35
src/middlewares/user.rs
Normal file
35
src/middlewares/user.rs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
use crate::helpers::jwt::get_token;
|
||||||
|
use crate::middlewares::error::ErrorResponse;
|
||||||
|
use crate::models::user::Users;
|
||||||
|
use actix_web::http::{header, StatusCode};
|
||||||
|
use actix_web::HttpRequest;
|
||||||
|
|
||||||
|
pub fn get_user(req: HttpRequest) -> Result<Users, ErrorResponse> {
|
||||||
|
let authorization = req.headers().get(header::AUTHORIZATION);
|
||||||
|
|
||||||
|
match authorization {
|
||||||
|
Some(header) => {
|
||||||
|
let claims = get_token(header.to_str().unwrap());
|
||||||
|
|
||||||
|
match claims {
|
||||||
|
Ok(claims) => {
|
||||||
|
let user = Users::find(claims["user_id"].as_str())?;
|
||||||
|
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(ErrorResponse {
|
||||||
|
message: e.to_string(),
|
||||||
|
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
return Err(ErrorResponse {
|
||||||
|
message: "Not Authorized".to_string(),
|
||||||
|
status: StatusCode::UNAUTHORIZED,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
src/models/mod.rs
Normal file
3
src/models/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod playlist;
|
||||||
|
pub mod tracks;
|
||||||
|
pub mod user;
|
86
src/models/playlist.rs
Normal file
86
src/models/playlist.rs
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
use crate::helpers::db;
|
||||||
|
use crate::models::tracks::Tracks;
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct PlaylistCreator {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AsChangeset, Insertable, Queryable, Selectable, Deserialize, Serialize)]
|
||||||
|
#[diesel(table_name = crate::schema::playlists)]
|
||||||
|
#[diesel(belongs_to(Users))]
|
||||||
|
#[diesel(check_for_backend(diesel::pg::Pg))]
|
||||||
|
pub struct Playlist {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub creator_id: String,
|
||||||
|
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
pub updated_at: Option<NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Queryable, Serialize)]
|
||||||
|
pub struct Playlists {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub creator_id: String,
|
||||||
|
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
pub updated_at: Option<NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Playlists {
|
||||||
|
pub fn find(id: &str) -> Result<Self, Error> {
|
||||||
|
let conn = &mut db::connection()?;
|
||||||
|
let playlist = playlists::table.filter(playlists::id.eq(id)).first(conn)?;
|
||||||
|
Ok(playlist)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create(playlist: Playlist) -> Result<Self, Error> {
|
||||||
|
let conn = &mut db::connection()?;
|
||||||
|
let playlist = diesel::insert_into(playlists::table)
|
||||||
|
.values(Playlist::from(playlist))
|
||||||
|
.get_result(conn)?;
|
||||||
|
Ok(playlist)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_for_user(user_id: &str) -> Result<Vec<Playlists>, Error> {
|
||||||
|
let conn = &mut db::connection()?;
|
||||||
|
let playlists = playlists::table
|
||||||
|
.filter(playlists::creator_id.eq(user_id))
|
||||||
|
.get_results(conn)?;
|
||||||
|
Ok(playlists)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_tracks(&self) -> Result<Vec<Tracks>, Error> {
|
||||||
|
let tracks = Tracks::find_by_playlist(&self.id)?;
|
||||||
|
Ok(tracks)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_creator(&self) -> Result<Users, Error> {
|
||||||
|
let creator = Users::find(&self.creator_id)?;
|
||||||
|
Ok(creator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Playlist {
|
||||||
|
fn from(playlist: Playlist) -> Playlist {
|
||||||
|
Playlist {
|
||||||
|
id: playlist.id,
|
||||||
|
name: playlist.name,
|
||||||
|
creator_id: playlist.creator_id,
|
||||||
|
|
||||||
|
created_at: playlist.created_at,
|
||||||
|
updated_at: playlist.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
93
src/models/tracks.rs
Normal file
93
src/models/tracks.rs
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
use crate::helpers::db;
|
||||||
|
use crate::schema::tracks;
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use diesel::result::Error;
|
||||||
|
use diesel::{
|
||||||
|
AsChangeset, ExpressionMethods, Insertable, QueryDsl, Queryable, RunQueryDsl, Selectable,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::schema::playlists_tracks;
|
||||||
|
|
||||||
|
#[derive(AsChangeset, Insertable, Queryable, Selectable, Deserialize, Serialize)]
|
||||||
|
#[diesel(table_name = crate::schema::tracks)]
|
||||||
|
#[diesel(check_for_backend(diesel::pg::Pg))]
|
||||||
|
pub struct Track {
|
||||||
|
pub id: String,
|
||||||
|
pub title: String,
|
||||||
|
pub duration_ms: i32,
|
||||||
|
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
pub updated_at: Option<NaiveDateTime>,
|
||||||
|
|
||||||
|
pub spotify_id: Option<String>,
|
||||||
|
pub tidal_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Queryable, Serialize)]
|
||||||
|
pub struct Tracks {
|
||||||
|
pub id: String,
|
||||||
|
pub title: String,
|
||||||
|
pub duration_ms: i32,
|
||||||
|
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
pub updated_at: Option<NaiveDateTime>,
|
||||||
|
|
||||||
|
pub spotify_id: Option<String>,
|
||||||
|
pub tidal_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tracks {
|
||||||
|
pub fn find(id: String) -> Result<Self, Error> {
|
||||||
|
let conn = &mut db::connection()?;
|
||||||
|
let playlist = tracks::table.filter(tracks::id.eq(id)).first(conn)?;
|
||||||
|
Ok(playlist)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create(track: Track) -> Result<Self, Error> {
|
||||||
|
let conn = &mut db::connection()?;
|
||||||
|
let playlist = diesel::insert_into(tracks::table)
|
||||||
|
.values(Track::from(track))
|
||||||
|
.get_result(conn)?;
|
||||||
|
Ok(playlist)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_by_playlist(playlist_id: &str) -> Result<Vec<Tracks>, Error> {
|
||||||
|
let conn = &mut db::connection()?;
|
||||||
|
let tracks: Vec<(String, String)> = playlists_tracks::table
|
||||||
|
.filter(playlists_tracks::playlist_id.eq(playlist_id))
|
||||||
|
.get_results::<(String, String)>(conn)?;
|
||||||
|
|
||||||
|
let tracks = tracks
|
||||||
|
.into_iter()
|
||||||
|
.map(|(playlist_id, track_id)| {
|
||||||
|
println!("{}: {}", playlist_id, track_id);
|
||||||
|
Tracks::find(track_id).unwrap()
|
||||||
|
})
|
||||||
|
.collect::<Vec<Tracks>>();
|
||||||
|
|
||||||
|
Ok(tracks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub fn get_artist(&self) -> Result<Artist, Error> {
|
||||||
|
// let conn = &mut db::connection();
|
||||||
|
// let artist = ;
|
||||||
|
// Ok(artist)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Track {
|
||||||
|
fn from(track: Track) -> Track {
|
||||||
|
Track {
|
||||||
|
id: track.id,
|
||||||
|
title: track.title,
|
||||||
|
duration_ms: track.duration_ms,
|
||||||
|
|
||||||
|
created_at: track.created_at,
|
||||||
|
updated_at: track.updated_at,
|
||||||
|
|
||||||
|
spotify_id: track.spotify_id,
|
||||||
|
tidal_id: track.tidal_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
73
src/models/user.rs
Normal file
73
src/models/user.rs
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
use crate::helpers::db;
|
||||||
|
use crate::schema::users;
|
||||||
|
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::users)]
|
||||||
|
#[diesel(check_for_backend(diesel::pg::Pg))]
|
||||||
|
pub struct User {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub email: String,
|
||||||
|
|
||||||
|
pub password: String,
|
||||||
|
|
||||||
|
pub updated_at: Option<NaiveDateTime>,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Queryable, Serialize)]
|
||||||
|
pub struct Users {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub email: String,
|
||||||
|
|
||||||
|
pub password: String,
|
||||||
|
|
||||||
|
pub updated_at: Option<NaiveDateTime>,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Users {
|
||||||
|
pub fn find(id: &str) -> Result<Self, Error> {
|
||||||
|
let conn = &mut db::connection()?;
|
||||||
|
let user = users::table.filter(users::id.eq(id)).first(conn)?;
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create(user: User) -> Result<Self, Error> {
|
||||||
|
let conn = &mut db::connection()?;
|
||||||
|
let user = diesel::insert_into(users::table)
|
||||||
|
.values(User::from(user))
|
||||||
|
.get_result(conn)?;
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_by_email(email: &str) -> Result<Self, Error> {
|
||||||
|
let conn = &mut db::connection()?;
|
||||||
|
let user = users::table.filter(users::email.eq(email)).first(conn)?;
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_password(password: &str, user: &Users) -> bool {
|
||||||
|
bcrypt::verify(password, &user.password).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
fn from(user: User) -> User {
|
||||||
|
User {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
password: user.password,
|
||||||
|
created_at: user.created_at,
|
||||||
|
updated_at: user.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
51
src/routes/auth.rs
Normal file
51
src/routes/auth.rs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
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 serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
#[post("/auth/login")]
|
||||||
|
async fn login(body: web::Json<LoginBody>) -> Result<HttpResponse, ErrorResponse> {
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
struct Response {
|
||||||
|
access_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = Users::find_by_email(&body.email);
|
||||||
|
match user {
|
||||||
|
Ok(user) => {
|
||||||
|
let password = Users::verify_password(&body.password, &user);
|
||||||
|
|
||||||
|
if password == false {
|
||||||
|
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(),
|
||||||
|
status: StatusCode::BAD_REQUEST,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct LoginBody {
|
||||||
|
email: String,
|
||||||
|
password: String,
|
||||||
|
}
|
41
src/routes/me.rs
Normal file
41
src/routes/me.rs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
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 serde::Serialize;
|
||||||
|
|
||||||
|
pub fn routes() -> Scope {
|
||||||
|
web::scope("/me").service(me).service(me_playlists)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Response {
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
email: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("")]
|
||||||
|
async fn me(req: HttpRequest) -> Result<HttpResponse, ErrorResponse> {
|
||||||
|
let user = get_user(req)?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(Response {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/playlists")]
|
||||||
|
async fn me_playlists(req: HttpRequest) -> Result<HttpResponse, ErrorResponse> {
|
||||||
|
let user = get_user(req)?;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Response {
|
||||||
|
playlists: Vec<Playlists>,
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(Response {
|
||||||
|
playlists: Playlists::find_for_user(&user.id)?,
|
||||||
|
}))
|
||||||
|
}
|
4
src/routes/mod.rs
Normal file
4
src/routes/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod auth;
|
||||||
|
pub mod me;
|
||||||
|
pub mod playlists;
|
||||||
|
pub mod users;
|
31
src/routes/playlists.rs
Normal file
31
src/routes/playlists.rs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
use crate::middlewares::error::ErrorResponse;
|
||||||
|
use crate::models::playlist::{PlaylistCreator, Playlists};
|
||||||
|
use crate::models::tracks::Tracks;
|
||||||
|
use actix_web::{get, web, HttpResponse};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
struct GetPlaylistResponse {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub creator: PlaylistCreator,
|
||||||
|
pub tracks: Vec<Tracks>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/{playlist_id}")]
|
||||||
|
pub async fn get_playlist(path: web::Path<String>) -> Result<HttpResponse, ErrorResponse> {
|
||||||
|
let playlist_id = path.into_inner();
|
||||||
|
let playlist = Playlists::find(playlist_id.as_str())?;
|
||||||
|
|
||||||
|
let creator = playlist.get_creator()?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(GetPlaylistResponse {
|
||||||
|
id: playlist.id.to_string(),
|
||||||
|
name: playlist.name.to_string(),
|
||||||
|
creator: PlaylistCreator {
|
||||||
|
id: creator.id,
|
||||||
|
name: creator.name,
|
||||||
|
},
|
||||||
|
tracks: playlist.get_tracks()?,
|
||||||
|
}))
|
||||||
|
}
|
38
src/routes/users.rs
Normal file
38
src/routes/users.rs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
use crate::middlewares::error::ErrorResponse;
|
||||||
|
use crate::models::user::Users;
|
||||||
|
use actix_web::http::StatusCode;
|
||||||
|
use actix_web::{get, web, HttpResponse, Result};
|
||||||
|
use diesel::result::Error as DBError;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct GetUserResponse {
|
||||||
|
id: String,
|
||||||
|
name: 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());
|
||||||
|
|
||||||
|
match user {
|
||||||
|
Ok(user) => Ok(HttpResponse::Ok().json(GetUserResponse {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
})),
|
||||||
|
Err(DBError::NotFound) => {
|
||||||
|
return Err(ErrorResponse {
|
||||||
|
message: "User not found".to_string(),
|
||||||
|
status: StatusCode::NOT_FOUND,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(ErrorResponse {
|
||||||
|
message: "Unknown error".to_string(),
|
||||||
|
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
64
src/schema.rs
Normal file
64
src/schema.rs
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
// @generated automatically by Diesel CLI.
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
playlists (id) {
|
||||||
|
#[max_length = 24]
|
||||||
|
id -> Varchar,
|
||||||
|
#[max_length = 255]
|
||||||
|
name -> Varchar,
|
||||||
|
#[max_length = 24]
|
||||||
|
creator_id -> Varchar,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Nullable<Timestamp>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
playlists_tracks (playlist_id, track_id) {
|
||||||
|
#[max_length = 24]
|
||||||
|
playlist_id -> Varchar,
|
||||||
|
#[max_length = 24]
|
||||||
|
track_id -> Varchar,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
tracks (id) {
|
||||||
|
#[max_length = 24]
|
||||||
|
id -> Varchar,
|
||||||
|
#[max_length = 255]
|
||||||
|
title -> Varchar,
|
||||||
|
duration_ms -> Int4,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Nullable<Timestamp>,
|
||||||
|
#[max_length = 21]
|
||||||
|
spotify_id -> Nullable<Varchar>,
|
||||||
|
#[max_length = 10]
|
||||||
|
tidal_id -> Nullable<Varchar>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
users (id) {
|
||||||
|
#[max_length = 24]
|
||||||
|
id -> Varchar,
|
||||||
|
#[max_length = 255]
|
||||||
|
name -> Varchar,
|
||||||
|
#[max_length = 255]
|
||||||
|
email -> Varchar,
|
||||||
|
password -> Text,
|
||||||
|
updated_at -> Nullable<Timestamp>,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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!(
|
||||||
|
playlists,
|
||||||
|
playlists_tracks,
|
||||||
|
tracks,
|
||||||
|
users,
|
||||||
|
);
|
8
src/utils.rs
Normal file
8
src/utils.rs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
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)
|
||||||
|
}
|
Loading…
Reference in a new issue