From a2643ee7da914ad2ff598874641fc836c3106129 Mon Sep 17 00:00:00 2001 From: Quentin Date: Fri, 23 Aug 2024 00:20:00 +0200 Subject: [PATCH 1/7] feat(dashboard): dashboard page (not finished) --- .../api/repos/[owner]/[repo]/commit/route.ts | 69 ++ src/app/dashboard/layout.tsx | 21 - src/app/dashboard/page.tsx | 619 +++++++++++++++++- src/app/layout.tsx | 2 +- 4 files changed, 676 insertions(+), 35 deletions(-) create mode 100644 src/app/api/repos/[owner]/[repo]/commit/route.ts delete mode 100644 src/app/dashboard/layout.tsx diff --git a/src/app/api/repos/[owner]/[repo]/commit/route.ts b/src/app/api/repos/[owner]/[repo]/commit/route.ts new file mode 100644 index 0000000..8f20c13 --- /dev/null +++ b/src/app/api/repos/[owner]/[repo]/commit/route.ts @@ -0,0 +1,69 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getServerSession } from 'next-auth'; +import { authOptions } from "../../../../../api/auth/[...nextauth]/route"; +import { z } from "zod"; +import fetchURL from "../../../../utils/utils"; + +const querySchema = z.object({ + owner: z.string(), + repo: z.string(), + per_page: z.string().optional(), + page: z.string().optional(), +}); + +export async function GET( + req: NextRequest, + { params }: { params: { owner: string; repo: string; } } +) { + try { + const session = await getServerSession({ req, ...authOptions }); + if (!session) { + return NextResponse.json( + { message: 'Unauthorized' }, + { status: 401 } + ); + } + + const queryParams = { + ...params, + per_page: req.nextUrl.searchParams.get('per_page') || undefined, + page: req.nextUrl.searchParams.get('page') || undefined, + }; + + const result = querySchema.safeParse(queryParams); + if (!result.success) { + return NextResponse.json( + { message: "Invalid query parameters" }, + { status: 400 } + ); + } + + const { owner, repo, per_page = "100", page = "1" } = result.data; + + const encodedOwner = encodeURIComponent(owner); + const encodedRepo = encodeURIComponent(repo); + + const apiUrl = `https://api.github.com/repos/${encodedOwner}/${encodedRepo}/commits?per_page=${per_page}&page=${page}`; + + const response = await fetchURL(req, apiUrl, "GET"); + + if (!response.ok) { + return NextResponse.json( + { message: `Failed to fetch commits: ${response.statusText}` }, + { status: response.status } + ); + } + + const commits = await response.json(); + + return NextResponse.json( + commits, + { status: 200 } + ); + } catch (error) { + return NextResponse.json( + { message: 'Failed to fetch commits' }, + { status: 500 } + ); + } +}; diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx deleted file mode 100644 index 737835d..0000000 --- a/src/app/dashboard/layout.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { SideBar } from "../../components/ui/side-bar" - -export const metadata = { - title: 'Next.js', - description: 'Generated by Next.js', -} - -export default function RootLayout({ - children, -}: { - children: React.ReactNode -}) { - - return ( - - - {children} - - - ) -} diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index d5cb8b1..a546af5 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,15 +1,608 @@ -import { SideBar } from "../../components/ui/side-bar"; -import { TypographyH1 } from "../../components/ui/typography/h1"; -import { getServerSession } from "next-auth"; -import { redirect } from "next/navigation"; - -const Dashboard = async () => { - const session = await getServerSession(); - console.log('Session:', session); - - if (!session || !session.user) { - redirect("/"); - } +"use client"; + +import { useRef, useEffect, useState } from "react"; +import Loading from "../../../src/components/ui/loading"; +import { CircleAlert, ChevronsUp, GitGraph } from "lucide-react"; + +interface Repository { + id: string; + name: string; + owner?: { + avatar_url: string; + login: string; + }; + version: string; + LastPush : string; + created_at: string; + stargazers_count: number; + forks_count: number; + watchers_count: number; + updated_at: string; + clone_url: string; } -export default Dashboard; \ No newline at end of file +interface Commit { + commit: { + message: string; + author: { + date: string; + avatar_url: string; + login: string; + }; + }; + html_url: string; + author: string; + commitDate: string; + commitAuthor: { + avatar_url: string; + login: string; + }; + commitMessage: string; + commitUrl: string; + repoName: string; +} + +type FilterOption = 'Last Week' | 'Last Month' | 'Last Year' | 'All Time'; + + +export default function Dashboard() { + + const [recentCommits, setRecentCommits] = useState([]); + const [dropDownOption, setDropDownOption] = useState('All time'); + const [dropdownOpen, setDropdownOpen] = useState(false); + const [repos, setRepos] = useState([]); + const [searchTerm, setSearchTerm] = useState(""); + const [searchTermCommits, setsearchTermCommits] = useState(""); + + const toggleDropdown = () => { + setDropdownOpen(!dropdownOpen); + }; + + const dropdownRef = useRef(null); + + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setDropdownOpen(false); + } + }; + + useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + useEffect(() => { + const fetchRepositories = async () => { + try { + const reposResponse = await fetch('/api/repos/repository'); + if (!reposResponse.ok) { + throw new Error('Failed to fetch repositories'); + } + + let repos: Repository[] = await reposResponse.json(); + + const filters: Record Repository[]> = { + 'Last Week': (repos) => { + const lastWeek = new Date(); + lastWeek.setDate(lastWeek.getDate() - 7); + return repos.filter((repo) => new Date(repo.updated_at) > lastWeek); + }, + 'Last Month': (repos) => { + const lastMonth = new Date(); + lastMonth.setMonth(lastMonth.getMonth() - 1); + return repos.filter((repo) => new Date(repo.updated_at) > lastMonth); + }, + 'Last Year': (repos) => { + const lastYear = new Date(); + lastYear.setFullYear(lastYear.getFullYear() - 1); + return repos.filter((repo) => new Date(repo.updated_at) > lastYear); + }, + 'All Time': (repos) => repos + }; + + const applyFilter = (repos: Repository[], dropDownOption: FilterOption): Repository[] => { + const filterFunction = filters[dropDownOption]; + return filterFunction ? filterFunction(repos) : repos; + }; + + repos = applyFilter(repos, dropDownOption as FilterOption); + setRepos(repos); + + } catch (error) { + console.error(error); + } + }; + + fetchRepositories(); + }, [dropDownOption]); + + useEffect(() => { + const fetchCommits = async () => { + try { + const allCommits: Commit[] = []; + const counts: { repoName: string }[] = []; + + for (const repo of repos) { + let page = 1; + + const commitsResponse = await fetch( + `api/repos/${repo.owner?.login}/${repo.name}/commit?per_page=100&page=${page}` + ); + + if (!commitsResponse.ok) { + throw new Error(`Failed to fetch commits for repo: ${repo.name}`); + } + + const commitData = await commitsResponse.json(); + allCommits.push( + ...commitData.map((commit: Commit) => ({ + repoName: repo.name, + commitMessage: commit.commit.message, + commitDate: commit.commit.author.date, + commitUrl: commit.html_url, + commitAuthor: commit.author, + })) + ); + page++; + + counts.push({ repoName: repo.name }); + } + + const sortedCommits = allCommits.sort((a: Commit, b: Commit) => { + const dateA: Date = new Date(a.commitDate); + const dateB: Date = new Date(b.commitDate); + + if (isNaN(dateA.getTime()) || isNaN(dateB.getTime())) { + throw new Error('Invalid date format'); + } + + return dateB.getTime() - dateA.getTime(); + }); + + const recentSortedCommits = sortedCommits.slice(0, 15); + setRecentCommits(recentSortedCommits); + + } catch (error) { + console.error(error); + } + }; + + if (repos.length > 0) { + fetchCommits(); + } + }, [repos]); + + useEffect(() => { + console.log(recentCommits) + } , [recentCommits]) + + const formatDate = (isoDateString: string) => { + const date = new Date(isoDateString); + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }); + }; + + const filteredRepos = repos.filter(repo => + repo.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const filteredCommits = recentCommits.filter(commit => + commit.commitMessage.toLowerCase().includes(searchTermCommits.toLowerCase()) + ); + + return ( +
+

+ Dashboard +

+
+ +

New Vulnerability :

+
+
+

3

+ +
+
+
+
+
+ + +
+ +
+
+ +
+ setSearchTerm(e.target.value)} + /> +
+
+
+ + + + + + + + + + + + + {repos.length == 0 ? ( + + + + ) : ( + filteredRepos.map((repo, index) => ( + + + + + + + + + )) + )} + +
+ Repo name + + Owner + + Vulnerability + + Updated At + + More Details + + GIT URL +
+
+
+

+ Loading ... +

+
+
+ +
+
+
+ {repo.name} + + +
+
{repo.owner?.login}
+
+
+
+ { index % 3 >= 2 ? ( +
+ ) : ( +
+ )} + {index % 3} +
+
{formatDate(repo.updated_at)} + + View + + + + View + +
+
+
+
+ +
+
+ +
+
+ +

Last Commits

+
+
+
+ +
+
+ +
+ setsearchTermCommits(e.target.value)} + /> +
+
+
+ + + + + + + + + + + {recentCommits.length === 0 ? ( + + + + ) : ( + filteredCommits.map((commit) => ( + + + + + + + )) + )} + +
NameCommitRepoCommit URL
+
+
+

