Sfoglia il codice sorgente

add: Authentication

gugdun 10 mesi fa
parent
commit
f0f2292e41

+ 2 - 0
compose.yaml

@@ -15,5 +15,7 @@ services:
     image: "postgres:alpine"
     restart: always
     shm_size: 128mb
+    ports:
+      - 5432:5432
     environment:
       POSTGRES_PASSWORD: change-me

+ 95 - 1
package-lock.json

@@ -9,10 +9,14 @@
       "version": "1.0.0",
       "license": "SEE LICENSE IN LICENSE",
       "dependencies": {
+        "body-parser": "^2.2.0",
+        "connect-pg-simple": "^10.0.0",
+        "cookie-parser": "^1.4.7",
         "dotenv": "^16.5.0",
         "ejs": "^3.1.10",
         "express": "^5.1.0",
         "express-longpoll": "^0.0.6",
+        "express-session": "^1.18.1",
         "passport": "^0.7.0",
         "passport-local": "^1.0.0",
         "pg-promise": "^11.13.0"
@@ -256,6 +260,17 @@
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
       "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
     },
+    "node_modules/connect-pg-simple": {
+      "version": "10.0.0",
+      "resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-10.0.0.tgz",
+      "integrity": "sha512-pBGVazlqiMrackzCr0eKhn4LO5trJXsOX0nQoey9wCOayh80MYtThCbq8eoLsjpiWgiok/h+1/uti9/2/Una8A==",
+      "dependencies": {
+        "pg": "^8.12.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=22.0.0"
+      }
+    },
     "node_modules/content-disposition": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
@@ -283,6 +298,23 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/cookie-parser": {
+      "version": "1.4.7",
+      "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
+      "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
+      "dependencies": {
+        "cookie": "0.7.2",
+        "cookie-signature": "1.0.6"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/cookie-parser/node_modules/cookie-signature": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+      "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
+    },
     "node_modules/cookie-signature": {
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
@@ -463,6 +495,42 @@
         "lodash": ">=4.17.5"
       }
     },
