Skip to content

Commit

Permalink
Adding security features
Browse files Browse the repository at this point in the history
  • Loading branch information
hgorges committed Sep 10, 2024
1 parent e0cbec5 commit aa06a6e
Show file tree
Hide file tree
Showing 23 changed files with 594 additions and 88 deletions.
292 changes: 292 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
"scripts": {
"clean": "rimraf ./dist",
"build:watch": "tsc --watch",
"start": "nodemon",
"start": "NODE_ENV=production node ./dist/server.js",
"start:debug": "nodemon",
"test": "jest",
"lint:check": "eslint .",
"format:check": "prettier . --check",
"format:fix": "prettier . --write",
"docker:up": "docker compose --env-file ./secrets/.env up",
"docker:down": "docker-compose --env-file ./secrets/.env down",
"docker:rmi": "docker rmi daily-dash postgres redis",
"docker:rmi": "docker rmi hgorges/daily-dash postgres redis",
"seed": "knex --knexfile ./dist/db/knexfile.js seed:run"
},
"repository": {
Expand Down Expand Up @@ -49,6 +50,7 @@
"@types/helmet": "^4.0.0",
"@types/jest": "^29.5.11",
"@types/json-schema": "^7.0.15",
"@types/morgan": "^1.9.9",
"@types/node": "^20.10.7",
"@types/nodemailer": "^6.4.15",
"@types/nodemailer-sendgrid": "^1.0.3",
Expand Down Expand Up @@ -103,13 +105,15 @@
"jsonwebtoken": "^9.0.2",
"knex": "^3.1.0",
"locale-codes": "^1.3.1",
"morgan": "^1.10.0",
"nodemailer": "^6.9.14",
"nodemailer-sendgrid": "^1.0.3",
"nodemailer-sendgrid-transport": "^0.2.0",
"pg": "^8.12.0",
"redis": "^4.6.14",
"rss-parser": "^3.13.0",
"serve-favicon": "^2.5.0",
"winston": "^3.14.2",
"xml-js": "^1.6.11"
}
}
102 changes: 46 additions & 56 deletions public/css/content/admin.css
Original file line number Diff line number Diff line change
@@ -1,76 +1,66 @@
.content-container {
display: flex;
#user-management {
display: grid;
grid-template-columns: 1fr 1fr;
justify-content: space-between;
overflow: auto;
}

#dashboard-summary,
#user-list {
flex: 1;
margin: 10px;
}

#dashboard-summary {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
.user-view {
gap: 20px;
max-width: 1200px;
margin: 20px;
padding: 20px;
margin-bottom: 20px;
}

#dashboard-summary h2 {
color: #333;
margin-top: 0;
}

.stat {
margin-bottom: 10px;
}

.stat-title {
font-weight: bold;
}

.stat-value {
float: right;
}

#user-list {
background-color: #fff;
background-color: var(--background-color);
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 20px;
}

#user-list h2 {
color: #333;
margin-top: 0;
#user-management h2 {
font-size: 1.5em;
color: var(--primary-color);
margin-bottom: 10px;
}

#user-list ul {
#user-management ul {
list-style: none;
padding: 0;
margin: 0;
}

#user-list li {
border-bottom: 1px solid #eee;
padding: 10px 0;
}

#user-list li:last-child {
border-bottom: none;
#user-management li {
color: var(--primary-color);
background-color: var(--secondary-color);
border: 1px solid var(--primary-color);
padding: 10px;
margin: 10px;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}

#user-list a {
color: #007bff;
text-decoration: none;
.admin-button {
background-color: var(--background-color);
color: var(--primary-color);
border: none;
padding: 5px 10px;
margin: 0 15px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
font-size: large;
}

#user-list a:hover {
text-decoration: underline;
.admin-button:hover {
background-color: var(--primary-color);
color: var(--secondary-color);
}

.stat:after {
content: '';
display: table;
clear: both;
}
@media (max-width: 768px) {
#user-management {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
justify-content: space-between;
}
}
44 changes: 44 additions & 0 deletions public/js/admin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
document.addEventListener('DOMContentLoaded', () => {
const approveButtons = document.getElementsByClassName('approve-button');
Array.from(approveButtons).forEach((approveButton) => {
approveButton.addEventListener('click', async (event) => {
try {
await fetch(
`/admin/approve-user/${event.target.dataset.userId}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'CSRF-Token': document
.querySelector('meta[name="csrf-token"]')
.getAttribute('content'),
},
},
);
window.location.reload();
} catch (error) {
console.error('Error:', error);
}
});
});

const lockButtons = document.getElementsByClassName('lock-button');
Array.from(lockButtons).forEach((lockButton) => {
lockButton.addEventListener('click', async (event) => {
try {
await fetch(`/admin/lock-user/${event.target.dataset.userId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'CSRF-Token': document
.querySelector('meta[name="csrf-token"]')
.getAttribute('content'),
},
});
window.location.reload();
} catch (error) {
console.error('Error:', error);
}
});
});
});
31 changes: 31 additions & 0 deletions src/config/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import winston from 'winston';
import fs from 'fs';
import path from 'path';
import morgan from 'morgan';

