diff --git a/Dockerfile b/Dockerfile index 7e9d3e4c..7d7499fb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,7 +31,7 @@ FROM rust:1.81 AS backend-builder WORKDIR /usr/src RUN apt-get update && apt-get install -y --no-install-recommends \ - sqlite3 libsqlite3-dev wget make build-essential pkg-config libssl-dev \ + sqlite3 libsqlite3-dev wget make build-essential pkg-config libssl-dev ffmpeg \ && rm -rf /var/lib/apt/lists/* RUN wget https://www.nasm.us/pub/nasm/releasebuilds/2.16/nasm-2.16.tar.gz \ @@ -67,23 +67,16 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libsqlite3-dev \ wget \ ca-certificates \ + ffmpeg \ && rm -rf /var/lib/apt/lists/* -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 --ingroup nodejs nextjs +# Change ownership of the /app directory to the root user +# RUN chown -R root:root /app -# Change ownership of the /app directory to the nextjs user -RUN chown -R nextjs:nodejs /app - -# Create and set permissions for the directories -RUN mkdir -p /ParsonLabsMusic /music && \ - chown -R nextjs:nodejs /ParsonLabsMusic /music && \ - chmod -R 755 /ParsonLabsMusic /music - -USER nextjs -COPY --from=backend-builder --chown=nextjs:nodejs /usr/src/crates/backend/target/release/music-server /usr/local/bin/music-server -COPY --from=backend-builder --chown=nextjs:nodejs /usr/src/crates/backend/music.db /usr/src/crates/backend/music.db -COPY --from=installer --chown=nextjs:nodejs /app/apps/web/out ./apps/web/out +# Copy files as root +COPY --from=backend-builder /usr/src/crates/backend/target/release/music-server /usr/local/bin/music-server +COPY --from=backend-builder /usr/src/crates/backend/music.db /usr/src/crates/backend/music.db +COPY --from=installer /app/apps/web/out ./apps/web/out ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt ENV RUNNING_IN_DOCKER=true diff --git a/apps/web/app/(app)/album/AlbumComponent.tsx b/apps/web/app/(app)/album/AlbumComponent.tsx index 2916be5e..a0be187f 100644 --- a/apps/web/app/(app)/album/AlbumComponent.tsx +++ b/apps/web/app/(app)/album/AlbumComponent.tsx @@ -87,7 +87,7 @@ export default function AlbumComponent() { ); return ( -
+
-
+

{album.name}

{album.release_group_album?.rating.value !== 0 && renderStars(album.release_group_album?.rating.value || 0)} - -
+
-

-

{releaseDate}

-

-

{album.songs.length} Songs

-

-

{formatDuration(totalDuration)}

-
+
+

{releaseDate}

+

+

{album.songs.length} Songs

+

+

{formatDuration(totalDuration)}

+
+

{album.release_group_album?.genres.map((tag) => ( { - async function checkConfig() { - const config = await hasConfig(); - if (config) { - setConfigExists(true); - } else { - setConfigExists(false); - setGradient("#FFFFFF"); - } - } - - checkConfig(); - }, [setGradient]); - - useEffect(() => { - const savedConfig = localStorage.getItem("layoutConfig"); - if (savedConfig) { - setComponents(JSON.parse(savedConfig)); - } - }, [setComponents]); - - type ComponentConfig = { - id: string; - name: string; - visible: boolean; - pinned: boolean; - }; - - const renderComponent = (component: ComponentConfig): JSX.Element | null => { - if (!component.visible) return null; - switch (component.id) { - case "LandingCarousel": - return ; - case "ListenAgain": - return ; - case "SimilarTo": - return ; - case "RecommendedAlbums": - return ; - case "RandomSongs": - return ; - case "FromYourLibrary": - return ; - case "MusicVideos": - return ; - default: - return null; - } - }; - - return configExists ? ( -

-
- -
- - {components - .filter((component) => component.pinned) - .map(renderComponent)} - {components - .filter((component) => !component.pinned) - .map(renderComponent)} - -
- ) : ( -
-

No Config Found!

-

- - Head to the setup page to index your library - -

- - - -
- ); - } \ No newline at end of file +"use client"; + +import FromYourLibrary from "@/components/Home/FromYourLibrary"; +import LandingCarousel from "@/components/Home/LandingCarousel"; +import ListenAgain from "@/components/Home/ListenAgain"; +import MusicVideos from "@/components/Home/MusicVideos"; +import RandomSongs from "@/components/Home/RandomSongs"; +import RecommendedAlbums from "@/components/Home/RecommendedAlbums"; +import SimilarTo from "@/components/Home/SimilarTo"; +import CustomiseFeed from "@/components/Layout/CustomiseFeed"; +import GenreButtons from "@/components/Layout/GenreButtons"; +import { useGradientHover } from "@/components/Providers/GradientHoverProvider"; +import { hasConfig } from "@music/sdk"; +import { Button } from "@music/ui/components/button"; +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { useLayoutConfig } from "@/components/Providers/LayoutConfigContext"; + +export default function Home() { + const [configExists, setConfigExists] = useState(true); + const { components, setComponents } = useLayoutConfig(); + const { setGradient } = useGradientHover(); + const [isMobile, setIsMobile] = useState(false); + + useEffect(() => { + async function checkConfig() { + const config = await hasConfig(); + if (config) { + setConfigExists(true); + } else { + setConfigExists(false); + setGradient("#FFFFFF"); + } + } + + checkConfig(); + }, [setGradient]); + + useEffect(() => { + const savedConfig = localStorage.getItem("layoutConfig"); + if (savedConfig) { + setComponents(JSON.parse(savedConfig)); + } + }, [setComponents]); + + useEffect(() => { + const handleResize = () => { + setIsMobile(window.innerWidth < 768); + }; + + window.addEventListener('resize', handleResize); + + handleResize(); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + type ComponentConfig = { + id: string; + name: string; + visible: boolean; + pinned: boolean; + }; + + const renderComponent = (component: ComponentConfig): JSX.Element | null => { + if (!component.visible) return null; + switch (component.id) { + case "LandingCarousel": + return ; + case "ListenAgain": + return ; + case "SimilarTo": + return ; + case "RecommendedAlbums": + return ; + case "RandomSongs": + return ; + case "FromYourLibrary": + return ; + case "MusicVideos": + return ; + default: + return null; + } + }; + + return configExists ? ( +
+
+ {!isMobile && } +
+ + {components + .filter((component) => component.pinned) + .map(renderComponent)} + {components + .filter((component) => !component.pinned) + .map(renderComponent)} + +
+ ) : ( +
+

No Config Found!

+

+ + Head to the setup page to index your library + +

+ + + +
+ ); +} \ No newline at end of file diff --git a/apps/web/app/(app)/layout.tsx b/apps/web/app/(app)/layout.tsx index 90125715..2a94adf2 100644 --- a/apps/web/app/(app)/layout.tsx +++ b/apps/web/app/(app)/layout.tsx @@ -1,36 +1,9 @@ +import SplashScreen from "@/components/Layout/SplashScreen"; import "@music/ui/globals.css"; -import { Inter as FontSans } from "next/font/google"; +import { cn } from "@music/ui/lib/utils"; import { Metadata, Viewport } from "next"; +import { Inter as FontSans } from "next/font/google"; import MainLayout from "./main-layout"; -import SplashScreen from "@/components/Layout/SplashScreen"; -import { cn } from "@music/ui/lib/utils"; -import AuthProvider from "@/components/Providers/AuthProvider"; - -export const metadata: Metadata = { - applicationName: "ParsonLabs Music", - title: { - default: "ParsonLabs Music", - template: "%s | ParsonLabs Music", - }, - description: "Own your music.", - manifest: "/manifest.json", - appleWebApp: { - capable: true, - statusBarStyle: "default", - title: "ParsonLabs Music", - }, - formatDetection: { - telephone: false, - }, - openGraph: { - type: "website", - title: { - default: "ParsonLabs Music", - template: "%s | ParsonLabs Music", - }, - description: "Own your music.", - }, -}; const fontSans = FontSans({ subsets: ["latin"], @@ -49,13 +22,9 @@ export default async function RootLayout({ children }: any) { "min-h-screen bg-background font-sans antialiased bg-gray-900 texxt-white", fontSans.variable )}> - - - - {children} - - - + + {children} + ); diff --git a/apps/web/app/(dashboard)/layout.tsx b/apps/web/app/(dashboard)/layout.tsx index 2691a515..0c365423 100644 --- a/apps/web/app/(dashboard)/layout.tsx +++ b/apps/web/app/(dashboard)/layout.tsx @@ -4,14 +4,11 @@ import "@music/ui/globals.css" import { Inter as FontSans } from "next/font/google" import pl from "@/assets/pl-tp.png" +import SettingsAuth from "@/components/Layout/Settings/SettingsAuth" import SettingsSidebar from "@/components/Layout/Settings/Sidebar" import { cn } from "@music/ui/lib/utils" -import Link from "next/link" import Image from "next/image" -import { usePathname, useRouter } from "next/navigation" -import { useEffect } from "react" -import AuthProvider, { useSession } from "@/components/Providers/AuthProvider" -import SettingsAuth from "@/components/Layout/Settings/SettingsAuth" +import Link from "next/link" const fontSans = FontSans({ subsets: ["latin"], @@ -39,7 +36,6 @@ export default function RootLayout({ children }: RootLayoutProps) {
-
@@ -47,7 +43,6 @@ export default function RootLayout({ children }: RootLayoutProps) {
{children} -
) diff --git a/apps/web/app/(unprotected)/layout.tsx b/apps/web/app/(unprotected)/layout.tsx index 682e5d6f..95e7bbde 100644 --- a/apps/web/app/(unprotected)/layout.tsx +++ b/apps/web/app/(unprotected)/layout.tsx @@ -5,17 +5,12 @@ import pl from "@/assets/pl-tp.png" import { cn } from "@music/ui/lib/utils" import { Metadata } from "next" import Image from "next/image" -import AuthProvider from "@/components/Providers/AuthProvider" const fontSans = FontSans({ subsets: ["latin"], variable: "--font-sans", }) -export const metadata: Metadata = { - title: "ParsonLabs Music" -} - type RootLayoutProps = { children: React.ReactNode } @@ -36,9 +31,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
- - {children} - + {children} ) diff --git a/apps/web/app/(unprotected)/login/page.tsx b/apps/web/app/(unprotected)/login/page.tsx index 3eddb166..8d67fce1 100644 --- a/apps/web/app/(unprotected)/login/page.tsx +++ b/apps/web/app/(unprotected)/login/page.tsx @@ -1,5 +1,6 @@ "use client"; +import { useSession } from "@/components/Providers/AuthProvider"; import getBaseURL from "@/lib/Server/getBaseURL"; import { zodResolver } from '@hookform/resolvers/zod'; import { getServerInfo } from "@music/sdk"; @@ -13,7 +14,6 @@ import { FormMessage, } from "@music/ui/components/form"; import { Input } from '@music/ui/components/input'; -import { setCookie } from 'cookies-next'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { SubmitHandler, useForm } from 'react-hook-form'; @@ -27,13 +27,21 @@ const schema = z.object({ type FormData = z.infer; export default function Login() { - const router = useRouter(); + const { push } = useRouter(); const [errorMessage, setErrorMessage] = useState(null); const [serverInfo, setServerInfo] = useState<{ login_disclaimer?: string } | null>(null); const form = useForm({ resolver: zodResolver(schema), }); + const { session } = useSession() + + useEffect(() => { + if (session && session?.username) { + push("/home") + } + }, [session?.username, session, push]) + useEffect(() => { async function fetchServerInfo() { try { @@ -44,8 +52,12 @@ export default function Login() { } }; + if (session && session?.username) { + push("/home") + } + fetchServerInfo(); - }, []); + }, [session, push]); const { handleSubmit } = form; @@ -59,15 +71,15 @@ export default function Login() { body: JSON.stringify(data), credentials: 'include' }); - - if (response.ok) { - router.push("/"); + + const result = await response.json(); + + if (response.ok && result.status) { + push("/home"); } else { - const errorMessage = await response.text(); - setErrorMessage(errorMessage || 'Authentication failed'); + setErrorMessage(result.message || 'Authentication failed'); } } catch (error) { - console.error('Login error:', error); setErrorMessage('An error occurred during login'); } }; diff --git a/apps/web/app/(unprotected)/page.tsx b/apps/web/app/(unprotected)/page.tsx index 3969c088..cee5c205 100644 --- a/apps/web/app/(unprotected)/page.tsx +++ b/apps/web/app/(unprotected)/page.tsx @@ -87,10 +87,16 @@ export default function MainPage() { const onSubmit: SubmitHandler = async (data) => { setLoading(true); - + try { - localStorage.setItem("server", JSON.stringify({ local_address: data.serverUrl })); - let serverInfo = await getServerInfo(); + const response = await fetch(`${data.serverUrl}/api/s/server/info`, { + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include' + }); + + const serverInfo = await response.json() if (serverInfo.product_name && serverInfo.startup_wizard_completed) { localStorage.setItem("server", JSON.stringify(serverInfo)); @@ -101,6 +107,7 @@ export default function MainPage() { push("/login"); } } else { + localStorage.setItem("server", JSON.stringify({ local_address: data.serverUrl })); if (!serverInfo.startup_wizard_completed && session) { push("/setup/library"); } else { diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx new file mode 100644 index 00000000..c654ae1f --- /dev/null +++ b/apps/web/app/layout.tsx @@ -0,0 +1,59 @@ +import "@music/ui/globals.css"; +import { Inter as FontSans } from "next/font/google"; +import { Metadata, Viewport } from "next"; +import SplashScreen from "@/components/Layout/SplashScreen"; +import { cn } from "@music/ui/lib/utils"; +import AuthProvider from "@/components/Providers/AuthProvider"; + +export const metadata: Metadata = { + applicationName: "ParsonLabs Music", + title: { + default: "ParsonLabs Music", + template: "%s | ParsonLabs Music", + }, + description: "Own your music.", + manifest: "/manifest.json", + appleWebApp: { + capable: true, + statusBarStyle: "default", + title: "ParsonLabs Music", + }, + formatDetection: { + telephone: false, + }, + openGraph: { + type: "website", + title: { + default: "ParsonLabs Music", + template: "%s | ParsonLabs Music", + }, + description: "Own your music.", + }, +}; + +const fontSans = FontSans({ + subsets: ["latin"], + variable: "--font-sans", +}) + +export const viewport: Viewport = { + themeColor: "#FFFFFF", +}; + +export default async function RootLayout({ children }: any) { + return ( + + + + + + {children} + + + + + ); +} \ No newline at end of file diff --git a/apps/web/components/Home/LandingCarousel.tsx b/apps/web/components/Home/LandingCarousel.tsx index aef8d095..fa0df354 100644 --- a/apps/web/components/Home/LandingCarousel.tsx +++ b/apps/web/components/Home/LandingCarousel.tsx @@ -1,4 +1,4 @@ -"use client" +"use client"; import getBaseURL from "@/lib/Server/getBaseURL"; import setCache, { getCache } from "@/lib/Caching/cache"; @@ -9,7 +9,7 @@ import { Skeleton } from "@music/ui/components/skeleton"; import { Play } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; -import { useEffect, useState, useRef } from "react"; +import { useEffect, useState } from "react"; import { FastAverageColor } from 'fast-average-color'; async function getRandomAlbumAndSongs(): Promise<{ album: Album & { artist_object: Artist }, songs: LibrarySong[] }> { @@ -62,7 +62,7 @@ export function LandingCarouselSkeleton() { />
- ) + ); } export default function LandingCarousel() { @@ -73,7 +73,7 @@ export default function LandingCarousel() { useEffect(() => { async function fetchAlbumAndSongs() { const cachedData = getCache("landingCarousel"); - + if (cachedData) { setAlbum(cachedData.album); setSongs(cachedData.songs); @@ -84,27 +84,29 @@ export default function LandingCarousel() { setCache("landingCarousel", { album, songs }, 86400000); } } - + fetchAlbumAndSongs(); }, []); useEffect(() => { async function getButtonColour() { - const albumCoverURL = `${getBaseURL()}/image/${encodeURIComponent(album?.cover_url || "")}?raw=true` + const albumCoverURL = `${getBaseURL()}/image/${encodeURIComponent(album?.cover_url || "")}?raw=true`; const fac = new FastAverageColor(); - const color = await fac.getColorAsync(albumCoverURL) + const color = await fac.getColorAsync(albumCoverURL); setButtonColor(color.hex); } - getButtonColour() - }) + if (album) { + getButtonColour(); + } + }, [album]); if (!album) return null; const albumCoverURL = `${getBaseURL()}/image/${encodeURIComponent(album.cover_url)}?raw=true`; return ( -
+
{`${album.name} { + const handleResize = () => { + if (window.innerWidth < 768) { + closeSidebar(); + } else { + openSidebar(); + } + }; + + window.addEventListener('resize', handleResize); + + handleResize(); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [closeSidebar, openSidebar]); + if (currentPath === null) { return null; } diff --git a/apps/web/components/Layout/Sidebar/AdminPanellayout.tsx b/apps/web/components/Layout/Sidebar/AdminPanellayout.tsx index 7e28cab2..df4bb64d 100644 --- a/apps/web/components/Layout/Sidebar/AdminPanellayout.tsx +++ b/apps/web/components/Layout/Sidebar/AdminPanellayout.tsx @@ -1,6 +1,5 @@ "use client"; -import AuthProvider from "@/components/Providers/AuthProvider"; import { cn } from "@music/ui/lib/utils"; import Sidebar from "./Sidebar"; @@ -19,16 +18,14 @@ export default function AdminPanelLayout({ return ( <> - -
- {children} -
-
+
+ {children} +
); } diff --git a/apps/web/components/Layout/SplashScreen.tsx b/apps/web/components/Layout/SplashScreen.tsx index 9af9f9ed..adec660f 100644 --- a/apps/web/components/Layout/SplashScreen.tsx +++ b/apps/web/components/Layout/SplashScreen.tsx @@ -1,7 +1,6 @@ "use client" import pl from '@/assets/pl-tp.png'; -import getSession from '@/lib/Authentication/JWT/getSession'; import { Loader2Icon } from 'lucide-react'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; @@ -12,29 +11,6 @@ interface SplashScreenProps { children: React.ReactNode; } -const setItemWithExpiry = (key: string, value: string, ttl: number) => { - const now = new Date(); - const item = { - value: value, - expiry: now.getTime() + ttl, - }; - localStorage.setItem(key, JSON.stringify(item)); -}; - -const getItemWithExpiry = (key: string) => { - const itemStr = localStorage.getItem(key); - if (!itemStr) { - return null; - } - const item = JSON.parse(itemStr); - const now = new Date(); - if (now.getTime() > item.expiry) { - localStorage.removeItem(key); - return null; - } - return item.value; -}; - const SplashScreen: React.FC = ({ children }) => { const [loading, setLoading] = useState(true); const { push } = useRouter(); @@ -48,41 +24,29 @@ const SplashScreen: React.FC = ({ children }) => { ); if (response.ok) { - if (session) { + if (session?.username) { const currentPath = window.location.pathname; const queryParams = window.location.search; - if (currentPath === "/") { + if (currentPath === "/" || currentPath === "/login" || currentPath === "/login/") { push("/home"); } else { push(`${currentPath}${queryParams}`); } setLoading(false); - setItemWithExpiry("loading", "false", 3600000); } else { push("/login"); setLoading(false); - setItemWithExpiry("loading", "false", 3600000); } } else { push("/") setLoading(false); - setItemWithExpiry("loading", "false", 3600000); } }; checkServerUrl(); }, [push, session]); - useEffect(() => { - const storedLoading = getItemWithExpiry("loading"); - if (storedLoading === "false") { - setLoading(false); - return; - } - - }, [push]); - if (loading) { return (
diff --git a/apps/web/components/Music/Album/AlbumTable.tsx b/apps/web/components/Music/Album/AlbumTable.tsx index 1494f653..1bdb27ae 100644 --- a/apps/web/components/Music/Album/AlbumTable.tsx +++ b/apps/web/components/Music/Album/AlbumTable.tsx @@ -133,9 +133,9 @@ export default function AlbumTable({ songs, album, artist }: PlaylistTableProps) handlePlay(album.cover_url, song, `${getBaseURL()}/api/stream/${encodeURIComponent(song.path)}?bitrate=${bitrate}`, artist)}> {song.track_number} -
+
-
+
{artist.name} diff --git a/apps/web/components/Setup/Server/ServerSelectIcon.tsx b/apps/web/components/Setup/Server/ServerSelectIcon.tsx index 7ed0a1df..51cb217e 100644 --- a/apps/web/components/Setup/Server/ServerSelectIcon.tsx +++ b/apps/web/components/Setup/Server/ServerSelectIcon.tsx @@ -44,7 +44,7 @@ export default function ServerSelectIcon() { const handleDelete = () => { localStorage.removeItem("server"); deleteCookie("server"); - router.push("/"); + router.refresh() }; return ( diff --git a/apps/web/components/User/NavbarProfilePicture.tsx b/apps/web/components/User/NavbarProfilePicture.tsx index 106c800b..2ea29a8b 100644 --- a/apps/web/components/User/NavbarProfilePicture.tsx +++ b/apps/web/components/User/NavbarProfilePicture.tsx @@ -18,21 +18,18 @@ import { } from "@music/ui/components/dropdown-menu" import { - Dialog, - DialogTrigger, + Dialog } from "@music/ui/components/dialog" import { Avatar, AvatarFallback, AvatarImage } from "@music/ui/components/avatar" import Link from "next/link" import SettingsDialog from "./SettingsDialog" -import getSession from "@/lib/Authentication/JWT/getSession" import { getProfilePicture } from "@music/sdk" import { deleteCookie } from "cookies-next" import { Inter as FontSans } from "next/font/google" import { useRouter } from "next/navigation" import { useEffect, useState } from "react" -import { cn } from "@music/ui/lib/utils" import { useSession } from "../Providers/AuthProvider" const fontSans = FontSans({ @@ -62,7 +59,9 @@ export default function NavbarProfilePicture() { const { push } = useRouter() function signOut() { - deleteCookie("music_jwt") + deleteCookie("plm_accessToken") + deleteCookie("plm_refreshToken") + push("/login") } diff --git a/apps/web/components/User/Username.tsx b/apps/web/components/User/Username.tsx index 9cc8163c..6037526d 100644 --- a/apps/web/components/User/Username.tsx +++ b/apps/web/components/User/Username.tsx @@ -6,7 +6,6 @@ import { zodResolver } from "@hookform/resolvers/zod" import { useForm } from "react-hook-form" import { z } from "zod" -import getSession from "@/lib/Authentication/JWT/getSession" import { Button } from "@music/ui/components/button" import { Form, @@ -40,7 +39,7 @@ export default function Username() { }) function onSubmit(data: z.infer) { - console.log(data.username) + // console.log(data.username) } return ( diff --git a/crates/backend/src/routes/authentication.rs b/crates/backend/src/routes/authentication.rs index 06b8914c..b054f4b7 100644 --- a/crates/backend/src/routes/authentication.rs +++ b/crates/backend/src/routes/authentication.rs @@ -116,7 +116,8 @@ pub async fn login(form: web::Json) -> impl Responder { if let Err(_) = response.add_cookie( &Cookie::build("plm_refreshToken", generated_refresh_token) .http_only(true) - .same_site(SameSite::Lax) + .same_site(SameSite::None) + .secure(true) .path("/") .finish(), ) { @@ -127,10 +128,11 @@ pub async fn login(form: web::Json) -> impl Responder { message: Some(String::from("Failed to set refresh token cookie")), }); } - + if let Err(_) = response.add_cookie( &Cookie::build("plm_accessToken", generated_access_token) - .same_site(SameSite::Lax) + .same_site(SameSite::None) + .secure(true) .path("/") .finish(), ) {