mirror of
https://github.com/MaciejkaG/statki.git
synced 2024-11-30 03:42:55 +01:00
MaciejkaG
a212161733
- Improved PWA support on mobile. - Added Vs. AI game menu. - Added the first AI algorithm. - Vs. AI is now available with every option leading to simple bot - Fixed a bug where placing ships in certain rotation around the boundaries of the board would wrongly colour them when sunk. - Improved code clarity and comments - Added an option in the settings to change your nickname - Added version display in the settings - Slightly improved box scaling and layout - Made multiple improvements to the PWA support - Added multiple minor features - Brought back full SPA URL functionality, now if you copy a URL to specific view/page in the menu and go into it, you will actually arrive at that page if it's not restricted.
299 lines
12 KiB
JavaScript
299 lines
12 KiB
JavaScript
import nodemailer from 'nodemailer';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import geoip from 'geoip-lite';
|
|
|
|
import mysql from 'mysql';
|
|
import { Lang } from './localisation.js';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
export class MailAuth {
|
|
constructor(redis, options, mysqlOptions) {
|
|
this.redis = redis;
|
|
mysqlOptions.multipleStatements = true;
|
|
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 ? options.user : "root",
|
|
pass: options.pass,
|
|
},
|
|
});
|
|
|
|
this.mailFrom = `"Statki" <${options.user}>`
|
|
}
|
|
|
|
async timer(tId, time, callback) {
|
|
let timerEnd = new Date().getTime() / 1000 + time;
|
|
|
|
await this.redis.set(`loginTimer:${tId}`, timerEnd);
|
|
|
|
let interval = setInterval(async () => {
|
|
if (new Date().getTime() < timerEnd) {
|
|
clearInterval(interval);
|
|
|
|
callback();
|
|
return;
|
|
}
|
|
|
|
let lastUpdate = await this.redis.get(`loginTimer:${tId}`);
|
|
if (timerEnd != lastUpdate) {
|
|
clearInterval(interval);
|
|
return;
|
|
}
|
|
}, 1000);
|
|
|
|
// let timeout = setTimeout(callback, time * 1000);
|
|
|
|
// let interval = setInterval(async () => {
|
|
// if (timeout._destroyed) {
|
|
// clearInterval(interval);
|
|
// return;
|
|
// }
|
|
|
|
// let lastUpdate = await this.redis.get(`loginTimer:${tId}`);
|
|
// if (localLastUpdate != lastUpdate) {
|
|
// clearTimeout(timeout);
|
|
// clearInterval(interval);
|
|
// return;
|
|
// }
|
|
// }, 200);
|
|
}
|
|
|
|
async resetTimer(tId) {
|
|
let lastUpdate = await this.redis.get(`loginTimer:${tId}`);
|
|
await this.redis.set(`loginTimer:${tId}`, -lastUpdate);
|
|
}
|
|
|
|
loginAuthless(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); return; }
|
|
|
|
if (response.length === 0 || response[0].nickname == null) {
|
|
if (response.length === 0) {
|
|
conn.query(`INSERT INTO accounts(email) VALUES (${conn.escape(email)});`, (error) => { if (error) reject(error) });
|
|
}
|
|
|
|
conn.query(`SELECT user_id FROM accounts WHERE email = ${conn.escape(email)}`, async (error, response) => {
|
|
if (error) reject(error);
|
|
const row = response[0];
|
|
|
|
conn.end();
|
|
resolve({ status: 1, uid: row.user_id });
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
const row = response[0];
|
|
|
|
conn.end();
|
|
resolve({ status: 1, uid: row.user_id });
|
|
});
|
|
});
|
|
}
|
|
|
|
getLanguage(userId) {
|
|
return new Promise((resolve, reject) => {
|
|
const conn = mysql.createConnection(this.mysqlOptions);
|
|
conn.query(`SELECT language FROM accounts WHERE user_id = ${conn.escape(userId)}`, (error, response) => {
|
|
if (error) { reject(error); return; }
|
|
|
|
if (response.length !== 0) {
|
|
resolve(response[0].language);
|
|
} else {
|
|
resolve(null);
|
|
}
|
|
|
|
conn.end();
|
|
});
|
|
});
|
|
}
|
|
|
|
startVerification(email, ip, agent, langId) {
|
|
return new Promise((resolve, reject) => {
|
|
const conn = mysql.createConnection(this.mysqlOptions);
|
|
const lang = new Lang([langId]);
|
|
|
|
conn.query(`SELECT user_id, nickname FROM accounts WHERE email = ${conn.escape(email)}`, async (error, response) => {
|
|
if (error) { reject(error); conn.end(); return; }
|
|
if (response.length !== 0) {
|
|
let timer = await this.redis.get(`loginTimer:${response[0].user_id}`);
|
|
if (timer && timer > 0) {
|
|
conn.end();
|
|
resolve({ status: -1, uid: response[0].user_id, });
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (response.length === 0 || response[0].nickname == null) {
|
|
if (response.length === 0) {
|
|
conn.query(`INSERT INTO accounts(email) VALUES (${conn.escape(email)});`, (error) => { 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-${langId}.html`), 'utf8');
|
|
let authCode = genCode();
|
|
|
|
await this.redis.set(`codeAuth:${authCode}`, row.user_id);
|
|
|
|
await this.timer(row.user_id, 600, async () => {
|
|
await this.redis.unlink(`codeAuth:${authCode}`);
|
|
});
|
|
|
|
authCode = authCode.slice(0, 4) + " " + authCode.slice(4);
|
|
|
|
const lookup = geoip.lookup(ip);
|
|
|
|
var lookupData;
|
|
if (lookup) {
|
|
lookupData = `User-Agent: ${agent}\nAdres IP: ${ip}\nKraj: ${lookup.country}\nRegion: ${lookup.region}\nMiasto: ${lookup.city}`;
|
|
} else {
|
|
lookupData = `IP lookup failed`;
|
|
}
|
|
|
|
try {
|
|
await this.mail.sendMail({
|
|
from: this.mailFrom,
|
|
to: email,
|
|
subject: lang.t('email.This is your Statki authorisation code').replace("%s", authCode),
|
|
html: html.replace("{{ CODE }}", authCode).replace("{{ LOOKUP }}", lookupData),
|
|
});
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
|
|
conn.end();
|
|
resolve({ status: 1, uid: row.user_id, code: authCode });
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
const row = response[0];
|
|
|
|
const html = fs.readFileSync(path.join(__dirname, `mail/auth-code-${langId}.html`), 'utf8');
|
|
let authCode = genCode();
|
|
|
|
await this.redis.set(`codeAuth:${authCode}`, row.user_id);
|
|
|
|
await this.timer(row.user_id, 600, async () => {
|
|
await this.redis.unlink(`codeAuth:${authCode}`);
|
|
});
|
|
|
|
authCode = authCode.slice(0, 4) + " " + authCode.slice(4);
|
|
|
|
const lookup = geoip.lookup(ip);
|
|
|
|
var lookupData;
|
|
if (lookup) {
|
|
lookupData = `User-Agent: ${agent}\nAdres IP: ${ip}\nKraj: ${lookup.country}\nRegion: ${lookup.region}\nMiasto: ${lookup.city}`;
|
|
} else {
|
|
lookupData = `IP lookup failed`;
|
|
}
|
|
|
|
try {
|
|
await this.mail.sendMail({
|
|
from: this.mailFrom,
|
|
to: email,
|
|
subject: lang.t('email.This is your Statki authorisation code').replace("%s", authCode),
|
|
html: html.replace("{{ NICKNAME }}", row.nickname).replace("{{ CODE }}", authCode).replace("{{ LOOKUP }}", lookupData),
|
|
});
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
|
|
|
|
conn.end();
|
|
resolve({ status: 1, uid: row.user_id });
|
|
});
|
|
});
|
|
}
|
|
|
|
saveMatch(matchId, duration, type, hostId, guestId, boards, winnerIdx, aitype = null) {
|
|
return new Promise((resolve, reject) => {
|
|
const conn = mysql.createConnection(this.mysqlOptions);
|
|
conn.query(`INSERT INTO matches(match_id, match_type, host_id, guest_id, duration${aitype == null ? "" : ", ai_type"}) VALUES (${conn.escape(matchId)}, ${conn.escape(type)}, ${conn.escape(hostId)}, ${conn.escape(guestId)}, ${conn.escape(duration)}${aitype == null ? "" : ", " + conn.escape(aitype)})`, async (error) => {
|
|
if (error) reject(error);
|
|
else conn.query(`INSERT INTO statistics(match_id, user_id, board, won) VALUES (${conn.escape(matchId)}, ${conn.escape(hostId)}, ${conn.escape(JSON.stringify(boards[0]))}, ${conn.escape(winnerIdx ? 1 : 0)}), (${conn.escape(matchId)}, ${conn.escape(guestId)}, ${conn.escape(JSON.stringify(boards[1]))}, ${conn.escape(winnerIdx ? 0 : 1)})`, async (error, response) => {
|
|
if (error) reject(error);
|
|
else resolve();
|
|
});
|
|
|
|
conn.end();
|
|
});
|
|
});
|
|
}
|
|
|
|
getProfile(userId) {
|
|
return new Promise((resolve, reject) => {
|
|
const conn = mysql.createConnection(this.mysqlOptions);
|
|
conn.query(`SELECT nickname, account_creation FROM accounts WHERE user_id = ${conn.escape(userId)}; SELECT ROUND((AVG(statistics.won)) * 100) AS winrate, COUNT(statistics.match_id) AS alltime_matches, COUNT(CASE WHEN (YEAR(matches.date) = YEAR(NOW()) AND MONTH(matches.date) = MONTH(NOW())) THEN matches.match_id END) AS monthly_matches FROM accounts NATURAL JOIN statistics NATURAL JOIN matches WHERE accounts.user_id = ${conn.escape(userId)}; SELECT statistics.match_id, accounts.nickname AS opponent, matches.match_type, statistics.won, matches.ai_type, matches.duration, matches.date FROM statistics JOIN matches ON matches.match_id = statistics.match_id JOIN accounts ON accounts.user_id = (CASE WHEN matches.host_id != statistics.user_id THEN matches.host_id ELSE matches.guest_id END) WHERE statistics.user_id = ${conn.escape(userId)} ORDER BY matches.date DESC LIMIT 10;`, async (error, response) => {
|
|
if (error) reject(error);
|
|
else {
|
|
if (response[0].length === 0 || response[1].length === 0) {
|
|
reject(0);
|
|
return;
|
|
}
|
|
|
|
const [[profile], [stats], matchHistory] = response;
|
|
|
|
resolve({ profile, stats, matchHistory });
|
|
}
|
|
|
|
conn.end();
|
|
});
|
|
});
|
|
}
|
|
|
|
async finishVerification(uid, authCode) {
|
|
authCode = authCode.replace(/\s+/g, "");
|
|
const rUid = await this.redis.get(`codeAuth:${authCode}`);
|
|
if (rUid != null && rUid === uid) {
|
|
this.resetTimer(rUid);
|
|
await this.redis.unlink(`codeAuth:${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();
|
|
|
|
conn.end();
|
|
});
|
|
});
|
|
}
|
|
|
|
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);
|
|
});
|
|
|
|
conn.end();
|
|
});
|
|
}
|
|
}
|
|
|
|
function genCode() {
|
|
return Math.floor(10000000 + Math.random() * 90000000).toString();
|
|
} |