mirror of
https://github.com/BreizhHardware/portfolio.git
synced 2026-01-18 16:37:22 +01:00
Merge pull request #236 from BreizhHardware/fix/security-audit
Fix/security audit
This commit is contained in:
14
.devcontainer/devcontainer.json
Normal file
14
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "Node.js 24 & pnpm",
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
|
"nodeGypDependencies": true,
|
||||||
|
"installYarnUsingApt": true,
|
||||||
|
"version": "lts",
|
||||||
|
"pnpmVersion": "latest",
|
||||||
|
"nvmVersion": "latest"
|
||||||
|
},
|
||||||
|
"ghcr.io/devcontainers/features/git-lfs:1": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
.github/workflows/audit.yml
vendored
Normal file
47
.github/workflows/audit.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: Security Audit
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, dev]
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- '**'
|
||||||
|
schedule:
|
||||||
|
- cron: '0 8 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
audit:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Run security audit
|
||||||
|
id: audit
|
||||||
|
run: pnpm audit --audit-level moderate
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Create issue on failure
|
||||||
|
if: steps.audit.outcome == 'failure'
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
github.rest.issues.create({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
title: 'Security Audit Failed',
|
||||||
|
body: 'The daily security audit has failed. Please check the workflow run for details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}',
|
||||||
|
labels: ['security', 'audit']
|
||||||
|
});
|
||||||
18
package.json
18
package.json
@@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emailjs/browser": "^4.4.1",
|
"@emailjs/browser": "^4.4.1",
|
||||||
"@tailwindcss/cli": "^4.1.14",
|
"@tailwindcss/cli": "^4.1.17",
|
||||||
"@tailwindcss/vite": "^4.1.17",
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
@@ -12,18 +12,18 @@
|
|||||||
"@types/react": "19.2.2",
|
"@types/react": "19.2.2",
|
||||||
"@types/react-dom": "19.2.2",
|
"@types/react-dom": "19.2.2",
|
||||||
"aos": "^2.3.4",
|
"aos": "^2.3.4",
|
||||||
"browserslist": "^4.26.3",
|
"browserslist": "^4.28.1",
|
||||||
"browserslist-useragent": "^4.0.0",
|
"browserslist-useragent": "^4.0.0",
|
||||||
"i18next": "^25.6.1",
|
"i18next": "^25.7.2",
|
||||||
"i18next-http-backend": "^3.0.2",
|
"i18next-http-backend": "^3.0.2",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"react-github-btn": "^1.4.0",
|
"react-github-btn": "^1.4.0",
|
||||||
"react-i18next": "^16.2.4",
|
"react-i18next": "^16.4.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-particles": "^2.12.2",
|
"react-particles": "^2.12.2",
|
||||||
"sass": "^1.94.2",
|
"sass": "^1.94.2",
|
||||||
"tailwindcss": "^4.1.15",
|
"tailwindcss": "^4.1.17",
|
||||||
"tsparticles-slim": "^2.12.0",
|
"tsparticles-slim": "^2.12.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"web-vitals": "^5.1.0"
|
"web-vitals": "^5.1.0"
|
||||||
@@ -58,10 +58,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@simonsmith/cypress-image-snapshot": "^10.0.3",
|
"@simonsmith/cypress-image-snapshot": "^10.0.3",
|
||||||
"@vitejs/plugin-react": "^5.0.4",
|
"@vitejs/plugin-react": "^5.1.2",
|
||||||
"cypress": "^15.5.0",
|
"cypress": "^15.7.1",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.7.4",
|
||||||
"vite": "^7.1.12",
|
"vite": "^7.2.7",
|
||||||
"vite-plugin-svgr": "^4.5.0",
|
"vite-plugin-svgr": "^4.5.0",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
}
|
}
|
||||||
|
|||||||
1305
pnpm-lock.yaml
generated
1305
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,6 @@ import data from "./assets/DATA";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import i18n from './i18n';
|
import i18n from './i18n';
|
||||||
import {createRoot} from "react-dom/client";
|
import {createRoot} from "react-dom/client";
|
||||||
import GitHubStatsSection from 'components/GitHubStatsSection';
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
// Initialise le thème depuis localStorage ou détecte le thème système
|
// Initialise le thème depuis localStorage ou détecte le thème système
|
||||||
@@ -83,8 +82,6 @@ function App() {
|
|||||||
...item,
|
...item,
|
||||||
type: item.type as "work" | "education" | "achievement"
|
type: item.type as "work" | "education" | "achievement"
|
||||||
}))} />
|
}))} />
|
||||||
<hr className="text-gray-800 dark:text-gray-200 mt-4" id="github" />
|
|
||||||
<GitHubStatsSection />
|
|
||||||
<hr className="text-gray-800 dark:text-gray-200 mt-4" id="projects" />
|
<hr className="text-gray-800 dark:text-gray-200 mt-4" id="projects" />
|
||||||
<Project projects={data.projects} />
|
<Project projects={data.projects} />
|
||||||
<hr className="text-gray-800 dark:text-gray-200 mt-4" id="contact" />
|
<hr className="text-gray-800 dark:text-gray-200 mt-4" id="contact" />
|
||||||
|
|||||||
@@ -1,234 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { FaGithub, FaStar, FaCodeBranch, FaCode, FaCalendarAlt } from 'react-icons/fa';
|
|
||||||
|
|
||||||
interface GitHubStats {
|
|
||||||
totalRepos: number;
|
|
||||||
totalStars: number;
|
|
||||||
totalForks: number;
|
|
||||||
totalCommits: number;
|
|
||||||
languages: { [key: string]: number };
|
|
||||||
contributions: Array<{ date: string; count: number }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const GitHubStatsSection: React.FC = () => {
|
|
||||||
const [stats, setStats] = useState<GitHubStats | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchStats = async () => {
|
|
||||||
try {
|
|
||||||
const username = "BreizhHardware"; // à adapter si besoin
|
|
||||||
const token = import.meta.env.VITE_GITHUB_TOKEN;
|
|
||||||
// Infos utilisateur REST
|
|
||||||
const userRes = await fetch(`https://api.github.com/users/${username}`, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
Accept: "application/vnd.github.v3+json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const userData = await userRes.json();
|
|
||||||
|
|
||||||
// Repos pour stars/forks REST
|
|
||||||
const reposRes = await fetch(`https://api.github.com/users/${username}/repos?per_page=100`, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
Accept: "application/vnd.github.v3+json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const reposData = await reposRes.json();
|
|
||||||
|
|
||||||
// Calculs REST
|
|
||||||
const totalStars = reposData.reduce((acc: number, repo: any) => acc + repo.stargazers_count, 0);
|
|
||||||
const totalForks = reposData.reduce((acc: number, repo: any) => acc + repo.forks_count, 0);
|
|
||||||
// Langages
|
|
||||||
const langCount: { [key: string]: number } = {};
|
|
||||||
reposData.forEach((repo: any) => {
|
|
||||||
if (repo.language) {
|
|
||||||
langCount[repo.language] = (langCount[repo.language] || 0) + 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const totalRepos = userData.public_repos;
|
|
||||||
|
|
||||||
// GraphQL pour contributions
|
|
||||||
const today = new Date();
|
|
||||||
const lastYear = new Date(today);
|
|
||||||
lastYear.setFullYear(today.getFullYear() - 1);
|
|
||||||
const fromDate = lastYear.toISOString(); // format complet ISO 8601
|
|
||||||
const toDate = today.toISOString();
|
|
||||||
const graphQLQuery = {
|
|
||||||
query: `
|
|
||||||
query {
|
|
||||||
user(login: "${username}") {
|
|
||||||
contributionsCollection(from: \"${fromDate}\", to: \"${toDate}\") {
|
|
||||||
contributionCalendar {
|
|
||||||
totalContributions
|
|
||||||
weeks {
|
|
||||||
contributionDays {
|
|
||||||
date
|
|
||||||
contributionCount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
};
|
|
||||||
const graphQLRes = await fetch("https://api.github.com/graphql", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(graphQLQuery),
|
|
||||||
});
|
|
||||||
const graphQLData = await graphQLRes.json();
|
|
||||||
const calendar = graphQLData.data.user.contributionsCollection.contributionCalendar;
|
|
||||||
const totalCommits = calendar.totalContributions;
|
|
||||||
// Flatten days
|
|
||||||
const contributions: Array<{ date: string; count: number }> = [];
|
|
||||||
calendar.weeks.forEach((week: any) => {
|
|
||||||
week.contributionDays.forEach((day: any) => {
|
|
||||||
contributions.push({ date: day.date, count: day.contributionCount });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
setStats({
|
|
||||||
totalRepos,
|
|
||||||
totalStars,
|
|
||||||
totalForks,
|
|
||||||
totalCommits,
|
|
||||||
languages: langCount,
|
|
||||||
contributions,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
setStats(null);
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
fetchStats();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const topLanguages = stats ? Object.entries(stats.languages).sort((a, b) => b[1] - a[1]).slice(0, 5) : [];
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="max-w-4xl mx-auto mt-16">
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-8 shadow-lg">
|
|
||||||
<div className="animate-pulse">
|
|
||||||
<div className="h-8 bg-gray-300 dark:bg-gray-600 rounded w-1/3 mx-auto mb-6"></div>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
||||||
{[1, 2, 3, 4].map((i) => (
|
|
||||||
<div key={i} className="h-20 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="max-w-4xl mx-auto mt-16">
|
|
||||||
<div className="text-center mb-8">
|
|
||||||
<h2 className="text-3xl md:text-4xl font-bold text-gray-800 dark:text-gray-200 mb-4 flex items-center justify-center gap-3">
|
|
||||||
{t('github.title')}
|
|
||||||
</h2>
|
|
||||||
<p className="text-lg text-gray-600 dark:text-gray-400">
|
|
||||||
{t('github.description')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{stats && (
|
|
||||||
<div className="space-y-8">
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover text-center">
|
|
||||||
<FaCode className="text-3xl text-blue-500 mx-auto mb-3" />
|
|
||||||
<p className="text-2xl font-bold text-gray-800 dark:text-gray-200">{stats.totalRepos}</p>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">{t('github.repositories')}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover text-center">
|
|
||||||
<FaStar className="text-3xl text-yellow-500 mx-auto mb-3" />
|
|
||||||
<p className="text-2xl font-bold text-gray-800 dark:text-gray-200">{stats.totalStars}</p>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">{t('github.stars')}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover text-center">
|
|
||||||
<FaCodeBranch className="text-3xl text-green-500 mx-auto mb-3" />
|
|
||||||
<p className="text-2xl font-bold text-gray-800 dark:text-gray-200">{stats.totalForks}</p>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">{t('github.forks')}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover text-center">
|
|
||||||
<FaCalendarAlt className="text-3xl text-purple-500 mx-auto mb-3" />
|
|
||||||
<p className="text-2xl font-bold text-gray-800 dark:text-gray-200">{stats.totalCommits !== -1 ? stats.totalCommits : "N/A"}</p>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">{t('github.commits')}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover">
|
|
||||||
<h3 className="text-xl font-bold text-gray-800 dark:text-gray-200 mb-6 text-center">
|
|
||||||
{t('github.languages')}
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{topLanguages.map(([language, percentage], index) => (
|
|
||||||
<div key={language} className="animate-slideInLeft" style={{ animationDelay: `${index * 100}ms` }}>
|
|
||||||
<div className="flex justify-between items-center mb-2">
|
|
||||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">{language}</span>
|
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">{percentage}%</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-3 overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full bg-gradient-to-r from-blue-500 to-purple-500 rounded-full progress-fill transition-all duration-1000 ease-out"
|
|
||||||
style={{
|
|
||||||
width: `${percentage}%`,
|
|
||||||
animationDelay: `${index * 200}ms`
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover">
|
|
||||||
<h3 className="text-xl font-bold text-gray-800 dark:text-gray-200 mb-6 text-center">
|
|
||||||
{t('github.activity')}
|
|
||||||
</h3>
|
|
||||||
{stats.contributions && (
|
|
||||||
<div className="grid grid-cols-12 gap-1 max-w-3xl mx-auto">
|
|
||||||
{stats.contributions.map((day, i) => {
|
|
||||||
let color = 'bg-gray-200 dark:bg-gray-700';
|
|
||||||
if (day.count > 10) color = 'bg-green-500';
|
|
||||||
else if (day.count > 5) color = 'bg-green-400';
|
|
||||||
else if (day.count > 2) color = 'bg-green-300';
|
|
||||||
else if (day.count > 0) color = 'bg-green-200';
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={day.date}
|
|
||||||
className={`w-3 h-3 rounded-sm ${color} animate-fadeInUp`}
|
|
||||||
style={{ animationDelay: `${i * 2}ms` }}
|
|
||||||
title={`${day.date}: ${day.count} commit${day.count > 1 ? 's' : ''}`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="text-center mt-4">
|
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{t('github.legend')}<br/>
|
|
||||||
{t('github.totalCommits')} : {stats.totalCommits}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GitHubStatsSection;
|
|
||||||
@@ -10,7 +10,6 @@ function Menu() {
|
|||||||
<li><a href="#top" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">Félix MARQUET</a></li>
|
<li><a href="#top" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">Félix MARQUET</a></li>
|
||||||
<li><a href="#about" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">{t('nav.about')}</a></li>
|
<li><a href="#about" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">{t('nav.about')}</a></li>
|
||||||
<li><a href="#experience" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">{t('nav.experience')}</a></li>
|
<li><a href="#experience" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">{t('nav.experience')}</a></li>
|
||||||
<li><a href="#github" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">{t('nav.github')}</a></li>
|
|
||||||
<li><a href="#projects" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">{t('nav.projects')}</a></li>
|
<li><a href="#projects" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">{t('nav.projects')}</a></li>
|
||||||
<li><a href="#contact" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">{t('nav.contact')}</a></li>
|
<li><a href="#contact" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">{t('nav.contact')}</a></li>
|
||||||
<li><a href="#cv" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">{t('nav.cv')}</a></li>
|
<li><a href="#cv" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">{t('nav.cv')}</a></li>
|
||||||
|
|||||||
Reference in New Issue
Block a user