added users and groups

This commit is contained in:
2024-10-26 05:31:22 +03:00
parent cf8559aa13
commit e07ee822fb
21 changed files with 2797 additions and 0 deletions

53
src/controllers/group.js Normal file
View File

@@ -0,0 +1,53 @@
import GroupService from '../services/group.js';
import UserService from '../services/user.js';
import jwt from 'jsonwebtoken';
import config from '../../config.json' with {type: "json"};
import statuses from '../utils/status.js';
import log from '../utils/log.js';
const TAG = "/controllers/group.js"
class GroupController {
async create(req, res) {
try {
let { name } = req.params;
let user = jwt.decode(req.headers.authorization.split(' ')[1], config.secret);
let status = await GroupService.create(name, user.login.id);
log.info(`New group with name ${name} was just created by user ${user.login.username}`);
UserService.joinGroup(user.login.id, status.id);
return res.status(200).send("Successfull");
} catch (e) { return res.status(500).send(log.unknownError(`${TAG}/create: ${e}`)); }
}
async join(req, res) {
try {
let { id } = req.params;
await GroupService.getById(id);
let user = jwt.decode(req.headers.authorization.split(' ')[1], config.secret);
let status = await UserService.joinGroup(user.login.id, id);
if (status == statuses.duplicate) return res.status(400).send("Already in group");
log.info(`User ${user.login.username} has just joined group with ID ${id}`);
return res.status(200).send("Successfull");
} catch (e) { return res.status(500).send(log.unknownError(`${TAG}/join: ${e}`)); }
}
async updatePassword(req, res) {
try {
let { id } = req.params;
let { password } = req.body;
await GroupService.updatePassword(id, password);
log.info(`Password for group with ID ${id} was updated`);
return res.status(200).send("Successfull");
} catch (e) { return res.status(500).send(log.unknownError(`${TAG}/updatePassword ${e}`)); }
}
}
export default new GroupController();

33
src/controllers/user.js Normal file
View File

@@ -0,0 +1,33 @@
import UserService from '../services/user.js';
import log from '../utils/log.js';
import bcrypt from 'bcrypt';
import genToken from '../utils/jwt.js';
const TAG = "/controllers/userjs"
class UserController {
async register (req, res) {
try {
const {username, password} = req.body;
await UserService.create(username, password);
log.info(`New user with name ${username} has just registered`);
return res.status(200).send("Successfull register")
} catch (e) { res.status(500).send(log.unknownError(`${TAG}/register: ${e}`)); }
}
async login(req, res) {
try {
const {username, password} = req.body;
const user = await UserService.getByUsername(username);
if (!bcrypt.compareSync(password, user.password)) return res.status(401).send("Wrong password");
const token = genToken(user);
return res.status(200).send(token);
} catch (e) { res.status(500).send(log.unknownError(`${TAG}/login: ${e}`)); }
}
}
export default new UserController()

28
src/db.js Normal file
View File

@@ -0,0 +1,28 @@
import pg from 'pg';
import log from './utils/log.js'
import fs from 'fs'
import config from '../config.json' with {type: "json"};
const { Pool } = pg;
log.debug(
`Connecting to PG database ${config.dbname}@${config.dbhost}:${config.dbport} with credentials:
username: ${config.dbuser}
password: <hidden>`
);
const pool = new Pool({
user: config.dbuser,
host: config.dbhost,
port: config.dbport,
database: config.dbname,
password: config.dbpassword
});
log.debug("Database connection successful. Creating tables");
pool.query(fs.readFileSync('./db_schema.psql').toString());
log.debug("Tables succesfully created");
export default pool;

19
src/index.js Normal file
View File

@@ -0,0 +1,19 @@
import express from 'express';
import UserRouter from './routers/user.js';
import GroupRouter from './routers/group.js';
// import AdminRouter from './routers/admin.js';
import log from './utils/log.js'
import config from '../config.json' with {type: "json"};
const app = express();
app.use(express.urlencoded({extended: true}));
app.use(express.json());
app.use('/api/user/', UserRouter);
app.use('/api/group/', GroupRouter);
// app.use('/api/admin/', AdminRouter);
app.listen(config.port, () => {
log.info(`Application has started on port ${config.port}`)
})

