Merge branch 'user-accounts' into 'develop'

Add user account functionality

See merge request sudoer777/cvcs-score-tracker!8
main
Ethan Reece 2021-11-26 20:03:58 +00:00
commit 2c747fc6fa
37 changed files with 1840 additions and 124 deletions

30
app.js
View File

@ -3,14 +3,42 @@ var express = require('express');
var path = require('path'); var path = require('path');
var cookieParser = require('cookie-parser'); var cookieParser = require('cookie-parser');
var logger = require('morgan'); var logger = require('morgan');
var random = require('./database/accounts/random');
const passport = require('passport');
const session = require('express-session');
const accounts = require('./database/accounts/accounts');
var flash = require('connect-flash');
var indexRouter = require('./routes/index'); var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users'); var usersRouter = require('./routes/users');
var dataRouter = require('./routes/data'); var dataRouter = require('./routes/data');
var manageRouter = require('./routes/manage'); var manageRouter = require('./routes/manage');
var authRouter = require('./routes/auth');
var adminRouter = require('./routes/admin');
var app = express(); var app = express();
// flash setup
app.use(flash());
// session setup
app.use(
session({
secret: random.makeid(20),
resave: false,
saveUninitialized: true,
})
);
// passport setup
app.use(passport.initialize());
app.use(passport.session());
//passport.use(accounts.createStrategy());
//passport.serializeUser(accounts.serializeUser());
//passport.deserializeUser(accounts.deserializeUser());
// view engine setup // view engine setup
app.set('views', path.join(__dirname, 'views')); app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug'); app.set('view engine', 'pug');
@ -25,6 +53,8 @@ app.use('/', indexRouter);
app.use('/users', usersRouter); app.use('/users', usersRouter);
app.use('/data', dataRouter); app.use('/data', dataRouter);
app.use('/manage', manageRouter); app.use('/manage', manageRouter);
app.use('/auth', authRouter);
app.use('/admin', adminRouter);
// catch 404 and forward to error handler // catch 404 and forward to error handler

View File

@ -0,0 +1,141 @@
const database = require('./../database');
const passport = require('passport');
const localStrategy = require('passport-local').Strategy;
const bcrypt = require('bcrypt');
class User {
constructor(id, email, isAdmin) {
this.id = id;
this.email = email;
this.isAdmin = isAdmin;
}
}
async function checkForAdminAccount() {
const adminUsersQuery = `SELECT *
FROM accounts.users
WHERE admin = true;`;
const adminUsers = await database.executeQuery(adminUsersQuery);
if(adminUsers.length == 0) {
const passwordHash = await generateHash('admin');
const createTempAdminQuery = `INSERT INTO accounts.users(email, password, admin)
VALUES('admin@example.com', $1, true);`;
database.executeQuery(createTempAdminQuery, [passwordHash]);
console.log("Created temp admin account 'admin@example.com' with password 'admin'.");
}
}
checkForAdminAccount();
passport.use(new localStrategy({
usernameField: 'email',
passwordField: 'password'},
(username, password, cb) => {
query = `SELECT user_id, email, password, admin
FROM accounts.users
WHERE email = $1`;
database.executeQuery(query, [username])
.then(result => {
if(result.length > 0) {
const first = result[0];
const matches = bcrypt.compareSync(password, first[2]);
if(matches) {
return cb(null, { id: first[0], email: first[1], admin: first[3] })
}
else
{
return cb(null, false)
}
} else {
return cb(null, false)
}
});
}));
passport.serializeUser((user, done) => {
done(null, user.id)
})
passport.deserializeUser((id, cb) => {
query = `SELECT user_id, email, admin
FROM accounts.users
WHERE user_id = $1`;
database.executeQuery(query, [parseInt(id, 10)])
.then(result => {
cb(null, result[0]);
});
});
async function generateHash(password) {
const salt = bcrypt.genSaltSync();
return bcrypt.hashSync(password, salt);
}
async function create(email, password, isAdmin) {
const hash = await generateHash(password);
const query = `INSERT INTO accounts.users(email, password, admin)
VALUES($1, $2, $3)`;
await database.executeQuery(query, [email, hash, isAdmin]);
}
async function edit(id, email, password, isAdmin) {
if(password) {
const hash = await generateHash(password);
const query = `UPDATE accounts.users
SET email = $2,
password = $3,
admin = $4
WHERE user_id = $1;`;
await database.executeQuery(query, [id, email, hash, isAdmin]);
} else {
const query = `UPDATE accounts.users
SET email = $2,
admin = $3
WHERE user_id = $1;`;
await database.executeQuery(query, [id, email, isAdmin]);
}
return new User(id, email, isAdmin);
}
async function remove(id) {
const query = `DELETE FROM accounts.users
WHERE user_id = $1
RETURNING email, admin;`;
const row = (await database.executeQuery(query, [id]))[0];
return new User(id, row[0], row[1]);
}
async function retrieveAll() {
const query = `SELECT user_id, email, admin
FROM accounts.users
ORDER BY email;`
const table = await database.executeQuery(query);
const accountsList = [];
table.forEach((row) => {
accountsList.push(new User(row[0], row[1], row[2]));
});
return accountsList;
}
async function getFromID(id) {
const query = `SELECT user_id, email, admin
FROM accounts.users
WHERE user_id = $1;`;
const row = (await database.executeQuery(query, [id]))[0];
return new User(id, row[1], row[2]);
}
exports.create = create;
exports.edit = edit;
exports.remove = remove;
exports.retrieveAll = retrieveAll;
exports.getFromID = getFromID;
exports.passport = passport;

