mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-02-18 16:36:27 +00:00
Update 2FA
This commit is contained in:
@@ -5,7 +5,7 @@ import { Button } from "./ui/button"
|
|||||||
import { Input } from "./ui/input"
|
import { Input } from "./ui/input"
|
||||||
import { Label } from "./ui/label"
|
import { Label } from "./ui/label"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
|
||||||
import { Shield, Lock, User, AlertCircle, CheckCircle, Info, LogOut, Wrench, Package, Key, Copy, Eye, EyeOff, Ruler } from 'lucide-react'
|
import { Shield, Lock, User, AlertCircle, CheckCircle, Info, LogOut, Wrench, Package, Key, Copy, Eye, EyeOff, Ruler, Trash2, RefreshCw, Clock } from 'lucide-react'
|
||||||
import { APP_VERSION } from "./release-notes-modal"
|
import { APP_VERSION } from "./release-notes-modal"
|
||||||
import { getApiUrl, fetchApi } from "../lib/api-config"
|
import { getApiUrl, fetchApi } from "../lib/api-config"
|
||||||
import { TwoFactorSetup } from "./two-factor-setup"
|
import { TwoFactorSetup } from "./two-factor-setup"
|
||||||
@@ -18,6 +18,15 @@ interface ProxMenuxTool {
|
|||||||
enabled: boolean
|
enabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ApiTokenEntry {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
token_prefix: string
|
||||||
|
created_at: string
|
||||||
|
expires_at: string
|
||||||
|
revoked: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export function Settings() {
|
export function Settings() {
|
||||||
const [authEnabled, setAuthEnabled] = useState(false)
|
const [authEnabled, setAuthEnabled] = useState(false)
|
||||||
const [totpEnabled, setTotpEnabled] = useState(false)
|
const [totpEnabled, setTotpEnabled] = useState(false)
|
||||||
@@ -56,13 +65,20 @@ export function Settings() {
|
|||||||
const [generatingToken, setGeneratingToken] = useState(false)
|
const [generatingToken, setGeneratingToken] = useState(false)
|
||||||
const [tokenCopied, setTokenCopied] = useState(false)
|
const [tokenCopied, setTokenCopied] = useState(false)
|
||||||
|
|
||||||
|
// Token list state
|
||||||
|
const [existingTokens, setExistingTokens] = useState<ApiTokenEntry[]>([])
|
||||||
|
const [loadingTokens, setLoadingTokens] = useState(false)
|
||||||
|
const [revokingTokenId, setRevokingTokenId] = useState<string | null>(null)
|
||||||
|
const [tokenName, setTokenName] = useState("API Token")
|
||||||
|
|
||||||
const [networkUnitSettings, setNetworkUnitSettings] = useState<"Bytes" | "Bits">("Bytes")
|
const [networkUnitSettings, setNetworkUnitSettings] = useState<"Bytes" | "Bits">("Bytes")
|
||||||
const [loadingUnitSettings, setLoadingUnitSettings] = useState(true)
|
const [loadingUnitSettings, setLoadingUnitSettings] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkAuthStatus()
|
checkAuthStatus()
|
||||||
loadProxmenuxTools()
|
loadProxmenuxTools()
|
||||||
getUnitsSettings() // Load units settings on mount
|
loadApiTokens()
|
||||||
|
getUnitsSettings()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const checkAuthStatus = async () => {
|
const checkAuthStatus = async () => {
|
||||||
@@ -293,6 +309,47 @@ export function Settings() {
|
|||||||
window.location.reload()
|
window.location.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadApiTokens = async () => {
|
||||||
|
try {
|
||||||
|
setLoadingTokens(true)
|
||||||
|
const data = await fetchApi("/api/auth/api-tokens")
|
||||||
|
if (data.success) {
|
||||||
|
setExistingTokens(data.tokens || [])
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Silently fail - tokens section is optional
|
||||||
|
} finally {
|
||||||
|
setLoadingTokens(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRevokeToken = async (tokenId: string) => {
|
||||||
|
if (!confirm("Are you sure you want to revoke this token? Any integration using it will stop working immediately.")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setRevokingTokenId(tokenId)
|
||||||
|
setError("")
|
||||||
|
setSuccess("")
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await fetchApi(`/api/auth/api-tokens/${tokenId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
})
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setSuccess("Token revoked successfully")
|
||||||
|
setExistingTokens((prev) => prev.filter((t) => t.id !== tokenId))
|
||||||
|
} else {
|
||||||
|
setError(data.message || "Failed to revoke token")
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to revoke token")
|
||||||
|
} finally {
|
||||||
|
setRevokingTokenId(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleGenerateApiToken = async () => {
|
const handleGenerateApiToken = async () => {
|
||||||
setError("")
|
setError("")
|
||||||
setSuccess("")
|
setSuccess("")
|
||||||
@@ -316,6 +373,7 @@ export function Settings() {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
password: tokenPassword,
|
password: tokenPassword,
|
||||||
totp_token: totpEnabled ? tokenTotpCode : undefined,
|
totp_token: totpEnabled ? tokenTotpCode : undefined,
|
||||||
|
token_name: tokenName || "API Token",
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -333,6 +391,8 @@ export function Settings() {
|
|||||||
setSuccess("API token generated successfully! Make sure to copy it now as you won't be able to see it again.")
|
setSuccess("API token generated successfully! Make sure to copy it now as you won't be able to see it again.")
|
||||||
setTokenPassword("")
|
setTokenPassword("")
|
||||||
setTokenTotpCode("")
|
setTokenTotpCode("")
|
||||||
|
setTokenName("API Token")
|
||||||
|
loadApiTokens()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Failed to generate API token. Please try again.")
|
setError(err instanceof Error ? err.message : "Failed to generate API token. Please try again.")
|
||||||
} finally {
|
} finally {
|
||||||
@@ -340,10 +400,36 @@ export function Settings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyApiToken = () => {
|
const copyToClipboard = async (text: string) => {
|
||||||
navigator.clipboard.writeText(apiToken)
|
try {
|
||||||
setTokenCopied(true)
|
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
|
||||||
setTimeout(() => setTokenCopied(false), 2000)
|
await navigator.clipboard.writeText(text)
|
||||||
|
} else {
|
||||||
|
// Fallback for non-secure contexts (HTTP on local network)
|
||||||
|
const textarea = document.createElement("textarea")
|
||||||
|
textarea.value = text
|
||||||
|
textarea.style.position = "fixed"
|
||||||
|
textarea.style.left = "-9999px"
|
||||||
|
textarea.style.top = "-9999px"
|
||||||
|
textarea.style.opacity = "0"
|
||||||
|
document.body.appendChild(textarea)
|
||||||
|
textarea.focus()
|
||||||
|
textarea.select()
|
||||||
|
document.execCommand("copy")
|
||||||
|
document.body.removeChild(textarea)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyApiToken = async () => {
|
||||||
|
const ok = await copyToClipboard(apiToken)
|
||||||
|
if (ok) {
|
||||||
|
setTokenCopied(true)
|
||||||
|
setTimeout(() => setTokenCopied(false), 2000)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleVersion = (version: string) => {
|
const toggleVersion = (version: string) => {
|
||||||
@@ -763,6 +849,22 @@ export function Settings() {
|
|||||||
Enter your credentials to generate a new long-lived API token
|
Enter your credentials to generate a new long-lived API token
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="token-name">Token Name</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Key className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
id="token-name"
|
||||||
|
type="text"
|
||||||
|
placeholder="e.g. Homepage, Home Assistant"
|
||||||
|
value={tokenName}
|
||||||
|
onChange={(e) => setTokenName(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
disabled={generatingToken}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="token-password">Password</Label>
|
<Label htmlFor="token-password">Password</Label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -811,6 +913,7 @@ export function Settings() {
|
|||||||
setShowApiTokenSection(false)
|
setShowApiTokenSection(false)
|
||||||
setTokenPassword("")
|
setTokenPassword("")
|
||||||
setTokenTotpCode("")
|
setTokenTotpCode("")
|
||||||
|
setTokenName("API Token")
|
||||||
setError("")
|
setError("")
|
||||||
}}
|
}}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -896,6 +999,78 @@ export function Settings() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Existing Tokens List */}
|
||||||
|
{!loadingTokens && existingTokens.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-semibold text-muted-foreground">Active Tokens</h3>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={loadApiTokens}
|
||||||
|
className="h-7 px-2 text-xs text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-3 w-3 mr-1" />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{existingTokens.map((token) => (
|
||||||
|
<div
|
||||||
|
key={token.id}
|
||||||
|
className="flex items-center justify-between p-3 bg-muted/50 rounded-lg border border-border"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-blue-500/10 flex items-center justify-center flex-shrink-0">
|
||||||
|
<Key className="h-4 w-4 text-blue-500" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="text-sm font-medium truncate">{token.name}</p>
|
||||||
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<code className="font-mono">{token.token_prefix}</code>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Clock className="h-3 w-3" />
|
||||||
|
{token.created_at
|
||||||
|
? new Date(token.created_at).toLocaleDateString()
|
||||||
|
: "Unknown"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleRevokeToken(token.id)}
|
||||||
|
disabled={revokingTokenId === token.id}
|
||||||
|
className="h-8 px-2 text-red-500 hover:text-red-400 hover:bg-red-500/10 flex-shrink-0"
|
||||||
|
>
|
||||||
|
{revokingTokenId === token.id ? (
|
||||||
|
<div className="animate-spin h-4 w-4 border-2 border-red-500 border-t-transparent rounded-full" />
|
||||||
|
) : (
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
<span className="ml-1 text-xs hidden sm:inline">Revoke</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loadingTokens && (
|
||||||
|
<div className="flex items-center justify-center py-4">
|
||||||
|
<div className="animate-spin h-5 w-5 border-2 border-blue-500 border-t-transparent rounded-full" />
|
||||||
|
<span className="ml-2 text-sm text-muted-foreground">Loading tokens...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loadingTokens && existingTokens.length === 0 && !showApiTokenSection && !apiToken && (
|
||||||
|
<div className="text-center py-4 text-sm text-muted-foreground">
|
||||||
|
No API tokens created yet
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -89,14 +89,34 @@ export function TwoFactorSetup({ open, onClose, onSuccess }: TwoFactorSetupProps
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyToClipboard = (text: string, type: "secret" | "codes") => {
|
const copyToClipboard = async (text: string, type: "secret" | "codes") => {
|
||||||
navigator.clipboard.writeText(text)
|
try {
|
||||||
if (type === "secret") {
|
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
|
||||||
setCopiedSecret(true)
|
await navigator.clipboard.writeText(text)
|
||||||
setTimeout(() => setCopiedSecret(false), 2000)
|
} else {
|
||||||
} else {
|
// Fallback for non-secure contexts (HTTP)
|
||||||
setCopiedCodes(true)
|
const textarea = document.createElement("textarea")
|
||||||
setTimeout(() => setCopiedCodes(false), 2000)
|
textarea.value = text
|
||||||
|
textarea.style.position = "fixed"
|
||||||
|
textarea.style.left = "-9999px"
|
||||||
|
textarea.style.top = "-9999px"
|
||||||
|
textarea.style.opacity = "0"
|
||||||
|
document.body.appendChild(textarea)
|
||||||
|
textarea.focus()
|
||||||
|
textarea.select()
|
||||||
|
document.execCommand("copy")
|
||||||
|
document.body.removeChild(textarea)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "secret") {
|
||||||
|
setCopiedSecret(true)
|
||||||
|
setTimeout(() => setCopiedSecret(false), 2000)
|
||||||
|
} else {
|
||||||
|
setCopiedCodes(true)
|
||||||
|
setTimeout(() => setCopiedCodes(false), 2000)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
console.error("Failed to copy to clipboard")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,9 @@ def load_auth_config():
|
|||||||
"configured": bool,
|
"configured": bool,
|
||||||
"totp_enabled": bool, # 2FA enabled flag
|
"totp_enabled": bool, # 2FA enabled flag
|
||||||
"totp_secret": str, # TOTP secret key
|
"totp_secret": str, # TOTP secret key
|
||||||
"backup_codes": list # List of backup codes
|
"backup_codes": list, # List of backup codes
|
||||||
|
"api_tokens": list, # List of stored API token metadata
|
||||||
|
"revoked_tokens": list # List of revoked token hashes
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
if not AUTH_CONFIG_FILE.exists():
|
if not AUTH_CONFIG_FILE.exists():
|
||||||
@@ -69,7 +71,9 @@ def load_auth_config():
|
|||||||
"configured": False,
|
"configured": False,
|
||||||
"totp_enabled": False,
|
"totp_enabled": False,
|
||||||
"totp_secret": None,
|
"totp_secret": None,
|
||||||
"backup_codes": []
|
"backup_codes": [],
|
||||||
|
"api_tokens": [],
|
||||||
|
"revoked_tokens": []
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -81,6 +85,8 @@ def load_auth_config():
|
|||||||
config.setdefault("totp_enabled", False)
|
config.setdefault("totp_enabled", False)
|
||||||
config.setdefault("totp_secret", None)
|
config.setdefault("totp_secret", None)
|
||||||
config.setdefault("backup_codes", [])
|
config.setdefault("backup_codes", [])
|
||||||
|
config.setdefault("api_tokens", [])
|
||||||
|
config.setdefault("revoked_tokens", [])
|
||||||
return config
|
return config
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error loading auth config: {e}")
|
print(f"Error loading auth config: {e}")
|
||||||
@@ -92,7 +98,9 @@ def load_auth_config():
|
|||||||
"configured": False,
|
"configured": False,
|
||||||
"totp_enabled": False,
|
"totp_enabled": False,
|
||||||
"totp_secret": None,
|
"totp_secret": None,
|
||||||
"backup_codes": []
|
"backup_codes": [],
|
||||||
|
"api_tokens": [],
|
||||||
|
"revoked_tokens": []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -141,11 +149,18 @@ def verify_token(token):
|
|||||||
"""
|
"""
|
||||||
Verify a JWT token
|
Verify a JWT token
|
||||||
Returns username if valid, None otherwise
|
Returns username if valid, None otherwise
|
||||||
|
Also checks if the token has been revoked
|
||||||
"""
|
"""
|
||||||
if not JWT_AVAILABLE or not token:
|
if not JWT_AVAILABLE or not token:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Check if the token has been revoked
|
||||||
|
token_hash = hashlib.sha256(token.encode()).hexdigest()
|
||||||
|
config = load_auth_config()
|
||||||
|
if token_hash in config.get("revoked_tokens", []):
|
||||||
|
return None
|
||||||
|
|
||||||
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
|
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
|
||||||
return payload.get('username')
|
return payload.get('username')
|
||||||
except jwt.ExpiredSignatureError:
|
except jwt.ExpiredSignatureError:
|
||||||
@@ -156,6 +171,88 @@ def verify_token(token):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def store_api_token_metadata(token, token_name="API Token"):
|
||||||
|
"""
|
||||||
|
Store API token metadata (hash, name, creation date) for listing and revocation.
|
||||||
|
The actual token is never stored - only a hash for identification.
|
||||||
|
"""
|
||||||
|
config = load_auth_config()
|
||||||
|
token_hash = hashlib.sha256(token.encode()).hexdigest()
|
||||||
|
token_id = token_hash[:16]
|
||||||
|
|
||||||
|
token_entry = {
|
||||||
|
"id": token_id,
|
||||||
|
"name": token_name,
|
||||||
|
"token_hash": token_hash,
|
||||||
|
"token_prefix": token[:12] + "...",
|
||||||
|
"created_at": datetime.utcnow().isoformat() + "Z",
|
||||||
|
"expires_at": (datetime.utcnow() + timedelta(days=365)).isoformat() + "Z"
|
||||||
|
}
|
||||||
|
|
||||||
|
config.setdefault("api_tokens", [])
|
||||||
|
config["api_tokens"].append(token_entry)
|
||||||
|
save_auth_config(config)
|
||||||
|
return token_entry
|
||||||
|
|
||||||
|
|
||||||
|
def list_api_tokens():
|
||||||
|
"""
|
||||||
|
List all stored API token metadata (no actual tokens are returned).
|
||||||
|
Returns list of token entries with id, name, prefix, creation and expiration dates.
|
||||||
|
"""
|
||||||
|
config = load_auth_config()
|
||||||
|
tokens = config.get("api_tokens", [])
|
||||||
|
revoked = set(config.get("revoked_tokens", []))
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for t in tokens:
|
||||||
|
entry = {
|
||||||
|
"id": t.get("id"),
|
||||||
|
"name": t.get("name", "API Token"),
|
||||||
|
"token_prefix": t.get("token_prefix", "***"),
|
||||||
|
"created_at": t.get("created_at"),
|
||||||
|
"expires_at": t.get("expires_at"),
|
||||||
|
"revoked": t.get("token_hash") in revoked
|
||||||
|
}
|
||||||
|
result.append(entry)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def revoke_api_token(token_id):
|
||||||
|
"""
|
||||||
|
Revoke an API token by its ID.
|
||||||
|
Adds the token hash to the revoked list so it fails verification.
|
||||||
|
Returns (success: bool, message: str)
|
||||||
|
"""
|
||||||
|
config = load_auth_config()
|
||||||
|
tokens = config.get("api_tokens", [])
|
||||||
|
|
||||||
|
target = None
|
||||||
|
for t in tokens:
|
||||||
|
if t.get("id") == token_id:
|
||||||
|
target = t
|
||||||
|
break
|
||||||
|
|
||||||
|
if not target:
|
||||||
|
return False, "Token not found"
|
||||||
|
|
||||||
|
token_hash = target.get("token_hash")
|
||||||
|
config.setdefault("revoked_tokens", [])
|
||||||
|
|
||||||
|
if token_hash in config["revoked_tokens"]:
|
||||||
|
return False, "Token is already revoked"
|
||||||
|
|
||||||
|
config["revoked_tokens"].append(token_hash)
|
||||||
|
|
||||||
|
# Remove from the active tokens list
|
||||||
|
config["api_tokens"] = [t for t in tokens if t.get("id") != token_id]
|
||||||
|
|
||||||
|
if save_auth_config(config):
|
||||||
|
return True, "Token revoked successfully"
|
||||||
|
else:
|
||||||
|
return False, "Failed to save configuration"
|
||||||
|
|
||||||
|
|
||||||
def get_auth_status():
|
def get_auth_status():
|
||||||
"""
|
"""
|
||||||
Get current authentication status
|
Get current authentication status
|
||||||
@@ -243,6 +340,8 @@ def disable_auth():
|
|||||||
config["totp_enabled"] = False
|
config["totp_enabled"] = False
|
||||||
config["totp_secret"] = None
|
config["totp_secret"] = None
|
||||||
config["backup_codes"] = []
|
config["backup_codes"] = []
|
||||||
|
config["api_tokens"] = []
|
||||||
|
config["revoked_tokens"] = []
|
||||||
|
|
||||||
if save_auth_config(config):
|
if save_auth_config(config):
|
||||||
return True, "Authentication disabled"
|
return True, "Authentication disabled"
|
||||||
|
|||||||
@@ -262,6 +262,9 @@ def generate_api_token():
|
|||||||
'iat': datetime.datetime.utcnow()
|
'iat': datetime.datetime.utcnow()
|
||||||
}, auth_manager.JWT_SECRET, algorithm='HS256')
|
}, auth_manager.JWT_SECRET, algorithm='HS256')
|
||||||
|
|
||||||
|
# Store token metadata for listing and revocation
|
||||||
|
auth_manager.store_api_token_metadata(api_token, token_name)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": True,
|
"success": True,
|
||||||
"token": api_token,
|
"token": api_token,
|
||||||
@@ -276,3 +279,35 @@ def generate_api_token():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[ERROR] generate_api_token: {str(e)}") # Log error for debugging
|
print(f"[ERROR] generate_api_token: {str(e)}") # Log error for debugging
|
||||||
return jsonify({"success": False, "message": f"Internal error: {str(e)}"}), 500
|
return jsonify({"success": False, "message": f"Internal error: {str(e)}"}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/api/auth/api-tokens', methods=['GET'])
|
||||||
|
def list_api_tokens():
|
||||||
|
"""List all generated API tokens (metadata only, no actual token values)"""
|
||||||
|
try:
|
||||||
|
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||||
|
if not token or not auth_manager.verify_token(token):
|
||||||
|
return jsonify({"success": False, "message": "Unauthorized"}), 401
|
||||||
|
|
||||||
|
tokens = auth_manager.list_api_tokens()
|
||||||
|
return jsonify({"success": True, "tokens": tokens})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"success": False, "message": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/api/auth/api-tokens/<token_id>', methods=['DELETE'])
|
||||||
|
def revoke_api_token_route(token_id):
|
||||||
|
"""Revoke an API token by its ID"""
|
||||||
|
try:
|
||||||
|
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||||
|
if not token or not auth_manager.verify_token(token):
|
||||||
|
return jsonify({"success": False, "message": "Unauthorized"}), 401
|
||||||
|
|
||||||
|
success, message = auth_manager.revoke_api_token(token_id)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return jsonify({"success": True, "message": message})
|
||||||
|
else:
|
||||||
|
return jsonify({"success": False, "message": message}), 400
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"success": False, "message": str(e)}), 500
|
||||||
|
|||||||
Reference in New Issue
Block a user