aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Lee <andrew@alee14.me>2025-01-23 15:52:49 -0500
committerAndrew Lee <andrew@alee14.me>2025-01-23 15:52:49 -0500
commita1b29429b53f0c97a014403534312596da0f4cc1 (patch)
tree8d99d064fa65ddb0200cb763a4c7314f8730f46f
downloadbnbaim-auth-a1b29429b53f0c97a014403534312596da0f4cc1.tar.gz
bnbaim-auth-a1b29429b53f0c97a014403534312596da0f4cc1.tar.bz2
bnbaim-auth-a1b29429b53f0c97a014403534312596da0f4cc1.zip
Initial commit
-rw-r--r--.gitignore179
-rw-r--r--README.md19
-rwxr-xr-xbun.lockbbin0 -> 91866 bytes
-rw-r--r--index.js209
-rw-r--r--package.json23
-rw-r--r--public/css/style.css156
-rw-r--r--public/img/background.webpbin0 -> 657984 bytes
-rw-r--r--public/img/logo.pngbin0 -> 13443 bytes
-rw-r--r--tsconfig.json27
-rw-r--r--views/dashboard.ejs18
-rw-r--r--views/error.ejs16
-rw-r--r--views/index.ejs18
-rw-r--r--views/password.ejs26
-rw-r--r--views/register.ejs29
-rw-r--r--views/success.ejs17
15 files changed, 737 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a51afe2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,179 @@
+# 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
+
+*.db
+
+.idea/ \ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f60e483
--- /dev/null
+++ b/README.md
@@ -0,0 +1,19 @@
+# bnbaim-discord-auth
+Web authentication for AIM (registering accounts, changing passwords) using Discord authentication.
+
+To install dependencies:
+
+```bash
+bun install
+```
+
+To run:
+
+```bash
+bun run index.js
+```
+
+# How this works?
+The website requires to log into Discord, and it checks if the user is on a specific server (as defined on the .env file). Then it checks if the user is registered on a local database, if not then it prompts the user to register a FreeSO account. After, once the user registers the account, it makes a POST request to `/userapi/registration`. Otherwise, if the user is registered, then it redirects the user to the dashboard which has options to change the password and download the client.
+
+This project was created using `bun init` in bun v1.1.38. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
diff --git a/bun.lockb b/bun.lockb
new file mode 100755
index 0000000..fd95491
--- /dev/null
+++ b/bun.lockb
Binary files differ
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..d127d85
--- /dev/null
+++ b/index.js
@@ -0,0 +1,209 @@
+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 sqlite3 from 'sqlite3';
+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 db = new sqlite3.Database('./database.db');
+
+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'));
+
+db.run(`CREATE TABLE IF NOT EXISTS users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ discord_id TEXT NOT NULL,
+ aim_username TEXT NOT NULL
+)`);
+
+// 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", "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, guilds } = req.user;
+ const isInGuild = guilds.some((guild) => guild.id === process.env.GUILD_ID);
+
+ if (isInGuild) {
+ db.get(`SELECT * FROM users WHERE discord_id = ?`, [id], (err, row) => {
+ if (err) {
+ console.error("Error querying the database:", err);
+ return res.render('error', { error: 'An error occurred while checking user data.' });
+ }
+
+ if (row) {
+ return res.render('dashboard', { ...req.user, aim_username: row.aim_username, serverName: process.env.SERVER_NAME || 'FreeSO' });
+ } else {
+ return res.render('register', req.user);
+ }
+ });
+ } else {
+ return res.render('error', { error: 'You must be a member of that server to access this page.' });
+ }
+ } else {
+ res.render('index', { serverName: process.env.SERVER_NAME || 'AIM', discordName: process.env.DISCORD_NAME || 'Discord' });
+ }
+});
+
+app.post("/register", async (req, res) => {
+ if (req.isAuthenticated()) {
+ const { id } = req.user;
+ const { username, password, passwordconfirm } = req.body;
+
+ if (password !== passwordconfirm) {
+ return res.render('register', { ...req.user, error: "Passwords do not match" });
+ }
+
+ try {
+ const response = await axios.post(`${process.env.API_URL}/user`, {
+ screen_name: username,
+ password: password
+ });
+
+ if (response.data.error) {
+ const errorMessage = response.data;
+ return res.render('register', { ...req.user, error: errorMessage });
+ } else {
+ db.run(`INSERT INTO users (discord_id, aim_username) VALUES (?, ?)`, [id, username], function(err) {
+ if (err) {
+ console.error("Error inserting user data into database:", err);
+ return res.render('register', { ...req.user, error: "An error occurred during registration, contact server operator." });
+ }
+ return res.render('success', { ...req.user, success: "Created account successfully!"});
+ });
+ }
+ } catch (error) {
+ if (error.response && error.response.status === 409) {
+ const errorMessage = error.response.data;
+ return res.render('register', { ...req.user, error: errorMessage });
+ } else {
+ console.error("Error during registration:", error);
+ return res.render('register', { ...req.user, error: "An error occurred during registration, contact server operator." });
+ }
+ }
+ } else {
+ res.status(401).send("Unauthorized.");
+ }
+});
+
+app.get('/password', (req, res) => {
+ if (req.isAuthenticated()) {
+ res.render('password');
+ } else {
+ res.redirect("/login");
+ }
+});
+
+app.post('/password/change', async (req, res) => {
+ if (req.isAuthenticated()) {
+ const { id } = req.user;
+ const { newpassword, newpassword2 } = req.body;
+
+ if (newpassword !== newpassword2) {
+ return res.render('password', { ...req.user, error: "Passwords do not match" });
+ }
+
+ try {
+ db.get(`SELECT * FROM users WHERE discord_id = ?`, [id], async (err, row) => {
+ if (err) {
+ console.error("Error querying the database:", err);
+ return res.render('password', {...req.user, error: "An error occurred while checking user data."});
+ }
+
+ if (row) {
+ const form = new FormData();
+ form.append('username', row.aim_username);
+ form.append('new_password', newpassword);
+
+ const response = await axios.put(`${process.env.API_URL}/user/password`, {
+ screen_name: row.aim_username,
+ password: newpassword
+ });
+
+ if (response.data.error) {
+ const errorMessage = response.data;
+
+ return res.render('password', { ...req.user, error: errorMessage });
+ } else {
+ return res.render('success', { ...req.user, success: "Password changed successfully!" });
+ }
+ }
+ });
+ } catch (error) {
+ console.error("Error during password change:", error);
+ return res.render('password', { ...req.user, error: "An error occurred during password change, contact server operator." });
+ }
+ } else {
+ res.status(401).send("Unauthorized.");
+ }
+});
+
+app.get(
+ "/login",
+ passport.authenticate("discord", { scope: ["identify", "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}`));
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..9cdf7b8
--- /dev/null
+++ b/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "bnbaim-auth",
+ "module": "index.js",
+ "type": "module",
+ "scripts": {
+ "start": "node index.js"
+ },
+ "devDependencies": {
+ "@types/bun": "latest"
+ },
+ "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"
+ }
+}
diff --git a/public/css/style.css b/public/css/style.css
new file mode 100644
index 0000000..fcf210f
--- /dev/null
+++ b/public/css/style.css
@@ -0,0 +1,156 @@
+@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;
+}
+
+.button {
+ display: inline-block;
+ text-decoration: none;
+ color: #fff;
+ background-color: #535353;
+ padding: 10px 20px;
+ border-radius: 5px;
+ transition: 0.2s;
+ margin-top: 10px;
+
+}
+
+.button:hover {
+ background-color: #8a8a8a;
+ color: #fff;
+}
+
+.button:active {
+ background-color: #454545;
+ color: #fff;
+}
+
+.logout {
+ background-color: #630000;
+
+}
+
+.logout:hover {
+ background-color: #990000;
+}
+
+.logout:active {
+ background-color: #360000;
+}
+
+
+
+.discord {
+ background-color: #5865F2;
+
+}
+
+.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;
+ font-size: 1em;
+ color: white;
+ padding: 14px 20px;
+ margin: 8px 0;
+ border: none;
+ cursor: pointer;
+ border-radius: 5px;
+ transition: 0.2s;
+}
+
+button:hover {
+ background-color: #008c4a;
+}
+
+button:active {
+ background-color: #008c4a;
+}
+
+.error {
+ color: #f88c8c;
+ font-size: 1.5em;
+}
+
+.success {
+ color: #a3f88c;
+ font-size: 1.5em;
+}
diff --git a/public/img/background.webp b/public/img/background.webp
new file mode 100644
index 0000000..4946e1e
--- /dev/null
+++ b/public/img/background.webp
Binary files differ
diff --git a/public/img/logo.png b/public/img/logo.png
new file mode 100644
index 0000000..15498d0
--- /dev/null
+++ b/public/img/logo.png
Binary files differ
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..238655f
--- /dev/null
+++ b/tsconfig.json
@@ -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
+ }
+}
diff --git a/views/dashboard.ejs b/views/dashboard.ejs
new file mode 100644
index 0000000..b4dec17
--- /dev/null
+++ b/views/dashboard.ejs
@@ -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>bnbAIM Dashboard</title>
+</head>
+<body>
+ <div class="background"></div>
+ <div class="container">
+ <h1>Welcome, <%= username %>!</h1>
+ <h2>Screen Name: <%= aim_username %></h2>
+ <a href="/password" class="button">Change Password</a>
+ <a href="/logout" class="button logout">Logout</a>
+ </div>
+</body>
+</html>
diff --git a/views/error.ejs b/views/error.ejs
new file mode 100644
index 0000000..d273621
--- /dev/null
+++ b/views/error.ejs
@@ -0,0 +1,16 @@
+<!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>bnbAIM</title>
+</head>
+<body>
+ <div class="background"></div>
+ <div class="container">
+ <h1>Oh no! Something went wrong!</h1>
+ <p><%= error %></p>
+ </div>
+</body>
+</html>
diff --git a/views/index.ejs b/views/index.ejs
new file mode 100644
index 0000000..9e9e551
--- /dev/null
+++ b/views/index.ejs
@@ -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>bnbAIM</title>
+</head>
+<body>
+ <div class="background"></div>
+ <div class="container">
+ <img src="/img/logo.png" alt="logo" width="100">
+ <p>Log into your Discord account to get access to <%= serverName %>.</p>
+ <p><i>You must be a <%= discordName %> member.</i></p>
+ <a class="button discord" href="/login">Login with Discord</a>
+ </div>
+</body>
+</html>
diff --git a/views/password.ejs b/views/password.ejs
new file mode 100644
index 0000000..94f42f1
--- /dev/null
+++ b/views/password.ejs
@@ -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>bnbAIM Change Password</title>
+</head>
+<body>
+ <div class="background"></div>
+ <div class="container">
+ <h1>Change Password</h1>
+ <form method="post" action="/password/change">
+ <label for="newpassword">New Password:</label>
+ <input type="password" id="newpassword" name="newpassword">
+ <label for="newpassword2">Confirm New Password:</label>
+ <input type="password" id="newpassword2" name="newpassword2">
+ <button type="submit">Change Password</button>
+ </form>
+ <a href="/" class="button">Dashboard</a>
+ <% if (typeof error !== 'undefined') { %>
+ <div class="error"><%= error %></div>
+ <% } %>
+ </div>
+</body>
+</html>
diff --git a/views/register.ejs b/views/register.ejs
new file mode 100644
index 0000000..6187031
--- /dev/null
+++ b/views/register.ejs
@@ -0,0 +1,29 @@
+<!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>bnbAIM Register</title>
+</head>
+<body>
+ <div class="background"></div>
+ <div class="container">
+ <img src="/img/logo.png" alt="logo" width="100">
+ <h1>Welcome to bnbAIM!</h1>
+ <p>You will be sending the following information to register your bnbAIM account</p>
+ <form method="post" action="/register">
+ <label for="username">Screen Name:</label>
+ <input type="text" id="username" name="username">
+ <label for="password">Password:</label>
+ <input type="password" id="password" name="password">
+ <label for="passwordconfirm">Confirm Password:</label>
+ <input type="password" id="passwordconfirm" name="passwordconfirm">
+ <button type="submit">Register</button>
+ </form>
+ <% if (typeof error !== 'undefined') { %>
+ <div class="error"><%= error %></div>
+ <% } %>
+ </div>
+</body>
+</html>
diff --git a/views/success.ejs b/views/success.ejs
new file mode 100644
index 0000000..520f177
--- /dev/null
+++ b/views/success.ejs
@@ -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>bnbAIM</title>
+</head>
+<body>
+ <div class="background"></div>
+ <div class="container">
+ <h1>Success!</h1>
+ <p class="success"><%= success %></p>
+ <a href="/">Dashboard</a>
+ </div>
+</body>
+</html>