export const logger = winston.createLogger({
exitOnError: false,
format: winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss',
}),
winston.format.colorize(),
winston.format.errors({ stack: true }),
winston.format.printf(
(info) => `${info.timestamp} ${info.level}: ${info.message}`,
),
),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'logfile.log' }),
],
});

const accessLogStream = fs.createWriteStream(
path.join(__dirname, '..', '..', 'access.log'),
{
flags: 'a',
},
);

export const accessLogger = morgan('short', { stream: accessLogStream });
3 changes: 2 additions & 1 deletion src/config/store.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import RedisStore from 'connect-redis';
import { createClient } from 'redis';
import { logger } from './logger';

const redisClient = createClient({
url: `redis://default:${process.env.REDIS_PASSWORD}@cache:${process.env.REDIS_PORT}`,
});
redisClient.connect().catch(console.error);
redisClient.connect().catch(logger.error);

export default new RedisStore({
client: redisClient,
Expand Down
45 changes: 45 additions & 0 deletions src/controller/admin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NextFunction, Request, Response } from 'express-serve-static-core';
import userModel from '../models/userModel';

export async function renderAdminPage(
req: Request,
Expand All @@ -10,8 +11,52 @@ export async function renderAdminPage(
path: '/admin',
csrfToken: res.locals.csrfToken,
isAdmin: req.session.isAdmin,
approvedUsers: (
(await userModel.getUsersByApprovalState(true)) ?? []
).map((user) => {
return {
id: user.user_id,
username: user.username,
email: user.email,
};
}),
lockedUsers: (
(await userModel.getUsersByApprovalState(false)) ?? []
).map((user) => {
return {
id: user.user_id,
username: user.username,
email: user.email,
};
}),
});
} else {
res.redirect('/');
}
}

export async function approveUser(
req: Request,
res: Response,
_next: NextFunction,
): Promise<void> {
if (req.session.isAdmin) {
await userModel.updateUserApprovalStatus(req.params.id, true);
res.status(200).send();
} else {
res.redirect('/');
}
}

export async function lockUser(
req: Request,
res: Response,
_next: NextFunction,
): Promise<void> {
if (req.session.isAdmin) {
await userModel.updateUserApprovalStatus(req.params.id, false);
res.status(200).send();
} else {
res.redirect('/');
}
}
1 change: 1 addition & 0 deletions src/controller/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export async function renderDashboardPage(
res.status(200).render('dashboard', {
path: '/',
csrfToken: res.locals.csrfToken,
cspNonce: res.locals.cspNonce,
isAdmin: req.session.isAdmin,
...(await newsModel.getNewsData()),
weatherData: await weatherModel.getWeatherData(
Expand Down
3 changes: 2 additions & 1 deletion src/controller/error.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NextFunction, Request, Response } from 'express-serve-static-core';
import { logger } from '../config/logger';

export function renderErrorPage(
req: Request,
Expand All @@ -18,6 +19,6 @@ export function errorHandler(
_next: NextFunction,
): void {
// TODO render error page here to allow custom status codes
console.error(error);
logger.error(error);
res.redirect(`/error`);
}
11 changes: 11 additions & 0 deletions src/controller/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,19 @@ export async function login(
return;
}

if (!user.is_approved) {
req.flash('error', 'Your account has not been approved yet!');
renderLogin(req, res, next, {
statusCode: 403,
errors: [],
...req.body,
});
return;
}

req.session.username = user.username;
req.session.isAdmin = user.is_admin;
req.session.isApproved = user.is_approved;
req.session.isAuthenticated = true;
req.session.isHome = true;
req.session.save();
Expand Down
2 changes: 1 addition & 1 deletion src/controller/passwordReset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export async function createPasswordResetToken(
username: user.username,
firstName: user.first_name,
lastName: user.last_name,
resetLink: `https://cryptospace.dev/password-change/${randomUuid}`,
resetLink: `https://daily-dash.cryptospace.dev/password-change/${randomUuid}`,
},
),
});
Expand Down
1 change: 1 addition & 0 deletions src/db/migrations/002_users_migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export async function up(knex: Knex): Promise<void> {
table.string('created_by', 30).defaultTo('system').notNullable();
table.dateTime('updated_at').defaultTo('now()').notNullable();
table.string('updated_by', 30).defaultTo('system').notNullable();
table.boolean('is_approved').defaultTo(false).notNullable();
});
await knex.raw(/* sql */ `ALTER TABLE "users" OWNER TO postgres;`);
}
Expand Down
2 changes: 2 additions & 0 deletions src/db/seeds/001_users_seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export async function seed(knex: Knex): Promise<void> {
created_by: 'system',
updated_at: knex.raw('now()'),
updated_by: 'system',
is_approved: false,
},
{
username: 'test',
Expand All @@ -35,6 +36,7 @@ export async function seed(knex: Knex): Promise<void> {
created_by: 'system',
updated_at: knex.raw('now()'),
updated_by: 'system',
is_approved: false,
},
]);
}
2 changes: 1 addition & 1 deletion src/middleware/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export async function checkAuthentication(
res: Response,
next: NextFunction,
): Promise<void> {
if (req.session.isAuthenticated) {
if (req.session.isAuthenticated && req.session.isApproved) {
const user = await userModel.getUserByUsername(req.session.username);
assert(user, 'User not found in database');

Expand Down
Loading

0 comments on commit aa06a6e

Please sign in to comment.