diff --git a/frontend/package.json b/frontend/package.json index 17f489e..98b066d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "@fontsource/roboto": "^5.0.6", "@mui/icons-material": "^5.11.16", "@mui/material": "^5.13.5", + "fp-ts": "^2.16.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-helmet": "^6.1.0", diff --git a/frontend/src/lib/httpClient.ts b/frontend/src/lib/httpClient.ts index cd981ad..7fdeb01 100644 --- a/frontend/src/lib/httpClient.ts +++ b/frontend/src/lib/httpClient.ts @@ -1,13 +1,24 @@ -export async function ffetch( - url: string, - onSuccess: (res: T) => void, - onError: (err: string) => void, - opt?: RequestInit, -) { +import { tryCatch } from 'fp-ts/TaskEither' +import { flow } from 'fp-ts/lib/function' + +export const ffetch = (url: string, opt?: RequestInit) => flow( + tryCatch( + () => fetcher(url, opt), + (e) => `error while fetching: ${e}` + ) +) + +const fetcher = async (url: string, opt?: RequestInit) => { const res = await fetch(url, opt) - if (!res.ok) { - onError(await res.text()) - return + + if (opt && !opt.headers) { + opt.headers = { + 'Content-Type': 'application/json', + } } - onSuccess(await res.json() as T) + + if (!res.ok) { + throw await res.text() + } + return res.json() as T } \ No newline at end of file diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts index 00e979c..acfc6ac 100644 --- a/frontend/src/utils.ts +++ b/frontend/src/utils.ts @@ -1,3 +1,4 @@ +import { pipe } from 'fp-ts/lib/function' import type { RPCResponse } from "./types" /** @@ -10,15 +11,6 @@ export function validateIP(ipAddr: string): boolean { return ipRegex.test(ipAddr) } -/** - * Validate a domain via regex. - * The validation pass if the domain respects the following formats: - * - localhost - * - domain.tld - * - dir.domain.tld - * @param domainName - * @returns domain validity test - */ export function validateDomain(url: string): boolean { const urlRegex = /(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/ const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/ @@ -28,17 +20,6 @@ export function validateDomain(url: string): boolean { return urlRegex.test(url) || name === 'localhost' && slugRegex.test(slug) } -/** - * Validate a domain via regex. - * Exapmples - * - http://example.com - * - https://example.com - * - http://www.example.com - * - https://www.example.com - * - http://10.0.0.1/[something]/[something-else] - * @param url - * @returns url validity test - */ export function isValidURL(url: string): boolean { let urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/ return urlRegex.test(url) @@ -93,4 +74,11 @@ export function mapProcessStatus(status: number) { } export const prefersDarkMode = () => - window.matchMedia('(prefers-color-scheme: dark)').matches \ No newline at end of file + window.matchMedia('(prefers-color-scheme: dark)').matches + +export const base64URLEncode = (s: string) => pipe( + s, + s => String.fromCodePoint(...new TextEncoder().encode(s)), + btoa, + encodeURIComponent +) \ No newline at end of file diff --git a/frontend/src/views/Archive.tsx b/frontend/src/views/Archive.tsx index 097a478..49acc16 100644 --- a/frontend/src/views/Archive.tsx +++ b/frontend/src/views/Archive.tsx @@ -26,22 +26,26 @@ import FolderIcon from '@mui/icons-material/Folder' import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile' import VideoFileIcon from '@mui/icons-material/VideoFile' +import { matchW } from 'fp-ts/lib/TaskEither' +import { pipe } from 'fp-ts/lib/function' import { useEffect, useMemo, useState, useTransition } from 'react' import { useNavigate } from 'react-router-dom' import { useRecoilValue } from 'recoil' import { BehaviorSubject, Subject, combineLatestWith, map, share } from 'rxjs' import { serverURL } from '../atoms/settings' import { useObservable } from '../hooks/observable' +import { useToast } from '../hooks/toast' import { useI18n } from '../hooks/useI18n' import { ffetch } from '../lib/httpClient' import { DeleteRequest, DirectoryEntry } from '../types' -import { roundMiB } from '../utils' +import { base64URLEncode, roundMiB } from '../utils' export default function Downloaded() { const serverAddr = useRecoilValue(serverURL) const navigate = useNavigate() const { i18n } = useI18n() + const { pushMessage } = useToast() const [openDialog, setOpenDialog] = useState(false) @@ -50,20 +54,24 @@ export default function Downloaded() { const [isPending, startTransition] = useTransition() - const fetcher = () => ffetch( - `${serverAddr}/archive/downloaded`, - (d) => files$.next(d), - () => navigate('/login'), - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', + const fetcher = () => pipe( + ffetch( + `${serverAddr}/archive/downloaded`, + { + method: 'POST', + body: JSON.stringify({ + subdir: '', + }) + } + ), + matchW( + (e) => { + pushMessage(e) + navigate('/login') }, - body: JSON.stringify({ - subdir: '', - }) - } - ) + (d) => files$.next(d), + ) + )() const fetcherSubfolder = (sub: string) => { const folders = sub.startsWith('/') @@ -137,9 +145,7 @@ export default function Downloaded() { }, [serverAddr]) const onFileClick = (path: string) => startTransition(() => { - const encoded = btoa( - String.fromCodePoint(...new TextEncoder().encode(path)) - ) + const encoded = base64URLEncode(path) window.open(`${serverAddr}/archive/d/${encoded}`) }) diff --git a/go.mod b/go.mod index f0ec5ab..a97fabb 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,12 @@ go 1.20 require ( github.com/go-chi/chi/v5 v5.0.10 + github.com/go-chi/cors v1.2.1 github.com/goccy/go-json v0.10.2 github.com/golang-jwt/jwt/v5 v5.0.0 github.com/google/uuid v1.3.1 github.com/gorilla/websocket v1.5.0 github.com/marcopeocchi/fazzoletti v0.0.0-20230308161120-c545580f79fa - golang.org/x/sys v0.12.0 + golang.org/x/sys v0.13.0 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 20cd1f1..602fbc6 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,21 @@ github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/marcopeocchi/fazzoletti v0.0.0-20230308161120-c545580f79fa h1:uaAQLGhN4SesB9inOQ1Q6EH+BwTWHQOvwhR0TIJvnYc= github.com/marcopeocchi/fazzoletti v0.0.0-20230308161120-c545580f79fa/go.mod h1:RvfVo/6Sbnfra9kkvIxDW8NYOOaYsHjF0DdtMCs9cdo= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/server/handlers/archive.go b/server/handlers/archive.go index 820a13a..1b8f34a 100644 --- a/server/handlers/archive.go +++ b/server/handlers/archive.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "fmt" "net/http" + "net/url" "os" "path/filepath" "sort" @@ -130,6 +131,12 @@ func SendFile(w http.ResponseWriter, r *http.Request) { return } + path, err := url.QueryUnescape(path) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + decoded, err := base64.StdEncoding.DecodeString(path) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) @@ -137,7 +144,7 @@ func SendFile(w http.ResponseWriter, r *http.Request) { } decodedStr := string(decoded) - fmt.Println(decodedStr) + fmt.Println("decoded", decodedStr) root := config.Instance().GetConfig().DownloadPath diff --git a/server/server.go b/server/server.go index 371505b..16e8f29 100644 --- a/server/server.go +++ b/server/server.go @@ -14,6 +14,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/cors" "github.com/marcopeocchi/yt-dlp-web-ui/server/handlers" "github.com/marcopeocchi/yt-dlp-web-ui/server/internal" middlewares "github.com/marcopeocchi/yt-dlp-web-ui/server/middleware" @@ -54,7 +55,7 @@ func newServer(c serverConfig) *http.Server { r := chi.NewRouter() - r.Use(middlewares.CORS) + r.Use(cors.AllowAll().Handler) r.Use(middleware.Logger) app := http.FileServer(http.FS(c.frontend))