Skip to content

Commit

Permalink
Fix Stale Authentication Tokens, Update Gradient when Searching & Fal…
Browse files Browse the repository at this point in the history
…lback URL for Invidious (#153)
  • Loading branch information
WillKirkmanM authored Dec 15, 2024
2 parents c151643 + 7a48c72 commit dc98229
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 79 deletions.
3 changes: 2 additions & 1 deletion apps/web/app/(app)/search/SearchComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { DialogTrigger, DialogContent } from "@radix-ui/react-dialog";
import ReactPlayer from "react-player";
import Image from "next/image";
import { YoutubeIcon } from "lucide-react";
import PageGradient from "@/components/Layout/PageGradient";

interface YouTubeVideo {
id: string;
Expand Down Expand Up @@ -119,7 +120,7 @@ export default function SearchComponent() {
<TopResultCardSkeleton />
</div>
) : (
results && results[0] && <TopResultsCard result={results[0]} />
results && results[0] && <><TopResultsCard result={results[0]} /> <PageGradient imageSrc={results[0].album_object.cover_url}/></>
)}
</div>
</div>
Expand Down
15 changes: 13 additions & 2 deletions apps/web/components/Layout/PageGradient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,26 @@
import { useEffect } from "react";
import { useGradientHover } from "../Providers/GradientHoverProvider";
import { FastAverageColor } from "fast-average-color";
import getBaseURL from "@/lib/Server/getBaseURL";

export default function PageGradient({ imageSrc }: { imageSrc: string }) {
const { setGradient } = useGradientHover()

useEffect(() => {
const fac = new FastAverageColor();
const getColor = async () => {
const color = await fac.getColorAsync(imageSrc)
setGradient(color.hex)
const processedImageSrc = !imageSrc.startsWith("http://") && !imageSrc.startsWith("https://")
? `${getBaseURL()}/image/${encodeURIComponent(imageSrc)}?raw=true`
: imageSrc;


try {
const color = await fac.getColorAsync(processedImageSrc)
setGradient(color.hex)
} catch (error) {
console.error('Error getting color:', error)
setGradient('#000000')
}
}

getColor()
Expand Down
26 changes: 14 additions & 12 deletions apps/web/components/Layout/SplashScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import Image from "next/image";
import { useRouter } from "next/navigation";
import React, { useEffect, useState } from "react";
import { useSession } from "../Providers/AuthProvider";
import { isValid } from "@music/sdk";
import { deleteCookie } from "cookies-next";

interface SplashScreenProps {
children: React.ReactNode;
Expand All @@ -20,21 +22,18 @@ const SplashScreen: React.FC<SplashScreenProps> = ({ children }) => {
const checkServerUrl = async () => {
if (isLoading) return;

const currentPath = window.location.pathname;
if (
session?.username &&
currentPath !== "/" &&
currentPath !== "/login" &&
currentPath !== "/login/" &&
currentPath.startsWith("/home")
) {
setLoading(false);
return;
}

try {
setLoading(true);

const validationResult = await isValid();

if (!validationResult.status) {
deleteCookie('plm_accessToken');
deleteCookie('plm_refreshToken');
push('/login');
return;
}

const storedServer = localStorage.getItem("server");
const serverUrl = storedServer
? JSON.parse(storedServer).local_address
Expand All @@ -54,6 +53,7 @@ const SplashScreen: React.FC<SplashScreenProps> = ({ children }) => {
}

if (session?.username) {
const currentPath = window.location.pathname;
if (
currentPath === "/" ||
currentPath === "/login" ||
Expand All @@ -64,9 +64,11 @@ const SplashScreen: React.FC<SplashScreenProps> = ({ children }) => {
return;
}

const currentPath = window.location.pathname;
if (currentPath !== "/login" && currentPath !== "/login/") {
push("/login");
}

} catch (error) {
console.error("Server check failed:", error);
push("/");
Expand Down
6 changes: 3 additions & 3 deletions crates/backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use actix_cors::Cors;
use actix_web::HttpResponse;
use actix_web::{middleware, web, App, HttpServer};
use actix_web_httpauth::middleware::HttpAuthentication;
use routes::authentication::{admin_guard, refresh};
use routes::authentication::{admin_guard, is_valid, refresh};
use tokio::task;
use tracing::{info, Level};
use tracing_subscriber::FmtSubscriber;
Expand All @@ -21,8 +21,7 @@ use routes::authentication::{login, register, validator};
use routes::filesystem;
use routes::image::image;
use routes::music::{
format_contributing_artists_route, index_library_no_cover_url, process_library, songs_list,
stream_song, test,
index_library_no_cover_url, process_library,
};
use routes::playlist;
use routes::search::{self, populate_search_data};
Expand Down Expand Up @@ -188,6 +187,7 @@ async fn main() -> std::io::Result<()> {
.service(login)
.service(register)
.service(refresh)
.service(is_valid)
)
.service(image)
.route("/ws", web::get().to(ws))
Expand Down
61 changes: 59 additions & 2 deletions crates/backend/src/routes/authentication.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::env;
use std::{env, time::{SystemTime, UNIX_EPOCH}};

use actix_web::{cookie::{Cookie, SameSite}, dev::ServiceRequest, http::header, post, web, HttpRequest, HttpResponse, Responder};
use actix_web::{cookie::{Cookie, SameSite}, dev::ServiceRequest, get, http::header, post, web, HttpRequest, HttpResponse, Responder};
use actix_web_httpauth::extractors::bearer::BearerAuth;
use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Expand All @@ -12,6 +12,7 @@ use dotenvy::dotenv;
use futures::future::{ready, Ready};
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use serde_json::json;

use crate::utils::{config::get_jwt_secret, database::{database::establish_connection, models::NewUser}};

Expand Down Expand Up @@ -80,6 +81,62 @@ fn generate_refresh_token(user_id: i32, username: &str, role: &String) -> String
encode(&header, &claims, &EncodingKey::from_secret(secret.as_ref())).expect("Token encoding failed")
}

#[get("/is-valid")]
pub async fn is_valid(req: HttpRequest) -> impl Responder {
let token = if let Some(cookie_header) = req.headers().get(header::COOKIE) {
if let Ok(cookie_str) = cookie_header.to_str() {
cookie_str.split(';')
.filter_map(|cookie| Cookie::parse_encoded(cookie.trim()).ok())
.find(|cookie| cookie.name() == "plm_accessToken")
.map(|cookie| cookie.value().to_string())
} else {
None
}
} else {
None
};

let token = match token {
Some(t) => t,
None => return HttpResponse::Unauthorized().json(json!({
"status": false,
"message": "No token found in cookies"
}))
};

let secret = get_jwt_secret();
let mut validation = Validation::new(Algorithm::HS256);
validation.leeway = 60;
validation.validate_exp = true;

match decode::<Claims>(&token, &DecodingKey::from_secret(secret.as_ref()), &validation) {
Ok(token_data) => {
let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as usize;

if token_data.claims.exp < current_time {
return HttpResponse::Unauthorized().json(json!({
"status": false,
"message": "Token expired",
"token_type": token_data.claims.token_type
}));
}

HttpResponse::Ok().json(json!({
"status": true,
"token_type": token_data.claims.token_type,
"claims": token_data.claims
}))
},
Err(e) => HttpResponse::Unauthorized().json(json!({
"status": false,
"message": format!("Invalid token: {}", e)
}))
}
}

#[post("/login")]
pub async fn login(form: web::Json<AuthData>) -> impl Responder {
use crate::utils::database::schema::user::dsl::*;
Expand Down
87 changes: 48 additions & 39 deletions crates/backend/src/routes/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use chrono::NaiveDateTime;
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
use dirs;
use lazy_static::lazy_static;
use rand::seq::SliceRandom;
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use serde_json::{self, from_str, json};
Expand All @@ -17,7 +18,7 @@ use tantivy::query::{FuzzyTermQuery, QueryParser};
use tantivy::schema::{*, Term};
use tantivy::{doc, Index, IndexWriter, ReloadPolicy};
use tokio::task;
use tracing::error;
use tracing::{error, info};

use crate::routes::album::fetch_album_info;
use crate::routes::artist::fetch_artist_info;
Expand Down Expand Up @@ -547,22 +548,20 @@ async fn search_youtube(query: web::Query<SearchRequest>) -> Result<impl Respond
let client = reqwest::Client::new();
let search_query = &query.q;

let response = match env::var("INVIDIOUS_URL") {
Ok(invidious_url) => {
client
.get(format!(
"{}/api/v1/search?q={}&type=video&page=1",
invidious_url.trim_end_matches('/'),
search_query
))
.header("Accept", "application/json")
.send()
.await?
.json::<Vec<InvidiousVideo>>()
.await?
},
Err(_) => { Vec::new() }
};
let invidious_url = env::var("INVIDIOUS_URL")
.unwrap_or_else(|_| get_random_invidious_instance());

let response = client
.get(format!(
"{}/api/v1/search?q={}&type=video&page=1",
invidious_url.trim_end_matches('/'),
search_query
))
.header("Accept", "application/json")
.send()
.await?
.json::<Vec<InvidiousVideo>>()
.await?;

let limited_results = response.into_iter()
.take(10)
Expand Down Expand Up @@ -652,35 +651,45 @@ struct Replies {
continuation: Option<String>,
}

fn get_random_invidious_instance() -> String {
let instances = vec![
"https://invidious.nerdvpn.de",
"https://inv.nadeko.net",
"https://invidious.jing.rocks"
];
instances.choose(&mut rand::thread_rng())
.unwrap_or(&"https://invidious.snopyta.org")
.to_string()
}

#[get("/youtube/comments")]
async fn get_youtube_comments(query: web::Query<CommentsRequest>) -> Result<impl Responder, Box<dyn Error>> {
let client = reqwest::Client::new();
let video_id = &query.video_id;

let comments = match env::var("INVIDIOUS_URL") {
Ok(invidious_url) => {
let response = client
.get(format!(
"{}/api/v1/comments/{}",
invidious_url.trim_end_matches('/'),
video_id
))
.header("Accept", "application/json")
.send()
.await?;

if !response.status().is_success() {
return Ok(HttpResponse::InternalServerError().json(json!({
"error": "Failed to fetch comments"
})));
}
let invidious_url = env::var("INVIDIOUS_URL")
.unwrap_or_else(|_| get_random_invidious_instance());

let root = response.json::<CommentInfo>().await?;
root
},
Err(_) => CommentInfo::default()
};
info!("Using Invidious instance: {}", invidious_url);

let response = client
.get(format!(
"{}/api/v1/comments/{}",
invidious_url.trim_end_matches('/'),
video_id
))
.header("Accept", "application/json")
.send()
.await?;

if !response.status().is_success() {
error!("Failed to fetch comments: {}", response.status());
return Ok(HttpResponse::InternalServerError().json(json!({
"error": "Failed to fetch comments"
})));
}

let comments = response.json::<CommentInfo>().await?;
Ok(HttpResponse::Ok().json(comments))
}

Expand Down
23 changes: 23 additions & 0 deletions packages/music-sdk/src/lib/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,29 @@ export async function register(data: AuthData): Promise<ResponseAuthData> {
return response.data;
}

interface TokenValidationResponse {
status: boolean;
token_type?: string;
claims?: {
sub: string;
exp: number;
username: string;
bitrate: number;
token_type: string;
role: string;
};
message?: string;
}

/**
* Check if the current token is valid.
* @returns {Promise<TokenValidationResponse>} - A promise that resolves to the validation response.
*/
export async function isValid(): Promise<TokenValidationResponse> {
const response = await axios.get('/auth/is-valid');
return response.data;
}

/**
* Login a user.
* @param {AuthData} data - The login data.
Expand Down
Loading

0 comments on commit dc98229

Please sign in to comment.