63
src/middlewares/auth.js Normal file
View File

@@ -0,0 +1,63 @@
import log from '../utils/log.js'
import jwt from 'jsonwebtoken';
import config from '../../config.json' with {type: "json"};
import GroupService from '../services/group.js';
const TAG = "/middlewares/auth.js"
const requireUsernameAndPassword = async (req, res, next) => {
if (req.method == "OPTIONS") next();
try {
const {username, password} = req.body;
if (!username) return res.status(400).send("Username is required");
if (!password) return res.status(400).send("Password is required");
next();
} catch (e) { return res.status(500).send(unknownError(`${TAG}/requireUsernameAndPassword: ${e}`)); }
}
const authenticate = async (req, res, next) => {
if (req.method == "OPTIONS") next();
try {
const token = req.headers.authorization.split(' ')[1]
if (!token) return res.status(401).send("No authorization token supplied");
if (!jwt.verify(token, config.secret)) return res.status(403).send("Authorization token is incorrect");
next();
} catch (e) { return res.status(500).send(log.unknownError(`${TAG}/authenticate: ${e}`)); }
}
const authorizeGroupOwner = async (req, res, next) => {
if (req.method == "OPTIONS") next();
try {
const token = req.headers.authorization.split(' ')[1]
if (!token) return res.status(401).send("No authorization token supplied");
const { id } = req.params;
let user = jwt.decode(token, config.secret)
let adminId = await GroupService.getAdminId(id);
if (user.login.id != adminId) return res.status(403).send("Not your group");
next();
} catch (e) { return res.status(500).send(log.unknownError(`${TAG}/authorizeGroupOwner: ${e}`)); }
}
const checkGroupPassword = async (req, res, next) => {
if (req.method == "OPTIONS") next();
try {
const { id } = req.params;
const { password } = req.body;
const groupPassword = await GroupService.getPassword(id);
if (groupPassword != password) return res.status(403).send("Wrong password");
next();
} catch (e) {return res.status(500).send(log.unknownError(`${TAG}/checkGroupPassword: ${e}`));}
}
export default { requireUsernameAndPassword, authenticate, authorizeGroupOwner, checkGroupPassword }

View File

@@ -0,0 +1,51 @@
import UserService from '../services/user.js';
import GroupService from '../services/group.js';
import log from '../utils/log.js';
import statuses from '../utils/status.js';
const TAG = "/middlewares/existance.js"
const usernameExists = async (req, res, next) => {
try {
let { username } = req.body;
let user = await UserService.getByUsername(username);
if (!user || user == statuses.not_found) return res.status(404).send("User not found");
next();
} catch (e) { return res.status(500).send(log.unknownError(`${TAG}/usernameExists: ${e}`)); }
};
const usernameDoesntExist = async (req, res, next) => {
try {
let { username } = req.body;
let user = await UserService.getByUsername(username);
if (user || user != statuses.not_found) return res.status(400).send("Such username already taken");
next();
} catch (e) { return res.status(500).send(log.unknownError(`${TAG}/usernameDoesntExist: ${e}`)); }
}
const groupExists = async (req, res, next) => {
try {
const { id } = req.params;
let group = await GroupService.getById(id);
if (!group || group == statuses.not_found) return res.status(404).send("Group not found");
next();
} catch (e) { return res.status(500).send(log.unknownError(`${TAG}/groupExists: ${e}`)) }
}
const groupDoesntExist = async (req, res, next) => {
try {
const { id } = req.params;
let group = await GroupService.getById(id);
if (group || group != statuses.not_found) return res.status(400).send("Such group already exists");
next();
} catch (e) { return res.status(500).send(log.unknownError(`${TAG}/groupDoesntExist: ${e}`)) }
}
export default { usernameExists, usernameDoesntExist, groupExists, groupDoesntExist }

0
src/routers/admin.js Normal file
View File

12
src/routers/group.js Normal file
View File

@@ -0,0 +1,12 @@
import { Router } from 'express';
import auth from '../middlewares/auth.js';
import GroupController from '../controllers/group.js'
import existance from '../middlewares/existance.js';
const GroupRouter = new Router();
GroupRouter.post('/create/:name', auth.authenticate, existance.groupDoesntExist, GroupController.create);
GroupRouter.post('/join/:id', auth.authenticate, existance.groupExists, auth.checkGroupPassword, GroupController.join);
GroupRouter.post('/password/:id', auth.authenticate, existance.groupExists, auth.authorizeGroupOwner, GroupController.updatePassword)
export default GroupRouter;

