feat: implement basic security scan, update tips
This commit is contained in:
parent
ab474abaca
commit
16747bf162
76
app/api/users/security/route.ts
Normal file
76
app/api/users/security/route.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { NextResponse } from "next/server"
|
||||||
|
import { auth } from "@/auth"
|
||||||
|
import { prisma } from "@/lib/prisma"
|
||||||
|
import axios from "axios"
|
||||||
|
|
||||||
|
interface Authenticator {
|
||||||
|
name: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SecurityResults {
|
||||||
|
authentik: {
|
||||||
|
authenticators: Authenticator[],
|
||||||
|
passwordChangeDate: Date | null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const session = await auth()
|
||||||
|
if (!session || !session.user?.email) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { email: session.user.email },
|
||||||
|
})
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: "User not found" }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: SecurityResults = {
|
||||||
|
authentik: {
|
||||||
|
authenticators: [],
|
||||||
|
passwordChangeDate: null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== AUTHENTIK ===== */
|
||||||
|
|
||||||
|
/* 1. Get user info for future requests
|
||||||
|
- User ID
|
||||||
|
- Password change date
|
||||||
|
*/
|
||||||
|
const atkUserRes = await axios.get(`${process.env.AUTHENTIK_API_URL}/core/users/?email=${encodeURIComponent(user.email)}`, {
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${process.env.AUTHENTIK_API_KEY}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const userID = atkUserRes.data.results[0].pk
|
||||||
|
if (atkUserRes.data.results.length > 1) {
|
||||||
|
return NextResponse.json({ error: "Multiple Authentik accounts found" }, { status: 400 })
|
||||||
|
}
|
||||||
|
const passwordChangeDate = atkUserRes.data.results[0].password_change_date
|
||||||
|
if (passwordChangeDate) {
|
||||||
|
results.authentik.passwordChangeDate = new Date(passwordChangeDate) // pushes to results array
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check authenticators
|
||||||
|
const atkAuthenticatorsRes = await axios.get(`${process.env.AUTHENTIK_API_URL}/authenticators/admin/all/?user=${userID}`, {
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${process.env.AUTHENTIK_API_KEY}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const authenticators = atkAuthenticatorsRes.data
|
||||||
|
if (authenticators.length === 0) {
|
||||||
|
return NextResponse.json({ error: "No authenticators found" }, { status: 400 })
|
||||||
|
}
|
||||||
|
authenticators.forEach((authenticator: Authenticator) => {
|
||||||
|
results.authentik.authenticators.push({
|
||||||
|
name: authenticator.name,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { SecurityResults, Authenticator }
|
@ -1,38 +1,130 @@
|
|||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { ShieldCheck } from "lucide-react";
|
import { AlertCircle, CheckCircleIcon, XCircleIcon, Loader2, ShieldCheck, Search, Lightbulb } from "lucide-react"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { SiAuthentik } from "react-icons/si"
|
||||||
|
import { type SecurityResults } from "@/app/api/users/security/route"
|
||||||
|
|
||||||
export const SecurityTab = () => {
|
export const SecurityTab = () => {
|
||||||
|
const [scanning, setScanning] = useState(false)
|
||||||
|
const [scanResults, setScanResults] = useState<SecurityResults | null>(null)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const scanAcc = async () => {
|
||||||
|
setScanning(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/users/security")
|
||||||
|
const data = await res.json()
|
||||||
|
setScanResults(data)
|
||||||
|
setScanning(false)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
setError(error instanceof Error ? error.message : "An unknown error occurred")
|
||||||
|
setScanning(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If password was changed over 3 months ago, this will be true
|
||||||
|
const shouldResetPass = scanResults?.authentik?.passwordChangeDate && scanResults?.authentik?.passwordChangeDate < new Date(Date.now() - 90 * 24 * 60 * 60 * 1000)
|
||||||
|
|
||||||
|
// If user has no 2FA methods setup, this will be true
|
||||||
|
const insufficient2FA = scanResults?.authentik?.authenticators.length === 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-6 grid-cols-1 md:grid-cols-2">
|
<div className="grid gap-6 grid-cols-1 md:grid-cols-2">
|
||||||
<Card>
|
<Card>
|
||||||
{/* TODO: Implement security checks */}
|
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Security Check</CardTitle>
|
<CardTitle className="flex items-center gap-1">
|
||||||
<CardDescription>Evaluate the security of your account with a simple check!</CardDescription>
|
<ShieldCheck size={18} />
|
||||||
|
<span className="text-xl">Account Security Scan</span>
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Evaluate the security of your account with a simple button click!</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p className="text-sm mb-6">Automatic security scans will be arriving shortly!</p>
|
{error ? (
|
||||||
<Button className="w-full" disabled>
|
<div className="text-red-500">
|
||||||
<ShieldCheck className="h-4 w-4" /> Run Security Scan
|
<div className="flex items-center gap-1 mb-2">
|
||||||
</Button>
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<p className="font-bold">Error</p>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm">{error}</p>
|
||||||
|
</div>
|
||||||
|
) : scanResults ? (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<SiAuthentik size={20} />
|
||||||
|
<h3 className="text-xl">Authentik</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{shouldResetPass ? (
|
||||||
|
<div className="flex items-center gap-2 mt-2">
|
||||||
|
<XCircleIcon className="h-4 w-4 text-red-500" />
|
||||||
|
<p className="text-sm">Password last changed {"on " + (scanResults?.authentik?.passwordChangeDate ? new Date(scanResults.authentik.passwordChangeDate).toLocaleDateString() : "never")}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2 mt-2">
|
||||||
|
<CheckCircleIcon className="h-4 w-4 text-green-500" />
|
||||||
|
<p className="text-sm">Password last changed {"on " + (scanResults?.authentik?.passwordChangeDate ? new Date(scanResults.authentik.passwordChangeDate).toLocaleDateString() : "never")}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{insufficient2FA ? (
|
||||||
|
<div className="flex items-center gap-2 mt-2">
|
||||||
|
<XCircleIcon className="h-4 w-4 text-red-500" />
|
||||||
|
<p className="text-sm">No 2FA methods setup</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2 mt-2">
|
||||||
|
<CheckCircleIcon className="h-4 w-4 text-green-500" />
|
||||||
|
<p className="text-sm"><span className="font-bold">{scanResults?.authentik?.authenticators.length}</span> two-factor authentication methods setup</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{scanning ? (
|
||||||
|
<Button className="w-full cursor-pointer" disabled>
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" /> Scanning...
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button className="w-full cursor-pointer" onClick={scanAcc}>
|
||||||
|
<Search className="h-4 w-4" /> Scan my Account
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Security Recommendations</CardTitle>
|
<CardTitle className="flex items-center gap-1">
|
||||||
|
<Lightbulb size={18} />
|
||||||
|
<span className="text-xl">Recommendations</span>
|
||||||
|
</CardTitle>
|
||||||
<CardDescription>Steps you can take to improve your account's security</CardDescription>
|
<CardDescription>Steps you can take to improve your account's security</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<ul className="list-disc pl-5 space-y-2">
|
<ul className="list-disc pl-5 space-y-2">
|
||||||
<li>Enable Two-Factor Authentication</li>
|
<li className="text-sm">Enable Two-Factor Authentication</li>
|
||||||
<li>Use a strong and unique password</li>
|
<li className="text-sm">Use a strong and unique password</li>
|
||||||
<li>Run security checks often (just in case)</li>
|
<li className="text-sm">Always make sure the URL matches <span className="font-bold">librecloud.cc</span></li>
|
||||||
<li>Always double-check the URL (librecloud.cc only!)</li>
|
<div className="border border-green-400 rounded-md p-1 w-full flex items-center justify-between">
|
||||||
|
<span className="text-sm flex items-center">
|
||||||
|
<Search size={16} className="mx-1" />
|
||||||
|
https://<span className="font-bold text-green-400">librecloud.cc</span>
|
||||||
|
</span>
|
||||||
|
<CheckCircleIcon size={16} className="text-green-500 mr-1" />
|
||||||
|
</div>
|
||||||
|
<div className="border border-red-400 rounded-md p-1 w-full flex items-center justify-between">
|
||||||
|
<span className="text-sm flex items-center">
|
||||||
|
<Search size={16} className="mx-1" />
|
||||||
|
https://<span className="font-bold text-red-400">libre-cloud-login.com</span>
|
||||||
|
</span>
|
||||||
|
<XCircleIcon size={16} className="text-red-500 mr-1" />
|
||||||
|
</div>
|
||||||
</ul>
|
</ul>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user