+    "node_modules/express-session": {
+      "version": "1.18.1",
+      "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz",
+      "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==",
+      "dependencies": {
+        "cookie": "0.7.2",
+        "cookie-signature": "1.0.7",
+        "debug": "2.6.9",
+        "depd": "~2.0.0",
+        "on-headers": "~1.0.2",
+        "parseurl": "~1.3.3",
+        "safe-buffer": "5.2.1",
+        "uid-safe": "~2.1.5"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/express-session/node_modules/cookie-signature": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+      "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="
+    },
+    "node_modules/express-session/node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/express-session/node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+    },
     "node_modules/filelist": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
@@ -892,6 +960,14 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/on-headers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+      "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/once": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -961,7 +1037,6 @@
       "version": "8.15.6",
       "resolved": "https://registry.npmjs.org/pg/-/pg-8.15.6.tgz",
       "integrity": "sha512-yvao7YI3GdmmrslNVsZgx9PfntfWrnXwtR+K/DjI0I/sTKif4Z623um+sjVZ1hk5670B+ODjvHDAckKdjmPTsg==",
-      "peer": true,
       "dependencies": {
         "pg-connection-string": "^2.8.5",
         "pg-pool": "^3.9.6",
@@ -1190,6 +1265,14 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/random-bytes": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
+      "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/range-parser": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -1473,6 +1556,17 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/uid-safe": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
+      "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
+      "dependencies": {
+        "random-bytes": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/undefsafe": {
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",

+ 4 - 0
package.json

@@ -26,10 +26,14 @@
   },
   "homepage": "https://github.com/gugdun/svin-chat#readme",
   "dependencies": {
+    "body-parser": "^2.2.0",
+    "connect-pg-simple": "^10.0.0",
+    "cookie-parser": "^1.4.7",
     "dotenv": "^16.5.0",
     "ejs": "^3.1.10",
     "express": "^5.1.0",
     "express-longpoll": "^0.0.6",
+    "express-session": "^1.18.1",
     "passport": "^0.7.0",
     "passport-local": "^1.0.0",
     "pg-promise": "^11.13.0"

+ 12 - 33
public/css/main.css

@@ -36,6 +36,13 @@ body {
     margin: auto auto;
 }
 
+.error-container {
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+    margin: 16px;
+}
+
 .hero-logo {
     display: block;
     box-sizing: border-box;
@@ -52,7 +59,7 @@ body {
     font-weight: 700;
 }
 
-.login-logo {
+.auth-logo {
     display: block;
     box-sizing: border-box;
     margin: 0 auto;
@@ -60,7 +67,7 @@ body {
     height: 50%;
 }
 
-.login-label {
+.auth-label {
     display: block;
     width: 100%;
     text-align: center;
@@ -68,22 +75,6 @@ body {
     font-weight: 700;
 }
 
-.register-logo {
-    display: block;
-    box-sizing: border-box;
-    margin: 0 auto;
-    width: 33%;
-    height: 33%;
-}
-
-.register-label {
-    display: block;
-    width: 100%;
-    text-align: center;
-    font-size: 18pt;
-    font-weight: 700;
-}
-
 .button {
     display: block;
     width: 100%;
@@ -152,14 +143,10 @@ body {
         font-size: 20pt;
     }
 
-    .login-label {
+    .auth-label {
         font-size: 18pt;
     }
 
-    .register-label {
-        font-size: 16pt;
-    }
-
     .button {
         font-size: 12pt;
     }
@@ -186,14 +173,10 @@ body {
         font-size: 18pt;
     }
 
-    .login-label {
+    .auth-label {
         font-size: 16pt;
     }
 
-    .register-label {
-        font-size: 14pt;
-    }
-
     .button {
         font-size: 10pt;
     }
@@ -220,14 +203,10 @@ body {
         font-size: 16pt;
     }
 
-    .login-label {
+    .auth-label {
         font-size: 14pt;
     }
 
-    .register-label {
-        font-size: 12pt;
-    }
-
     .button {
         font-size: 8pt;
     }

+ 7 - 0
src/db.js

@@ -0,0 +1,7 @@
+// Copyright (c) 2025 gugdun
+// All rights reserved. Unauthorized use, copying, or distribution is strictly prohibited.
+
+const pgp = require('pg-promise')();
+const db = pgp(process.env.POSTGRES_CONNECTION);
+db.none("CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, username TEXT UNIQUE, hashed_password BYTEA, salt BYTEA )");
+module.exports = db;

+ 42 - 17
src/index.js

@@ -1,37 +1,62 @@
 // Copyright (c) 2025 gugdun
 // All rights reserved. Unauthorized use, copying, or distribution is strictly prohibited.
 
-var express = require("express");
-var ejs = require("ejs");
 require("dotenv").config();
 
+const path = require("path");
+const express = require("express");
+const cookieParser = require("cookie-parser");
+const passport = require("passport");
+const session = require("express-session");
+const pgSession = require("connect-pg-simple")(session);
+
+const db = require("./db");
+const indexRouter = require("./routes/index");
+const authRouter = require("./routes/auth");
+
 const PORT = process.env.PORT || 5000;
 
-var app = express();
+const app = express();
 app.set("view engine", "ejs");
-app.use(express.static("public"));
+app.set("views", path.join(__dirname, "views"));
 
-var longpoll = require("express-longpoll")(app);
+app.use(express.json());
+app.use(express.urlencoded({ extended: false }));
+app.use(cookieParser());
+app.use(express.static("public"));
+app.use(session({
+    secret: process.env.SESSION_SECRET,
+    resave: false,
+    saveUninitialized: false,
+    store: new pgSession({
+        tableName: "sessions",
+        conString: process.env.POSTGRES_CONNECTION,
+        createTableIfMissing: true
+    })
+}));
+app.use(passport.authenticate("session"));
+
+const longpoll = require("express-longpoll")(app);
 longpoll.create("/poll");
 
-app.get("/", async (req, res) => {
-    res.render("layout", { child: await ejs.renderFile("views/home.ejs") });
-});
+app.use(indexRouter);
+app.use(authRouter);
 
-app.get("/login", async (req, res) => {
-    res.render("layout", { child: await ejs.renderFile("views/login.ejs") });
+app.use(function (req, res, next) {
+    next(createError(404));
 });
 
-app.get("/register", async (req, res) => {
-    res.render("layout", { child: await ejs.renderFile("views/register.ejs") });
+app.use(function (err, req, res, next) {
+    res.locals.message = err.message;
+    res.locals.error = err;
+    res.status(err.status || 500);
+    res.render('error');
 });
 
-app.listen(PORT, function() {
+app.listen(PORT, function () {
     console.log(`Listening on port ${PORT}`);
 });
 
-var data = { message: "Test" };
-longpoll.publish("/poll", data);
-setInterval(function () { 
-    longpoll.publish("/poll", data);
+setInterval(function () {
+    longpoll.publish("/poll", { message: "Test" });
 }, 5000);

+ 73 - 0
src/routes/auth.js

@@ -0,0 +1,73 @@
+// Copyright (c) 2025 gugdun
+// All rights reserved. Unauthorized use, copying, or distribution is strictly prohibited.
+
+const express = require("express");
+const passport = require("passport");
+const LocalStrategy = require("passport-local");
+const crypto = require("crypto");
+const db = require("../db");
+
+passport.use(new LocalStrategy(async (username, password, cb) => {
+    db.one("SELECT * FROM users WHERE username = $1 ", [
+        username
+    ]).then(data => {
+        if (!data) { return cb(null, false, { message: "Incorrect username or password." }); }
+        crypto.pbkdf2(password, data.salt, 310000, 32, "sha256", (err, hashedPassword) => {
+            if (err) { return cb(err); }
+            if (!crypto.timingSafeEqual(data.hashed_password, hashedPassword)) {
+                return cb(null, false, { message: "Incorrect username or password." });
+            }
+            return cb(null, data);
+        });
+    }).catch(err => {
+        return cb(err);
+    });
+}));
+
+passport.serializeUser(function (user, cb) {
+    process.nextTick(function () {
+        cb(null, { id: user.id, username: user.username });
+    });
+});
+
+passport.deserializeUser(function (user, cb) {
+    process.nextTick(function () {
+        return cb(null, user);
+    });
+});
+
+const router = express.Router();
+
+router.post("/login", passport.authenticate("local", {
+    successRedirect: "/",
+    failureRedirect: "/login"
+}));
+
+router.post("/register", (req, res, next) => {
+    var salt = crypto.randomBytes(16);
+    crypto.pbkdf2(req.body.password, salt, 310000, 32, "sha256", (err, hashedPassword) => {
+        if (err) { return next(err); }
+        db.one("INSERT INTO users (username, hashed_password, salt) VALUES ($1, $2, $3) RETURNING id, username", [
+            req.body.username,
+            hashedPassword,
+            salt
+        ]).then(data => {
+            req.login({
+                id: data.id,
+                username: data.username
+            }, (err) => {
+                if (err) { return next(err); }
+                res.redirect("/");
+            });
+        }).catch(err => res.redirect("/register"));
+    });
+});
+
+router.post("/logout", function (req, res, next) {
+    req.logout(function (err) {
+        if (err) { return next(err); }
+        res.redirect("/");
+    });
+});
+
+module.exports = router;

+ 31 - 0
src/routes/index.js

@@ -0,0 +1,31 @@
+// Copyright (c) 2025 gugdun
+// All rights reserved. Unauthorized use, copying, or distribution is strictly prohibited.
+
+const path = require("path");
+const express = require("express");
+const ejs = require("ejs");
+const db = require("../db");
+
+const views = path.join(__dirname, "..", "views");
+
+const router = express.Router();
+
+router.get("/", async (req, res) => {
+    res.render("layout", {
+        child: await ejs.renderFile(path.join(views, "home.ejs"))
+    });
+});
+
+router.get("/login", async (req, res) => {
+    res.render("layout", {
+        child: await ejs.renderFile(path.join(views, "login.ejs"))
+    });
+});
+
+router.get("/register", async (req, res) => {
+    res.render("layout", {
+        child: await ejs.renderFile(path.join(views, "register.ejs"))
+    });
+});
+
+module.exports = router;

+ 17 - 0
src/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" />
+    <link rel="icon" href="/favicon.ico" type="image/x-icon">
+    <link rel="stylesheet" href="/css/main.css">
+    <title>Error</title>
+</head>
+<body>
+    <div class="error-container">
+        <h1><%= message %></h1>
+        <h2><%= error.status %></h2>
+        <p><%= error.stack %></p>
+    </div>
+</body>
+</html>

+ 0 - 0
views/home.ejs → src/views/home.ejs


+ 0 - 0
views/layout.ejs → src/views/layout.ejs


+ 12 - 0
src/views/login.ejs

@@ -0,0 +1,12 @@
+<div class="container">
+    <img src="/img/logo.png" class="auth-logo" />
+    <p class="auth-label">SvinChat</p>
+    <div class="hs"></div>
+    <form method="POST" action="/login">
+        <input type="text" name="username" placeholder="Nickname" class="text-input" required />
+        <div class="hs"></div>
+        <input type="password" name="password" placeholder="Password" class="text-input" required />
+        <div class="hs"></div>
+        <input type="submit" value="LOGIN" class="button" />
+    </form>
+</div>

+ 12 - 0
src/views/register.ejs

@@ -0,0 +1,12 @@
+<div class="container">
+    <img src="/img/logo.png" class="auth-logo" />
+    <p class="auth-label">SvinChat</p>
+    <div class="hs"></div>
+    <form method="POST" action="/register">
+        <input type="text" name="username" placeholder="Nickname" class="text-input" required />
+        <div class="hs"></div>
+        <input type="password" name="password" placeholder="Password" class="text-input" required />
+        <div class="hs"></div>
+        <input type="submit" value="REGISTER" class="button" />
+    </form>
+</div>

+ 0 - 12
views/login.ejs

@@ -1,12 +0,0 @@
-<div class="container">
-    <img src="/img/logo.png" class="login-logo" />
-    <p class="login-label">SvinChat</p>
-    <div class="hs"></div>
-    <form method="POST" action="/login">
-        <input type="text" placeholder="Nickname" class="text-input" />
-        <div class="hs"></div>
-        <input type="password" placeholder="Password" class="text-input" />
-        <div class="hs"></div>
-        <input type="submit" value="LOGIN" class="button" />
-    </form>
-</div>

+ 0 - 14
views/register.ejs

@@ -1,14 +0,0 @@
-<div class="container">
-    <img src="/img/logo.png" class="register-logo" />
-    <p class="register-label">SvinChat</p>
-    <div class="hs"></div>
-    <form method="POST" action="/register">
-        <input type="text" placeholder="Nickname" class="text-input" />
-        <div class="hs"></div>
-        <input type="password" placeholder="Password" class="text-input" />
-        <div class="hs"></div>
-        <input type="password" placeholder="Repeat Password" class="text-input" />
-        <div class="hs"></div>
-        <input type="submit" value="REGISTER" class="button" />
-    </form>
-</div>