mirror of
https://github.com/MaciejkaG/statki.git
synced 2024-11-30 05:32:54 +01:00
Working MySQL-based auth
This commit is contained in:
parent
dde5280c98
commit
eff3ed1914
131
index.js
131
index.js
@ -10,7 +10,8 @@ import { v4 as uuidv4, validate } from 'uuid';
|
||||
import session from "express-session";
|
||||
import { engine } from 'express-handlebars';
|
||||
import { createClient } from 'redis';
|
||||
import * as bships from './utils/battleships.js'
|
||||
import * as bships from './utils/battleships.js';
|
||||
import { MailAuth } from './utils/auth.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@ -31,6 +32,17 @@ redis.on('error', err => console.log('Redis Client Error', err));
|
||||
await redis.connect();
|
||||
|
||||
const GInfo = new bships.GameInfo(redis, io);
|
||||
const auth = new MailAuth(redis, {
|
||||
host: process.env.mail_host,
|
||||
user: process.env.mail_user,
|
||||
pass: process.env.mail_pass
|
||||
},
|
||||
{
|
||||
host: process.env.db_host,
|
||||
user: process.env.db_user,
|
||||
password: process.env.db_pass,
|
||||
database: 'statki'
|
||||
});
|
||||
|
||||
app.set('trust proxy', 1);
|
||||
const sessionMiddleware = session({
|
||||
@ -45,37 +57,110 @@ app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
io.engine.use(sessionMiddleware);
|
||||
|
||||
app.get("/", async (req, res) => {
|
||||
if (req.session.nickname == null) {
|
||||
res.redirect("/setup");
|
||||
app.get('/', async (req, res) => {
|
||||
let login = loginState(req);
|
||||
|
||||
if (login != 2) {
|
||||
res.redirect('/login');
|
||||
} else if (req.session.nickname == null) {
|
||||
auth.getNickname(req.session.userId).then(nickname => {
|
||||
if (nickname != null) {
|
||||
req.session.nickname = nickname;
|
||||
res.render('index');
|
||||
} else {
|
||||
res.redirect('/nickname');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
res.render('index');
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/setup", (req, res) => {
|
||||
if (req.session.nickname != null) {
|
||||
app.get('/login', (req, res) => {
|
||||
let login = loginState(req);
|
||||
if (!login) {
|
||||
res.render('login');
|
||||
} else if (login == 1) {
|
||||
res.redirect('/auth');
|
||||
} else {
|
||||
res.redirect('/');
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/auth', (req, res) => {
|
||||
let login = loginState(req);
|
||||
if (!login) { // Niezalogowany
|
||||
res.redirect('/login');
|
||||
} else if (login == 1) { // W trakcie autoryzacji
|
||||
res.render('auth');
|
||||
} else { // Zalogowany
|
||||
res.redirect('/auth');
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/nickname', (req, res) => {
|
||||
let login = loginState(req);
|
||||
if (!login) { // Niezalogowany
|
||||
res.redirect('/login');
|
||||
} else {
|
||||
res.render('setup');
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/login', (req, res) => {
|
||||
let login = loginState(req);
|
||||
if (login == 2) {
|
||||
res.redirect('/');
|
||||
} else if (login == 0 && req.body.email != null && validateEmail(req.body.email)) {
|
||||
auth.startVerification(req.body.email).then(result => {
|
||||
if (result.status) {
|
||||
req.session.userId = result.uid;
|
||||
|
||||
req.session.loggedIn = 1;
|
||||
res.redirect('/auth');
|
||||
} else {
|
||||
res.sendStatus(500);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
res.sendStatus(403);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/auth', async (req, res) => {
|
||||
let login = loginState(req);
|
||||
if (login == 2) {
|
||||
res.redirect('/');
|
||||
} else if (login == 1 && req.body.code != null && req.body.code.length <= 10 && req.body.code.length >= 8) {
|
||||
let finishResult = await auth.finishVerification(req.session.userId, req.body.code);
|
||||
if (finishResult) {
|
||||
req.session.loggedIn = 2;
|
||||
res.redirect('/');
|
||||
} else {
|
||||
res.render("setup");
|
||||
res.sendStatus(401);
|
||||
}
|
||||
} else {
|
||||
res.sendStatus(403);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/setup-profile', (req, res) => {
|
||||
if (req.session.nickname == null && 3 <= req.body.nickname.length && req.body.nickname.length <= 16) {
|
||||
app.post('/api/nickname', (req, res) => {
|
||||
if (loginState(req) == 2 && req.session.nickname == null && req.body.nickname != null && 3 <= req.body.nickname.length && req.body.nickname.length <= 16) {
|
||||
req.session.nickname = req.body.nickname;
|
||||
req.session.playerID = uuidv4();
|
||||
req.session.activeGame = null;
|
||||
auth.setNickname(req.session.userId, req.body.nickname).then(() => {
|
||||
res.redirect('/');
|
||||
});
|
||||
} else {
|
||||
res.sendStatus(400);
|
||||
}
|
||||
|
||||
res.redirect("/")
|
||||
});
|
||||
|
||||
app.get("/game", async (req, res) => {
|
||||
app.get('/game', async (req, res) => {
|
||||
const game = await redis.json.get(`game:${req.query.id}`);
|
||||
if (req.session.nickname == null) {
|
||||
res.redirect("/setup");
|
||||
} else if (req.query.id == null || game == null || game.state == "expired" || req.session.activeGame == null) {
|
||||
res.redirect('/setup');
|
||||
} else if (req.query.id == null || game == null || game.state == 'expired' || req.session.activeGame == null) {
|
||||
res.status(400).send('badGameId');
|
||||
} else {
|
||||
res.render('board');
|
||||
@ -376,3 +461,19 @@ function AFKEnd(gameId) {
|
||||
function roomMemberIterator(id) {
|
||||
return io.sockets.adapter.rooms.get(id) == undefined ? null : io.sockets.adapter.rooms.get(id).entries();
|
||||
}
|
||||
|
||||
function loginState(req) {
|
||||
if (req.session.loggedIn == null) {
|
||||
return 0;
|
||||
} else {
|
||||
return req.session.loggedIn;
|
||||
}
|
||||
}
|
||||
|
||||
function validateEmail(email) {
|
||||
return String(email)
|
||||
.toLowerCase()
|
||||
.match(
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|.(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
);
|
||||
};
|
167
utils/auth.js
Normal file
167
utils/auth.js
Normal file
@ -0,0 +1,167 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
import uuid4 from 'uuid4';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import mysql from 'mysql';
|
||||
import { createClient } from 'redis';
|
||||
import readline from "node:readline";
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const saltRounds = 10;
|
||||
|
||||
export class MailAuth {
|
||||
constructor(redis, options, mysqlOptions) {
|
||||
this.redis = redis;
|
||||
this.mysqlOptions = mysqlOptions;
|
||||
|
||||
this.mail = nodemailer.createTransport({
|
||||
host: options.host ? options.host : "localhost",
|
||||
port: options.port ? options.port : "465",
|
||||
secure: options.secure ? options.secure : true,
|
||||
auth: {
|
||||
user: options.user,
|
||||
pass: options.pass,
|
||||
},
|
||||
});
|
||||
|
||||
this.mailFrom = `"Statki" <${options.user}>`
|
||||
}
|
||||
|
||||
async timer(tId, time, callback) {
|
||||
await this.redis.set(`timer:${tId}`, new Date().getTime() / 1000);
|
||||
let localLastUpdate = await this.redis.get(`timer:${tId}`);
|
||||
|
||||
let timeout = setTimeout(callback, time * 1000);
|
||||
|
||||
let interval = setInterval(async () => {
|
||||
if (timeout._destroyed) {
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
|
||||
let lastUpdate = await this.redis.get(`timer:${tId}`);
|
||||
if (localLastUpdate != lastUpdate) {
|
||||
clearTimeout(timeout);
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
async resetTimer(tId) {
|
||||
let lastUpdate = await this.redis.get(`timer:${tId}`);
|
||||
await this.redis.set(`timer:${tId}`, -lastUpdate);
|
||||
}
|
||||
|
||||
startVerification(email) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const conn = mysql.createConnection(this.mysqlOptions);
|
||||
conn.query(`SELECT user_id, nickname FROM accounts WHERE email = ${conn.escape(email)}`, async (error, response) => {
|
||||
if (error) reject(error);
|
||||
if (response.length === 0 || response[0].nickname == null) {
|
||||
|
||||
conn.query(`DELETE FROM accounts WHERE email = ${conn.escape(email)};`, (error, response) => { if (error) reject(error) });
|
||||
conn.query(`INSERT INTO accounts(email) VALUES (${conn.escape(email)});`, (error, response) => { if (error) reject(error) });
|
||||
conn.query(`SELECT user_id, nickname FROM accounts WHERE email = ${conn.escape(email)}`, async (error, response) => {
|
||||
if (error) reject(error);
|
||||
const row = response[0];
|
||||
|
||||
const html = fs.readFileSync(path.join(__dirname, 'mail/auth-code-firsttime.html'), 'utf8');
|
||||
let authCode = genCode();
|
||||
let tId = uuid4();
|
||||
|
||||
await this.redis.json.set(`code_auth:${authCode}`, "$", { uid: row.user_id, tid: tId });
|
||||
|
||||
await this.timer(tId, 600, async () => {
|
||||
await this.redis.json.del(`code_auth:${authCode}`);
|
||||
});
|
||||
|
||||
authCode = authCode.slice(0, 4) + " " + authCode.slice(4);
|
||||
|
||||
try {
|
||||
await this.mail.sendMail({
|
||||
from: this.mailFrom,
|
||||
to: email,
|
||||
subject: `${authCode} to twój kod autoryzacji do Statków`,
|
||||
html: html.replace("{{ CODE }}", authCode),
|
||||
});
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
|
||||
resolve({ status: 1, uid: row.user_id });
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const row = response[0];
|
||||
|
||||
const html = fs.readFileSync(path.join(__dirname, 'mail/auth-code.html'), 'utf8');
|
||||
let authCode = genCode();
|
||||
let tId = uuid4();
|
||||
|
||||
await this.redis.json.set(`code_auth:${authCode}`, "$", { uid: row.user_id, tid: tId });
|
||||
|
||||
await this.timer(tId, 600, async () => {
|
||||
await this.redis.json.del(`code_auth:${authCode}`);
|
||||
});
|
||||
|
||||
authCode = authCode.slice(0, 4) + " " + authCode.slice(4);
|
||||
|
||||
await this.mail.sendMail({
|
||||
from: this.mailFrom,
|
||||
to: email,
|
||||
subject: `${authCode} to twój kod logowania do Statków`,
|
||||
html: html.replace("{{ NICKNAME }}", row.nickname).replace("{{ CODE }}", authCode),
|
||||
});
|
||||
|
||||
resolve({ status: 1, uid: row.user_id });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async finishVerification(uid, authCode) {
|
||||
authCode = authCode.replace(/\s+/g, "");
|
||||
let redisRes = await this.redis.json.get(`code_auth:${authCode}`);
|
||||
if (redisRes != null && redisRes.uid === uid) {
|
||||
this.resetTimer(redisRes.tid);
|
||||
await this.redis.del(`code_auth:${authCode}`);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
setNickname(uid, nickname) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const conn = mysql.createConnection(this.mysqlOptions);
|
||||
conn.query(`UPDATE accounts SET nickname = ${conn.escape(nickname)} WHERE user_id = ${conn.escape(uid)}`, (error) => {
|
||||
if (error) reject(error);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getNickname(uid) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const conn = mysql.createConnection(this.mysqlOptions);
|
||||
conn.query(`SELECT nickname FROM accounts WHERE user_id = ${conn.escape(uid)}`, (error, response) => {
|
||||
if (error) reject(error);
|
||||
resolve(response[0].nickname);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function genCode() {
|
||||
return Math.floor(10000000 + Math.random() * 90000000).toString();
|
||||
}
|
22
utils/mail/auth-code-firsttime.html
Normal file
22
utils/mail/auth-code-firsttime.html
Normal file
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;1,400;1,700&family=Poppins:ital,wght@0,600;1,600&family=Roboto+Mono:ital@0;1&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body style="background: rgb(0, 0, 0);color: white;;font-family: 'Lato', sans-serif;padding: 30px;">
|
||||
<div
|
||||
style="margin:30px;text-align: center;display:flex;flex-direction: column;justify-content: center;font-size:1.3em;margin: auto;width: 35%;min-width:25em;padding:20px;">
|
||||
<h2 style="font-family: 'Poppins', sans-serif;">Cześć!</h2>
|
||||
<p>Ktoś próbował utworzyć konto w Statkach za pomocą tego e-maila, jeśli to nie byłeś ty - zignoruj tę wiadomość.<br>Poniżej znajduje się kod autoryzacyjny, pamiętaj by nie udostępniać go nikomu.</p>
|
||||
<div style="border: solid white 1px; border-radius: 15px;font-family: 'Roboto Mono', monospace;padding:15px;font-size: 1.5em;">
|
||||
{{ CODE }}
|
||||
</div>
|
||||
<p>Powyższy kod wygasa po 10 minutach</p>
|
||||
</div>
|
||||
<p style="color:white;margin-bottom: 20px;margin-top: 20px;text-align: center;"><a href="https://maciejka.xyz/"
|
||||
style="text-decoration: none;color:white;">Copyright © 2024 MCJK</a> | <a href="https://statki.maciejka.xyz/"
|
||||
style="text-decoration: none;color: rgb(85, 111, 255);">statki.maciejka.xyz</a><br>Ta wiadomość została wysłana automatycznie, nie odpowiadaj na nią.</p>
|
||||
</body>
|
||||
</html>
|
22
utils/mail/auth-code.html
Normal file
22
utils/mail/auth-code.html
Normal file
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;1,400;1,700&family=Poppins:ital,wght@0,600;1,600&family=Roboto+Mono:ital@0;1&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body style="background: rgb(0, 0, 0);color: white;;font-family: 'Lato', sans-serif;padding: 30px;">
|
||||
<div
|
||||
style="margin:30px;text-align: center;display:flex;flex-direction: column;justify-content: center;font-size:1.3em;margin: auto;width: 35%;min-width:25em;padding:20px;">
|
||||
<h2 style="font-family: 'Poppins', sans-serif;">Hej, {{ NICKNAME }}!</h2>
|
||||
<p>Ktoś próbował się zalogować na twoje konto w Statkach, jeśli to nie byłeś ty - zignoruj tę wiadomość.<br>Poniżej znajduje się kod autoryzacyjny, pamiętaj by nie udostępniać go nikomu.</p>
|
||||
<div style="border: solid white 1px; border-radius: 15px;font-family: 'Roboto Mono', monospace;padding:15px;font-size: 1.5em;">
|
||||
{{ CODE }}
|
||||
</div>
|
||||
<p>Powyższy kod wygasa po 10 minutach</p>
|
||||
</div>
|
||||
<p style="color:white;margin-bottom: 20px;margin-top: 20px;text-align: center;"><a href="https://maciejka.xyz/"
|
||||
style="text-decoration: none;color:white;">Copyright © 2024 MCJK</a> | <a href="https://statki.maciejka.xyz/"
|
||||
style="text-decoration: none;color: rgb(85, 111, 255);">statki.maciejka.xyz</a><br>Ta wiadomość została wysłana automatycznie, nie odpowiadaj na nią.</p>
|
||||
</body>
|
||||
</html>
|
24
views/auth.handlebars
Normal file
24
views/auth.handlebars
Normal file
@ -0,0 +1,24 @@
|
||||
<style>
|
||||
.container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#pvpJoinView .modes div {
|
||||
height: 13rem;
|
||||
}
|
||||
</style>
|
||||
<div class="container" id="pvpJoinView">
|
||||
<div>
|
||||
<h1>Statki</h1>
|
||||
<h2>Autoryzacja</h2>
|
||||
<div class="modes">
|
||||
<div>
|
||||
<p>Podaj kod wysłany do Twojej skrzynki e-mail</p>
|
||||
<form action="/api/auth" method="post">
|
||||
<input type="text" name="code" placeholder="Kod" style="font-size: 1rem;" minlength="8" maxlength="10" autocomplete="off">
|
||||
<input type="submit" value="Zaloguj się">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
23
views/login.handlebars
Normal file
23
views/login.handlebars
Normal file
@ -0,0 +1,23 @@
|
||||
<style>
|
||||
.container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#pvpJoinView .modes div {
|
||||
height: 7rem;
|
||||
}
|
||||
</style>
|
||||
<div class="container" id="pvpJoinView">
|
||||
<div>
|
||||
<h1>Statki</h1>
|
||||
<h2>Logowanie</h2>
|
||||
<div class="modes">
|
||||
<div>
|
||||
<form action="/api/login" method="post">
|
||||
<input type="email" name="email" placeholder="Adres e-mail" style="font-size: 1rem;">
|
||||
<input type="submit" value="Przejdź dalej">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -2,6 +2,10 @@
|
||||
.container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#pvpJoinView .modes div {
|
||||
height: 11rem;
|
||||
}
|
||||
</style>
|
||||
<div class="container" id="pvpJoinView">
|
||||
<div>
|
||||
@ -9,8 +13,9 @@
|
||||
<h2>Konfiguracja profilu</h2>
|
||||
<div class="modes">
|
||||
<div>
|
||||
<form action="/api/setup-profile" method="post">
|
||||
<input type="text" name="nickname" placeholder="Nickname" style="font-size: 1rem;">
|
||||
<p>Twój nickname będzie widoczny dla innych graczy</p>
|
||||
<form action="/api/nickname" method="post">
|
||||
<input type="text" name="nickname" placeholder="Nazwa użytkownika" style="font-size: 1rem;">
|
||||
<input type="submit" value="Zatwierdź">
|
||||
</form>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user