diff options
| author | Andrew Lee <andrew@alee14.me> | 2024-12-24 13:56:06 -0500 |
|---|---|---|
| committer | Andrew Lee <andrew@alee14.me> | 2024-12-24 13:56:06 -0500 |
| commit | 4811a3eabc1e7afaf17b45f9dea541d987c45717 (patch) | |
| tree | 7368782062805aa60dcbeeb3377c35076658decc | |
| download | freeso-discord-auth-4811a3eabc1e7afaf17b45f9dea541d987c45717.tar.gz freeso-discord-auth-4811a3eabc1e7afaf17b45f9dea541d987c45717.tar.bz2 freeso-discord-auth-4811a3eabc1e7afaf17b45f9dea541d987c45717.zip | |
Initial commit
| -rw-r--r-- | .gitignore | 175 | ||||
| -rw-r--r-- | README.md | 16 | ||||
| -rwxr-xr-x | bun.lockb | bin | 0 -> 125899 bytes | |||
| -rw-r--r-- | index.js | 140 | ||||
| -rw-r--r-- | package.json | 24 | ||||
| -rw-r--r-- | public/css/style.css | 112 | ||||
| -rw-r--r-- | public/img/background.webp | bin | 0 -> 657984 bytes | |||
| -rw-r--r-- | public/img/logo.png | bin | 0 -> 27658 bytes | |||
| -rw-r--r-- | status.json | 29 | ||||
| -rw-r--r-- | tailwind.config.js | 9 | ||||
| -rw-r--r-- | tsconfig.json | 27 | ||||
| -rw-r--r-- | views/error.ejs | 17 | ||||
| -rw-r--r-- | views/index.ejs | 17 | ||||
| -rw-r--r-- | views/login.ejs | 26 | ||||
| -rw-r--r-- | views/register.ejs | 31 | ||||
| -rw-r--r-- | views/success.ejs | 18 |
16 files changed, 641 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9b1ee42 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..386f684 --- /dev/null +++ b/README.md @@ -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. diff --git a/bun.lockb b/bun.lockb Binary files differnew file mode 100755 index 0000000..43e8e7a --- /dev/null +++ b/bun.lockb diff --git a/index.js b/index.js new file mode 100644 index 0000000..c109ab2 --- /dev/null +++ b/index.js @@ -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}`)); diff --git a/package.json b/package.json new file mode 100644 index 0000000..8027b21 --- /dev/null +++ b/package.json @@ -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" + } +}
\ No newline at end of file diff --git a/public/css/style.css b/public/css/style.css new file mode 100644 index 0000000..31d3530 --- /dev/null +++ b/public/css/style.css @@ -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; +}
\ No newline at end of file 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..b98c6ed --- /dev/null +++ b/public/img/logo.png diff --git a/status.json b/status.json new file mode 100644 index 0000000..eb1f9e9 --- /dev/null +++ b/status.json @@ -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." + } + } +
\ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..ddd5cd3 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,9 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ["./views/**/*.{html,js,ejs}"], + theme: { + extend: {}, + }, + plugins: [], +} + 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/error.ejs b/views/error.ejs new file mode 100644 index 0000000..dc138e6 --- /dev/null +++ b/views/error.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>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>
\ No newline at end of file diff --git a/views/index.ejs b/views/index.ejs new file mode 100644 index 0000000..d46a61a --- /dev/null +++ b/views/index.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>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>
\ No newline at end of file diff --git a/views/login.ejs b/views/login.ejs new file mode 100644 index 0000000..a5a51ae --- /dev/null +++ b/views/login.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>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>
\ No newline at end of file diff --git a/views/register.ejs b/views/register.ejs new file mode 100644 index 0000000..fcff088 --- /dev/null +++ b/views/register.ejs @@ -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>
\ No newline at end of file diff --git a/views/success.ejs b/views/success.ejs new file mode 100644 index 0000000..c967d24 --- /dev/null +++ b/views/success.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>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>
\ No newline at end of file |