View File

@ -0,0 +1,12 @@
function makeid(length) {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for ( var i = 0; i < length; i++ ) {
result += characters.charAt(Math.floor(Math.random() *
charactersLength));
}
return result;
}
exports.makeid = makeid;

View File

@ -26,16 +26,14 @@ async function Initialize() {
} }
async function checkForDatabaseInitialization() { async function checkForDatabaseInitialization() {
const query = `SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'scores'`; const scoresSchemaExistsQuery = `SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'scores'`;
let result = await executeQuery(query); let result = await executeQuery(scoresSchemaExistsQuery);
const scoresSchemaExists = result.length !== 0; const scoresSchemaExists = result.length !== 0;
if(!scoresSchemaExists) { if(!scoresSchemaExists) {
Initialize(); await Initialize();
} }
} }
checkForDatabaseInitialization(); checkForDatabaseInitialization();

View File

@ -22,10 +22,7 @@ scores:
accounts: accounts:
users: users:
*user_id* | email | salt | password_hash | role | approved *user_id* | email | password | admin
sessions:
*session_id* | ~user_id~ | expiration_date
*/ */
@ -33,6 +30,18 @@ accounts:
BEGIN; BEGIN;
CREATE SCHEMA IF NOT EXISTS accounts;
CREATE TABLE IF NOT EXISTS accounts.users(
user_id BIGINT GENERATED ALWAYS AS IDENTITY,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
admin BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY(user_id)
);
CREATE SCHEMA IF NOT EXISTS scores; CREATE SCHEMA IF NOT EXISTS scores;
@ -81,7 +90,7 @@ CREATE TABLE IF NOT EXISTS scores.games(
team2_id BIGINT, team2_id BIGINT,
team1_score INTEGER, team1_score INTEGER,
team2_score INTEGER, team2_score INTEGER,
/* submitter_id BIGINT,*/ submitter_id BIGINT,
updated_timestamp TIMESTAMP WITH TIME ZONE DEFAULT now(), updated_timestamp TIMESTAMP WITH TIME ZONE DEFAULT now(),
PRIMARY KEY(game_id), PRIMARY KEY(game_id),
CONSTRAINT fk_division CONSTRAINT fk_division
@ -95,11 +104,10 @@ CREATE TABLE IF NOT EXISTS scores.games(
REFERENCES scores.teams(team_id), REFERENCES scores.teams(team_id),
CONSTRAINT fk_team2 CONSTRAINT fk_team2
FOREIGN KEY(team2_id) FOREIGN KEY(team2_id)
REFERENCES scores.teams(team_id) REFERENCES scores.teams(team_id),
/* CONSTRAINT fk_submitter CONSTRAINT fk_submitter
FOREIGN KEY(submitter_id) FOREIGN KEY(submitter_id)
REFERENCES accounts.users(user_id)*/ REFERENCES accounts.users(user_id)
); );
COMMIT; COMMIT;

View File

