cos tam dziala

This commit is contained in:
Patryk Koreń
2026-07-09 20:14:56 +02:00
parent ff707dd762
commit 65303e655b
18 changed files with 235 additions and 153 deletions

View File

@@ -16,17 +16,21 @@ export async function encodeFrames({
"-y", "-y",
"-pattern_type", "glob", "-pattern_type", "glob",
"-framerate", fps.toString(), "-framerate", fps.toString(),
"-i", inputPattern, "-i", inputPattern.toString(),
"-i", path.resolve("./music.mp3"), "-i", path.resolve("./music.mp3"),
"-vf",
"transpose=2,scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2",
// "-autorotate",
"-c:v", "libx264", "-c:v", "libx264",
"-pix_fmt", "yuv420p", "-pix_fmt", "yuv420p",
"-c:a", "mp3", "-c:a", "mp3",
"-shortest", "-shortest",
"-fps_mode", "cfr",
// "-preset", "medium", // "-preset", "medium",
// "-crf", "18", // "-crf", "18",
output, output,

View File

@@ -18,9 +18,9 @@ export async function downloadMissing(remoteDir: string = REMOTE_DIR) {
const exisitngFiles = await fs.readdir(LOCAL_DIR); const exisitngFiles = await fs.readdir(LOCAL_DIR);
const missing = contents.filter(f => !exisitngFiles.includes(f.basename)) const missing = contents.filter(f => !exisitngFiles.includes(f.basename));
console.log(missing) console.log(missing);
let i = 0; let i = 0;
for (const item of missing) { for (const item of missing) {

View File

@@ -2,30 +2,17 @@ import jwt from "jsonwebtoken";
import type { Request, Response, NextFunction } from "express"; import type { Request, Response, NextFunction } from "express";
export function authMiddleware(req: Request, res: Response, next: NextFunction) { export function authMiddleware(req: Request, res: Response, next: NextFunction) {
// const header = req.headers.authorization;
// const error = "You must be logged in to access this page"
// if (!header) {
// return res.status(401).json({ error });
// }
// const token = header.split(" ")[1]; // "Bearer TOKEN"
// try {
// if (!token) throw new Error("No Bearer Token")
// const decoded = jwt.verify(token, process.env.JWT_SECRET!);
// (req as any).user = decoded;
// next();
// } catch (err) {
// return res.status(401).json({ error });
// }
const token = req.cookies.token;
if (!token) return res.sendStatus(401);
try { try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as jwt.JwtPayload; const token = req.cookies.token;
req.user = decoded;
if (!token) { return res.sendStatus(401); }
console.log("Token: ", token)
console.log("Secret: ", process.env.JWT_SECRET)
jwt.verify(token, process.env.JWT_SECRET!)
next(); next();
} catch { } catch {
res.sendStatus(403); res.sendStatus(403);
} }

View File

@@ -39,4 +39,41 @@ router.post("/login", async (req, res) => {
} }
}); });
router.post("/logout", async (req, res) => {
res.clearCookie("token", {
httpOnly: true,
secure: false,
sameSite: "lax",
});
res.sendStatus(204);
});
router.get("/me", async (req, res) => {
try {
const token = req.cookies.token;
if (!token) {
return res.status(401).json(null);
}
const payload = jwt.verify(
token,
process.env.JWT_SECRET!
) as {
sub: string;
username: string;
};
res.json({
username: payload.username,
dn: payload.sub,
});
} catch (err) {
console.log(err)
return res.status(401).json(null);
}
});
export default router; export default router;

View File

@@ -3,11 +3,23 @@ import { Router } from "express";
import { downloadMissing, LOCAL_DIR } from "../lib/nextcloud"; import { downloadMissing, LOCAL_DIR } from "../lib/nextcloud";
import path from "node:path"; import path from "node:path";
import { encodeFrames } from "../lib/crateClip"; import { encodeFrames } from "../lib/crateClip";
import fs from "node:fs"
const router = Router(); const router = Router();
router.get("/video", async (req, res) => { router.get("/video", async (req, res) => {
const videoPath = path.resolve("./out.mp4"); const videoPath = path.resolve("./out.mp4");
const stat = fs.statSync(videoPath);
// Simple ETag based on file size + modification time
const etag = `"${stat.size}-${stat.mtimeMs}"`;
res.set("ETag", etag);
if (req.headers["if-none-match"] === etag) {
return res.status(304).end();
}
res.sendFile(videoPath); res.sendFile(videoPath);
}); });
@@ -18,18 +30,18 @@ router.post("/createclip", async (req, res) => {
output: path.resolve("./out.mp4"), output: path.resolve("./out.mp4"),
fps: 12, fps: 12,
}); });
res.end(200); res.status(200).end();
} catch { } catch {
res.end(501); res.status(501).end();
} }
}) })
router.post("/refreshfiles", async (req, res) => { router.post("/refreshfiles", async (req, res) => {
try { try {
await downloadMissing(); await downloadMissing();
res.end(200); res.status(200).end();
} catch { } catch {
res.end(501); res.status(501).end();
} }
}) })

