diff options
| author | Andrew Lee <andrew@alee14.me> | 2025-01-23 15:52:49 -0500 |
|---|---|---|
| committer | Andrew Lee <andrew@alee14.me> | 2025-01-23 15:52:49 -0500 |
| commit | a1b29429b53f0c97a014403534312596da0f4cc1 (patch) | |
| tree | 8d99d064fa65ddb0200cb763a4c7314f8730f46f | |
| download | bnbaim-auth-a1b29429b53f0c97a014403534312596da0f4cc1.tar.gz bnbaim-auth-a1b29429b53f0c97a014403534312596da0f4cc1.tar.bz2 bnbaim-auth-a1b29429b53f0c97a014403534312596da0f4cc1.zip | |
Initial commit
| -rw-r--r-- | .gitignore | 179 | ||||
| -rw-r--r-- | README.md | 19 | ||||
| -rwxr-xr-x | bun.lockb | bin | 0 -> 91866 bytes | |||
| -rw-r--r-- | index.js | 209 | ||||
| -rw-r--r-- | package.json | 23 | ||||
| -rw-r--r-- | public/css/style.css | 156 | ||||
| -rw-r--r-- | public/img/background.webp | bin | 0 -> 657984 bytes | |||
| -rw-r--r-- | public/img/logo.png | bin | 0 -> 13443 bytes | |||
| -rw-r--r-- | tsconfig.json | 27 | ||||
| -rw-r--r-- | views/dashboard.ejs | 18 | ||||
| -rw-r--r-- | views/error.ejs | 16 | ||||
| -rw-r--r-- | views/index.ejs | 18 | ||||
| -rw-r--r-- | views/password.ejs | 26 | ||||
| -rw-r--r-- | views/register.ejs | 29 | ||||
| -rw-r--r-- | views/success.ejs | 17 |
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 Binary files differnew file mode 100755 index 0000000..fd95491 --- /dev/null +++ b/bun.lockb 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 Binary files differnew file mode 100644 index 0000000..4946e1e --- /dev/null +++ b/public/img/background.webp diff --git a/public/img/logo.png b/public/img/logo.png Binary files differnew file mode 100644 index 0000000..15498d0 --- /dev/null +++ b/public/img/logo.png 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> |