@ -5,7 +5,7 @@ const database = require('./../database');
class Game { class Game {
constructor(id, date, team1ID, team2ID, team1Score, team2Score, divisionID, seasonID) { constructor(id, date, team1ID, team2ID, team1Score, team2Score, divisionID, seasonID, submitterID) {
this.id = id; this.id = id;
this.date = date; this.date = date;
this.team1ID = team1ID; this.team1ID = team1ID;
@ -14,17 +14,18 @@ class Game {
this.team2Score = team2Score; this.team2Score = team2Score;
this.divisionID = divisionID; this.divisionID = divisionID;
this.seasonID = seasonID; this.seasonID = seasonID;
this.submitterID = submitterID;
} }
} }
async function add(divisionID, seasonID, date, team1ID, team2ID, team1Score, team2Score) { async function add(divisionID, seasonID, date, team1ID, team2ID, team1Score, team2Score, userID) {
const query = `INSERT INTO scores.games(division_id, season_id, game_date, team1_id, team2_id, team1_score, team2_score) const query = `INSERT INTO scores.games(division_id, season_id, game_date, team1_id, team2_id, team1_score, team2_score, submitter_id)
VALUES($1, $2, $3, $4, $5, $6, $7) VALUES($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING game_id;`; RETURNING game_id;`;
const id = (await database.executeQuery(query, [divisionID, seasonID, date, team1ID, team2ID, team1Score, team2Score]))[0][0]; const id = (await database.executeQuery(query, [divisionID, seasonID, date, team1ID, team2ID, team1Score, team2Score, userID]))[0][0];
return new Game(id, date, team1ID, team2ID, team1Score, team2Score); return new Game(id, date, team1ID, team2ID, team1Score, team2Score);
} }
@ -71,6 +72,20 @@ async function retrieve(teamID, divisionID, seasonID) {
return gamesList; return gamesList;
} }
async function retrieveByUser(userID) {
const query = `SELECT game_id, division_id, season_id, game_date, team1_id, team2_id, team1_score, team2_score
FROM scores.games
WHERE submitter_id = $1
ORDER BY game_date DESC;`;
const table = await database.executeQuery(query, [userID]);
const gamesList = [];
table.forEach((row) => {
gamesList.push(new Game(row[0], row[3].toISOString().slice(0,10), row[4], row[5], row[6], row[7], row[1], row[2]));
});
return gamesList;
}
async function edit(gameID, divisionID, seasonID, date, team1ID, team2ID, team1Score, team2Score) { async function edit(gameID, divisionID, seasonID, date, team1ID, team2ID, team1Score, team2Score) {
const query = `UPDATE scores.games const query = `UPDATE scores.games
SET division_id = $2, SET division_id = $2,
@ -86,11 +101,11 @@ async function edit(gameID, divisionID, seasonID, date, team1ID, team2ID, team1S
} }
async function getFromID(gameID) { async function getFromID(gameID) {
const query = `SELECT game_id, division_id, season_id, game_date, team1_id, team2_id, team1_score, team2_score const query = `SELECT game_id, division_id, season_id, game_date, team1_id, team2_id, team1_score, team2_score, submitter_id
FROM scores.games FROM scores.games
WHERE game_id = $1;`; WHERE game_id = $1;`;
const row = (await database.executeQuery(query, [gameID]))[0]; const row = (await database.executeQuery(query, [gameID]))[0];
return new Game(row[0], row[3].toISOString().slice(0,10), row[4], row[5], row[6], row[7], row[1], row[2]); return new Game(row[0], row[3].toISOString().slice(0,10), row[4], row[5], row[6], row[7], row[1], row[2], row[8]);
} }
@ -100,5 +115,6 @@ async function getFromID(gameID) {
exports.add = add; exports.add = add;
exports.remove = remove; exports.remove = remove;
exports.retrieve = retrieve; exports.retrieve = retrieve;
exports.retrieveByUser = retrieveByUser;
exports.edit = edit; exports.edit = edit;
exports.getFromID = getFromID; exports.getFromID = getFromID;

View File

@ -69,7 +69,6 @@ async function getFromID(id) {
FROM scores.teams FROM scores.teams
WHERE team_id = $1;`; WHERE team_id = $1;`;
const row = (await database.executeQuery(query, [id]))[0]; const row = (await database.executeQuery(query, [id]))[0];
console.log(row);
return new Team(id, row[0], row[1]); return new Team(id, row[0], row[1]);
} }

1033
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,14 +8,18 @@
}, },
"dependencies": { "dependencies": {
"async": "^3.2.2", "async": "^3.2.2",
"bcrypt": "^5.0.1",
"connect-flash": "^0.1.1",
"cookie-parser": "~1.4.3", "cookie-parser": "~1.4.3",
"debug": "~2.6.9", "debug": "~2.6.9",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"express": "~4.16.0", "express": "~4.16.0",
"express-session": "^1.17.2",
"http-errors": "~1.6.2", "http-errors": "~1.6.2",
"morgan": "~1.9.0", "morgan": "~1.9.0",
"nodemailer": "^6.6.5", "nodemailer": "^6.6.5",
"passport": "^0.5.0", "passport": "^0.5.0",
"passport-local": "^1.0.0",
"pg": "^8.7.1", "pg": "^8.7.1",
"pug": "2.0.0-beta11" "pug": "2.0.0-beta11"
}, },

View File

@ -65,8 +65,27 @@ export async function getGames(teamID = undefined, divisionID = undefined, seaso
return gamesList; return gamesList;
} }
export async function getGamesByUser() {
let URL = '/data/games?user=1';
const response = await fetch(URL);
const gamesList = await response.json();
return gamesList;
}
export async function getGame(gameID) { export async function getGame(gameID) {
const response = await fetch(`/data/game?game=${gameID}`); const response = await fetch(`/data/game?game=${gameID}`);
const game = await response.json(); const game = await response.json();
return game; return game;
} }
export async function getAccounts() {
const response = await fetch(`/data/accounts`);
const accounts = await response.json();
return accounts;
}
export async function getAccount(accountID) {
const response = await fetch(`/data/account?account=${accountID}`);
const account = await response.json();
return account;
}

View File

@ -177,11 +177,14 @@ genderDropdown.onchange = listDivisions;
teamDropdown.onchange = listGames; teamDropdown.onchange = listGames;
seasonDropdown.onchange = listGames; seasonDropdown.onchange = listGames;
if(addScoreButton) {
addScoreButton.addEventListener('click', () => {
window.location.href = '/manage/game';
});
}
addScoreButton.addEventListener('click', () => { if(manageButton) {
window.location.href = '/manage/game'; manageButton.addEventListener('click', () => {
}); window.location.href = '/manage'
});
manageButton.addEventListener('click', () => { }
window.location.href = '/manage'
});

View File

@ -0,0 +1,21 @@
const logInButton = document.getElementById('login-button');
const logOutButton = document.getElementById('logout-button');
const homeButton = document.getElementById('home-button');
if(logInButton) {
logInButton.addEventListener('click', () => {
window.location.href = "/auth/login";
});
}
if(logOutButton) {
logOutButton.addEventListener('click', () => {
window.location.href = "/auth/logout";
});
}
if(homeButton) {
homeButton.addEventListener('click', () => {
window.location.href = '/';
});
}

View File

