Compare commits
29 Commits
Author | SHA1 | Date |
---|---|---|
|
beec72aed7 | |
|
b1d1eb47bf | |
|
2e5c1840c0 | |
|
2f01b4e822 | |
|
720e1c990a | |
|
78fc664a8c | |
|
525c8a11f9 | |
|
adc1726374 | |
|
7993539c96 | |
|
61366ac398 | |
|
3fca1a2c2e | |
|
45534502e8 | |
|
cd0092400a | |
|
48e3f5c0cb | |
|
8cbfa8a625 | |
|
677d64c3ba | |
|
46ba214a1f | |
|
3873bda1dd | |
|
caafc6133d | |
|
0c8953939f | |
|
d7ce2fd7e6 | |
|
de6d777005 | |
|
f793d9f93c | |
|
cd11385e1c | |
|
df315f1678 | |
|
92a298c487 | |
|
1627bc7df9 | |
|
200087fee3 | |
|
84177800c5 |
|
@ -1,15 +0,0 @@
|
|||
# Test results and reports
|
||||
tests/reports/
|
||||
playwright-report/
|
||||
test-results/
|
||||
|
||||
# Node modules
|
||||
node_modules/
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
|
@ -1,3 +1,4 @@
|
|||
# build 0
|
||||
labels:
|
||||
location: manager
|
||||
|
||||
|
@ -121,22 +122,22 @@ steps:
|
|||
event: [push, cron]
|
||||
|
||||
# Wait for Deploy Completion
|
||||
# wait-for-deploy-production:
|
||||
# name: wait-for-deploy-production
|
||||
# image: woodpeckerci/plugin-git
|
||||
# commands:
|
||||
# - echo "Waiting for deploy step to complete rollout."
|
||||
# - sleep 60
|
||||
# when:
|
||||
# branch: main
|
||||
# event: push
|
||||
wait-for-deploy-production:
|
||||
name: wait-for-deploy-production
|
||||
image: woodpeckerci/plugin-git
|
||||
commands:
|
||||
- echo "Waiting for deploy step to complete rollout."
|
||||
- sleep 60
|
||||
when:
|
||||
branch: main
|
||||
event: push
|
||||
|
||||
# Post-Deployment Smoke Tests
|
||||
# post-deploy-smoke-tests-git-nixc-us:
|
||||
# name: run-post-deploy-smoke-tests-git-nixc-us
|
||||
# image: codeberg.org/nixius/playwright:latest
|
||||
# environment:
|
||||
# BASE_URL: "https://git.nixc.us"
|
||||
# when:
|
||||
# branch: main
|
||||
# event: push
|
||||
post-deploy-smoke-tests-git-nixc-us:
|
||||
name: run-post-deploy-smoke-tests-git-nixc-us
|
||||
image: codeberg.org/nixius/playwright:latest
|
||||
environment:
|
||||
BASE_URL: "https://git.nixc.us"
|
||||
when:
|
||||
branch: main
|
||||
event: push
|
||||
|
|
96
README.md
96
README.md
|
@ -1,96 +0,0 @@
|
|||
# Colin Knapp Portfolio Resume
|
||||
|
||||
A professional portfolio website with accessibility features and automated testing.
|
||||
|
||||
## Features
|
||||
|
||||
- Responsive design for all device sizes
|
||||
- Light/dark mode toggle with system preference detection
|
||||
- High contrast accessible design
|
||||
- Keyboard navigable interface
|
||||
- WCAG 2.1 Level AA+ compliant
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js (v14 or higher)
|
||||
- npm (v6 or higher)
|
||||
|
||||
### Setup
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone git@git.nixc.us:colin/resume.git
|
||||
cd resume
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Set up Playwright browsers:
|
||||
```bash
|
||||
npm run setup
|
||||
```
|
||||
|
||||
### Local Development
|
||||
|
||||
To serve the site locally for development:
|
||||
|
||||
```bash
|
||||
npm run serve
|
||||
```
|
||||
|
||||
This will start a local development server at http://localhost:8080.
|
||||
|
||||
### Testing
|
||||
|
||||
The project includes automated testing using Playwright for functional tests and Lighthouse for performance and accessibility audits.
|
||||
|
||||
#### Running all tests
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
#### Running only Playwright tests
|
||||
|
||||
```bash
|
||||
npm run test:playwright
|
||||
```
|
||||
|
||||
#### Running only Lighthouse tests
|
||||
|
||||
```bash
|
||||
npm run test:lighthouse
|
||||
```
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
The site is deployed using Docker and Caddy. The deployment configuration is in the `docker` directory.
|
||||
|
||||
To build and run the Docker container locally:
|
||||
|
||||
```bash
|
||||
cd docker
|
||||
docker build -t resume:latest ./resume/
|
||||
docker run -p 8080:8080 resume:latest
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
This site is designed to meet WCAG 2.1 Level AA+ standards. Key accessibility features include:
|
||||
|
||||
- Proper heading hierarchy
|
||||
- Keyboard navigable interface with visible focus states
|
||||
- Color contrast ratios that exceed WCAG AA requirements
|
||||
- Semantic HTML structure
|
||||
- Accessible form controls and ARIA attributes
|
||||
- Light/dark mode support with system preference detection
|
||||
- Responsive design for all device sizes
|
||||
|
||||
## License
|
||||
|
||||
ISC © Colin Knapp
|
|
@ -1,6 +1,6 @@
|
|||
services:
|
||||
resume:
|
||||
lucky-ddg:
|
||||
build:
|
||||
context: ./docker/resume/
|
||||
dockerfile: Dockerfile.production
|
||||
image: git.nixc.us/colin/resume:production
|
||||
context: ./docker/lucky-ddg/
|
||||
dockerfile: Dockerfile
|
||||
image: git.nixc.us/nixius/lucky-ddg:production
|
|
@ -1,6 +1,6 @@
|
|||
services:
|
||||
resume:
|
||||
lucky-ddg:
|
||||
build:
|
||||
context: ./docker/resume/
|
||||
context: ./docker/lucky-ddg/
|
||||
dockerfile: Dockerfile
|
||||
image: git.nixc.us/colin/resume:staging
|
||||
image: git.nixc.us/nixius/lucky-ddg:staging
|
|
@ -0,0 +1,21 @@
|
|||
# Use the official Python image from Docker Hub
|
||||
FROM python:3.9-slim
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# Install duckduckgo_search globally
|
||||
RUN pip install --no-cache-dir duckduckgo_search
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the application code
|
||||
COPY . .
|
||||
|
||||
# Expose the Flask port
|
||||
EXPOSE 5000
|
||||
|
||||
# Run the application
|
||||
CMD ["python", "app.py"]
|
|
@ -0,0 +1,30 @@
|
|||
import subprocess
|
||||
from flask import Flask, request, redirect
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/search')
|
||||
def search():
|
||||
query = request.args.get('q')
|
||||
if not query:
|
||||
return "Query parameter 'q' is missing.", 400
|
||||
|
||||
try:
|
||||
# Execute the ddgs CLI command to perform the search
|
||||
result = subprocess.run(
|
||||
['ddgs', 'text', '-k', query, '-m', '1'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
# Parse the output to extract the first result URL
|
||||
output_lines = result.stdout.splitlines()
|
||||
for line in output_lines:
|
||||
if line.startswith('http'):
|
||||
return redirect(line)
|
||||
return "No results found.", 404
|
||||
except subprocess.CalledProcessError as e:
|
||||
return f"An error occurred: {e}", 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5000)
|
|
@ -0,0 +1 @@
|
|||
flask==2.2.2
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"name": "docker",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": ""
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
:8080 {
|
||||
root * .
|
||||
file_server
|
||||
encode gzip
|
||||
|
||||
# Performance optimizations
|
||||
header {
|
||||
# Remove default Caddy headers
|
||||
-Server
|
||||
-X-Powered-By
|
||||
|
||||
# HSTS
|
||||
# Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||
|
||||
# Basic security headers
|
||||
X-Frame-Options "DENY"
|
||||
X-Content-Type-Options "nosniff"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
|
||||
# Permissions policy
|
||||
Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"
|
||||
|
||||
# Cross-origin isolation headers
|
||||
Cross-Origin-Embedder-Policy "require-corp"
|
||||
Cross-Origin-Resource-Policy "same-origin"
|
||||
Cross-Origin-Opener-Policy "same-origin"
|
||||
|
||||
# Cache control for static assets
|
||||
Cache-Control "public, max-age=31536000, immutable"
|
||||
|
||||
# CSP with hashes for scripts and styles
|
||||
Content-Security-Policy "default-src 'none'; script-src 'self' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8='; style-src 'self' 'sha256-Mo+7o3oPEKpX7fqRvTtunvQHlIDhJ0SxAMG1PCNniCI='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"
|
||||
}
|
||||
|
||||
# Handle 404s
|
||||
handle_errors {
|
||||
respond "{err.status_code} {err.status_text}"
|
||||
}
|
||||
|
||||
# Logging
|
||||
log {
|
||||
output stdout
|
||||
format json
|
||||
}
|
||||
|
||||
# Enable static file serving with caching
|
||||
file_server {
|
||||
precompressed
|
||||
browse
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
FROM caddy:2-alpine
|
||||
|
||||
# Copy Caddyfile and static content
|
||||
COPY Caddyfile /etc/caddy/Caddyfile
|
||||
COPY index.html /srv/
|
||||
COPY theme.js /srv/
|
||||
COPY utils.js /srv/
|
||||
COPY styles.css /srv/
|
||||
|
||||
# Expose port 8080
|
||||
EXPOSE 8080
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /srv
|
||||
|
||||
# Run Caddy
|
||||
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile"]
|
|
@ -1 +0,0 @@
|
|||
FROM git.nixc.us/colin/resume:staging
|
|
@ -1,199 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Colin Knapp - Cybersecurity Expert and Software Developer Portfolio">
|
||||
<title>Colin Knapp Portfolio</title>
|
||||
<link rel="stylesheet" href="styles.css" integrity="sha256-Mo+7o3oPEKpX7fqRvTtunvQHlIDhJ0SxAMG1PCNniCI=" crossorigin="anonymous">
|
||||
<script src="theme.js" integrity="sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="theme-switch">
|
||||
<button
|
||||
id="themeToggle"
|
||||
aria-label="Theme mode: Auto"
|
||||
role="switch"
|
||||
aria-checked="false"
|
||||
title="Toggle between light, dark, and auto theme modes"
|
||||
tabindex="0"
|
||||
>🌓</button>
|
||||
</div>
|
||||
|
||||
<div class="container-fluid" role="main">
|
||||
<h1>Colin Knapp</h1>
|
||||
<p><strong>Location:</strong> Kitchener-Waterloo, Ontario, Canada<br>
|
||||
<strong>Contact:</strong> <a href="mailto:recruitme2025@colinknapp.com">recruitme2025@colinknapp.com</a> | <a href="https://colinknapp.com">colinknapp.com</a><br>
|
||||
<strong>Schedule a Meeting:</strong> <a href="https://cal.com/colin-/30min" target="_blank">30 Minute Meeting</a> | <a href="https://cal.com/colin-/60-min-meeting" target="_blank">60 Minute Meeting</a></p>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>Highlights & Measurables</h2>
|
||||
<ul>
|
||||
<li><strong>Cybersecurity Leadership:</strong> Currently spearheading <em><a href="http://ViperWire.ca">ViperWire.ca</a></em>, the public-facing arm of my AI-powered cybersecurity and development consultancy, delivering cutting-edge protection for digital assets (2023-Present).</li>
|
||||
<li><strong>Open-Source Impact:</strong> Co-created <em>FastAsyncWorldEdit</em> and <em>PlotSquared</em>, revolutionizing Minecraft development by enabling massive transformative edits—scaling from 50,000 server-crashing edits to billions without interruption—powering a $2 billion game brand with global contributor support (2014-Present).</li>
|
||||
<li><strong>Team Leadership:</strong> Managed a distributed team of 45 contractors at NitricConcepts, fostering collaboration and deploying advanced DevSecOps practices (2018-2021).</li>
|
||||
<li><strong>On-Premises Innovation:</strong> Architected self-managed, bare-metal infrastructure with orchestration for on-premises deployments, delivering performant, scalable systems compliant with WCAG 2.0 AA for clients like <a href="https://showerloop.cc">ShowerLoop</a>, meeting stringent government accessibility and compliance goals (2020-Present).</li>
|
||||
<li><strong>Government Projects:</strong> Delivered scalable, secure learning management systems for the US government and consulted on <a href="https://bishopairport.org">Flint Bishop International Airport</a>'s website and domain infrastructure via Addis Enterprises, building a geographically redundant DNS cluster with an A+ standard resilient to extreme scenarios (2019-Present).</li>
|
||||
<li><strong>Healthcare Infrastructure:</strong> Developed and deployed infrastructure for <a href="https://improvingmipractices.org">Improving MI Practices</a>, a critical healthcare education platform, ensuring high availability and security for sensitive medical training content (2023-Present).</li>
|
||||
<li><strong>Security Automation:</strong> Created a Docker-based utility for automated WordPress malware removal and hardening, successfully deployed to protect <a href="https://mlpp.org">MLPP</a> from persistent cyber attacks, reducing infection frequency from daily to zero (2023).</li>
|
||||
</ul>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>Project Experience</h2>
|
||||
<h3>DevSecOps at Addis Enterprises</h3>
|
||||
<p><strong>Timeframe:</strong> 2019-Present<br>
|
||||
<strong>Overview:</strong> Collaborated on US government projects and airport infrastructure, focusing on scalable, secure systems and domain resilience.<br>
|
||||
<strong>Key Contributions:</strong></p>
|
||||
<ul>
|
||||
<li>Partnered with senior professionals to deliver learning management systems meeting WCAG 2.0 AA compliance for government clients.</li>
|
||||
<li>Consulted for Flint Bishop International Airport, architecting a geographically redundant DNS cluster achieving an A+ standard, capable of withstanding extreme disruptions.</li>
|
||||
<li>Provided exceptional client service through effective communication and tailored solutions.<br>
|
||||
<strong>Impact:</strong> Strengthened government digital infrastructure and ensured robust, resilient airport domain systems.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Healthcare Platform Infrastructure</h3>
|
||||
<p><strong>Timeframe:</strong> 2019-Present<br>
|
||||
<strong>Overview:</strong> Led infrastructure design and operations for <a href="https://improvingmipractices.org">Improving MI Practices</a> (<a href="https://archive.is/D5HIb">archive</a>) through Addis Enterprises, a critical healthcare education platform.<br>
|
||||
<strong>Key Contributions:</strong></p>
|
||||
<ul>
|
||||
<li>Designed and implemented secure, scalable infrastructure for healthcare education content delivery.</li>
|
||||
<li>Administered CIS Level 1 and 2 security standards implementation for enhanced system hardening and security controls.</li>
|
||||
<li>Implemented automated deployment pipelines and monitoring systems for high availability.<br>
|
||||
<strong>Impact:</strong> Enabled reliable delivery of critical healthcare training content to medical professionals while maintaining robust security standards.</li>
|
||||
</ul>
|
||||
|
||||
<h3>WordPress Security Automation</h3>
|
||||
<p><strong>Timeframe:</strong> 2023<br>
|
||||
<strong>Overview:</strong> Developed an automated solution for WordPress malware removal and hardening.<br>
|
||||
<strong>Key Contributions:</strong></p>
|
||||
<ul>
|
||||
<li>Created a Docker-based utility for automated malware detection and removal.</li>
|
||||
<li>Implemented hardening measures to prevent reinfection.</li>
|
||||
<li>Successfully deployed to protect MLPP from persistent cyber attacks.<br>
|
||||
<strong>Impact:</strong> Reduced infection frequency from daily/weekly to zero, significantly improving site security and reliability.</li>
|
||||
</ul>
|
||||
|
||||
<h3>YouTube Game Development & Cybersecurity</h3>
|
||||
<p><strong>Timeframe:</strong> 2009-2022<br>
|
||||
<strong>Overview:</strong> Designed custom video games for prominent online creators, integrating advanced cybersecurity measures.<br>
|
||||
<strong>Key Contributions:</strong></p>
|
||||
<ul>
|
||||
<li>Built immersive gaming experiences for large audiences.</li>
|
||||
<li>Implemented DDoS defense, anti-phishing protocols, and data privacy measures.</li>
|
||||
<li>Managed hardware/software lifecycles and created comprehensive documentation.<br>
|
||||
<strong>Impact:</strong> Delivered secure, seamless gaming experiences to millions of users.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Web Design & Java Plugin Development</h3>
|
||||
<p><strong>Timeframe:</strong> 2009-2023<br>
|
||||
<strong>Overview:</strong> Developed web solutions and Java plugins focusing on CI/CD efficiency and client satisfaction.<br>
|
||||
<strong>Key Contributions:</strong></p>
|
||||
<ul>
|
||||
<li>Utilized Jenkins and GitLab CI/CD for streamlined workflows, leveraging a robust toolchain for rapid development.</li>
|
||||
<li>Managed complex systems and ensured WCAG 2.0 AA accessibility standards.</li>
|
||||
<li>Provided technical guidance and detailed client documentation, drawing on broad experience to resolve diverse issues.<br>
|
||||
<strong>Impact:</strong> Enhanced project delivery speed and quality for diverse computing environments through prolific development practices.</li>
|
||||
</ul>
|
||||
|
||||
<h3>App Development for Influencers</h3>
|
||||
<p><strong>Timeframe:</strong> 2013-2018<br>
|
||||
<strong>Overview:</strong> Created an ad revenue tracking app to optimize earnings and strategies for content creators.<br>
|
||||
<strong>Key Contributions:</strong></p>
|
||||
<ul>
|
||||
<li>Designed a user-friendly tool for real-time revenue monitoring using an optimized toolchain for efficiency.</li>
|
||||
<li>Ensured secure data handling and system performance with extensive problem-solving expertise.<br>
|
||||
<strong>Impact:</strong> Empowered creators to maximize earnings and refine content strategies.</li>
|
||||
</ul>
|
||||
|
||||
<h3>DevOps & Co-Founder at NitricConcepts</h3>
|
||||
<p><strong>Timeframe:</strong> 2018-2021<br>
|
||||
<strong>Overview:</strong> Led a global team in building secure, scalable gaming solutions.<br>
|
||||
<strong>Key Contributions:</strong></p>
|
||||
<ul>
|
||||
<li>Managed 45 contractors worldwide, implementing Docker, Fail2Ban, and Salt Stack as part of a comprehensive toolchain.</li>
|
||||
<li>Co-developed <em>FastAsyncWorldEdit</em> and <em>PlotSquared</em>, enabling billions of seamless edits for Minecraft creators.</li>
|
||||
<li>Fostered a collaborative, innovative team culture.<br>
|
||||
<strong>Impact:</strong> Transformed NitricConcepts into a thriving multinational entity through prolific and efficient development.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Entrepreneurial Ventures</h3>
|
||||
<h4><a href="http://Athion.net">Athion.net</a> Turnaround</h4>
|
||||
<p><strong>Timeframe:</strong> 2013-2017<br>
|
||||
<strong>Overview:</strong> Revitalized a struggling business into a self-sustaining operation in two weeks.<br>
|
||||
<strong>Key Contributions:</strong> Optimized systems and streamlined operations with rapid, effective solutions.<br>
|
||||
<strong>Impact:</strong> Created a profitable, independent venture.</p>
|
||||
|
||||
<h4><a href="http://MotherboardRepair.ca">MotherboardRepair.ca</a></h4>
|
||||
<p><strong>Timeframe:</strong> 2019-Present<br>
|
||||
<strong>Overview:</strong> Co-founded a company reducing e-waste through circuit board repairs.<br>
|
||||
<strong>Key Contributions:</strong> Leveraged industry expertise and a versatile toolchain for sustainable tech solutions.<br>
|
||||
<strong>Impact:</strong> Promoted environmental responsibility in electronics.</p>
|
||||
|
||||
<h4><a href="https://showerloop.cc">ShowerLoop Project</a></h4>
|
||||
<p><strong>Timeframe:</strong> 2016<br>
|
||||
<strong>Overview:</strong> Revamped the website for an eco-friendly recirculating shower system project, implementing WCAG 2.0 AA compliance and modern design principles.<br>
|
||||
<strong>Key Contributions:</strong> Designed and implemented a responsive, accessible website with improved user experience and technical documentation.<br>
|
||||
<strong>Impact:</strong> Enhanced the project's online presence and accessibility while maintaining the site's functionality through periodic maintenance.</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>Additional Information</h2>
|
||||
<h3>Personal Development</h3>
|
||||
<p><strong>Timeframe:</strong> 2009-Present</p>
|
||||
<ul>
|
||||
<li><strong>Self-Taught Mastery:</strong> Continuously honed cybersecurity and systems management skills, building a broad knowledge base to tackle unique challenges with a passion for innovation and problem-solving.</li>
|
||||
<li><strong>Open-Source Contributions:</strong> Actively maintain smaller self-run open-source projects; previously led <em>OhMyForm</em> (retired in favor of FormBricks) and contributed to <em>PlotSquared</em>, <em>FastAsyncWorldEdit</em>, and <em>PlotHider</em>, reflecting a prolific commitment to advancing technology.</li>
|
||||
<li><strong>Skill Maintenance:</strong> Regularly run Woodpecker CI and Gitea for on-premise source management, testing, and deployment, employing security scanning and unit testing to ensure core functionality and security baselines, alongside self-hosting exercises to sustain rapid, high-volume development capabilities across a vast array of innovative projects.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Relevant Links & Web Impact</h3>
|
||||
<ul>
|
||||
<li><strong>Repositories:</strong> <a href="https://github.com/IntellectualSites/PlotSquared">PlotSquared</a>, <a href="https://github.com/IntellectualSites/FastAsyncWorldEdit">FastAsyncWorldEdit</a>, <a href="https://github.com/OhMyForm/OhMyForm">OhMyForm</a>, <a href="https://github.com/IntellectualSites/plothider">PlotHider</a></li>
|
||||
<li><strong>Projects:</strong> <a href="https://viperwire.ca">ViperWire.ca</a>, <a href="https://nitricconcepts.com">NitricConcepts</a>, <a href="https://showerloop.cc">ShowerLoop</a></li>
|
||||
</ul>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="section" role="region" aria-labelledby="open-source-heading">
|
||||
<h2 id="open-source-heading">Open Source & Infrastructure</h2>
|
||||
<div class="entry">
|
||||
<h3>PlotSquared & FastAsyncWorldEdit</h3>
|
||||
<p class="date">2013-Present</p>
|
||||
<p class="overview">Contributor to major Minecraft server plugins, focusing on performance optimization and security enhancements.</p>
|
||||
<ul>
|
||||
<li>Contributed to <a href="https://github.com/IntellectualSites/PlotSquared" target="_blank">PlotSquared</a>, a land management plugin with 572+ stars and 809+ forks</li>
|
||||
<li>Enhanced <a href="https://github.com/IntellectualSites/FastAsyncWorldEdit" target="_blank">FastAsyncWorldEdit</a>, improving world manipulation performance with 664+ stars</li>
|
||||
<li>Implemented security improvements and performance optimizations for large-scale server operations</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="entry">
|
||||
<h3>Athion.net Infrastructure</h3>
|
||||
<p class="date">2013-Present</p>
|
||||
<p class="overview">Established and maintained critical infrastructure for Minecraft development community.</p>
|
||||
<ul>
|
||||
<li>Set up and maintained <a href="https://ci.athion.net/" target="_blank">Jenkins CI/CD pipeline</a> since 2013, supporting continuous integration for game content development</li>
|
||||
<li>Hosted infrastructure enabling collaboration between developers and Microsoft for game content creation</li>
|
||||
<li>Implemented robust security measures and performance optimizations for high-traffic development environments</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="experience-item">
|
||||
<h3>Software Engineer</h3>
|
||||
<p class="company">Oh My Form</p>
|
||||
<p class="date">2020 - Present</p>
|
||||
<p class="achievement">Led development of Oh My Form, achieving over 1.5 million Docker pulls as verified by <a href="https://hub.docker.com/u/ohmyform" target="_blank" rel="noopener noreferrer">Docker Hub</a> and <a href="https://archive.is/lZHAT" target="_blank" rel="noopener noreferrer">archived</a>.</p>
|
||||
<ul>
|
||||
<li>Developed and maintained a secure, high-performance form builder application</li>
|
||||
<li>Implemented robust security measures and best practices</li>
|
||||
<li>Optimized application performance and user experience</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<p class="accessibility-notice"><strong>Accessibility:</strong> This website is designed and developed to meet WCAG 2.1 Level AAA standards, ensuring the highest level of accessibility for all users. Features include high contrast ratios, keyboard navigation, screen reader compatibility, and responsive design. The site supports both light and dark modes with automatic system preference detection.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,187 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Colin Knapp - Cybersecurity Expert and Software Developer Portfolio">
|
||||
<title>Colin Knapp Portfolio</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<script src="theme.js" integrity="sha384-naUim5JriO81FOtYsICLhgAY4mC9xffM8+Zcsc2ztUoQLKKLrDgLFgBRau98yEN1" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="theme-switch">
|
||||
<button
|
||||
id="themeToggle"
|
||||
aria-label="Theme mode: Auto"
|
||||
aria-pressed="false"
|
||||
role="switch"
|
||||
aria-controls="html"
|
||||
title="Toggle between light, dark, and auto theme modes"
|
||||
>🌓</button>
|
||||
</div>
|
||||
|
||||
<div class="container-fluid">
|
||||
<h1>Colin Knapp</h1>
|
||||
<p><strong>Location:</strong> Kitchener-Waterloo, Ontario, Canada<br>
|
||||
<strong>Contact:</strong> <a href="mailto:recruitme2023@colinknapp.com">recruitme2023@colinknapp.com</a> | <a href="https://colinknapp.com">colinknapp.com</a></p>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>Highlights & Measurables</h2>
|
||||
<ul>
|
||||
<li><strong>Cybersecurity Leadership:</strong> Currently spearheading <em><a href="http://ViperWire.ca">ViperWire.ca</a></em>, the public-facing arm of my AI-powered cybersecurity and development consultancy, delivering cutting-edge protection for digital assets (2023-Present).</li>
|
||||
<li><strong>Open-Source Impact:</strong> Co-created <em>FastAsyncWorldEdit</em> and <em>PlotSquared</em>, revolutionizing Minecraft development by enabling massive transformative edits—scaling from 50,000 server-crashing edits to billions without interruption—powering a $2 billion game brand with global contributor support (2014-Present).</li>
|
||||
<li><strong>Team Leadership:</strong> Managed a distributed team of 45 contractors at NitricConcepts, fostering collaboration and deploying advanced DevSecOps practices (2018-2021).</li>
|
||||
<li><strong>On-Premises Innovation:</strong> Architected self-managed, bare-metal infrastructure with orchestration for on-premises deployments, delivering performant, scalable systems compliant with WCAG 2.0 AA for clients like <a href="https://showerloop.cc">ShowerLoop</a>, meeting stringent government accessibility and compliance goals (2020-Present).</li>
|
||||
<li><strong>Government Projects:</strong> Delivered scalable, secure learning management systems for the US government and consulted on <a href="https://bishopairport.org">Flint Bishop International Airport</a>'s website and domain infrastructure via Addis Enterprises, building a geographically redundant DNS cluster with an A+ standard resilient to extreme scenarios (2019-Present).</li>
|
||||
<li><strong>Healthcare Infrastructure:</strong> Developed and deployed infrastructure for <a href="https://improvingmipractices.org">Improving MI Practices</a>, a critical healthcare education platform, ensuring high availability and security for sensitive medical training content (2023-Present).</li>
|
||||
<li><strong>Security Automation:</strong> Created a Docker-based utility for automated WordPress malware removal and hardening, successfully deployed to protect <a href="https://mlpp.org">MLPP</a> from persistent cyber attacks, reducing infection frequency from daily to zero (2023).</li>
|
||||
</ul>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>Project Experience</h2>
|
||||
<h3>DevSecOps at Addis Enterprises</h3>
|
||||
<p><strong>Timeframe:</strong> 2019-Present<br>
|
||||
<strong>Overview:</strong> Collaborated on US government projects and airport infrastructure, focusing on scalable, secure systems and domain resilience.<br>
|
||||
<strong>Key Contributions:</strong></p>
|
||||
<ul>
|
||||
<li>Partnered with senior professionals to deliver learning management systems meeting WCAG 2.0 AA compliance for government clients.</li>
|
||||
<li>Consulted for Flint Bishop International Airport, architecting a geographically redundant DNS cluster achieving an A+ standard, capable of withstanding extreme disruptions.</li>
|
||||
<li>Provided exceptional client service through effective communication and tailored solutions.<br>
|
||||
<strong>Impact:</strong> Strengthened government digital infrastructure and ensured robust, resilient airport domain systems.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Healthcare Platform Infrastructure</h3>
|
||||
<p><strong>Timeframe:</strong> 2019-Present<br>
|
||||
<strong>Overview:</strong> Led infrastructure design and operations for <a href="https://improvingmipractices.org">Improving MI Practices</a> (<a href="https://archive.is/D5HIb">archive</a>) through Addis Enterprises, a critical healthcare education platform.<br>
|
||||
<strong>Key Contributions:</strong></p>
|
||||
<ul>
|
||||
<li>Designed and implemented secure, scalable infrastructure for healthcare education content delivery.</li>
|
||||
<li>Administered CIS Level 1 and 2 security standards implementation for enhanced system hardening and security controls.</li>
|
||||
<li>Implemented automated deployment pipelines and monitoring systems for high availability.<br>
|
||||
<strong>Impact:</strong> Enabled reliable delivery of critical healthcare training content to medical professionals while maintaining robust security standards.</li>
|
||||
</ul>
|
||||
|
||||
<h3>WordPress Security Automation</h3>
|
||||
<p><strong>Timeframe:</strong> 2023<br>
|
||||
<strong>Overview:</strong> Developed an automated solution for WordPress malware removal and hardening.<br>
|
||||
<strong>Key Contributions:</strong></p>
|
||||
<ul>
|
||||
<li>Created a Docker-based utility for automated malware detection and removal.</li>
|
||||
<li>Implemented hardening measures to prevent reinfection.</li>
|
||||
<li>Successfully deployed to protect MLPP from persistent cyber attacks.<br>
|
||||
<strong>Impact:</strong> Reduced infection frequency from daily/weekly to zero, significantly improving site security and reliability.</li>
|
||||
</ul>
|
||||
|
||||
<h3>YouTube Game Development & Cybersecurity</h3>
|
||||
<p><strong>Timeframe:</strong> 2009-2022<br>
|
||||
<strong>Overview:</strong> Designed custom video games for prominent online creators, integrating advanced cybersecurity measures.<br>
|
||||
<strong>Key Contributions:</strong></p>
|
||||
<ul>
|
||||
<li>Built immersive gaming experiences for large audiences.</li>
|
||||
<li>Implemented DDoS defense, anti-phishing protocols, and data privacy measures.</li>
|
||||
<li>Managed hardware/software lifecycles and created comprehensive documentation.<br>
|
||||
<strong>Impact:</strong> Delivered secure, seamless gaming experiences to millions of users.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Web Design & Java Plugin Development</h3>
|
||||
<p><strong>Timeframe:</strong> 2009-2023<br>
|
||||
<strong>Overview:</strong> Developed web solutions and Java plugins focusing on CI/CD efficiency and client satisfaction.<br>
|
||||
<strong>Key Contributions:</strong></p>
|
||||
<ul>
|
||||
<li>Utilized Jenkins and GitLab CI/CD for streamlined workflows, leveraging a robust toolchain for rapid development.</li>
|
||||
<li>Managed complex systems and ensured WCAG 2.0 AA accessibility standards.</li>
|
||||
<li>Provided technical guidance and detailed client documentation, drawing on broad experience to resolve diverse issues.<br>
|
||||
<strong>Impact:</strong> Enhanced project delivery speed and quality for diverse computing environments through prolific development practices.</li>
|
||||
</ul>
|
||||
|
||||
<h3>App Development for Influencers</h3>
|
||||
<p><strong>Timeframe:</strong> 2013-2018<br>
|
||||
<strong>Overview:</strong> Created an ad revenue tracking app to optimize earnings and strategies for content creators.<br>
|
||||
<strong>Key Contributions:</strong></p>
|
||||
<ul>
|
||||
<li>Designed a user-friendly tool for real-time revenue monitoring using an optimized toolchain for efficiency.</li>
|
||||
<li>Ensured secure data handling and system performance with extensive problem-solving expertise.<br>
|
||||
<strong>Impact:</strong> Empowered creators to maximize earnings and refine content strategies.</li>
|
||||
</ul>
|
||||
|
||||
<h3>DevOps & Co-Founder at NitricConcepts</h3>
|
||||
<p><strong>Timeframe:</strong> 2018-2021<br>
|
||||
<strong>Overview:</strong> Led a global team in building secure, scalable gaming solutions.<br>
|
||||
<strong>Key Contributions:</strong></p>
|
||||
<ul>
|
||||
<li>Managed 45 contractors worldwide, implementing Docker, Fail2Ban, and Salt Stack as part of a comprehensive toolchain.</li>
|
||||
<li>Co-developed <em>FastAsyncWorldEdit</em> and <em>PlotSquared</em>, enabling billions of seamless edits for Minecraft creators.</li>
|
||||
<li>Fostered a collaborative, innovative team culture.<br>
|
||||
<strong>Impact:</strong> Transformed NitricConcepts into a thriving multinational entity through prolific and efficient development.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Entrepreneurial Ventures</h3>
|
||||
<h4><a href="http://Athion.net">Athion.net</a> Turnaround</h4>
|
||||
<p><strong>Timeframe:</strong> 2013-2017<br>
|
||||
<strong>Overview:</strong> Revitalized a struggling business into a self-sustaining operation in two weeks.<br>
|
||||
<strong>Key Contributions:</strong> Optimized systems and streamlined operations with rapid, effective solutions.<br>
|
||||
<strong>Impact:</strong> Created a profitable, independent venture.</p>
|
||||
|
||||
<h4><a href="http://MotherboardRepair.ca">MotherboardRepair.ca</a></h4>
|
||||
<p><strong>Timeframe:</strong> 2019-Present<br>
|
||||
<strong>Overview:</strong> Co-founded a company reducing e-waste through circuit board repairs.<br>
|
||||
<strong>Key Contributions:</strong> Leveraged industry expertise and a versatile toolchain for sustainable tech solutions.<br>
|
||||
<strong>Impact:</strong> Promoted environmental responsibility in electronics.</p>
|
||||
|
||||
<h4><a href="https://showerloop.cc">ShowerLoop Project</a></h4>
|
||||
<p><strong>Timeframe:</strong> 2016<br>
|
||||
<strong>Overview:</strong> Revamped the website for an eco-friendly recirculating shower system project, implementing WCAG 2.0 AA compliance and modern design principles.<br>
|
||||
<strong>Key Contributions:</strong> Designed and implemented a responsive, accessible website with improved user experience and technical documentation.<br>
|
||||
<strong>Impact:</strong> Enhanced the project's online presence and accessibility while maintaining the site's functionality through periodic maintenance.</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>Additional Information</h2>
|
||||
<h3>Personal Development</h3>
|
||||
<p><strong>Timeframe:</strong> 2009-Present</p>
|
||||
<ul>
|
||||
<li><strong>Self-Taught Mastery:</strong> Continuously honed cybersecurity and systems management skills, building a broad knowledge base to tackle unique challenges with a passion for innovation and problem-solving.</li>
|
||||
<li><strong>Open-Source Contributions:</strong> Actively maintain smaller self-run open-source projects; previously led <em>OhMyForm</em> (retired in favor of FormBricks) and contributed to <em>PlotSquared</em>, <em>FastAsyncWorldEdit</em>, and <em>PlotHider</em>, reflecting a prolific commitment to advancing technology.</li>
|
||||
<li><strong>Skill Maintenance:</strong> Regularly run Woodpecker CI and Gitea for on-premise source management, testing, and deployment, employing security scanning and unit testing to ensure core functionality and security baselines, alongside self-hosting exercises to sustain rapid, high-volume development capabilities across a vast array of innovative projects.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Relevant Links & Web Impact</h3>
|
||||
<ul>
|
||||
<li><strong>Repositories:</strong> <a href="https://github.com/IntellectualSites/PlotSquared">PlotSquared</a>, <a href="https://github.com/IntellectualSites/FastAsyncWorldEdit">FastAsyncWorldEdit</a>, <a href="https://github.com/OhMyForm/OhMyForm">OhMyForm</a>, <a href="https://github.com/IntellectualSites/plothider">PlotHider</a></li>
|
||||
<li><strong>Projects:</strong> <a href="https://viperwire.ca">ViperWire.ca</a>, <a href="https://nitricconcepts.com">NitricConcepts</a>, <a href="https://showerloop.cc">ShowerLoop</a></li>
|
||||
</ul>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="section">
|
||||
<h2>Open Source & Infrastructure</h2>
|
||||
<div class="entry">
|
||||
<h3>PlotSquared & FastAsyncWorldEdit</h3>
|
||||
<p class="date">2013-Present</p>
|
||||
<p class="overview">Contributor to major Minecraft server plugins, focusing on performance optimization and security enhancements.</p>
|
||||
<ul>
|
||||
<li>Contributed to <a href="https://github.com/IntellectualSites/PlotSquared" target="_blank">PlotSquared</a>, a land management plugin with 572+ stars and 809+ forks</li>
|
||||
<li>Enhanced <a href="https://github.com/IntellectualSites/FastAsyncWorldEdit" target="_blank">FastAsyncWorldEdit</a>, improving world manipulation performance with 664+ stars</li>
|
||||
<li>Implemented security improvements and performance optimizations for large-scale server operations</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="entry">
|
||||
<h3>Athion.net Infrastructure</h3>
|
||||
<p class="date">2013-Present</p>
|
||||
<p class="overview">Established and maintained critical infrastructure for Minecraft development community.</p>
|
||||
<ul>
|
||||
<li>Set up and maintained <a href="https://ci.athion.net/" target="_blank">Jenkins CI/CD pipeline</a> since 2013, supporting continuous integration for game content development</li>
|
||||
<li>Hosted infrastructure enabling collaboration between developers and Microsoft for game content creation</li>
|
||||
<li>Implemented robust security measures and performance optimizations for high-traffic development environments</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<p class="accessibility-notice"><strong>Accessibility:</strong> This website is designed and developed to meet WCAG 2.1 Level AAA standards, ensuring the highest level of accessibility for all users. Features include high contrast ratios, keyboard navigation, screen reader compatibility, and responsive design. The site supports both light and dark modes with automatic system preference detection.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,186 +0,0 @@
|
|||
:root {
|
||||
--bg-color: #ffffff;
|
||||
--text-color: #333333;
|
||||
--accent-color: #0056b3;
|
||||
--border-color: #e0e0e0;
|
||||
--hover-color: #003d82;
|
||||
--theme-bg: #f5f5f5;
|
||||
--theme-border: #ddd;
|
||||
--theme-hover: #e0e0e0;
|
||||
--date-color: #555555;
|
||||
}
|
||||
|
||||
/* Dark theme variables when system prefers dark mode (auto setting) */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg-color: #1a1a1a;
|
||||
--text-color: #e0e0e0;
|
||||
--accent-color: #5fa9ff;
|
||||
--border-color: #404040;
|
||||
--hover-color: #8ac2ff;
|
||||
--theme-bg: #2d2d2d;
|
||||
--theme-border: #404040;
|
||||
--theme-hover: #3d3d3d;
|
||||
--date-color: #a0a0a0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Light theme variables when manually selected */
|
||||
html[data-theme='light'] {
|
||||
--bg-color: #ffffff;
|
||||
--text-color: #333333;
|
||||
--accent-color: #0056b3;
|
||||
--border-color: #e0e0e0;
|
||||
--hover-color: #003d82;
|
||||
--theme-bg: #f5f5f5;
|
||||
--theme-border: #ddd;
|
||||
--theme-hover: #e0e0e0;
|
||||
--date-color: #555555;
|
||||
}
|
||||
|
||||
/* Dark theme variables when manually selected */
|
||||
html[data-theme='dark'] {
|
||||
--bg-color: #1a1a1a;
|
||||
--text-color: #e0e0e0;
|
||||
--accent-color: #5fa9ff;
|
||||
--border-color: #404040;
|
||||
--hover-color: #8ac2ff;
|
||||
--theme-bg: #2d2d2d;
|
||||
--theme-border: #404040;
|
||||
--theme-hover: #3d3d3d;
|
||||
--date-color: #a0a0a0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--text-color);
|
||||
background-color: var(--bg-color);
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
color: var(--text-color);
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5em;
|
||||
border-bottom: 2px solid var(--accent-color);
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2em;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent-color);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
text-decoration-thickness: 1px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
a:hover, a:focus {
|
||||
color: var(--hover-color);
|
||||
text-decoration-thickness: 2px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a:focus {
|
||||
outline: 2px solid var(--accent-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 2em;
|
||||
padding: 1em;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.entry {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.date {
|
||||
color: var(--date-color);
|
||||
font-style: italic;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.overview {
|
||||
font-weight: 500;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0.5em 0;
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 0.3em 0;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border-color);
|
||||
margin: 2em 0;
|
||||
}
|
||||
|
||||
.theme-switch {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
#themeToggle {
|
||||
padding: 8px 16px;
|
||||
background-color: var(--theme-bg);
|
||||
border: 1px solid var(--theme-border);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: var(--text-color);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
#themeToggle:hover {
|
||||
background-color: var(--theme-hover);
|
||||
}
|
||||
|
||||
#themeToggle:focus {
|
||||
outline: 2px solid var(--accent-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const themeToggle = document.getElementById('themeToggle');
|
||||
const html = document.documentElement;
|
||||
|
||||
// Check for saved theme preference, default to auto
|
||||
const savedTheme = localStorage.getItem('theme') || 'auto';
|
||||
|
||||
// Set initial value for aria-checked attribute
|
||||
themeToggle.setAttribute('aria-checked', savedTheme !== 'auto');
|
||||
|
||||
updateTheme(savedTheme);
|
||||
|
||||
function updateTheme(theme) {
|
||||
// Update button state and labels
|
||||
const themeLabels = {
|
||||
light: 'Theme mode: Light',
|
||||
dark: 'Theme mode: Dark',
|
||||
auto: 'Theme mode: Auto'
|
||||
};
|
||||
|
||||
themeToggle.setAttribute('aria-label', themeLabels[theme]);
|
||||
themeToggle.setAttribute('aria-checked', theme !== 'auto');
|
||||
|
||||
// Update button icon
|
||||
const themeIcons = {
|
||||
light: '🌞',
|
||||
dark: '🌙',
|
||||
auto: '🌓'
|
||||
};
|
||||
|
||||
themeToggle.textContent = themeIcons[theme];
|
||||
|
||||
if (theme === 'auto') {
|
||||
html.removeAttribute('data-theme');
|
||||
} else {
|
||||
html.setAttribute('data-theme', theme);
|
||||
}
|
||||
}
|
||||
|
||||
themeToggle.addEventListener('click', () => {
|
||||
const currentTheme = html.getAttribute('data-theme') || 'auto';
|
||||
let newTheme;
|
||||
|
||||
switch(currentTheme) {
|
||||
case 'light':
|
||||
newTheme = 'dark';
|
||||
break;
|
||||
case 'dark':
|
||||
newTheme = 'auto';
|
||||
break;
|
||||
default:
|
||||
newTheme = 'light';
|
||||
}
|
||||
|
||||
updateTheme(newTheme);
|
||||
localStorage.setItem('theme', newTheme);
|
||||
});
|
||||
|
||||
// Handle keyboard navigation
|
||||
themeToggle.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
themeToggle.click();
|
||||
}
|
||||
});
|
||||
});
|
|
@ -1,60 +0,0 @@
|
|||
// Utility functions for the resume website
|
||||
|
||||
/**
|
||||
* Debounce a function to limit its execution rate
|
||||
* @param {Function} func - The function to debounce
|
||||
* @param {number} wait - The number of milliseconds to delay
|
||||
* @returns {Function} - The debounced function
|
||||
*/
|
||||
export function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an element is in the viewport
|
||||
* @param {Element} element - The element to check
|
||||
* @returns {boolean} - Whether the element is in the viewport
|
||||
*/
|
||||
export function isInViewport(element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return (
|
||||
rect.top >= 0 &&
|
||||
rect.left >= 0 &&
|
||||
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date in a consistent way
|
||||
* @param {Date|string} date - The date to format
|
||||
* @returns {string} - The formatted date string
|
||||
*/
|
||||
export function formatDate(date) {
|
||||
if (!date) return '';
|
||||
const d = new Date(date);
|
||||
return d.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely get a value from an object using a path
|
||||
* @param {Object} obj - The object to traverse
|
||||
* @param {string} path - The path to the value
|
||||
* @param {*} defaultValue - The default value if not found
|
||||
* @returns {*} - The value at the path or the default value
|
||||
*/
|
||||
export function get(obj, path, defaultValue = undefined) {
|
||||
return path.split('.')
|
||||
.reduce((acc, part) => acc && acc[part], obj) ?? defaultValue;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
36
package.json
36
package.json
|
@ -1,36 +0,0 @@
|
|||
{
|
||||
"name": "resume",
|
||||
"version": "1.0.0",
|
||||
"description": "Colin Knapp's professional resume website",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"serve": "node tests/serve.js",
|
||||
"test": "npm run test:lighthouse && npm run test:playwright",
|
||||
"test:playwright": "npx playwright test",
|
||||
"test:lighthouse": "node tests/lighthouse.js",
|
||||
"test:headers": "playwright test tests/headers.spec.js",
|
||||
"setup": "npx playwright install"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@git.nixc.us:colin/resume.git"
|
||||
},
|
||||
"keywords": [
|
||||
"resume",
|
||||
"portfolio",
|
||||
"accessibility"
|
||||
],
|
||||
"author": "Colin Knapp",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.42.1",
|
||||
"chrome-launcher": "^1.1.2",
|
||||
"lighthouse": "^11.4.0",
|
||||
"puppeteer": "^22.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"lighthouse": "^11.6.0",
|
||||
"playwright": "^1.42.1"
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
// @ts-check
|
||||
const { defineConfig, devices } = require('@playwright/test');
|
||||
|
||||
/**
|
||||
* @see https://playwright.dev/docs/test-configuration
|
||||
*/
|
||||
module.exports = defineConfig({
|
||||
testDir: './tests',
|
||||
timeout: 30 * 1000,
|
||||
expect: {
|
||||
timeout: 5000
|
||||
},
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: 'html',
|
||||
|
||||
use: {
|
||||
actionTimeout: 0,
|
||||
trace: 'on-first-retry',
|
||||
baseURL: 'http://localhost:8080',
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
],
|
||||
|
||||
webServer: {
|
||||
command: 'node tests/serve.js',
|
||||
port: 8080,
|
||||
reuseExistingServer: true,
|
||||
},
|
||||
});
|
|
@ -3,12 +3,12 @@ networks:
|
|||
external: true
|
||||
|
||||
services:
|
||||
resume:
|
||||
image: git.nixc.us/colin/resume:production
|
||||
lucky:
|
||||
image: git.nixc.us/nixius/lucky-ddg:production
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.hostname == ingress.nixc.us
|
||||
# placement:
|
||||
# constraints:
|
||||
# - node.hostname == ingress.nixc.us
|
||||
update_config:
|
||||
order: start-first
|
||||
# failure_action: rollback
|
||||
|
@ -19,12 +19,12 @@ services:
|
|||
labels:
|
||||
us.nixc.autodeploy: "true"
|
||||
traefik.enable: "true"
|
||||
traefik.http.routers.production_resume.tls: "true"
|
||||
traefik.http.services.production_resume.loadbalancer.server.port: "8080"
|
||||
traefik.http.routers.production_resume.rule: "Host(`resume.colinknapp.com`, `colinknapp.com`)"
|
||||
traefik.http.routers.production_resume.entrypoints: "websecure"
|
||||
traefik.http.routers.production_resume.tls.certresolver: "letsencryptresolver"
|
||||
traefik.http.routers.production_resume.service: "production_resume"
|
||||
traefik.http.routers.production_lucky-ddg.tls: "true"
|
||||
traefik.http.services.production_lucky-ddg.loadbalancer.server.port: "5000"
|
||||
traefik.http.routers.production_lucky-ddg.rule: "Host(`ddg.nixc.us`)"
|
||||
traefik.http.routers.production_lucky-ddg.entrypoints: "websecure"
|
||||
traefik.http.routers.production_lucky-ddg.tls.certresolver: "letsencryptresolver"
|
||||
traefik.http.routers.production_lucky-ddg.service: "production_lucky-ddg"
|
||||
traefik.docker.network: "traefik"
|
||||
networks:
|
||||
traefik:
|
||||
|
|
|
@ -1,38 +1,30 @@
|
|||
version: "3.7"
|
||||
networks:
|
||||
traefik:
|
||||
external: true
|
||||
|
||||
services:
|
||||
resume:
|
||||
image: git.nixc.us/colin/resume:staging
|
||||
image: git.nixc.us/nixius/lucky-ddg:production
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.hostname == ingress.nixc.us
|
||||
# placement:
|
||||
# constraints:
|
||||
# - node.hostname == ingress.nixc.us
|
||||
update_config:
|
||||
order: start-first
|
||||
failure_action: rollback
|
||||
delay: 5s
|
||||
delay: 10s
|
||||
# failure_action: rollback
|
||||
delay: 0s
|
||||
parallelism: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
labels:
|
||||
us.nixc.autodeploy: "true"
|
||||
traefik.enable: "true"
|
||||
traefik.http.routers.staging_resume.tls: "true"
|
||||
traefik.http.services.staging_resume.loadbalancer.server.port: "8080"
|
||||
traefik.http.routers.staging_resume.rule: "Host(`staging.resume.colinknapp.com`, `staging.colinknapp.com`)"
|
||||
traefik.http.routers.staging_resume.entrypoints: "websecure"
|
||||
traefik.http.routers.staging_resume.tls.certresolver: "letsencryptresolver"
|
||||
traefik.http.routers.staging_resume.service: "staging_resume"
|
||||
traefik.http.routers.production_lucky-ddg.tls: "true"
|
||||
traefik.http.services.production_lucky-ddg.loadbalancer.server.port: "5000"
|
||||
traefik.http.routers.production_lucky-ddg.rule: "Host(`ddg.staging.nixc.us`)"
|
||||
traefik.http.routers.production_lucky-ddg.entrypoints: "websecure"
|
||||
traefik.http.routers.production_lucky-ddg.tls.certresolver: "letsencryptresolver"
|
||||
traefik.http.routers.production_lucky-ddg.service: "production_lucky-ddg"
|
||||
traefik.docker.network: "traefik"
|
||||
# traefik.http.routers.staging_resume.middlewares: "authelia@docker"
|
||||
networks:
|
||||
traefik:
|
||||
# logging:
|
||||
# driver: "gelf"
|
||||
# options:
|
||||
# gelf-address: "udp://log.nixc.us:15124"
|
||||
# tag: "resume_resume"
|
||||
|
||||
|
|
|
@ -1,194 +0,0 @@
|
|||
const { test, expect } = require('@playwright/test');
|
||||
const { AxeBuilder } = require('@axe-core/playwright');
|
||||
|
||||
const PRODUCTION_URL = 'https://colinknapp.com';
|
||||
const LOCAL_URL = 'http://localhost:8080';
|
||||
|
||||
async function getPageUrl(page) {
|
||||
try {
|
||||
// Try production first
|
||||
await page.goto(PRODUCTION_URL, { timeout: 60000 });
|
||||
return PRODUCTION_URL;
|
||||
} catch (error) {
|
||||
console.log('Production site not available, falling back to local');
|
||||
await page.goto(LOCAL_URL, { timeout: 60000 });
|
||||
return LOCAL_URL;
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('Accessibility Tests', () => {
|
||||
test('should pass WCAG 2.1 Level AAA standards', async ({ page }) => {
|
||||
const url = await getPageUrl(page);
|
||||
console.log(`Running accessibility tests against ${url}`);
|
||||
|
||||
// Run axe accessibility tests
|
||||
const results = await new AxeBuilder({ page })
|
||||
.withTags(['wcag2a', 'wcag2aa', 'wcag2aaa'])
|
||||
.analyze();
|
||||
|
||||
// Check for any violations
|
||||
expect(results.violations).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should have proper ARIA attributes for theme toggle', async ({ page }) => {
|
||||
const url = await getPageUrl(page);
|
||||
console.log(`Running ARIA tests against ${url}`);
|
||||
|
||||
// Check theme toggle button
|
||||
const themeToggle = await page.locator('#themeToggle');
|
||||
expect(await themeToggle.getAttribute('aria-label')).toBe('Theme mode: Auto');
|
||||
expect(await themeToggle.getAttribute('role')).toBe('switch');
|
||||
expect(await themeToggle.getAttribute('aria-checked')).toBe('false');
|
||||
expect(await themeToggle.getAttribute('title')).toBe('Toggle between light, dark, and auto theme modes');
|
||||
expect(await themeToggle.getAttribute('tabindex')).toBe('0');
|
||||
});
|
||||
|
||||
test('should have proper heading structure', async ({ page }) => {
|
||||
const url = await getPageUrl(page);
|
||||
console.log(`Running heading structure tests against ${url}`);
|
||||
|
||||
// Check main content area
|
||||
const mainContent = await page.locator('.container-fluid');
|
||||
expect(await mainContent.getAttribute('role')).toBe('main');
|
||||
|
||||
// Check heading hierarchy
|
||||
const h1 = await page.locator('h1');
|
||||
expect(await h1.count()).toBe(1);
|
||||
|
||||
const h2s = await page.locator('h2');
|
||||
expect(await h2s.count()).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should have working external links', async ({ page, request }) => {
|
||||
const url = await getPageUrl(page);
|
||||
console.log(`Running link validation tests against ${url}`);
|
||||
await page.goto(url, { timeout: 60000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Get all external links
|
||||
const externalLinks = await page.$$('a[href^="http"]:not([href^="http://localhost"])');
|
||||
|
||||
// Skip test if no external links found
|
||||
if (externalLinks.length === 0) {
|
||||
console.log('No external links found, skipping test');
|
||||
return;
|
||||
}
|
||||
|
||||
const brokenLinks = [];
|
||||
for (const link of externalLinks) {
|
||||
const href = await link.getAttribute('href');
|
||||
if (!href) continue;
|
||||
|
||||
try {
|
||||
const response = await request.head(href);
|
||||
if (response.status() >= 400) {
|
||||
brokenLinks.push({
|
||||
href,
|
||||
status: response.status()
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
brokenLinks.push({
|
||||
href,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (brokenLinks.length > 0) {
|
||||
console.log('\nBroken or inaccessible links:');
|
||||
brokenLinks.forEach(link => {
|
||||
if (link.error) {
|
||||
console.log(`- ${link.href}: ${link.error}`);
|
||||
} else {
|
||||
console.log(`- ${link.href}: HTTP ${link.status}`);
|
||||
}
|
||||
});
|
||||
throw new Error('Some external links are broken or inaccessible');
|
||||
}
|
||||
});
|
||||
|
||||
test('should have proper color contrast', async ({ page }) => {
|
||||
const url = await getPageUrl(page);
|
||||
console.log(`Running color contrast tests against ${url}`);
|
||||
await page.goto(url, { timeout: 60000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Check text color contrast in both light and dark modes
|
||||
const contrastInfo = await page.evaluate(() => {
|
||||
const getContrastRatio = (color1, color2) => {
|
||||
const getLuminance = (r, g, b) => {
|
||||
const [rs, gs, bs] = [r, g, b].map(c => {
|
||||
c = c / 255;
|
||||
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
||||
});
|
||||
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
|
||||
};
|
||||
|
||||
const parseColor = (color) => {
|
||||
const rgb = color.match(/\d+/g).map(Number);
|
||||
return rgb.length === 3 ? rgb : [0, 0, 0];
|
||||
};
|
||||
|
||||
const l1 = getLuminance(...parseColor(color1));
|
||||
const l2 = getLuminance(...parseColor(color2));
|
||||
const ratio = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
||||
return ratio.toFixed(2);
|
||||
};
|
||||
|
||||
const style = getComputedStyle(document.body);
|
||||
const textColor = style.color;
|
||||
const backgroundColor = style.backgroundColor;
|
||||
const contrastRatio = getContrastRatio(textColor, backgroundColor);
|
||||
|
||||
return {
|
||||
textColor,
|
||||
backgroundColor,
|
||||
contrastRatio: parseFloat(contrastRatio)
|
||||
};
|
||||
});
|
||||
|
||||
console.log('Color contrast information:', contrastInfo);
|
||||
|
||||
// WCAG 2.1 Level AAA requires a contrast ratio of at least 7:1 for normal text
|
||||
expect(contrastInfo.contrastRatio).toBeGreaterThanOrEqual(7);
|
||||
});
|
||||
|
||||
test('should have alt text for all images', async ({ page }) => {
|
||||
const url = await getPageUrl(page);
|
||||
console.log(`Running image alt text tests against ${url}`);
|
||||
await page.goto(url, { timeout: 60000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Get all images
|
||||
const images = await page.$$('img');
|
||||
|
||||
// Skip test if no images found
|
||||
if (images.length === 0) {
|
||||
console.log('No images found, skipping test');
|
||||
return;
|
||||
}
|
||||
|
||||
const missingAlt = [];
|
||||
for (const img of images) {
|
||||
const alt = await img.getAttribute('alt');
|
||||
const src = await img.getAttribute('src');
|
||||
|
||||
// Skip decorative images (empty alt is fine)
|
||||
const role = await img.getAttribute('role');
|
||||
if (role === 'presentation' || role === 'none') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!alt) {
|
||||
missingAlt.push(src);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingAlt.length > 0) {
|
||||
console.log('\nImages missing alt text:');
|
||||
missingAlt.forEach(src => console.log(`- ${src}`));
|
||||
throw new Error('Some images are missing alt text');
|
||||
}
|
||||
});
|
||||
});
|
|
@ -1,61 +0,0 @@
|
|||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Security Headers Tests', () => {
|
||||
test('should have all required security headers', async ({ page }) => {
|
||||
// Navigate to the page
|
||||
await page.goto('http://localhost:8080');
|
||||
|
||||
// Get response headers
|
||||
const response = await page.waitForResponse('http://localhost:8080');
|
||||
const headers = response.headers();
|
||||
|
||||
// Define required headers and their expected values
|
||||
const requiredHeaders = {
|
||||
'Content-Security-Policy': expect.stringContaining("default-src 'self'"),
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
'X-Frame-Options': 'DENY',
|
||||
'X-XSS-Protection': '1; mode=block',
|
||||
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
||||
'Permissions-Policy': expect.stringContaining('geolocation=()'),
|
||||
'Strict-Transport-Security': expect.stringContaining('max-age=31536000'),
|
||||
};
|
||||
|
||||
// Check each required header
|
||||
for (const [header, expectedValue] of Object.entries(requiredHeaders)) {
|
||||
const headerValue = headers[header.toLowerCase()];
|
||||
expect(headerValue).toBeDefined();
|
||||
if (typeof expectedValue === 'string') {
|
||||
expect(headerValue).toBe(expectedValue);
|
||||
} else {
|
||||
expect(headerValue).toMatch(expectedValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should have correct CSP directives with nonce and hash', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080');
|
||||
const response = await page.waitForResponse('http://localhost:8080');
|
||||
const headers = response.headers();
|
||||
const csp = headers['content-security-policy'];
|
||||
|
||||
// Check for essential CSP directives
|
||||
expect(csp).toContain("default-src 'self'");
|
||||
expect(csp).toContain("script-src 'self' 'nonce-");
|
||||
expect(csp).toContain("'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='");
|
||||
expect(csp).toContain("style-src 'self' 'unsafe-inline'");
|
||||
expect(csp).toContain("img-src 'self' data: https: http:");
|
||||
expect(csp).toContain("font-src 'self'");
|
||||
expect(csp).toContain("connect-src 'self'");
|
||||
});
|
||||
|
||||
test('should have nonce attributes on script tags', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080');
|
||||
|
||||
// Check that all script tags have nonce attributes
|
||||
const scripts = await page.$$('script');
|
||||
for (const script of scripts) {
|
||||
const hasNonce = await script.evaluate(el => el.hasAttribute('nonce'));
|
||||
expect(hasNonce).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
|
@ -1,85 +0,0 @@
|
|||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
const PRODUCTION_URL = 'https://colinknapp.com';
|
||||
const LOCAL_URL = 'http://localhost:8080';
|
||||
|
||||
async function getPageUrl(page) {
|
||||
try {
|
||||
// Try production first
|
||||
await page.goto(PRODUCTION_URL, { timeout: 60000 });
|
||||
return PRODUCTION_URL;
|
||||
} catch (error) {
|
||||
console.log('Production site not available, falling back to local');
|
||||
await page.goto(LOCAL_URL, { timeout: 60000 });
|
||||
return LOCAL_URL;
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('Security Headers Tests', () => {
|
||||
test('should have all required security headers', async ({ page, request }) => {
|
||||
const url = await getPageUrl(page);
|
||||
console.log(`Running security header tests against ${url}`);
|
||||
|
||||
// Get headers directly from the main page
|
||||
const response = await request.get(url);
|
||||
const headers = response.headers();
|
||||
|
||||
// Check Content Security Policy
|
||||
const csp = headers['content-security-policy'];
|
||||
expect(csp).toBeTruthy();
|
||||
expect(csp).toContain("default-src 'none'");
|
||||
expect(csp).toContain("script-src 'self'");
|
||||
expect(csp).toContain("style-src 'self'");
|
||||
expect(csp).toContain("img-src 'self' data:");
|
||||
expect(csp).toContain("font-src 'self' data:");
|
||||
expect(csp).toContain("connect-src 'self'");
|
||||
expect(csp).toContain("object-src 'none'");
|
||||
expect(csp).toContain("frame-ancestors 'none'");
|
||||
expect(csp).toContain("base-uri 'none'");
|
||||
expect(csp).toContain("form-action 'none'");
|
||||
|
||||
// Check other security headers
|
||||
expect(headers['x-content-type-options']).toBe('nosniff');
|
||||
expect(headers['x-frame-options']).toBe('DENY');
|
||||
expect(headers['referrer-policy']).toBe('strict-origin-when-cross-origin');
|
||||
});
|
||||
|
||||
test('should have correct Subresource Integrity (SRI) attributes', async ({ page }) => {
|
||||
const url = await getPageUrl(page);
|
||||
console.log(`Running SRI tests against ${url}`);
|
||||
await page.goto(url, { timeout: 60000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Check stylesheet
|
||||
const stylesheet = await page.locator('link[rel="stylesheet"]').first();
|
||||
const stylesheetIntegrity = await stylesheet.getAttribute('integrity');
|
||||
const stylesheetCrossorigin = await stylesheet.getAttribute('crossorigin');
|
||||
expect(stylesheetIntegrity).toBeTruthy();
|
||||
expect(stylesheetCrossorigin).toBe('anonymous');
|
||||
|
||||
// Check script
|
||||
const script = await page.locator('script[src]').first();
|
||||
const scriptIntegrity = await script.getAttribute('integrity');
|
||||
const scriptCrossorigin = await script.getAttribute('crossorigin');
|
||||
expect(scriptIntegrity).toBeTruthy();
|
||||
expect(scriptCrossorigin).toBe('anonymous');
|
||||
});
|
||||
|
||||
test('should have correct caching headers for static assets', async ({ request }) => {
|
||||
const url = await getPageUrl({ goto: async () => {} });
|
||||
console.log(`Running caching header tests against ${url}`);
|
||||
const baseUrl = url.replace(/\/$/, '');
|
||||
|
||||
// Check styles.css
|
||||
const stylesResponse = await request.get(`${baseUrl}/styles.css`);
|
||||
const stylesCacheControl = stylesResponse.headers()['cache-control'];
|
||||
expect(stylesCacheControl).toContain('public');
|
||||
expect(stylesCacheControl).toContain('max-age=');
|
||||
|
||||
// Check theme.js
|
||||
const scriptResponse = await request.get(`${baseUrl}/theme.js`);
|
||||
const scriptCacheControl = scriptResponse.headers()['cache-control'];
|
||||
expect(scriptCacheControl).toContain('public');
|
||||
expect(scriptCacheControl).toContain('max-age=');
|
||||
});
|
||||
});
|
|
@ -1,89 +0,0 @@
|
|||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Set threshold values for each category
|
||||
const THRESHOLDS = {
|
||||
performance: 90,
|
||||
accessibility: 90,
|
||||
'best-practices': 90,
|
||||
seo: 90,
|
||||
};
|
||||
|
||||
async function runLighthouse(url) {
|
||||
// Create directory for reports if it doesn't exist
|
||||
const reportsDir = path.join(__dirname, 'reports');
|
||||
if (!fs.existsSync(reportsDir)) {
|
||||
fs.mkdirSync(reportsDir);
|
||||
}
|
||||
|
||||
// Generate report filename
|
||||
const reportPath = path.join(reportsDir, `lighthouse-report-${Date.now()}.json`);
|
||||
|
||||
try {
|
||||
// Run Lighthouse using CLI
|
||||
const command = `npx lighthouse "${url}" --output=json --output-path="${reportPath}" --only-categories=performance,accessibility,best-practices,seo --form-factor=desktop --throttling-method=simulate --screenEmulation.mobile=false`;
|
||||
|
||||
execSync(command, { stdio: 'inherit' });
|
||||
|
||||
// Read and parse the report
|
||||
const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
|
||||
|
||||
console.log('\nLighthouse Results:');
|
||||
console.log('--------------------');
|
||||
|
||||
// Process and display results
|
||||
let allPassed = true;
|
||||
Object.keys(THRESHOLDS).forEach(category => {
|
||||
const score = Math.round(report.categories[category].score * 100);
|
||||
const threshold = THRESHOLDS[category];
|
||||
const passed = score >= threshold;
|
||||
|
||||
if (!passed) {
|
||||
allPassed = false;
|
||||
}
|
||||
|
||||
console.log(`${category}: ${score}/100 - ${passed ? 'PASS' : 'FAIL'} (Threshold: ${threshold})`);
|
||||
});
|
||||
|
||||
// Display audits that failed
|
||||
console.log('\nFailed Audits:');
|
||||
console.log('--------------');
|
||||
let hasFailedAudits = false;
|
||||
|
||||
Object.values(report.audits).forEach(audit => {
|
||||
if (audit.score !== null && audit.score < 0.9) {
|
||||
hasFailedAudits = true;
|
||||
console.log(`- ${audit.title}: ${Math.round(audit.score * 100)}/100`);
|
||||
console.log(` ${audit.description}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasFailedAudits) {
|
||||
console.log('No significant audit failures!');
|
||||
}
|
||||
|
||||
console.log(`\nDetailed report saved to: ${reportPath}`);
|
||||
|
||||
return allPassed;
|
||||
} catch (error) {
|
||||
console.error('Error running Lighthouse:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// URL to test - this should be your local server address
|
||||
const url = process.argv[2] || 'http://localhost:8080';
|
||||
|
||||
// Start the tests
|
||||
console.log(`Starting Lighthouse tests on ${url}`);
|
||||
|
||||
runLighthouse(url)
|
||||
.then(passed => {
|
||||
console.log(`\nOverall: ${passed ? 'PASSED' : 'FAILED'}`);
|
||||
process.exit(passed ? 0 : 1);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error running Lighthouse:', error);
|
||||
process.exit(1);
|
||||
});
|
|
@ -1,73 +0,0 @@
|
|||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const url = require('url');
|
||||
|
||||
const PORT = 8080;
|
||||
const RESUME_DIR = path.join(__dirname, '..', 'docker', 'resume');
|
||||
|
||||
// MIME types for common file extensions
|
||||
const MIME_TYPES = {
|
||||
'.html': 'text/html',
|
||||
'.css': 'text/css',
|
||||
'.js': 'text/javascript',
|
||||
'.json': 'application/json',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.gif': 'image/gif',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.ico': 'image/x-icon',
|
||||
'.txt': 'text/plain',
|
||||
};
|
||||
|
||||
// Create a simple HTTP server
|
||||
const server = http.createServer((req, res) => {
|
||||
// Parse the request URL
|
||||
const parsedUrl = url.parse(req.url);
|
||||
let pathname = parsedUrl.pathname;
|
||||
|
||||
// Set default file to index.html
|
||||
if (pathname === '/') {
|
||||
pathname = '/index.html';
|
||||
}
|
||||
|
||||
// Construct the file path
|
||||
const filePath = path.join(RESUME_DIR, pathname);
|
||||
|
||||
// Get the file extension
|
||||
const ext = path.extname(filePath);
|
||||
|
||||
// Set the content type based on the file extension
|
||||
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
||||
|
||||
// Read the file and serve it
|
||||
fs.readFile(filePath, (err, data) => {
|
||||
if (err) {
|
||||
// If the file doesn't exist, return 404
|
||||
if (err.code === 'ENOENT') {
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||
res.end('404 Not Found');
|
||||
console.log(`404: ${pathname}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// For other errors, return 500
|
||||
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
||||
res.end('500 Internal Server Error');
|
||||
console.error(`Error serving ${pathname}:`, err);
|
||||
return;
|
||||
}
|
||||
|
||||
// If file is found, serve it with the correct content type
|
||||
res.writeHead(200, { 'Content-Type': contentType });
|
||||
res.end(data);
|
||||
console.log(`200: ${pathname}`);
|
||||
});
|
||||
});
|
||||
|
||||
// Start the server
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Server running at http://localhost:${PORT}/`);
|
||||
console.log(`Serving files from: ${RESUME_DIR}`);
|
||||
});
|
|
@ -1,60 +0,0 @@
|
|||
const express = require('express');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const app = express();
|
||||
const port = 8080;
|
||||
|
||||
// Generate a random nonce
|
||||
function generateNonce() {
|
||||
return crypto.randomBytes(16).toString('base64');
|
||||
}
|
||||
|
||||
// Security headers middleware
|
||||
app.use((req, res, next) => {
|
||||
const nonce = generateNonce();
|
||||
res.locals.nonce = nonce;
|
||||
|
||||
// Content Security Policy with nonce and hash
|
||||
res.setHeader(
|
||||
'Content-Security-Policy',
|
||||
"default-src 'self'; " +
|
||||
`script-src 'self' 'nonce-${nonce}' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; ` +
|
||||
"style-src 'self' 'unsafe-inline'; " +
|
||||
"img-src 'self' data: https: http:; " +
|
||||
"font-src 'self'; " +
|
||||
"connect-src 'self'"
|
||||
);
|
||||
|
||||
// Other security headers
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
res.setHeader('X-Frame-Options', 'DENY');
|
||||
res.setHeader('X-XSS-Protection', '1; mode=block');
|
||||
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
|
||||
// res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// Custom middleware to inject nonce into HTML
|
||||
app.use((req, res, next) => {
|
||||
if (req.path.endsWith('.html')) {
|
||||
const filePath = path.join(__dirname, '../docker/resume', req.path);
|
||||
let html = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Add nonce to all script tags
|
||||
html = html.replace(/<script/g, `<script nonce="${res.locals.nonce}"`);
|
||||
|
||||
res.send(html);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
// Serve static files from the docker/resume directory
|
||||
app.use(express.static(path.join(__dirname, '../docker/resume')));
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Local development server running at http://localhost:${port}`);
|
||||
});
|
|
@ -1,62 +0,0 @@
|
|||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Theme Toggle Tests', () => {
|
||||
test('theme toggle should cycle through modes', async ({ page }) => {
|
||||
// Serve the site locally for testing
|
||||
await page.goto('http://localhost:8080');
|
||||
|
||||
// Get the theme toggle button
|
||||
const themeToggle = await page.locator('#themeToggle');
|
||||
|
||||
// Verify initial state (should be auto)
|
||||
let ariaLabel = await themeToggle.getAttribute('aria-label');
|
||||
expect(ariaLabel).toBe('Theme mode: Auto');
|
||||
|
||||
// Click to change to light mode
|
||||
await themeToggle.click();
|
||||
ariaLabel = await themeToggle.getAttribute('aria-label');
|
||||
expect(ariaLabel).toBe('Theme mode: Light');
|
||||
|
||||
// Verify data-theme attribute is set to light
|
||||
const dataTheme = await page.evaluate(() => document.documentElement.getAttribute('data-theme'));
|
||||
expect(dataTheme).toBe('light');
|
||||
|
||||
// Click to change to dark mode
|
||||
await themeToggle.click();
|
||||
ariaLabel = await themeToggle.getAttribute('aria-label');
|
||||
expect(ariaLabel).toBe('Theme mode: Dark');
|
||||
|
||||
// Verify data-theme attribute is set to dark
|
||||
const darkDataTheme = await page.evaluate(() => document.documentElement.getAttribute('data-theme'));
|
||||
expect(darkDataTheme).toBe('dark');
|
||||
|
||||
// Click to change back to auto mode
|
||||
await themeToggle.click();
|
||||
ariaLabel = await themeToggle.getAttribute('aria-label');
|
||||
expect(ariaLabel).toBe('Theme mode: Auto');
|
||||
|
||||
// Verify data-theme attribute is removed
|
||||
const autoDataTheme = await page.evaluate(() => document.documentElement.getAttribute('data-theme'));
|
||||
expect(autoDataTheme).toBeNull();
|
||||
});
|
||||
|
||||
test('theme toggle should be keyboard accessible', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080');
|
||||
|
||||
// Focus the theme toggle button using Tab
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
// Verify the button is focused
|
||||
const isFocused = await page.evaluate(() => {
|
||||
return document.activeElement.id === 'themeToggle';
|
||||
});
|
||||
expect(isFocused).toBeTruthy();
|
||||
|
||||
// Activate with space key
|
||||
await page.keyboard.press('Space');
|
||||
|
||||
// Verify it changes to light mode
|
||||
const ariaLabel = await page.locator('#themeToggle').getAttribute('aria-label');
|
||||
expect(ariaLabel).toBe('Theme mode: Light');
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue