Initial commit

This commit is contained in:
Andrew Lee 2024-12-24 13:56:06 -05:00
commit 4811a3eabc
Signed by: andrew
SSH key fingerprint: SHA256:bbGg1DYG5CuKl2jo1DqzvUsaTeyvhM3tjCsej5lYMg4
16 changed files with 641 additions and 0 deletions

175
.gitignore vendored Normal file
View file

@ -0,0 +1,175 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Caches
.cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

16
README.md Normal file
View file

@ -0,0 +1,16 @@
# bnbso-auth
Web authentication for bnbSO (registering accounts, resetting passwords, changing passwords) using Discord authentication. Programmed for FreeSO-based servers.
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.1.38. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

BIN
bun.lockb Executable file

Binary file not shown.

140
index.js Normal file
View file

@ -0,0 +1,140 @@
import express from "express";
import session from "express-session";
import multer from "multer";
import passport from "passport";
import { Strategy as DiscordStrategy } from "passport-discord";
import path from "path";
import { fileURLToPath } from 'url';
import dotenv from "dotenv";
import axios from "axios";
import FormData from "form-data";
import fs from "fs";
dotenv.config();
// Load error messages from JSON file
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const statusMessages = JSON.parse(fs.readFileSync(path.join(__dirname, 'status.json'), 'utf8'));
const upload = multer();
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.set('view engine', 'ejs');
app.use(express.static(path.join(__dirname, 'public')));
app.set('views', path.join(__dirname, 'views'));
// Passport session setup
passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((obj, done) => done(null, obj));
// Configure Passport Discord strategy
passport.use(
new DiscordStrategy(
{
clientID: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
callbackURL: process.env.REDIRECT_URI,
scope: ["identify", "email", "guilds"],
},
(accessToken, refreshToken, profile, done) => {
return done(null, profile);
}
)
);
// Middleware
app.use(
session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
})
);
app.use(passport.initialize());
app.use(passport.session());
// Routes
app.get("/", async (req, res) => {
if (req.isAuthenticated()) {
const { id, username, email, guilds } = req.user;
const isInGuild = guilds.some((guild) => guild.id === process.env.GUILD_ID);
if (isInGuild) {
let userExists = true;
if (userExists) {
return res.render('login');
} else {
return res.render('register', req.user);
}
} else {
return res.render('error', { error: 'You must be a member of the bits & Bytes server to access this page.' });
}
} else {
res.render('index');
}
});
app.post("/register", upload.none(), async (req, res) => {
if (req.isAuthenticated()) {
const { id } = req.user;
const { username, email, password, password2 } = req.body;
if (password !== password2) {
return res.render('register', { ...req.user, error: "Passwords do not match" });
}
try {
const form = new FormData();
form.append('username', username);
form.append('email', email);
form.append('password', password);
const response = await axios.post(`${process.env.API_URL}/userapi/registration`, form, {
headers: form.getHeaders()
});
if (response.data.error) {
const errorKey = response.data.error_description || "default";
const errorMessage = statusMessages.registration_errors[errorKey] || "Something went wrong";
return res.render('register', { ...req.user, error: errorMessage });
} else {
console.log(`Discord ID: ${id}`)
return res.render('success');
}
} catch (error) {
console.error("Error during registration:", error);
return res.render('register', { ...req.user, error: "An error occurred during registration" });
}
} else {
res.redirect("/");
}
});
app.get(
"/auth/discord",
passport.authenticate("discord", { scope: ["identify", "email", "guilds"] })
);
app.get(
"/callback",
passport.authenticate("discord", { failureRedirect: "/" }),
(req, res) => {
res.redirect("/");
}
);
app.get("/logout", (req, res) => {
req.logout((err) => {
if (err) return next(err);
res.redirect("/");
});
});
const port = 3000;
app.listen(PORT, () => console.log(`Server running on http://localhost:${port}`));

24
package.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "bnbso-auth",
"module": "index.ts",
"type": "module",
"devDependencies": {
"@types/bun": "latest",
"tailwindcss": "^3.4.17"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"axios": "^1.7.9",
"dotenv": "^16.4.7",
"ejs": "^3.1.10",
"express": "^4.21.2",
"express-session": "^1.18.1",
"form-data": "^4.0.1",
"multer": "^1.4.5-lts.1",
"passport": "^0.7.0",
"passport-discord": "^0.1.4",
"sqlite3": "^5.1.7"
}
}

112
public/css/style.css Normal file
View file