@ -294,6 +294,49 @@ CATEGORIES.push(new Category(
} }
)); ));
CATEGORIES.push(new Category(
"accounts",
async function getAccounts() {
return await Data.getAccounts();
},
async function listAccountHeaders() {
const headerRow = document.createElement('tr');
const emailHeader = document.createElement('th');
emailHeader.textContent = "Email";
headerRow.appendChild(emailHeader);
const spacerHeader = document.createElement('th');
spacerHeader.classList.add('spacer-column');
headerRow.appendChild(spacerHeader);
const adminHeader = document.createElement('th');
adminHeader.textContent = "Admin?";
headerRow.appendChild(adminHeader);
itemsListTable.appendChild(headerRow);
},
function listAccount(account, row) {
const emailCell = document.createElement('td');
emailCell.textContent = account.email;
row.appendChild(emailCell);
const spacerCell = document.createElement('td');
row.appendChild(spacerCell);
const adminCell = document.createElement('td');
adminCell.textContent = account.isAdmin;
row.appendChild(adminCell);
},
async function addAccount() {
window.location.href = "/manage/account";
},
async function editAccount(id) {
window.location.href = `/manage/account?account=${id}`;
}
));
async function listItems(category) { async function listItems(category) {

View File

@ -0,0 +1,58 @@
import * as Data from "../data.js";
import * as Form from "../form.js";
const submissionForm = document.getElementById('submission-form');
const emailTextbox = document.getElementById('email-textbox');
const passwordTextbox = document.getElementById('password-textbox');
const adminCheckboxSection = document.getElementById('admin-checkbox-section');
const adminCheckbox = document.getElementById('admin-checkbox');
const submitButton = document.getElementById('submit-button');
const deleteButton = document.getElementById('delete-button');
async function Initialize() {
let params = new URLSearchParams(location.search);
let accountID = params.get('account') || (document.getElementById('account-id') ? document.getElementById('account-id').value : null);
if(accountID) {
const account = await Data.getAccount(accountID);
console.log(account);
emailTextbox.value = account.email;
passwordTextbox.placeholder = "leave unchanged";
adminCheckbox.checked = account.isAdmin;
if(!document.getElementById('account-id')) {
adminCheckboxSection.style.visibility = "visible";
adminCheckbox.disabled = false;
Form.addHiddenValue('account', accountID, submissionForm);
}
deleteButton.style.visibility = "visible";
deleteButton.disabled = false;
}
else
{
adminCheckboxSection.style.visibility = "visible";
adminCheckbox.disabled = false;
}
emailTextbox.disabled = false;
emailTextbox.addEventListener('keyup', checkDataValidity);
passwordTextbox.disabled = false;
passwordTextbox.addEventListener('keyup', checkDataValidity);
checkDataValidity();
}
Initialize();
async function checkDataValidity() {
let dataIsValid = true;
if(!passwordTextbox.value && !passwordTextbox.placeholder) dataIsValid = false;
if(!emailTextbox.value) dataIsValid = false;
if(dataIsValid) submitButton.disabled = false;
else submitButton.disabled = true;
}
Form.addRemoveFunction(deleteButton, submissionForm, "account");

View File

@ -25,6 +25,8 @@ async function initializeForm() {
const game = await Data.getGame(gameID); const game = await Data.getGame(gameID);
Form.addHiddenValue('game', gameID, submissionForm);
Form.populateSeasons(seasonDropdown, game.seasonID); Form.populateSeasons(seasonDropdown, game.seasonID);
Data.getDivision(game.divisionID) Data.getDivision(game.divisionID)
.then(data => { .then(data => {
@ -65,6 +67,19 @@ async function initializeForm() {
team1ScoreTextbox.disabled = false; team1ScoreTextbox.disabled = false;
team2ScoreTextbox.disabled = false; team2ScoreTextbox.disabled = false;
submitButton.disabled = false; submitButton.disabled = false;
sportDropdown.onchange = () => {
Form.populateGenders(genderDropdown, sportDropdown.value)
.then(() => {
Form.populateDivisions(divisionDropdown, sportDropdown.value, genderDropdown.value);
});
Form.populateTeams(team1Dropdown, sportDropdown.value);
Form.populateTeams(team2Dropdown, sportDropdown.value);
};
genderDropdown.onchange = () => {
Form.populateDivisions(divisionDropdown, sportDropdown.value, genderDropdown.value);
};
} }
initializeForm(); initializeForm();

View File

@ -0,0 +1,132 @@
import * as Data from "./../data.js";
const gamesListTable = document.getElementById('games-list');
const addNewButton = document.getElementById('add-new-button');
const manageAccountButton = document.getElementById('manage-account-button');
function getGenderLetter(genderName) {
return genderName == "female" ? "F" : "M";
}
async function listGameHeaders() {
const headerRow = document.createElement('tr');
const teamsHeader = document.createElement('th');
teamsHeader.textContent = "Teams";
headerRow.appendChild(teamsHeader);
const scoreHeader = document.createElement('th');
headerRow.appendChild(scoreHeader);
const spacerHeader = document.createElement('th');
spacerHeader.classList.add('spacer-column');
headerRow.appendChild(spacerHeader);
const sportNameHeader = document.createElement('th');
sportNameHeader.textContent = "Sport";
headerRow.appendChild(sportNameHeader);
const dateHeader = document.createElement('th');
dateHeader.textContent = "Date";
headerRow.appendChild(dateHeader);
gamesListTable.appendChild(headerRow);
}
function listGame(game, row) {
const teamsCell = document.createElement('td');
const team1NameSpan = document.createElement('span');
Data.getTeam(game.team1ID)
.then(data => team1NameSpan.textContent = data.name);
teamsCell.appendChild(team1NameSpan);
const team2NameSpan = document.createElement('span');
Data.getTeam(game.team2ID)
.then(data => team2NameSpan.textContent = data.name);
teamsCell.appendChild(team2NameSpan);
row.appendChild(teamsCell);
const scoresCell = document.createElement('td');
const team1ScoreSpan = document.createElement('span');
team1ScoreSpan.textContent = game.team1Score;
scoresCell.appendChild(team1ScoreSpan);
const team2ScoreSpan = document.createElement('span');
team2ScoreSpan.textContent = game.team2Score;
scoresCell.appendChild(team2ScoreSpan);
row.appendChild(scoresCell);
const spacerCell = document.createElement('td');
row.appendChild(spacerCell);
const sportCell = document.createElement('td');
const sportSpan = document.createElement('span');
const divisionSpan = document.createElement('span');
divisionSpan.classList.add('flat-content');
const divisionNameSpan = document.createElement('span');
const divisionGenderSpan = document.createElement('span');
divisionSpan.appendChild(divisionNameSpan);
divisionSpan.appendChild(divisionGenderSpan);
Data.getDivision(game.divisionID)
.then(data => {
Data.getSportName(data.sportID)
.then(data => sportSpan.textContent = data);
divisionNameSpan.textContent = data.name;
divisionGenderSpan.textContent = getGenderLetter(data.gender.name);
});
sportCell.appendChild(sportSpan);
sportCell.appendChild(divisionSpan);
row.appendChild(sportCell);
const dateCell = document.createElement('td');
const yearSpan = document.createElement('span');
yearSpan.textContent = game.date.slice(0,4);
dateCell.appendChild(yearSpan);
const dateSpan = document.createElement('span');
dateSpan.textContent = game.date.slice(5);
dateCell.appendChild(dateSpan);
row.appendChild(dateCell);
}
async function addGame() {
window.location.href = "/manage/game";
}
async function editGame(id) {
window.location.href = `/manage/game?game=${id}`;
}
async function listItems() {
const gamesList = await Data.getGamesByUser();
await listGameHeaders();
gamesList.forEach(game => {
const row = document.createElement('tr');
listGame(game, row);
const manageCell = document.createElement('td');
const editSpan = document.createElement('span');
const editButton = document.createElement('button');
editButton.textContent = "E";
editButton.addEventListener('click', () => {
editGame(game.id);
});
editSpan.appendChild(editButton);
manageCell.appendChild(editSpan);
row.appendChild(manageCell);
gamesListTable.appendChild(row);
});
}
listItems();
addNewButton.addEventListener('click', () => addGame());
manageAccountButton.addEventListener('click', () => {
window.location.href = '/manage/account';
});

View File

@ -21,7 +21,6 @@ async function initializeForm() {
Form.populateSports(sportDropdown, team.sportID); Form.populateSports(sportDropdown, team.sportID);
console.log(team.sportID);
Form.addHiddenValue('team', teamID, submissionForm); Form.addHiddenValue('team', teamID, submissionForm);
} }
else { else {

View File

@ -36,3 +36,12 @@ form {
#delete-button { #delete-button {
visibility: hidden; visibility: hidden;
} }
.form-section-checkbox {
flex-direction: row;
align-items: center;
}
#admin-checkbox {
width: auto;
}

View File

@ -26,7 +26,5 @@ tr {
} }
#actions-div { #actions-div {
display: flex;
flex-direction: column;
margin-left: auto; margin-left: auto;
} }

View File

@ -17,4 +17,17 @@ a {
.flat-content { .flat-content {
flex-direction: row; flex-direction: row;
justify-content: space-between;
}
.send-to-right {
margin-left: auto;
}
#actions-div {
display: flex;
}
#home-button {
margin-right: auto;
} }

View File

@ -1,3 +1,7 @@
h1 { h1 {
text-align: center; text-align: center;
} }
#admin-checkbox-section {
visibility: hidden;
}