Loading ...

+
+
+
+
+ {commit.commitAuthor.login} +
+
{commit.commitAuthor.login}
+
+
{commit.commitMessage} +
+ {commit.repoName} +
+
+ view +
+
+
+
+ +
+
+ +

Last Vulnerability

+
+
+
+ +
+
+ +
+ setsearchTermVulnerabilities(e.target.value)}*/ + /> +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ReposLineTypeSeverityDate
Corewarasm/db : 27Buffer Overflow +
+
High
+
+
+
2021-10-06
Corewarasm/db : 27Buffer Overflow +
+
High
+
+
+
2021-10-06
Corewarasm/db : 27Buffer Overflow +
+
High
+
+
+
2021-10-06
Corewarasm/db : 27Buffer Overflow +
+
High
+
+
+
2021-10-06
+
+
+
+
+
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 5c8cd64..7a70b22 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -7,7 +7,7 @@ import { SideBar } from "../components/ui/side-bar"; import ThemeProvider from "../components/ui/theme-provider"; export const metadata: Metadata = { - title: "Create Next App", + title: "Secure-CI", description: "Generated by create next app", }; From 66c170c356ab9aac5bada9ea3b5c77114465129e Mon Sep 17 00:00:00 2001 From: Quentin Date: Fri, 23 Aug 2024 00:20:35 +0200 Subject: [PATCH 2/7] fix(navbar): syntax --- src/components/auth/navbar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/auth/navbar.tsx b/src/components/auth/navbar.tsx index 218f11d..318c58e 100644 --- a/src/components/auth/navbar.tsx +++ b/src/components/auth/navbar.tsx @@ -7,7 +7,7 @@ import Image from "next/image"; import { signIn } from "next-auth/react"; const Navbar = () => { - const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false); + const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false); const [darkMode, setDarkMode] = useState(false); const toggleNavbar = () => { From 922b57a8892ff87432c2ea9ec88503745aee5628 Mon Sep 17 00:00:00 2001 From: Quentin Date: Fri, 23 Aug 2024 00:21:06 +0200 Subject: [PATCH 3/7] feat(tailwind/config): add new classes for tailwind (mostly height change --- tailwind.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tailwind.config.ts b/tailwind.config.ts index 246032d..5544028 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -83,6 +83,7 @@ const config = { '110': '28rem', '128': '32rem', '144': '36rem', + 'half': '50%', }, }, }, From 207bd2de39c2106717b1984c564142e7529d1ca2 Mon Sep 17 00:00:00 2001 From: Quentin Date: Sat, 24 Aug 2024 00:25:10 +0200 Subject: [PATCH 4/7] feat(misc): small changes --- package.json | 5 ++++- src/app/dashboard/page.tsx | 2 +- src/components/auth/footer.tsx | 1 + src/components/auth/navbar.tsx | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index bd22fc5..f2c84be 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "cy:open": "cypress open", + "cy:run": "cypress run" }, "dependencies": { "@amcharts/amcharts5": "^5.10.1", @@ -46,6 +48,7 @@ "zod": "^3.23.8" }, "devDependencies": { + "@playwright/test": "^1.46.1", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index a546af5..6594ff1 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -324,7 +324,7 @@ export default function Dashboard() { type="text" id="table-search" className="block p-2 ps-10 text-sm text-gray-900 border rounded-lg w-80 bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:border-slate-400 dark:bg-gray-700 dark:placeholder-gray-400 dark:outline-none dark:text-white" - placeholder="Search for items" + placeholder="Search for repositories" value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} /> diff --git a/src/components/auth/footer.tsx b/src/components/auth/footer.tsx index 5e5917b..f44045c 100644 --- a/src/components/auth/footer.tsx +++ b/src/components/auth/footer.tsx @@ -1,4 +1,5 @@ import { resourcesLinks, platformLinks, communityLinks } from "../../constants"; + const Footer = () => { return (