mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2025-11-17 19:16:25 +00:00
Update AppImage
This commit is contained in:
221
AppImage/components/auth-setup.tsx
Normal file
221
AppImage/components/auth-setup.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Button } from "./ui/button"
|
||||
import { Dialog, DialogContent } from "./ui/dialog"
|
||||
import { Input } from "./ui/input"
|
||||
import { Label } from "./ui/label"
|
||||
import { Shield, Lock, User, AlertCircle } from "lucide-react"
|
||||
import { getApiUrl } from "../lib/api-config"
|
||||
|
||||
interface AuthSetupProps {
|
||||
onComplete: () => void
|
||||
}
|
||||
|
||||
export function AuthSetup({ onComplete }: AuthSetupProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [step, setStep] = useState<"choice" | "setup">("choice")
|
||||
const [username, setUsername] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
const [confirmPassword, setConfirmPassword] = useState("")
|
||||
const [error, setError] = useState("")
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Check if onboarding is complete and auth setup is needed
|
||||
const hasSeenOnboarding = localStorage.getItem("proxmenux-onboarding-seen")
|
||||
const authSetupComplete = localStorage.getItem("proxmenux-auth-setup-complete")
|
||||
|
||||
if (hasSeenOnboarding && !authSetupComplete) {
|
||||
// Small delay to show after onboarding closes
|
||||
setTimeout(() => setOpen(true), 500)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleSkipAuth = async () => {
|
||||
setLoading(true)
|
||||
setError("")
|
||||
|
||||
try {
|
||||
const response = await fetch(getApiUrl("/api/auth/setup"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ enable_auth: false }),
|
||||
})
|
||||
|
||||
if (!response.ok) throw new Error("Failed to save preference")
|
||||
|
||||
localStorage.setItem("proxmenux-auth-setup-complete", "true")
|
||||
setOpen(false)
|
||||
onComplete()
|
||||
} catch (err) {
|
||||
setError("Failed to save preference. Please try again.")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSetupAuth = async () => {
|
||||
setError("")
|
||||
|
||||
if (!username || !password) {
|
||||
setError("Please fill in all fields")
|
||||
return
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setError("Passwords do not match")
|
||||
return
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
setError("Password must be at least 6 characters")
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const response = await fetch(getApiUrl("/api/auth/setup"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
password,
|
||||
enable_auth: true,
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || "Failed to setup authentication")
|
||||
}
|
||||
|
||||
// Save token
|
||||
localStorage.setItem("proxmenux-auth-token", data.token)
|
||||
localStorage.setItem("proxmenux-auth-setup-complete", "true")
|
||||
|
||||
setOpen(false)
|
||||
onComplete()
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to setup authentication")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="max-w-md">
|
||||
{step === "choice" ? (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center space-y-2">
|
||||
<div className="mx-auto w-16 h-16 bg-blue-500/10 rounded-full flex items-center justify-center">
|
||||
<Shield className="h-8 w-8 text-blue-500" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold">Protect Your Dashboard?</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Add an extra layer of security to protect your Proxmox data when accessing from non-private networks.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Button onClick={() => setStep("setup")} className="w-full bg-blue-500 hover:bg-blue-600" size="lg">
|
||||
<Lock className="h-4 w-4 mr-2" />
|
||||
Yes, Setup Password
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSkipAuth}
|
||||
variant="outline"
|
||||
className="w-full bg-transparent"
|
||||
size="lg"
|
||||
disabled={loading}
|
||||
>
|
||||
No, Continue Without Protection
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-center text-muted-foreground">You can always enable this later in Settings</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center space-y-2">
|
||||
<div className="mx-auto w-16 h-16 bg-blue-500/10 rounded-full flex items-center justify-center">
|
||||
<Lock className="h-8 w-8 text-blue-500" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold">Setup Authentication</h2>
|
||||
<p className="text-muted-foreground">Create a username and password to protect your dashboard</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-3 flex items-start gap-2">
|
||||
<AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-red-500">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="username">Username</Label>
|
||||
<div className="relative">
|
||||
<User className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="username"
|
||||
type="text"
|
||||
placeholder="Enter username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="pl-10"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Enter password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="pl-10"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirm-password">Confirm Password</Label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="confirm-password"
|
||||
type="password"
|
||||
placeholder="Confirm password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className="pl-10"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Button onClick={handleSetupAuth} className="w-full bg-blue-500 hover:bg-blue-600" disabled={loading}>
|
||||
{loading ? "Setting up..." : "Setup Authentication"}
|
||||
</Button>
|
||||
<Button onClick={() => setStep("choice")} variant="ghost" className="w-full" disabled={loading}>
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
141
AppImage/components/login.tsx
Normal file
141
AppImage/components/login.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Button } from "./ui/button"
|
||||
import { Input } from "./ui/input"
|
||||
import { Label } from "./ui/label"
|
||||
import { Lock, User, AlertCircle, Server } from "lucide-react"
|
||||
import { getApiUrl } from "../lib/api-config"
|
||||
import Image from "next/image"
|
||||
|
||||
interface LoginProps {
|
||||
onLogin: () => void
|
||||
}
|
||||
|
||||
export function Login({ onLogin }: LoginProps) {
|
||||
const [username, setUsername] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
const [error, setError] = useState("")
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError("")
|
||||
|
||||
if (!username || !password) {
|
||||
setError("Please enter username and password")
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const response = await fetch(getApiUrl("/api/auth/login"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password }),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || "Login failed")
|
||||
}
|
||||
|
||||
// Save token
|
||||
localStorage.setItem("proxmenux-auth-token", data.token)
|
||||
onLogin()
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Login failed")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md space-y-8">
|
||||
<div className="text-center space-y-4">
|
||||
<div className="flex justify-center">
|
||||
<div className="w-20 h-20 relative flex items-center justify-center bg-primary/10 rounded-lg">
|
||||
<Image
|
||||
src="/images/proxmenux-logo.png"
|
||||
alt="ProxMenux Logo"
|
||||
width={80}
|
||||
height={80}
|
||||
className="object-contain"
|
||||
priority
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement
|
||||
target.style.display = "none"
|
||||
const fallback = target.parentElement?.querySelector(".fallback-icon")
|
||||
if (fallback) {
|
||||
fallback.classList.remove("hidden")
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Server className="h-12 w-12 text-primary absolute fallback-icon hidden" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">ProxMenux Monitor</h1>
|
||||
<p className="text-muted-foreground mt-2">Sign in to access your dashboard</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-card border border-border rounded-lg p-6 shadow-lg">
|
||||
<form onSubmit={handleLogin} className="space-y-4">
|
||||
{error && (
|
||||
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-3 flex items-start gap-2">
|
||||
<AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-red-500">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="login-username">Username</Label>
|
||||
<div className="relative">
|
||||
<User className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="login-username"
|
||||
type="text"
|
||||
placeholder="Enter your username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="pl-10"
|
||||
disabled={loading}
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="login-password">Password</Label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="login-password"
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="pl-10"
|
||||
disabled={loading}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full bg-blue-500 hover:bg-blue-600" disabled={loading}>
|
||||
{loading ? "Signing in..." : "Sign In"}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-sm text-muted-foreground">ProxMenux Monitor v1.0.0</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import { VirtualMachines } from "./virtual-machines"
|
||||
import Hardware from "./hardware"
|
||||
import { SystemLogs } from "./system-logs"
|
||||
import { OnboardingCarousel } from "./onboarding-carousel"
|
||||
import { AuthSetup } from "./auth-setup"
|
||||
import { Login } from "./login"
|
||||
import { getApiUrl } from "../lib/api-config"
|
||||
import {
|
||||
RefreshCw,
|
||||
@@ -63,6 +65,10 @@ export function ProxmoxDashboard() {
|
||||
const [activeTab, setActiveTab] = useState("overview")
|
||||
const [showNavigation, setShowNavigation] = useState(true)
|
||||
const [lastScrollY, setLastScrollY] = useState(0)
|
||||
const [authChecked, setAuthChecked] = useState(false)
|
||||
const [authRequired, setAuthRequired] = useState(false)
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
||||
const [authSetupComplete, setAuthSetupComplete] = useState(false)
|
||||
|
||||
const fetchSystemData = useCallback(async () => {
|
||||
console.log("[v0] Fetching system data from Flask server...")
|
||||
@@ -219,10 +225,119 @@ export function ProxmoxDashboard() {
|
||||
}
|
||||
}
|
||||
|
||||
const setupTokenRefresh = () => {
|
||||
let refreshTimeout: ReturnType<typeof setTimeout>
|
||||
|
||||
const refreshToken = async () => {
|
||||
const token = localStorage.getItem("proxmenux-auth-token")
|
||||
if (!token) return
|
||||
|
||||
try {
|
||||
const response = await fetch(getApiUrl("/api/auth/refresh"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
localStorage.setItem("proxmenux-auth-token", data.token)
|
||||
console.log("[v0] Token refreshed successfully")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[v0] Failed to refresh token:", error)
|
||||
}
|
||||
}
|
||||
|
||||
const resetRefreshTimer = () => {
|
||||
clearTimeout(refreshTimeout)
|
||||
// Refresh token every 25 minutes (before 30 min expiry)
|
||||
refreshTimeout = setTimeout(refreshToken, 25 * 60 * 1000)
|
||||
}
|
||||
|
||||
// Refresh on user activity
|
||||
const events = ["mousedown", "keydown", "scroll", "touchstart"]
|
||||
events.forEach((event) => {
|
||||
window.addEventListener(event, resetRefreshTimer, { passive: true })
|
||||
})
|
||||
|
||||
resetRefreshTimer()
|
||||
|
||||
return () => {
|
||||
clearTimeout(refreshTimeout)
|
||||
events.forEach((event) => {
|
||||
window.removeEventListener(event, resetRefreshTimer)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleAuthSetupComplete = () => {
|
||||
setAuthSetupComplete(true)
|
||||
setIsAuthenticated(true)
|
||||
}
|
||||
|
||||
const handleLoginSuccess = () => {
|
||||
setIsAuthenticated(true)
|
||||
setupTokenRefresh()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem("proxmenux-auth-token")
|
||||
const headers: HeadersInit = { "Content-Type": "application/json" }
|
||||
|
||||
if (token) {
|
||||
headers["Authorization"] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
const response = await fetch(getApiUrl("/api/auth/status"), {
|
||||
headers,
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
setAuthRequired(data.auth_enabled)
|
||||
setIsAuthenticated(data.authenticated)
|
||||
setAuthSetupComplete(localStorage.getItem("proxmenux-auth-setup-complete") === "true")
|
||||
setAuthChecked(true)
|
||||
|
||||
// Setup token refresh if authenticated
|
||||
if (data.authenticated && token) {
|
||||
setupTokenRefresh()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[v0] Failed to check auth status:", error)
|
||||
setAuthChecked(true)
|
||||
}
|
||||
}
|
||||
|
||||
checkAuth()
|
||||
}, [])
|
||||
|
||||
if (!authChecked) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<div className="text-center space-y-4">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto" />
|
||||
<p className="text-muted-foreground">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (authRequired && !isAuthenticated) {
|
||||
return <Login onLogin={handleLoginSuccess} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<OnboardingCarousel />
|
||||
|
||||
{!authSetupComplete && <AuthSetup onComplete={handleAuthSetupComplete} />}
|
||||
|
||||
{!isServerConnected && (
|
||||
<div className="bg-red-500/10 border-b border-red-500/20 px-6 py-3">
|
||||
<div className="container mx-auto">
|
||||
|
||||
@@ -9,7 +9,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type,
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"flex h-10 w-full rounded-lg border border-input bg-background px-4 py-2 text-sm shadow-sm transition-all file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 hover:border-ring/50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
17
AppImage/components/ui/label.tsx
Normal file
17
AppImage/components/ui/label.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "../../lib/utils"
|
||||
|
||||
const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70")
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
@@ -22,6 +22,72 @@ import xml.etree.ElementTree as ET # Added for XML parsing
|
||||
import math # Imported math for format_bytes function
|
||||
import urllib.parse # Added for URL encoding
|
||||
import platform # Added for platform.release()
|
||||
import hashlib
|
||||
import secrets
|
||||
import jwt
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
|
||||
# Authentication configuration
|
||||
AUTH_CONFIG_DIR = Path.home() / ".config" / "proxmenux-monitor"
|
||||
AUTH_CONFIG_FILE = AUTH_CONFIG_DIR / "auth.json"
|
||||
JWT_SECRET = secrets.token_hex(32) # Generate a random secret for JWT
|
||||
SESSION_TIMEOUT = 30 * 60 # 30 minutes in seconds
|
||||
|
||||
# Ensure config directory exists
|
||||
AUTH_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
"""Hash a password using SHA-256"""
|
||||
return hashlib.sha256(password.encode()).hexdigest()
|
||||
|
||||
def load_auth_config():
|
||||
"""Load authentication configuration from file"""
|
||||
if not AUTH_CONFIG_FILE.exists():
|
||||
return {"auth_enabled": False}
|
||||
|
||||
try:
|
||||
with open(AUTH_CONFIG_FILE, 'r') as f:
|
||||
return json.load(f)
|
||||
except:
|
||||
return {"auth_enabled": False}
|
||||
|
||||
def save_auth_config(config):
|
||||
"""Save authentication configuration to file"""
|
||||
with open(AUTH_CONFIG_FILE, 'w') as f:
|
||||
json.dump(config, f, indent=2)
|
||||
|
||||
def require_auth(f):
|
||||
"""Decorator to require authentication for endpoints"""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
auth_config = load_auth_config()
|
||||
|
||||
# If auth is not enabled, allow access
|
||||
if not auth_config.get("auth_enabled", False):
|
||||
return f(*args, **kwargs)
|
||||
|
||||
# Check for Authorization header
|
||||
auth_header = request.headers.get('Authorization')
|
||||
if not auth_header or not auth_header.startswith('Bearer '):
|
||||
return jsonify({"error": "Authentication required"}), 401
|
||||
|
||||
token = auth_header.split(' ')[1]
|
||||
|
||||
try:
|
||||
# Verify JWT token
|
||||
payload = jwt.decode(token, JWT_SECRET, algorithms=['HS256'])
|
||||
|
||||
# Check if token is expired
|
||||
if time.time() > payload.get('exp', 0):
|
||||
return jsonify({"error": "Token expired"}), 401
|
||||
|
||||
return f(*args, **kwargs)
|
||||
except jwt.InvalidTokenError:
|
||||
return jsonify({"error": "Invalid token"}), 401
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app) # Enable CORS for Next.js frontend
|
||||
|
||||
Reference in New Issue
Block a user