View File

@@ -8,6 +8,7 @@
"@fontsource-variable/figtree": "^5.2.10", "@fontsource-variable/figtree": "^5.2.10",
"@fontsource-variable/jetbrains-mono": "^5.2.8", "@fontsource-variable/jetbrains-mono": "^5.2.8",
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.1",
"@tanstack/react-query": "4",
"@types/ldapjs": "^3.0.6", "@types/ldapjs": "^3.0.6",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -469,6 +470,10 @@
"@tailwindcss/vite": ["@tailwindcss/vite@4.2.2", "", { "dependencies": { "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "tailwindcss": "4.2.2" }, "peerDependencies": { "vite": "7.3.2" } }, "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w=="], "@tailwindcss/vite": ["@tailwindcss/vite@4.2.2", "", { "dependencies": { "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "tailwindcss": "4.2.2" }, "peerDependencies": { "vite": "7.3.2" } }, "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w=="],
"@tanstack/query-core": ["@tanstack/query-core@4.44.0", "", {}, "sha512-swSgb7OiPRR3UuIL7NuDrZNSMGmQD+wdtHxPD7j60SvBEnxbXurl5XOirtGEX2gm2hbK6mC8kMV1I+uO3l0UOw=="],
"@tanstack/react-query": ["@tanstack/react-query@4.44.0", "", { "dependencies": { "@tanstack/query-core": "4.44.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-native": "*" }, "optionalPeers": ["react-dom", "react-native"] }, "sha512-RuIqHYrS98LrK/8kJJOJMMSQ/BCpojwsXDh7p0fBmp38ZOz6dlk+uyFRRusH+V+t3POoCsDOQ2zhomEYOeReXw=="],
"@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "3.3.3", "minimatch": "10.2.5", "path-browserify": "1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="], "@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "3.3.3", "minimatch": "10.2.5", "path-browserify": "1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="],
"@tsconfig/node10": ["@tsconfig/node10@1.0.12", "", {}, "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ=="], "@tsconfig/node10": ["@tsconfig/node10@1.0.12", "", {}, "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ=="],

View File

@@ -15,6 +15,7 @@
"@fontsource-variable/figtree": "^5.2.10", "@fontsource-variable/figtree": "^5.2.10",
"@fontsource-variable/jetbrains-mono": "^5.2.8", "@fontsource-variable/jetbrains-mono": "^5.2.8",
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.1",
"@tanstack/react-query": "4",
"@types/ldapjs": "^3.0.6", "@types/ldapjs": "^3.0.6",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",

12
front/src/global.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
type User = {
username: string;
dn?: string;
};
type AuthContextType = {
user: User | null;
isAuthenticated: boolean;
login: (username: string, password: string) => Promise<void>;
logout: () => void;
};

View File

@@ -0,0 +1,31 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
export function useLogin() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data: {
username: string;
password: string;
}) => {
const res = await fetch(`${import.meta.env.VITE_API_URL}/auth/login`, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (!res.ok) {
throw new Error("Invalid credentials");
}
return res.json();
},
onSuccess(data) {
queryClient.setQueryData(["me"], data.user);
},
});
}

View File

@@ -0,0 +1,18 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
export function useLogout() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => {
await fetch(`${import.meta.env.VITE_API_URL}/auth/logout`, {
method: "POST",
credentials: "include",
});
},
onSuccess() {
queryClient.setQueryData(["me"], null);
},
});
}

