cos tam dziala
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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=="],
|
||||||
|
|||||||
@@ -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
12
front/src/global.d.ts
vendored
Normal 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;
|
||||||
|
};
|
||||||
|
|
||||||
31
front/src/hooks/useLogin.ts
Normal file
31
front/src/hooks/useLogin.ts
Normal 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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
18
front/src/hooks/useLogout.ts
Normal file
18
front/src/hooks/useLogout.ts
Normal 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
23
front/src/hooks/useMe.ts
Normal 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();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
18
front/src/hooks/video/useCreateClip.ts
Normal file
18
front/src/hooks/video/useCreateClip.ts
Normal 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();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
18
front/src/hooks/video/useRefreshFiles.ts
Normal file
18
front/src/hooks/video/useRefreshFiles.ts
Normal 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();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 >
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user