@ -0,0 +1,112 @@
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap');
html, body {
height: 100%;
margin: 0;
padding: 0;
}
body {
display: flex;
font-family: "Open Sans", sans-serif;
justify-content: center;
align-items: center;
flex-direction: column;
}
a {
text-decoration: none;
color: #77a6ff;
transition: 0.2s;;
}
a:hover {
text-decoration: none;
color: #a5c4ff;
}
a:active {
text-decoration: none;
color: #2773ff;
}
.discord {
display: inline-block;
text-decoration: none;
color: #fff;
background-color: #5865F2;
padding: 10px 20px;
border-radius: 5px;
transition: 0.2s;
margin-top: 10px;
}
.discord:hover {
background-color: #727dee;
}
.discord:active {
background-color: #4e5bd1;
}
.container {
padding: 3em;
text-align: center;
background-color: #313131;
color: #ffffff;
border-radius: 7px;
}
.background {
position: absolute;
width: 100%;
height: 100%;
background-image: url('/img/background.webp');
background-size: cover;
z-index: -1;
}
.background::before {
content: "";
position: absolute;
width: 100%;
height: 100vh;
backdrop-filter: blur(5px);
}
form {
display: flex;
flex-direction: column;
}
form label,
form input,
form button {
margin-bottom: 5px;
}
input[type=text],
input[type=email],
input[type=password] {
width: 100%;
padding: 12px 20px;
margin: 8px 0;
box-sizing: border-box;
}
button {
background-color: #00633a;
color: white;
padding: 14px 20px;
margin: 8px 0;
border: none;
cursor: pointer;
border-radius: 5px;
transition: 0.2s;
}
.error {
color: rgb(248, 140, 140);
font-size: 1.5em;
}

BIN
public/img/background.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 KiB

BIN
public/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

29
status.json Normal file
View file

@ -0,0 +1,29 @@
{
"registration_errors": {
"missing_confirmation_token": "Registration failed: Missing confirmation token.",
"user_short": "Registration failed: Username is too short.",
"user_long": "Registration failed: Username is too long.",
"user_invalid": "Registration failed: Invalid username.",
"pass_required": "Registration failed: Password is required.",
"email_invalid": "Registration failed: Invalid email address.",
"ip_banned": "Registration failed: IP is banned.",
"registrations_too_frequent": "Registration failed: Too many registrations from this IP address.",
"user_exists": "Registration failed: User already exists.",
"smtp_disabled": "Registration failed: SMTP service is disabled.",
"email_taken": "Registration failed: Email address is already taken.",
"confirmation_pending": "Registration failed: Confirmation pending.",
"key_wrong": "Registration failed: Invalid registration key."
},
"password_reset_errors": {
"missing_fields": "Password reset failed: Missing required fields.",
"email_invalid": "Password reset failed: Invalid email address.",
"user_invalid": "Password reset failed: User does not exist.",
"incorrect_password": "Password reset failed: Incorrect password.",
"invalid_token": "Password reset failed: Invalid confirmation token."
},
"success_responses": {
"success": "Operation was successful.",
"email_failed": "Email sending failed."
}
}

9
tailwind.config.js Normal file
View file

@ -0,0 +1,9 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./views/**/*.{html,js,ejs}"],
theme: {
extend: {},
},
plugins: [],
}

27
tsconfig.json Normal file
View file

@ -0,0 +1,27 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}

17
views/error.ejs Normal file
View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/css/style.css">
<title>bnbSO</title>
</head>
<body>
<div class="background"></div>
<div class="container">
<img src="img/logo.png" alt="logo" width="200">
<h1>Oh no! Something went wrong!</h1>
<p><%= error %></p>
</div>
</body>
</html>

17
views/index.ejs Normal file
View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/css/style.css">
<title>bnbSO</title>
</head>
<body>
<div class="background"></div>
<div class="container">
<img src="img/logo.png" alt="logo" width="200">
<p>You must be a member of the bits & Bytes Discord server to join bnbSO.</p>
<a class="discord" href="/auth/discord">Login with Discord</a>
</div>
</body>
</html>

26
views/login.ejs Normal file
View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/css/style.css">
<title>bnbSO Login</title>
</head>
<body>
<div class="background"></div>
<div class="container">
<img src="img/logo.png" alt="logo" width="200">
<h1>Login</h1>
<form method="post" action="/login">
<label for="username">Username:</label>
<input type="text" id="username" name="username">
<label for="password">Password:</label>
<input type="password" id="password" name="password">
<button type="submit">Login</button>
</form>
<a href="#">Forgot Password</a>
<% if (typeof error !== 'undefined') { %>
<div class="error"><%= error %></div>
<% } %>
</body>
</html>

31
views/register.ejs Normal file
View file

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/css/style.css">
<title>bnbSO Register</title>
</head>
<body>
<div class="background"></div>
<div class="container">
<img src="img/logo.png" alt="logo" width="200">
<h1>Welcome to bnbSO!</h1>
<p>You will be sending the following information to register your bnbSO account</p>
<p>Please verify that the following information is correct. You can only change this <b>once</b>.</p>
<form method="post" action="/register">
<label for="username">Username:</label>
<input type="text" id="username" name="username" value="<%= username %>">
<label for="email">Email:</label>
<input type="email" id="email" name="email" value="<%= email %>">
<label for="password">Password:</label>
<input type="password" id="password" name="password">
<label for="password2">Confirm Password:</label>
<input type="password" id="password2" name="password2">
<button type="submit">Register</button>
</form>
<% if (typeof error !== 'undefined') { %>
<div class="error"><%= error %></div>
<% } %>
</body>
</html>

18
views/success.ejs Normal file
View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/css/style.css">
<title>bnbSO</title>
</head>
<body>
<div class="background"></div>
<div class="container">
<img src="img/logo.png" alt="logo" width="200">
<p>Created account successfully!</p>
<p>Check on your inbox for the confirmation code.</p>
<a href="/">Home</a>
</div>
</body>
</html>