diff --git a/.env.example b/.env.example index 78c65f9..9a27b2d 100644 --- a/.env.example +++ b/.env.example @@ -3,10 +3,3 @@ NEXT_PUBLIC_APP_NAME='RaspAdmin' NEXT_PUBLIC_API_URL='/api' NEXT_PUBLIC_API_QUERY='?path=/{0}' NEXT_PUBLIC_SRC_QUERY='?path=/{0}' - -# Coloque aqui uma chave aleatória para habilitar autenticação por senha -API_AUTH_KEY= -API_USERNAME='admin' -API_PASSWORD='admin' - -API_DIR='./files' \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index ccf2647..c3c657b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,12 @@ -FROM node:18 +FROM node:18 AS build WORKDIR /app -RUN apt install -y git -RUN git clone https://github.com/mundotv789123/raspadmin-files-react.git . +COPY . . RUN cp .env.example .env RUN npm install RUN npm run build -CMD ["npm", "run", "start"] \ No newline at end of file +FROM nginx +RUN mkdir -p /var/www +COPY --from=build /app/out /var/www/html +COPY ./nginx.conf /etc/nginx/conf.d/default.conf diff --git a/docker-compose.yml b/docker-compose.yml index f9d66a6..47f71a7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,11 @@ version: '3.1' services: - raspadmin: - image: 'mundotv789123/raspadmin' - container_name: 'raspadmin_files' + frontend: + image: 'mundotv789123/raspadmin:front' ports: - - '8080:3000' + - '8080:80' + + backend: + image: 'mundotv789123/raspadmin:java' volumes: - - './files:/app/files' - environment: - NEXT_PUBLIC_APP_NAME: 'RaspAdmin' - API_AUTH_KEY: '' - API_USERNAME: 'admin' - API_PASSWORD: 'admin' + - './data:/app/data' diff --git a/next.config.js b/next.config.js index 614c3cd..42c10fd 100644 --- a/next.config.js +++ b/next.config.js @@ -1,6 +1,6 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - // output: 'export', // descomente essa linha caso precise compilar o projeto para usar o back-end em java + output: 'export', reactStrictMode: true, } diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..4911099 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,8 @@ +server { + listen 80; + root /var/www/html; + + location /api { + proxy_pass http://backend:8080; + } +} \ No newline at end of file diff --git a/public/img/wallpaper.jpg b/public/img/wallpaper.jpg new file mode 100644 index 0000000..6e23b4d Binary files /dev/null and b/public/img/wallpaper.jpg differ diff --git a/src/components/AudioPlayer/PlayList.tsx b/src/components/AudioPlayer/PlayList.tsx index 200d1eb..f8ae565 100644 --- a/src/components/AudioPlayer/PlayList.tsx +++ b/src/components/AudioPlayer/PlayList.tsx @@ -4,7 +4,8 @@ import { useEffect, useState } from "react"; import styled from "styled-components"; const PlayListContent = styled.div` - background-color: #0B4F75; + backdrop-filter: blur(5px); + background-color: rgba(0, 0, 0, 0.7); box-shadow: 0 5px 5px 0 rgba(0, 0, 0, 0.25); display: flex; flex-direction: column; diff --git a/src/components/AudioPlayer/index.tsx b/src/components/AudioPlayer/index.tsx index a01fcbd..4a751de 100644 --- a/src/components/AudioPlayer/index.tsx +++ b/src/components/AudioPlayer/index.tsx @@ -1,6 +1,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { AudioContent, AudioDurationContent, AudioDurationCount, AudioElement, AudioProgress, AudioTitle, ContentHeader, ControlButton, ControlContent, LoadingSpin, VolumeControl, VolumeProgress } from "./styles"; -import { faAngleDown, faAngleUp, faBackwardStep, faEye, faEyeSlash, faForwardStep, faPause, faPlay, faShuffle, faVolumeMute, faVolumeUp } from "@fortawesome/free-solid-svg-icons"; +import { faAngleUp, faBackwardStep, faEye, faEyeSlash, faForwardStep, faPause, faPlay, faShuffle, faVolumeMute, faVolumeUp } from "@fortawesome/free-solid-svg-icons"; import { useEffect, useRef, useState } from "react"; import PlayList from "./PlayList"; import Range from "../../elements/range"; diff --git a/src/components/AudioPlayer/styles.ts b/src/components/AudioPlayer/styles.ts index b84e4b8..1e8c97f 100644 --- a/src/components/AudioPlayer/styles.ts +++ b/src/components/AudioPlayer/styles.ts @@ -27,7 +27,9 @@ export const AudioContent = styled.div` ` export const AudioElement = styled.div` - background: linear-gradient(90deg, #006DAC, #00A2FF); + backdrop-filter: blur(5px); + background: linear-gradient(90deg, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.8)); + border: solid 1px gray; box-shadow: 0 4px 4px 0px rgba(0, 0, 0, 0.25); display: flex; flex-direction: column; diff --git a/src/components/FileBlock.tsx b/src/components/FileBlock.tsx index c175a7d..a3f3ba9 100644 --- a/src/components/FileBlock.tsx +++ b/src/components/FileBlock.tsx @@ -47,6 +47,7 @@ const FileCont = styled.a` overflow: hidden; font-weight: 600; word-wrap: break-word; + text-shadow: 0 1px 5px black; transition: background-color 0.5s; border-radius: 5px; &:hover { diff --git a/src/pages/api/auth/login.ts b/src/pages/api/auth/login.ts deleted file mode 100644 index 20fd959..0000000 --- a/src/pages/api/auth/login.ts +++ /dev/null @@ -1,37 +0,0 @@ -import cookie from "cookie"; -import * as jwt from 'jsonwebtoken'; -import md5 from "md5"; -import { NextApiRequest, NextApiResponse } from "next"; - -const COOKIE_NAME = "token"; - -export function verifyToken(cookies: string | null): boolean { - if (!process.env.API_AUTH_KEY || process.env.API_AUTH_KEY === '') { - return true; - } - - if (!cookies) - return false; - let token = cookie.parse(cookies)[COOKIE_NAME]; - if (!token) - return false; - - try { - let decoded = jwt.verify(token, process.env.API_AUTH_KEY); - return (decoded['password'] === md5(process.env.API_PASSWORD)); - } catch (e) { } - - return false; -} - -export default function handler(req: NextApiRequest, res: NextApiResponse) { - if (process.env.API_AUTH_KEY && process.env.API_AUTH_KEY !== '') { - if (process.env.API_USERNAME !== req.body.username || process.env.API_PASSWORD !== req.body.password) { - res.status(401).send({ message: 'Senha ou usuário inválido' }); - return; - } - let token = jwt.sign({ name: process.env.API_USERNAME, password: md5(process.env.API_PASSWORD) }, process.env.API_AUTH_KEY, { expiresIn: '1d' }); - res.setHeader('Set-Cookie', cookie.serialize(COOKIE_NAME, token, { httpOnly: true, path: "/" })) - } - res.status(200).json({ message: 'Success' }); -} \ No newline at end of file diff --git a/src/pages/api/files/index.ts b/src/pages/api/files/index.ts deleted file mode 100644 index de5ee45..0000000 --- a/src/pages/api/files/index.ts +++ /dev/null @@ -1,63 +0,0 @@ -import fs from 'fs' -import { verifyToken } from '../auth/login' -import { NextApiRequest, NextApiResponse } from 'next' -import { FileModel } from '../../../services/models/FilesModel'; - -export default function handler(req: NextApiRequest, res: NextApiResponse) { - /* verificando autenticação */ - if (!verifyToken(req.headers.cookie)) { - res.status(401).json({ message: 'auth required' }); - return; - } - - /* pegando e validando caminho do arquivo */ - let path = (req.query.path ? req.query.path : '/'); - let url_path = process.env.API_DIR + (req.query.path ? req.query.path : '/'); - if (!fs.existsSync(url_path) || !fs.realpathSync(url_path).startsWith(fs.realpathSync(process.env.API_DIR))) { - res.status(404).send({ message: 'not found' }); - return; - } - - /* retornando arquivos em json */ - let file = fs.lstatSync(url_path); - let files_rest: Array = []; - - if (file.isDirectory()) { - let files = fs.readdirSync(url_path).filter(file => (!file.startsWith('.') && !file.startsWith('_'))); - files_rest = files.map(file => { - let fileModel = statsToFile(fs.lstatSync(url_path + '/' + file), file) - if (fileModel.is_dir) { - let icon = getFileIcon(url_path + '/' + file) - if (icon) - fileModel.icon = `${path}/${file}/${icon}` - } - return fileModel; - }); - } else { - let file_name = url_path.split(/\/([^\/])^/)[0] - files_rest.push(statsToFile(file, file_name, true)); - } - - /* retornando valores */ - if (files_rest.length === 0) { - res.status(204).send(null); - return; - } - - res.status(200).json({ files: files_rest }) -} - -function statsToFile(file: fs.Stats, name: string, open: boolean = false): FileModel { - let is_dir = file.isDirectory(); - let created_at = file.ctime; - return { name, is_dir, icon: null, open: open, created_at: created_at } -} - -function getFileIcon(url_path: string):string | null { - let files = fs.readdirSync(url_path); - for(let file of files) { - if (file.match(/^_icon\.(png|jpe?g|svg|webp)$/)) - return file; - } - return null; -} \ No newline at end of file diff --git a/src/pages/api/files/open.ts b/src/pages/api/files/open.ts deleted file mode 100644 index feea563..0000000 --- a/src/pages/api/files/open.ts +++ /dev/null @@ -1,77 +0,0 @@ -import fs from 'fs' -import path from 'path' -import { NextApiRequest, NextApiResponse } from "next"; -import { verifyToken } from "../auth/login"; -import { lookup } from 'mime-types'; - -/* para evitar erro de limite de resposta */ -export const config = { - api: {responseLimit: false} -} - -export default function handler(req: NextApiRequest, res: NextApiResponse) { - /* verificando autenticação */ - if (!verifyToken(req.headers.cookie)) { - res.status(401).json({ message: 'auth required' }); - return; - } - - /* pegando e validando caminho do arquivo */ - let url_path = process.env.API_DIR + (req.query.path ? req.query.path : '/'); - if (!fs.existsSync(url_path) || !fs.realpathSync(url_path).startsWith(fs.realpathSync(process.env.API_DIR))) { - res.status(404).send({ message: 'not found' }); - return; - } - - /* pegando informações do arquivo */ - res.setHeader('Cache-Control', 'max-age=86400') - let file_path = path.resolve(url_path) - let content_type = lookup(file_path); - if (content_type) - res.setHeader('Content-Type', content_type) - - /* verificando range */ - let file = fs.lstatSync(url_path); - let range = get_range(req.headers.range, file.size); - - if (range === null) { - if (content_type && content_type.split('/')[0] !== 'video') { - let file_stream = fs.createReadStream(file_path) - res.status(200).send(file_stream) - return - } - res.setHeader('Accept-Ranges', 'bytes'); - res.status(200).send(null); - return; - } - - /* enviando range */ - res.setHeader('Content-Range', 'bytes ' + range.Start + '-' + range.End + '/' + file.size); - let file_stream = fs.createReadStream(file_path, { start: range.Start, end: range.End }) - res.status(206).send(file_stream) -} - -function get_range(range: null | string, length: number) { - if (!range || range.length == 0) - return null; - - let buffer = 524288; - let array = range.split(/bytes=([0-9]*)-([0-9]*)/); - - let start = parseInt(array[1]); - let end = parseInt(array[2]); - - start = isNaN(start) ? 0 : start - end = isNaN(end) ? (start + buffer) : end - - if (end > length - 1) { - end = length - 1; - } - - if (start > end) { - start = 0; - end = buffer; - } - - return { Start: start, End: end }; -} \ No newline at end of file diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 762c29e..acf2325 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -19,12 +19,14 @@ const Container = styled.div` grid-template-areas: "h n" "m a"; height: 100vh; transition: grid-template-columns .2s; + background: radial-gradient(transparent, rgba(0, 0, 0, 0.4)); @media(max-width:950px) { grid-template-columns: 0 auto; } ` const Header = styled.header` + backdrop-filter: blur(5px); grid-area: h; display: flex; overflow: hidden; @@ -56,12 +58,14 @@ const Nav = styled.nav` background: rgba(0, 0, 0, 0.5); overflow-x: auto; white-space: nowrap; + backdrop-filter: blur(5px); ` const Main = styled.main` grid-area: m; overflow-y: scroll; background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(5px); ` const Aside = styled.aside` @@ -73,13 +77,25 @@ const Aside = styled.aside` const PathLink = styled.div` font-size: 11pt; font-weight: bold; - margin: auto 25px; + margin-left: 25px; + margin-right: auto; color: white; + display: flex; + overflow-x: scroll; + & p, a { + display: block; + align-self: center; + } & a { color: white; margin: 0 2px; padding: 2px; border-radius: 5px; + max-width: 120px; + min-width: 25px; + white-space: nowrap; + overflow-x: hidden; + text-overflow: ellipsis; &:hover { background-color: rgba(255, 255, 255, 0.3); } @@ -88,12 +104,19 @@ const PathLink = styled.div` const SearchInput = styled.input` background-color: rgba(0, 0, 0, 0.3); - margin: auto 25px auto auto; - border: none; + align-self: center; + margin-right: 25px; padding: 5px; outline: none; + border-radius: 7px; + border: solid 1px gray; color: white; font-size: 12pt; + width: 150px; + transition: width 100ms; + &:focus { + width: 250px; + } ` export default function App() { @@ -253,11 +276,11 @@ export default function App() { - /home +

/

home {path && path.split("/").map((p, i) => { let link = ''; path.split('/').forEach((l, li) => { if (li <= i) link += `/${l}` }); - return (<>/{p}) + return (<>

/

{p}) })}
setSearch(e.currentTarget.value)} value={search} /> diff --git a/src/styles/app.css b/src/styles/app.css index 9ebfb5b..3812452 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -9,6 +9,7 @@ html, body { color: white; } body { + background-image: url('/img/wallpaper.jpg'); background-size: cover; background-position: center; } \ No newline at end of file