24
routes/admin.js 100644
View File

@ -0,0 +1,24 @@
var express = require('express');
var router = express.Router();
const passport = require('passport');
const app = require('../app');
const accounts = require('./../database/accounts/accounts');
function adminLoggedIn(req, res, next) {
if (req.user && req.user[2]) {
next();
}
else {
req.flash('error', 'An admin account is required to access this page.');
res.redirect('/auth/login');
}
}
router.get('/', adminLoggedIn, (req, res, next) => {
res.render
});
module.exports = router;

39
routes/auth.js 100644
View File

@ -0,0 +1,39 @@
var express = require('express');
var router = express.Router();
const passport = require('passport');
const accounts = require('./../database/accounts/accounts');
const app = require('../app');
function adminLoggedIn(req, res, next) {
if (req.user && req.user[2]) {
next();
}
else {
req.flash('error', 'An admin account is required to access this page.');
res.redirect('/auth/login');
}
}
router.get('/login', (req, res, next) => {
res.render('accounts/login', { title : "Login", message: req.flash('error') });
});
router.get('/logout', (req, res, next) => {
req.logout();
res.redirect("/");
});
router.post('/login',
passport.authenticate('local', {
failureRedirect: '/auth/login',
successRedirect: '/',
failureFlash: "Invalid email or password.",
}),
(req, res, next) => {
console.log(req.user);
});
module.exports = router;

View File