23
front/src/hooks/useMe.ts Normal file
View File

@@ -0,0 +1,23 @@
import { useQuery } from "@tanstack/react-query";
export function useMe() {
return useQuery<User>({
queryKey: ["me"],
queryFn: async () => {
const res = await fetch(`${import.meta.env.VITE_API_URL}/auth/me`, {
credentials: "include",
});
if (res.status === 401) {
return null;
}
if (!res.ok) {
throw new Error("Failed to get current user");
}
return res.json();
},
});
}

View File

@@ -0,0 +1,18 @@
import { useMutation } from "@tanstack/react-query";
export function useCreateClip() {
return useMutation({
mutationFn: async () => {
const res = await fetch(`${import.meta.env.VITE_API_URL}/protected/createclip`, {
method: "POST",
credentials: "include",
});
if (!res.ok) {
throw new Error("Invalid credentials");
}
return res.json();
},
});
}

View File

@@ -0,0 +1,18 @@
import { useMutation } from "@tanstack/react-query";
export function useRefreshFiles() {
return useMutation({
mutationFn: async () => {
const res = await fetch(`${import.meta.env.VITE_API_URL}/protected/refreshfiles`, {
method: "POST",
credentials: "include",
});
if (!res.ok) {
throw new Error("Invalid credentials");
}
return res.json();
},
});
}

View File

