From c5d31cb445a46c3d7248763cda664a5799aa6f06 Mon Sep 17 00:00:00 2001 From: hgorges Date: Sat, 24 Aug 2024 18:26:54 +0200 Subject: [PATCH] Adding input error highlighting --- public/css/main.css | 5 + src/controller/{adminPage.ts => admin.ts} | 0 .../{dashboardPage.ts => dashboard.ts} | 0 src/controller/{loginPage.ts => login.ts} | 20 ++- .../{notFoundPage.ts => notFound.ts} | 0 ...asswordChangePage.ts => passwordChange.ts} | 20 ++- ...{passwordResetPage.ts => passwordReset.ts} | 3 + .../{settingsPage.ts => settings.ts} | 36 ++++- src/controller/{signupPage.ts => signup.ts} | 56 ++++++- src/routes/sessionRoutes.ts | 8 +- src/routes/userRoutes.ts | 8 +- src/utils/utils.ts | 28 ++-- src/validators/validateEmptyBody.ts | 2 +- src/validators/validateLogin.ts | 2 +- src/validators/validatePasswordChange.ts | 2 +- src/validators/validatePasswordReset.ts | 2 +- src/validators/validateSettings.ts | 3 +- src/validators/validateSignup.ts | 2 +- views/login.ejs | 18 ++- views/password-change.ejs | 22 ++- views/password-reset.ejs | 9 +- views/settings.ejs | 139 +++++++++++++----- views/signup.ejs | 60 ++++++-- 23 files changed, 358 insertions(+), 87 deletions(-) rename src/controller/{adminPage.ts => admin.ts} (100%) rename src/controller/{dashboardPage.ts => dashboard.ts} (100%) rename src/controller/{loginPage.ts => login.ts} (73%) rename src/controller/{notFoundPage.ts => notFound.ts} (100%) rename src/controller/{passwordChangePage.ts => passwordChange.ts} (80%) rename src/controller/{passwordResetPage.ts => passwordReset.ts} (94%) rename src/controller/{settingsPage.ts => settings.ts} (69%) rename src/controller/{signupPage.ts => signup.ts} (61%) diff --git a/public/css/main.css b/public/css/main.css index f72686f..1815390 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -97,6 +97,11 @@ p { border-left: 4px solid var(--error-color); } +.invalid { + /* HACK !important */ + border-color: red !important; +} + @media (max-width: 768px) { #page-content { height: calc(100svh - 3.5rem - 2rem); diff --git a/src/controller/adminPage.ts b/src/controller/admin.ts similarity index 100% rename from src/controller/adminPage.ts rename to src/controller/admin.ts diff --git a/src/controller/dashboardPage.ts b/src/controller/dashboard.ts similarity index 100% rename from src/controller/dashboardPage.ts rename to src/controller/dashboard.ts diff --git a/src/controller/loginPage.ts b/src/controller/login.ts similarity index 73% rename from src/controller/loginPage.ts rename to src/controller/login.ts index 04c60c2..b3e3e58 100644 --- a/src/controller/loginPage.ts +++ b/src/controller/login.ts @@ -1,5 +1,6 @@ import { NextFunction, Request, Response } from 'express-serve-static-core'; import userModel from '../models/userModel'; +import { ValidationError } from '../utils/utils'; export async function renderLogin( req: Request, @@ -7,6 +8,7 @@ export async function renderLogin( _next: NextFunction, renderOptions: { statusCode?: number; + errors?: ValidationError[]; username?: string; password?: string; } = {}, @@ -24,6 +26,7 @@ export async function renderLogin( errorMessage: errorMessage.length > 0 ? errorMessage[0] : null, username: '', password: '', + errors: [], ...renderOptions, }); } @@ -43,7 +46,22 @@ export async function login( const user = await userModel.getUserForLogin(username, password); if (!user) { req.flash('error', 'Invalid username or password!'); - renderLogin(req, res, next, { statusCode: 422, ...req.body }); + renderLogin(req, res, next, { + statusCode: 422, + errors: [ + { + param: 'username', + message: 'Invalid username or password!', + value: username, + }, + { + param: 'password', + message: 'Invalid username or password!', + value: password, + }, + ], + ...req.body, + }); return; } diff --git a/src/controller/notFoundPage.ts b/src/controller/notFound.ts similarity index 100% rename from src/controller/notFoundPage.ts rename to src/controller/notFound.ts diff --git a/src/controller/passwordChangePage.ts b/src/controller/passwordChange.ts similarity index 80% rename from src/controller/passwordChangePage.ts rename to src/controller/passwordChange.ts index 7c37b54..2e2e248 100644 --- a/src/controller/passwordChangePage.ts +++ b/src/controller/passwordChange.ts @@ -3,6 +3,7 @@ import userModel from '../models/userModel'; import mailer from '../config/mailer'; import { renderFile } from 'ejs'; import path from 'path'; +import { ValidationError } from '../utils/utils'; export async function renderPasswordChange( req: Request, @@ -10,6 +11,7 @@ export async function renderPasswordChange( _next: NextFunction, renderOptions: { statusCode?: number; + errors?: ValidationError[]; password?: string; confirm_password?: string; } = {}, @@ -34,6 +36,7 @@ export async function renderPasswordChange( errorMessage: errorMessage.length > 0 ? errorMessage[0] : null, password: '', confirm_password: '', + errors: [], ...renderOptions, }); } @@ -49,7 +52,22 @@ export async function changePassword( if (password !== confirm_password) { req.flash('error', 'Passwords do not match!'); req.params.passwordResetToken = passwordResetToken; - renderPasswordChange(req, res, next, { statusCode: 422, ...req.body }); + renderPasswordChange(req, res, next, { + statusCode: 422, + errors: [ + { + param: 'password', + message: 'Passwords do not match!', + value: password, + }, + { + param: 'confirm_password', + message: 'Passwords do not match!', + value: confirm_password, + }, + ], + ...req.body, + }); return; } diff --git a/src/controller/passwordResetPage.ts b/src/controller/passwordReset.ts similarity index 94% rename from src/controller/passwordResetPage.ts rename to src/controller/passwordReset.ts index 2e28e4b..e4137ff 100644 --- a/src/controller/passwordResetPage.ts +++ b/src/controller/passwordReset.ts @@ -3,6 +3,7 @@ import userModel from '../models/userModel'; import mailer from '../config/mailer'; import { renderFile } from 'ejs'; import path from 'path'; +import { ValidationError } from '../utils/utils'; export async function renderPasswordReset( req: Request, @@ -10,6 +11,7 @@ export async function renderPasswordReset( _next: NextFunction, renderOptions: { statusCode?: number; + errors?: ValidationError[]; email?: string; } = {}, ): Promise { @@ -25,6 +27,7 @@ export async function renderPasswordReset( infoMessage: infoMessage.length > 0 ? infoMessage[0] : null, errorMessage: errorMessage.length > 0 ? errorMessage[0] : null, email: '', + errors: [], ...renderOptions, }); } diff --git a/src/controller/settingsPage.ts b/src/controller/settings.ts similarity index 69% rename from src/controller/settingsPage.ts rename to src/controller/settings.ts index 2245157..8a39a49 100644 --- a/src/controller/settingsPage.ts +++ b/src/controller/settings.ts @@ -1,5 +1,6 @@ import { NextFunction, Request, Response } from 'express-serve-static-core'; import userModel from '../models/userModel'; +import { ValidationError } from '../utils/utils'; export async function renderSettingsPage( req: Request, @@ -7,6 +8,7 @@ export async function renderSettingsPage( _next: NextFunction, renderOptions: { statusCode?: number; + errors?: ValidationError[]; username?: string; first_name?: string; last_name?: string; @@ -40,6 +42,7 @@ export async function renderSettingsPage( work_longitude: user?.work_gps.y, password: '', confirm_password: '', + errors: [], ...renderOptions, }); } @@ -67,13 +70,38 @@ export async function saveSettings( if (password !== confirm_password) { req.flash('error', 'Passwords do not match!'); - renderSettingsPage(req, res, next, { statusCode: 422, ...req.body }); + renderSettingsPage(req, res, next, { + statusCode: 422, + errors: [ + { + param: 'password', + message: 'Passwords do not match!', + value: password, + }, + { + param: 'confirm_password', + message: 'Passwords do not match!', + value: confirm_password, + }, + ], + ...req.body, + }); return; } - if (password == null || password.length <= 7) { - req.flash('error', 'Password must be at least 8 characters long!'); - renderSettingsPage(req, res, next, { statusCode: 422, ...req.body }); + if (password != null && password.trim() !== '' && password.length <= 7) { + req.flash('error', 'Password must be at least s8 characters long!'); + renderSettingsPage(req, res, next, { + statusCode: 422, + errors: [ + { + param: 'password', + message: 'Password must be at least 8 characters long!', + value: password, + }, + ], + ...req.body, + }); return; } diff --git a/src/controller/signupPage.ts b/src/controller/signup.ts similarity index 61% rename from src/controller/signupPage.ts rename to src/controller/signup.ts index d7c7ba3..30c92eb 100644 --- a/src/controller/signupPage.ts +++ b/src/controller/signup.ts @@ -3,6 +3,7 @@ import userModel from '../models/userModel'; import mailer from '../config/mailer'; import { renderFile } from 'ejs'; import path from 'path'; +import { ValidationError } from '../utils/utils'; export async function renderSignup( req: Request, @@ -10,6 +11,7 @@ export async function renderSignup( _next: NextFunction, renderOptions: { statusCode?: number; + errors?: ValidationError[]; username?: string; first_name?: string; last_name?: string; @@ -32,6 +34,7 @@ export async function renderSignup( email: '', password: '', confirm_password: '', + errors: [], ...renderOptions, }); } @@ -53,23 +56,68 @@ export async function signup( const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; if (!emailRegex.test(email)) { req.flash('error', 'Valid email is required!'); - renderSignup(req, res, next, { statusCode: 422, ...req.body }); + renderSignup(req, res, next, { + statusCode: 422, + errors: [ + { + param: 'email', + message: 'Valid email is required!', + value: email, + }, + ], + ...req.body, + }); return; } if (await userModel.getUserByUsername(username)) { req.flash('error', 'Username is already taken!'); - renderSignup(req, res, next, { statusCode: 422, ...req.body }); + renderSignup(req, res, next, { + statusCode: 422, + errors: [ + { + param: 'username', + message: 'Username is already taken!', + value: username, + }, + ], + ...req.body, + }); return; } if (await userModel.getUserByEmail(email)) { req.flash('error', 'Email is already taken!'); - renderSignup(req, res, next, { statusCode: 422, ...req.body }); + renderSignup(req, res, next, { + statusCode: 422, + errors: [ + { + param: 'email', + message: 'Email is already taken!', + value: email, + }, + ], + ...req.body, + }); return; } if (password !== confirm_password) { req.flash('error', 'Passwords do not match!'); - renderSignup(req, res, next, { statusCode: 422, ...req.body }); + renderSignup(req, res, next, { + statusCode: 422, + errors: [ + { + param: 'password', + message: 'Passwords do not match!', + value: password, + }, + { + param: 'confirm_password', + message: 'Passwords do not match!', + value: confirm_password, + }, + ], + ...req.body, + }); return; } diff --git a/src/routes/sessionRoutes.ts b/src/routes/sessionRoutes.ts index a183825..40a87c4 100644 --- a/src/routes/sessionRoutes.ts +++ b/src/routes/sessionRoutes.ts @@ -7,13 +7,13 @@ import validatePasswordChange from '../validators/validatePasswordChange'; import { changePassword, renderPasswordChange, -} from '../controller/passwordChangePage'; +} from '../controller/passwordChange'; import { createPasswordResetToken, renderPasswordReset, -} from '../controller/passwordResetPage'; -import { login, renderLogin } from '../controller/loginPage'; -import { renderSignup, signup } from '../controller/signupPage'; +} from '../controller/passwordReset'; +import { login, renderLogin } from '../controller/login'; +import { renderSignup, signup } from '../controller/signup'; const sessionRouter = express.Router(); diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts index d03bf46..ab962d6 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -3,16 +3,16 @@ import { redirectFromGoogle, redirectToGoogle, } from '../middleware/google-auth'; -import { renderSettingsPage, saveSettings } from '../controller/settingsPage'; +import { renderSettingsPage, saveSettings } from '../controller/settings'; import cors from 'cors'; import { completeTodo, postponeTodo, renderDashboardPage, switchLocation, -} from '../controller/dashboardPage'; -import { renderAdminPage } from '../controller/adminPage'; -import { renderNotFoundPage } from '../controller/notFoundPage'; +} from '../controller/dashboard'; +import { renderAdminPage } from '../controller/admin'; +import { renderNotFoundPage } from '../controller/notFound'; import validateEmptyBody from '../validators/validateEmptyBody'; import validateSettings from '../validators/validateSettings'; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 7225f39..09a988a 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -12,6 +12,12 @@ export type AuthSession = Session & { googleCalendarAccessToken?: string; }; +export type ValidationError = { + param: string; + message: string | undefined; + value: any; +}; + export const fileRoot = path.join(__dirname, '..', '..'); export function validate( @@ -28,27 +34,25 @@ export function validate( ): void { const isValid = validateFunction(req.body); if (!isValid && validateFunction.errors) { - const error = parseErrors(validateFunction.errors); - req.flash('error', error.map((e) => e.message).join(', ')); - renderFunction(req, res, next, { statusCode: 422, ...req.body }); + const errors = parseErrors(validateFunction.errors); + req.flash('error', errors.map((error) => error.message).join(', ')); + renderFunction(req, res, next, { + statusCode: 422, + errors, + ...req.body, + }); return; } next(); } function parseErrors(validationErrors: ErrorObject[]): any[] { - const errors: any[] = []; + const errors: ValidationError[] = []; validationErrors.forEach((error) => { errors.push({ - param: - error.params['missingProperty'] !== undefined - ? error.params['missingProperty'] - : error.instancePath, + param: error.instancePath.replace(/[./]+/g, ''), message: error.message, - value: - error.params['missingProperty'] !== undefined - ? null - : error.data, + value: error.data, }); }); return errors; diff --git a/src/validators/validateEmptyBody.ts b/src/validators/validateEmptyBody.ts index 150991d..7a24f6b 100644 --- a/src/validators/validateEmptyBody.ts +++ b/src/validators/validateEmptyBody.ts @@ -2,7 +2,7 @@ import ajv from '../config/ajv'; import { JSONSchemaType } from 'ajv'; import { NextFunction, Request, Response } from 'express-serve-static-core'; import { validate } from '../utils/utils'; -import { renderLogin } from '../controller/loginPage'; +import { renderLogin } from '../controller/login'; const emptyBodySchema: JSONSchemaType<{ _csrf: string; diff --git a/src/validators/validateLogin.ts b/src/validators/validateLogin.ts index 61eec1a..f98bc90 100644 --- a/src/validators/validateLogin.ts +++ b/src/validators/validateLogin.ts @@ -2,7 +2,7 @@ import ajv from '../config/ajv'; import { JSONSchemaType } from 'ajv'; import { NextFunction, Request, Response } from 'express-serve-static-core'; import { validate } from '../utils/utils'; -import { renderLogin } from '../controller/loginPage'; +import { renderLogin } from '../controller/login'; const loginSchema: JSONSchemaType<{ username: string; diff --git a/src/validators/validatePasswordChange.ts b/src/validators/validatePasswordChange.ts index 9791c71..0d964cb 100644 --- a/src/validators/validatePasswordChange.ts +++ b/src/validators/validatePasswordChange.ts @@ -2,7 +2,7 @@ import ajv from '../config/ajv'; import { JSONSchemaType } from 'ajv'; import { NextFunction, Request, Response } from 'express-serve-static-core'; import { validate } from '../utils/utils'; -import { renderPasswordChange } from '../controller/passwordChangePage'; +import { renderPasswordChange } from '../controller/passwordChange'; const passwordChangeSchema: JSONSchemaType<{ userId: string; diff --git a/src/validators/validatePasswordReset.ts b/src/validators/validatePasswordReset.ts index 53c831b..e4448f4 100644 --- a/src/validators/validatePasswordReset.ts +++ b/src/validators/validatePasswordReset.ts @@ -2,7 +2,7 @@ import ajv from '../config/ajv'; import { JSONSchemaType } from 'ajv'; import { NextFunction, Request, Response } from 'express-serve-static-core'; import { validate } from '../utils/utils'; -import { renderPasswordReset } from '../controller/passwordResetPage'; +import { renderPasswordReset } from '../controller/passwordReset'; const passwordResetSchema: JSONSchemaType<{ email: string; diff --git a/src/validators/validateSettings.ts b/src/validators/validateSettings.ts index 78b77e3..2105298 100644 --- a/src/validators/validateSettings.ts +++ b/src/validators/validateSettings.ts @@ -3,7 +3,7 @@ import { JSONSchemaType } from 'ajv'; import { NextFunction, Request, Response } from 'express-serve-static-core'; import { validate } from '../utils/utils'; import locale from 'locale-codes'; -import { renderSettingsPage } from '../controller/settingsPage'; +import { renderSettingsPage } from '../controller/settings'; const settingsSchema: JSONSchemaType<{ username: string; @@ -77,6 +77,7 @@ const settingsSchema: JSONSchemaType<{ }, time_zone: { type: 'string', + enum: Intl.supportedValuesOf('timeZone'), }, _csrf: { type: 'string', diff --git a/src/validators/validateSignup.ts b/src/validators/validateSignup.ts index 3afcdc2..bbcb929 100644 --- a/src/validators/validateSignup.ts +++ b/src/validators/validateSignup.ts @@ -2,7 +2,7 @@ import ajv from '../config/ajv'; import { JSONSchemaType } from 'ajv'; import { NextFunction, Request, Response } from 'express-serve-static-core'; import { validate } from '../utils/utils'; -import { renderSignup } from '../controller/signupPage'; +import { renderSignup } from '../controller/signup'; const signupSchema: JSONSchemaType<{ username: string; diff --git a/views/login.ejs b/views/login.ejs index f3aa4b0..3a40f5e 100644 --- a/views/login.ejs +++ b/views/login.ejs @@ -24,11 +24,25 @@
- +
- +
diff --git a/views/password-change.ejs b/views/password-change.ejs index 69e290f..6d93010 100644 --- a/views/password-change.ejs +++ b/views/password-change.ejs @@ -25,13 +25,27 @@
- +
- +
diff --git a/views/password-reset.ejs b/views/password-reset.ejs index afa54a9..ad55b10 100644 --- a/views/password-reset.ejs +++ b/views/password-reset.ejs @@ -24,7 +24,14 @@
- +
diff --git a/views/settings.ejs b/views/settings.ejs index 1eee0a0..696e728 100644 --- a/views/settings.ejs +++ b/views/settings.ejs @@ -20,67 +20,138 @@
- +
- - + +
- +
- - + +
- - + +
- +
- +
- +
- +
- +
diff --git a/views/signup.ejs b/views/signup.ejs index bffc295..257ac7c 100644 --- a/views/signup.ejs +++ b/views/signup.ejs @@ -21,28 +21,68 @@
- +
- - + +
- +
- +
- +