12
src/routers/user.js Normal file
View File

@@ -0,0 +1,12 @@
import { Router } from 'express';
import auth from '../middlewares/auth.js';
import existance from '../middlewares/existance.js';
import UserController from '../controllers/user.js'
const UserRouter = new Router();
UserRouter.post('/register', auth.requireUsernameAndPassword, existance.usernameDoesntExist, UserController.register);
UserRouter.post('/login', auth.requireUsernameAndPassword, existance.usernameExists, UserController.login);
export default UserRouter;

34
src/services/group.js Normal file
View File

@@ -0,0 +1,34 @@
import db from '../db.js';
import status from '../utils/status.js';
class GroupService {
async create(name, creatorId) {
let res = await db.query("INSERT INTO groups (name, admin_id) VALUES ($1, $2) RETURNING ID", [name, creatorId]).catch (e => {
if (e.code == 23505) { // already exists
return status.duplicate;
}
})
return res.rows[0];
}
async getById(id) {
let res = (await db.query("SELECT * FROM groups WHERE id = $1", [id]))
if (res.rowCount == 0) return status.not_found;
return res.rows[0];
}
async getAdminId(id) {
return (await db.query("SELECT admin_id FROM groups WHERE ID = $1", [id])).rows[0].admin_id
}
async updatePassword(id, password) {
return (await db.query("UPDATE groups SET password = $1 WHERE id = $2", [password, id]));
}
async getPassword(id) {
return (await db.query("SELECT password FROM groups WHERE id = $1", [id])).rows[0].password;
}
};
export default new GroupService();

36
src/services/user.js Normal file
View File

@@ -0,0 +1,36 @@
import db from '../db.js'
import statuses from '../utils/status.js';
import bcrypt from 'bcrypt';
class UserService {
async create (username, password) {
await db.query("INSERT INTO users (username, password) VALUES ($1, $2)", [username, bcrypt.hashSync(password, 12)]).catch (e => {
if (e.code == 23505) { // already exists
return statuses.duplicate
}
})
return statuses.ok
}
async getByUsername(username) {
let user = (await db.query("SELECT * FROM Users WHERE username = $1", [username])).rows
if (user == undefined) return statuses.not_found
return (user[0]);
}
async getAll() {
return (await db.query("SELECT * FROM Users")).rows;
}
async isInGroup(userId, groupId) {
return (await db.query("SELECT * FROM Users WHERE ID = $1 AND $2 = ANY(groups)", [userId, groupId])).rowCount > 0;
}
async joinGroup(userId, groupId) {
if (await (this.isInGroup(userId, groupId))) return statuses.duplicate;
await db.query("UPDATE Users SET groups = array_append(groups, $1::integer) WHERE ID = $2", [groupId, userId]);
return statuses.ok;
}
}
export default new UserService();

9
src/utils/jwt.js Normal file
View File

@@ -0,0 +1,9 @@
import jwt from 'jsonwebtoken';
import config from '../../config.json' with {type: "json"};
const genToken = (login) => {
const payload = { login };
return jwt.sign(payload, config.secret, {expiresIn: "7d"});
};
export default genToken;

24
src/utils/log.js Normal file
View File

@@ -0,0 +1,24 @@
import config from '../../config.json' with {type: "json"};
const debug = (text) => {
if (config.debug) console.debug(`[D] [${Date()}]: ${text}`)
}
const info = (text) => {
console.log(`[I] [${Date()}]: ${text}`)
}
const error = (text) => {
console.error(`[E] [${Date()}]: ${text}`)
}
const warn = (text) => {
console.warn(`[W] [${Date()}]: ${text}`)
}
const unknownError = (text) => {
error(text);
return "Unknown server error. Please, report to the developer"
}
export default { debug, info, error, warn, unknownError }

7
src/utils/status.js Normal file
View File

@@ -0,0 +1,7 @@
const statuses = {
ok: "ok",
duplicate: "duplicate",
not_found: "not found"
}
export default statuses