commit 4811a3eabc1e7afaf17b45f9dea541d987c45717 Author: Andrew Lee Date: Tue Dec 24 13:56:06 2024 -0500 Initial commit 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 new file mode 100755 index 0000000..43e8e7a Binary files /dev/null and b/bun.lockb differ 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 new file mode 100644 index 0000000..4946e1e Binary files /dev/null and b/public/img/background.webp differ diff --git a/public/img/logo.png b/public/img/logo.png new file mode 100644 index 0000000..b98c6ed Binary files /dev/null and b/public/img/logo.png differ 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 @@ + + + + + + + bnbSO + + +
+
+ logo +

Oh no! Something went wrong!

+

<%= error %>

+
+ + \ 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 @@ + + + + + + + bnbSO + + +
+
+ logo +

You must be a member of the bits & Bytes Discord server to join bnbSO.

+ Login with Discord +
+ + \ 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 @@ + + + + + + + bnbSO Login + + +
+
+ logo +

Login

+
+ + + + + +
+ Forgot Password + <% if (typeof error !== 'undefined') { %> +
<%= error %>
+ <% } %> + + \ 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 @@ + + + + + + + bnbSO Register + + +
+
+ logo +

Welcome to bnbSO!

+

You will be sending the following information to register your bnbSO account

+

Please verify that the following information is correct. You can only change this once.

+
+ + + + + + + + + +
+ <% if (typeof error !== 'undefined') { %> +
<%= error %>
+ <% } %> + + \ 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 @@ + + + + + + + bnbSO + + +
+
+ logo +

Created account successfully!

+

Check on your inbox for the confirmation code.

+ Home +
+ + \ No newline at end of file