@@ -1,6 +1,7 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { useAuth } from "@/providers/AuthProvider"; import { useLogout } from "@/hooks/useLogout";
import { useMe } from "@/hooks/useMe";
import { type ReactNode } from "react"; import { type ReactNode } from "react";
import { Link, Outlet, useLocation } from "react-router"; import { Link, Outlet, useLocation } from "react-router";
@@ -18,7 +19,9 @@ function Path({ url, children }: { url: string, children: ReactNode }) {
} }
export function MainLayout() { export function MainLayout() {
const { isAuthenticated, user, logout } = useAuth() const { data: user } = useMe();
const isAuthenticated = !!user;
const logout = useLogout();
return ( return (
@@ -53,7 +56,7 @@ export function MainLayout() {
{/* Actions */} {/* Actions */}
<div className="flex items-center gap-2 shrink-0"> <div className="flex items-center gap-2 shrink-0">
{isAuthenticated && user && {user &&
<> <>
<div> <div>
{user.username} {user.username}
@@ -61,7 +64,10 @@ export function MainLayout() {
</> </>
} }
{isAuthenticated ? {isAuthenticated ?
<Button onClick={logout}>Logout</Button> <Button
disabled={logout.isPending}
onClick={() => { logout.mutate() }}
>Logout</Button>
: :
<Button asChild> <Button asChild>
<Link to="/auth/login">Login</Link> <Link to="/auth/login">Login</Link>

View File

@@ -1,20 +1,23 @@
import { StrictMode } from "react" import { StrictMode } from "react"
import { createRoot } from "react-dom/client" import { createRoot } from "react-dom/client"
import "./index.css"
import { ThemeProvider } from "@/components/theme-provider.tsx" import { ThemeProvider } from "@/components/theme-provider.tsx"
import { BrowserRouter, Route, Routes } from "react-router" import { BrowserRouter, Route, Routes } from "react-router"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import "./index.css"
import AuthLayout from "./layouts/AuthLayout.tsx" import AuthLayout from "./layouts/AuthLayout.tsx"
import { LoginForm } from "./pages/Login.tsx" import { LoginForm } from "./pages/Login.tsx"
import { Home } from "./pages/Home.tsx" import { Home } from "./pages/Home.tsx"
import { MainLayout } from "./layouts/MainLayout.tsx" import { MainLayout } from "./layouts/MainLayout.tsx"
import { AuthProvider } from "./providers/AuthProvider.tsx"
import { Video } from "./pages/Video.tsx" import { Video } from "./pages/Video.tsx"
const queryClient = new QueryClient();
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>
<ThemeProvider> <QueryClientProvider client={queryClient}>
<AuthProvider> <ThemeProvider>
<BrowserRouter> <BrowserRouter>
<Routes> <Routes>
<Route element={<MainLayout />}> <Route element={<MainLayout />}>
@@ -27,7 +30,7 @@ createRoot(document.getElementById("root")!).render(
</Route> </Route>
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
</AuthProvider> </ThemeProvider>
</ThemeProvider> </QueryClientProvider>
</StrictMode > </StrictMode >
) )

View File

@@ -15,33 +15,29 @@ import {
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { useState, type FormEvent } from "react" import { useState, type FormEvent } from "react"
import { useNavigate } from "react-router" import { useNavigate } from "react-router"
import { useAuth } from "@/providers/AuthProvider" import { useLogin } from "@/hooks/useLogin"
export function LoginForm({ export function LoginForm({
className, className,
...props ...props
}: React.ComponentProps<"div">) { }: React.ComponentProps<"div">) {
const { login } = useAuth(); const login = useLogin();
const navigate = useNavigate(); const navigate = useNavigate();
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [_error, setError] = useState<string | null>(null); const [_error, setError] = useState<string | null>(null);
async function handleSubmit(e: FormEvent) { async function handleSubmit(e: FormEvent) {
e.preventDefault(); e.preventDefault();
setLoading(true);
setError(null); setError(null);
try { try {
await login(username, password); login.mutate({ username, password });
navigate("/") navigate("/")
} catch (err: any) { } catch (err: any) {
setError(err.message || "Login failed"); setError(err.message || "Login failed");
} finally {
setLoading(false);
} }
} }
@@ -91,9 +87,9 @@ export function LoginForm({
<Field> <Field>
<Button <Button
type="submit" type="submit"
disabled={loading} disabled={login.isPending}
>{ >{
loading ? "Logging in..." : "Login" login.isPending ? "Logging in..." : "Login"
}</Button> }</Button>
{ /* { /*
<Button variant="outline" type="button"> <Button variant="outline" type="button">

View File

@@ -1,30 +1,27 @@
import { apiFetch } from "@/api/auth";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useEffect, useState } from "react"; import { useCreateClip } from "@/hooks/video/useCreateClip";
import { useRefreshFiles } from "@/hooks/video/useRefreshFiles";
export function Video() { export function Video() {
const refresh = async () => { const createClip = useCreateClip()
await apiFetch(`/protected/refreshfiles`, { const refreshImages = useRefreshFiles()
method: "POST",
});
}
const create = async () => {
await apiFetch(`/protected/createclip`, {
method: "POST",
});
}
return ( return (
<div className="flex flex-1 flex-col gap-4 py-16"> <div className="flex flex-1 flex-col gap-4 py-16">
<div> <div>
<Button <Button
onClick={() => { refresh() }} disabled={refreshImages.isPending}
>Refresh</Button> onClick={() => { refreshImages.mutate() }}
>{
refreshImages.isPending ? "Refreshing..." : "Refresh"
}</Button>
<Button <Button
onClick={() => { create() }} disabled={createClip.isPending}
>Create Clip</Button> onClick={() => { createClip.mutate() }}
>{
createClip.isPending ? "creating" : "Create Clip"
}</Button>
</div> </div>
<video controls width="600"> <video controls width="600">
<source <source

View File

@@ -1,86 +0,0 @@
import {
createContext,
useContext,
useEffect,
useState
} from "react";
type User = {
username: string;
dn?: string;
};
type AuthContextType = {
user: User | null;
token: string | null;
isAuthenticated: boolean;
login: (username: string, password: string) => Promise<void>;
logout: () => void;
};
const AuthContext = createContext<AuthContextType | null>(null);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(null);
// restore session
useEffect(() => {
const savedUser = localStorage.getItem("user");
if (savedUser) {
setUser(JSON.parse(savedUser));
}
}, []);
async function login(username: string, password: string) {
const res = await fetch(`${import.meta.env.VITE_API_URL}/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
credentials: "include",
});
if (!res.ok) {
throw new Error("Invalid credentials");
}
const data = await res.json();
setToken(data.token);
setUser(data.user);
localStorage.setItem("user", JSON.stringify(data.user));
}
function logout() {
setUser(null);
setToken(null);
localStorage.removeItem("user");
}
return (
<AuthContext.Provider
value={{
user,
token,
login,
logout,
isAuthenticated: !!token
}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) {
throw new Error("useAuth must be used inside AuthProvider");
}
return ctx;
}