@ -6,7 +6,26 @@ var genders = require('../database/scores/genders');
var divisions = require('../database/scores/divisions'); var divisions = require('../database/scores/divisions');
var teams = require('../database/scores/teams'); var teams = require('../database/scores/teams');
var games = require('../database/scores/games'); var games = require('../database/scores/games');
var accounts = require('../database/accounts/accounts');
function adminLoggedIn(req, res, next) {
if (req.user && req.user[2]) {
next();
}
else {
req.flash('error', 'An admin account is required to access this page.');
res.redirect('/auth/login');
}
}
function userLoggedIn(req, res, next) {
if (req.user) {
next();
}
else {
res.redirect('/auth/login');
}
}
router.get('/sports', function(req, res, next) { router.get('/sports', function(req, res, next) {
sports.retrieveAll() sports.retrieveAll()
@ -52,8 +71,9 @@ router.get('/team', function(req, res, next) {
}) })
router.get('/games', function(req, res, next) { router.get('/games', function(req, res, next) {
games.retrieve(req.query.team, req.query.division, req.query.season) const userID = req.user ? req.user[0] : null;
.then(data => res.json(data)); if(req.query.user) games.retrieveByUser(userID).then(data => res.json(data));
else games.retrieve(req.query.team, req.query.division, req.query.season).then(data => res.json(data));
}) })
router.get('/game', function(req, res, next) { router.get('/game', function(req, res, next) {
@ -61,4 +81,22 @@ router.get('/game', function(req, res, next) {
.then(data => res.json(data)); .then(data => res.json(data));
}) })
router.get('/accounts', adminLoggedIn, function(req, res, next) {
accounts.retrieveAll()
.then(data => res.json(data));
})
router.get('/account', userLoggedIn, function(req, res, next) {
const userIsAdmin = req.user[2];
const loggedInAccountID = req.user[0];
const requestedAccountID = req.query.account;
if(!userIsAdmin && loggedInAccountID != requestedAccountID) {
res.status(403).send("ACCESS DENIED");
} else {
accounts.getFromID(req.query.account)
.then(data => res.json(data));
}
})
module.exports = router; module.exports = router;

View File

@ -3,7 +3,7 @@ var router = express.Router();
/* GET home page. */ /* GET home page. */
router.get('/', function(req, res, next) { router.get('/', function(req, res, next) {
res.render('index', { title: 'View Scores' }); res.render('index', { title: 'View Scores', userLoggedIn: !!req.user, hideHomeButton: true });
}); });
module.exports = router; module.exports = router;

View File

@ -7,19 +7,40 @@ var sports = require('../database/scores/sports');
var divisions = require('../database/scores/divisions'); var divisions = require('../database/scores/divisions');
var genders = require('../database/scores/genders'); var genders = require('../database/scores/genders');
var teams = require('../database/scores/teams'); var teams = require('../database/scores/teams');
var accounts = require('../database/accounts/accounts');
function userLoggedIn(req, res, next) {
if (req.user) {
next();
}
else {
res.redirect('/auth/login');
}
}
function adminLoggedIn(req, res, next) {
if (req.user && req.user[2]) {
next();
}
else {
req.flash('error', 'An admin account is required to access this page.');
res.redirect('/auth/login');
}
}
router.get('/', function(req, res, next) { router.get('/' ,userLoggedIn, function(req, res, next) {
res.render('manage', { title: 'Score Management' }); if(req.user[2]) res.render('manage', { title: 'Score Management', userLoggedIn: !!req.user });
else res.render('manage/manage-nonadmin', { title: "My Games", userLoggedIn: !!req.user });
}); });
router.get('/game', function(req, res, next) { router.get('/game', userLoggedIn, function(req, res, next) {
let title = req.query.game ? 'Edit Game' : 'Submit Score' let title = req.query.game ? 'Edit Game' : 'Submit Score'
res.render('manage/addgame', { title }); res.render('manage/addgame', { title, userLoggedIn: !!req.user });
}); });
router.post('/game', function(req, res, next) { router.post('/game', userLoggedIn, function(req, res, next) {
const seasonID = req.body['year']; const seasonID = req.body['year'];
const sportID = req.body['sport']; const sportID = req.body['sport'];
const gender = (req.body['gender'] == "female") ? genders.FEMALE : genders.MALE; const gender = (req.body['gender'] == "female") ? genders.FEMALE : genders.MALE;
@ -29,23 +50,33 @@ router.post('/game', function(req, res, next) {
const team1Score = req.body['team1-score']; const team1Score = req.body['team1-score'];
const team2ID = req.body['team2']; const team2ID = req.body['team2'];
const team2Score = req.body['team2-score']; const team2Score = req.body['team2-score'];
const userID = req.user[0];
const id = req.body['game']; const id = req.body['game'];
const remove = req.body['delete']; const remove = req.body['remove'];
if(remove) games.remove(id) const loggedInUserID = req.user[0];
.then(res.redirect("/manage")); const loggedInUserIsAdmin = req.user[2];
else if(id) games.edit(id, divisionId, seasonID, date, team1ID, team2ID, team1Score, team2Score)
.then(res.redirect('/manage')); games.getFromID(id)
else games.add(divisionID, seasonID, date, team1ID, team2ID, team1Score, team2Score) .then(game => {
.then(res.redirect("/manage")); if(!loggedInUserIsAdmin && loggedInUserID != game.submitterID) {
res.status(403).send("ACCESS DENIED");
}
else if(remove) games.remove(id)
.then(res.redirect("/manage"));
else if(id) games.edit(id, divisionID, seasonID, date, team1ID, team2ID, team1Score, team2Score)
.then(res.redirect('/manage'));
else games.add(divisionID, seasonID, date, team1ID, team2ID, team1Score, team2Score, userID)
.then(res.redirect("/manage"));
});
}); });
router.get('/season', function(req, res, next) { router.get('/season', adminLoggedIn, function(req, res, next) {
res.render('manage/addseason', { title: 'Add Season', currentYear : (new Date()).getFullYear() }); res.render('manage/addseason', { title: 'Add Season', currentYear : (new Date()).getFullYear(), userLoggedIn: !!req.user });
}); });
router.post('/season', function(req, res, next) { router.post('/season', adminLoggedIn, function(req, res, next) {
const year = req.body['year']; const year = req.body['year'];
const seasonID = req.body['season']; const seasonID = req.body['season'];
@ -55,11 +86,11 @@ router.post('/season', function(req, res, next) {
else seasons.add(year).then(res.redirect("/manage")); else seasons.add(year).then(res.redirect("/manage"));
}); });
router.get('/sport', function(req, res, next) { router.get('/sport', adminLoggedIn, function(req, res, next) {
res.render('manage/addsport', { title: 'Add Sport' }); res.render('manage/addsport', { title: 'Add Sport', userLoggedIn: !!req.user });
}); });
router.post('/sport', function(req, res, next) { router.post('/sport', adminLoggedIn, function(req, res, next) {
const name = req.body['name']; const name = req.body['name'];
const id = req.body['sport']; const id = req.body['sport'];
const remove = req.body['remove']; const remove = req.body['remove'];
@ -69,13 +100,13 @@ router.post('/sport', function(req, res, next) {
else sports.add(name).then(res.redirect('/manage')); else sports.add(name).then(res.redirect('/manage'));
}); });
router.get('/division', function(req, res, next) { router.get('/division', adminLoggedIn, function(req, res, next) {
let title = req.query.division ? 'Edit Division' : 'Add Division' let title = req.query.division ? 'Edit Division' : 'Add Division'
res.render('manage/adddivision', { title }); res.render('manage/adddivision', { title, userLoggedIn: !!req.user });
}); });
router.post('/division', function(req, res, next) { router.post('/division', adminLoggedIn, function(req, res, next) {
const name = req.body['name']; const name = req.body['name'];
const sport = req.body['sport']; const sport = req.body['sport'];
const genderName = req.body['gender']; const genderName = req.body['gender'];
@ -100,13 +131,13 @@ router.post('/division', function(req, res, next) {
} }
}); });
router.get('/team', function(req, res, next) { router.get('/team', adminLoggedIn, function(req, res, next) {
let title = req.query.team ? 'Edit Team' : 'Add Team' let title = req.query.team ? 'Edit Team' : 'Add Team'
res.render('manage/addteam', { title }); res.render('manage/addteam', { title, userLoggedIn: !!req.user });
}); });
router.post('/team', function(req, res, next) { router.post('/team', adminLoggedIn, function(req, res, next) {
const name = req.body['name']; const name = req.body['name'];
const sport = req.body['sport']; const sport = req.body['sport'];
@ -118,4 +149,46 @@ router.post('/team', function(req, res, next) {
else teams.add(name, sport).then(res.redirect("/manage")); else teams.add(name, sport).then(res.redirect("/manage"));
}); });
router.get('/account', userLoggedIn, (req, res, next) => {
const userIsAdmin = req.user[2];
const accountID = req.user[0];
if(userIsAdmin) {
let title = req.query.account ? 'Manage User' : 'Create User'
res.render('accounts/createuser', { title, userLoggedIn: !!req.user });
}
else {
let title = 'Manage Account';
res.render('accounts/createuser', { title, accountID, userLoggedIn: !!req.user });
}
});
router.post('/account', userLoggedIn, (req, res, next) => {
const email = req.body.email;
const password = req.body.password;
const accountID = req.body.account;
const remove = req.body.remove;
const loggedInAccountIsAdmin = req.user[2];
const loggedInAccountID = req.user[0];
console.log(accountID);
console.log(loggedInAccountID);
if(!loggedInAccountIsAdmin && accountID != loggedInAccountID) {
res.status(403).send("ACCESS DENIED");
}
else {
const isAdmin = loggedInAccountIsAdmin ? !!req.body.admin : false;
if(remove) accounts.remove(accountID).then(res.redirect('/manage'));
if(accountID) accounts.edit(accountID, email, password, isAdmin).then(res.redirect('/manage'));
else accounts.create(req.body.email, req.body.password, !!req.body.admin).then(res.redirect('/manage'));
}
});
module.exports = router; module.exports = router;

View File

@ -0,0 +1,29 @@
extends ../layout
block stylesheets
link(rel='stylesheet', href='/stylesheets/submit.css')
link(rel='stylesheet', href='/stylesheets/form.css')
block content
form#submission-form(action='/manage/account', method='POST')
if accountID
input#account-id(type="hidden" name="account" value=accountID)
span(class='form-section')
label Email
span(class='form-section-input')
input#email-textbox(type="email", name="email" disabled)
span(class='form-section')
label Password
span(class='form-section-input' )
input#password-textbox(type="password" name="password" disabled)
span#admin-checkbox-section(class='form-section')
span(class='form-section-checkbox')
input#admin-checkbox(type="checkbox" name="admin" disabled)
label(for="admin-checkbox") Grant admin privileges
span(class='form-section')
button#submit-button(type="submit" disabled) Submit
span(class='form-section')
button#delete-button(type="delete" disabled) Delete
block scripts
script(src='/scripts/manage/account.js' type="module")

View File

@ -0,0 +1,19 @@
extends ../layout
block stylesheets
link(rel='stylesheet', href='/stylesheets/submit.css')
link(rel='stylesheet', href='/stylesheets/form.css')
block content
form(action='/auth/login', method='POST')
span(class='form-section')
label Email
span(class='form-section-input')
input(type="email", name="email")
span(class='form-section')
label Password
span(class='form-section-input')
input(type="password", name="password")
.error #{message}
span(class='form-section')
button#submit-button(type="submit") Submit

View File

@ -4,13 +4,13 @@ block stylesheets
link(rel='stylesheet', href='/stylesheets/index.css') link(rel='stylesheet', href='/stylesheets/index.css')
link(rel='stylesheet', href='/stylesheets/form.css') link(rel='stylesheet', href='/stylesheets/form.css')
block actions
if userLoggedIn
button#add-score-button Submit score
button#manage-button Manage...
block content block content
div#mobile-view
div#header-div
h1 Score Tracker
div#actions-div
button#add-score-button +
button#manage-button Manage
div div
span(class='form-section') span(class='form-section')
label Year label Year

View File

@ -1,10 +1,21 @@
doctype html doctype html
html html
head head
title= title title= title + ' - Score Tracker'
meta(name='viewport', content='width=device-width, initial-scale=1') meta(name='viewport', content='width=device-width, initial-scale=1')
link(rel='stylesheet', href='/stylesheets/style.css') link(rel='stylesheet', href='/stylesheets/style.css')
block stylesheets block stylesheets
body body
block content div#mobile-view
div#actions-div
if !hideHomeButton
button#home-button Home
block actions
if userLoggedIn
button#logout-button Log out
else if userLoggedIn !== undefined
button#login-button Log in
h1 #{title}
block content
block scripts block scripts
script(src='/scripts/main.js' type="module")

View File

@ -5,8 +5,6 @@ block stylesheets
link(rel='stylesheet', href='/stylesheets/form.css') link(rel='stylesheet', href='/stylesheets/form.css')
block content block content
div#mobile-view
h1 Management Panel
div div
span(class='form-section') span(class='form-section')
label Category label Category
@ -17,6 +15,7 @@ block content
option(value="divisions") Divisions option(value="divisions") Divisions
option(value="teams") Teams option(value="teams") Teams
option(value="games") Games option(value="games") Games
option(value="accounts") Accounts
div div
h2#table-header h2#table-header
table#items-list table#items-list

View File

@ -1,12 +1,10 @@
extends layout extends ../layout
block stylesheets block stylesheets
link(rel='stylesheet', href='/stylesheets/submit.css') link(rel='stylesheet', href='/stylesheets/submit.css')
link(rel='stylesheet', href='/stylesheets/form.css') link(rel='stylesheet', href='/stylesheets/form.css')
block content block content
div#mobile-view
h1 #{title}
form#submission-form(action='./division', method='POST') form#submission-form(action='./division', method='POST')
span(class='form-section') span(class='form-section')
label Sport label Sport

View File

@ -1,13 +1,11 @@
extends layout extends ../layout
block stylesheets block stylesheets
link(rel='stylesheet', href='/stylesheets/submit.css') link(rel='stylesheet', href='/stylesheets/submit.css')
link(rel='stylesheet', href='/stylesheets/form.css') link(rel='stylesheet', href='/stylesheets/form.css')
block content block content
div#mobile-view form#submission-form(action='./game', method='POST')
h1 #{title}
form#submission-form(action='./submitgame', method='POST')
span(class='form-section') span(class='form-section')
label Year label Year
span(class='form-section-input') span(class='form-section-input')

View File

@ -1,12 +1,10 @@
extends layout extends ../layout
block stylesheets block stylesheets
link(rel='stylesheet', href='/stylesheets/submit.css') link(rel='stylesheet', href='/stylesheets/submit.css')
link(rel='stylesheet', href='/stylesheets/form.css') link(rel='stylesheet', href='/stylesheets/form.css')
block content block content
div#mobile-view
h1 #{title}
form(action='./season', method='POST') form(action='./season', method='POST')
span(class='form-section') span(class='form-section')
label Ending year label Ending year

View File

@ -1,12 +1,10 @@
extends layout extends ../layout
block stylesheets block stylesheets
link(rel='stylesheet', href='/stylesheets/submit.css') link(rel='stylesheet', href='/stylesheets/submit.css')
link(rel='stylesheet', href='/stylesheets/form.css') link(rel='stylesheet', href='/stylesheets/form.css')
block content block content
div#mobile-view
h1#main-header Add Sport
form#submission-form(action='./sport', method='POST') form#submission-form(action='./sport', method='POST')
span(class='form-section') span(class='form-section')
label Sport name label Sport name

View File

@ -1,12 +1,10 @@
extends layout extends ../layout
block stylesheets block stylesheets
link(rel='stylesheet', href='/stylesheets/submit.css') link(rel='stylesheet', href='/stylesheets/submit.css')
link(rel='stylesheet', href='/stylesheets/form.css') link(rel='stylesheet', href='/stylesheets/form.css')
block content block content
div#mobile-view
h1 #{title}
form#submission-form(action='./team', method='POST') form#submission-form(action='./team', method='POST')
span(class='form-section') span(class='form-section')
label Sport label Sport

View File

@ -0,0 +1,17 @@
extends ../layout
block stylesheets
link(rel='stylesheet', href='/stylesheets/manage.css')
link(rel='stylesheet', href='/stylesheets/form.css')
block actions
button#manage-account-button(class="send-to-right") Manage account
block content
div
table#games-list
div
button#add-new-button Add new...
block scripts
script(src='/scripts/manage/manage-nonadmin.js' type="module")