refactor: streamline SVG handling and improve error messages in user and email controllers

This commit is contained in:
Ari Yeger
2025-07-23 16:24:47 -04:00
parent 8eaa0c3d06
commit 9add60735e
7 changed files with 72 additions and 72 deletions

View File

@ -1,35 +1,50 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watchEffect, computed} from 'vue'; import {ref, computed} from 'vue';
import vRecolorSvg from '@/directives/vRecolorSvg.ts'; import vRecolorSvg from '@/directives/vRecolorSvg.ts';
const props = defineProps<{ const props = defineProps<{
svgUrl: string; svg: string;
width?: number; width?: number;
height?: number; height?: number;
viewBox?: string; viewBox?: string;
}>(); }>();
const paths = ref<Array<{ clip: string; d: string; fill: "nonzero" | "evenodd" | "inherit" | undefined}>>([]); const paths = ref<Array<{
'clip-rule': string | undefined;
d: string | undefined;
'fill-rule': "nonzero" | "evenodd" | "inherit" | undefined
}>>([]);
watchEffect(async () => { const svgPaths = props.svg
if (!props.svgUrl) { .replace(/%20/g, " ")
paths.value = []; .replace(/%3c/g, "<")
return; .replace(/%3e/g, ">")
.split('path')
.map(chunk => chunk.trim())
.filter((chunk) => !chunk.startsWith('data:image'))
.map((path) => path
.split('/>')[0]
.replace(/fill=.* /g, "")
.replace(/["']/g, ""));
try {
for (const path of svgPaths) {
const match = {
'clip-rule': /clip-rule=(\w+)/.exec(path),
'd': /d=(.*Z)/.exec(path),
'fill-rule': /fill-rule=(\w+)/.exec(path),
} }
try { paths.value.push({
const response = await fetch(props.svgUrl); 'clip-rule': match['clip-rule'] ? match['clip-rule'][1].toString() : undefined,
const svgText = await response.text(); 'd': match['d'] ? match['d'][1].toString() : undefined,
const parser = new DOMParser(); 'fill-rule': match['fill-rule'] ? match['fill-rule'][1].toString() as "nonzero" | "evenodd" | "inherit" : undefined,
const doc = parser.parseFromString(svgText, 'image/svg+xml'); });
paths.value = Array.from(doc.querySelectorAll('path')).map(path => ({
clip: path.getAttribute('clip-rule') || 'nonzero',
d: path.getAttribute('d') || '',
fill: (path.getAttribute('fill-rule') as "nonzero" | "evenodd" | "inherit" | undefined) || "inherit",
}));
} catch {
paths.value = [];
} }
}); } catch (error) {
console.error('Error processing SVG paths:', error);
paths.value = [];
}
const svgStyle = computed(() => ({ const svgStyle = computed(() => ({
width: props.width ? `${props.width}px` : '100%', width: props.width ? `${props.width}px` : '100%',
@ -40,7 +55,8 @@ const svgStyle = computed(() => ({
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" :viewBox="svgStyle.viewBox" :width="svgStyle.width" <svg xmlns="http://www.w3.org/2000/svg" :viewBox="svgStyle.viewBox" :width="svgStyle.width"
:height="svgStyle.height"> :height="svgStyle.height">
<path v-for="(path, index) in paths" :key="index" :clip-rule="path.clip" :d="path.d" :fill-rule="path.fill" v-recolor-svg/> <path v-for="(path, index) in paths" :key="index" :clip-rule="path['clip-rule']" :d="path['d']"
:fill-rule="path['fill-rule']" v-recolor-svg/>
</svg> </svg>
</template> </template>

View File

@ -76,7 +76,7 @@ export function useReadableTextColor(bgColor: string): string {
let whiteRatio = (whiteLighter + 0.05) / (whiteDarker + 0.05); let whiteRatio = (whiteLighter + 0.05) / (whiteDarker + 0.05);
// Return black or white based on luminance // Return black or white based on luminance
if (blackRatio >= 4.5 || whiteRatio >= 4.5) { if (blackRatio >= 4.5 || whiteRatio >= 4.5) {
console.debug("bgL:", bgLuminance, "bL:", blackLuminance, "wL:", whiteLuminance, "bR:", blackRatio, "wR:", whiteRatio); //console.debug("bgL:", bgLuminance, "bL:", blackLuminance, "wL:", whiteLuminance, "bR:", blackRatio, "wR:", whiteRatio);
return blackRatio > whiteRatio ? '#181818' : '#E7E7E7'; return blackRatio > whiteRatio ? '#181818' : '#E7E7E7';
} }
// If contrast is not enough, use deep colors // If contrast is not enough, use deep colors
@ -87,7 +87,7 @@ export function useReadableTextColor(bgColor: string): string {
blackRatio = (blackLighter + 0.05) / (blackDarker + 0.05); blackRatio = (blackLighter + 0.05) / (blackDarker + 0.05);
whiteRatio = (whiteLighter + 0.05) / (whiteDarker + 0.05); whiteRatio = (whiteLighter + 0.05) / (whiteDarker + 0.05);
if (blackRatio >= 4.5 || whiteRatio >= 4.5) { if (blackRatio >= 4.5 || whiteRatio >= 4.5) {
console.debug("bgL:", bgLuminance, "bL:", blackLuminanceDeep, "wL:", whiteLuminanceDeep, "bR:", blackRatio, "wR:", whiteRatio); //console.debug("bgL:", bgLuminance, "bL:", blackLuminanceDeep, "wL:", whiteLuminanceDeep, "bR:", blackRatio, "wR:", whiteRatio);
return blackRatio > whiteRatio ? '#0B0B0B' : '#F4F4F4'; return blackRatio > whiteRatio ? '#0B0B0B' : '#F4F4F4';
} }
// If still not enough, use absolute colors // If still not enough, use absolute colors
@ -98,10 +98,10 @@ export function useReadableTextColor(bgColor: string): string {
blackRatio = (blackLighter + 0.05) / (blackDarker + 0.05); blackRatio = (blackLighter + 0.05) / (blackDarker + 0.05);
whiteRatio = (whiteLighter + 0.05) / (whiteDarker + 0.05); whiteRatio = (whiteLighter + 0.05) / (whiteDarker + 0.05);
if (blackRatio >= 4.5 || whiteRatio >= 4.5) { if (blackRatio >= 4.5 || whiteRatio >= 4.5) {
console.debug("bgL:", bgLuminance, "bL:", blackLuminanceAbsolute, "wL:", whiteLuminanceAbsolute, "bR:", blackRatio, "wR:", whiteRatio); //console.debug("bgL:", bgLuminance, "bL:", blackLuminanceAbsolute, "wL:", whiteLuminanceAbsolute, "bR:", blackRatio, "wR:", whiteRatio);
return blackRatio > whiteRatio ? '#000000' : '#FFFFFF'; return blackRatio > whiteRatio ? '#000000' : '#FFFFFF';
} }
console.warn(`Not enough contrast for background color: ${bgColor}. Using fallback colors.`); //console.warn(`Not enough contrast for background color: ${bgColor}. Using fallback colors.`);
return bgLuminance > 0.5 ? blackTextColor : whiteTextColor; // Fallback to default colors return bgLuminance > 0.5 ? blackTextColor : whiteTextColor; // Fallback to default colors
} }

View File

@ -27,6 +27,7 @@ export function useLogin() {
async login(username: string, password: string): Promise<SecureUser> { async login(username: string, password: string): Promise<SecureUser> {
return await api<SecureUser>("/users/login", {username, password}, "POST") return await api<SecureUser>("/users/login", {username, password}, "POST")
.then((response: DataEnvelope<SecureUser> ) => { .then((response: DataEnvelope<SecureUser> ) => {
if (!response.data) throw new Error("Invalid login credentials. Please try again.");
session.user = response.data; session.user = response.data;
if (!session.user) throw new Error("Invalid login credentials. Please try again."); if (!session.user) throw new Error("Invalid login credentials. Please try again.");
session.token = response.data.token || null; session.token = response.data.token || null;
@ -37,7 +38,9 @@ export function useLogin() {
localStorage.setItem("token", session.token ?? ""); localStorage.setItem("token", session.token ?? "");
return session.user; return session.user;
}) })
.catch((err)=>{throw err}) as SecureUser; .catch((envelope: DataEnvelope<any>)=>{
toast.error(envelope.message || envelope.error?.message || "An error occurred while trying to log in. Please try again later.")
}) as SecureUser;
}, },
async logout(): Promise<void> { async logout(): Promise<void> {
session.user = null; session.user = null;

View File

@ -2,16 +2,17 @@
import {ref} from 'vue'; import {ref} from 'vue';
import {useLogin} from "@models/session.ts"; import {useLogin} from "@models/session.ts";
import {isMobile} from "@models/globals.ts"; import {isMobile} from "@models/globals.ts";
import vRecolorSvg from '@/directives/vRecolorSvg.ts'; import eye from '@/assets/svg/eye.svg';
import eyeSlash from '@/assets/svg/eye-slash.svg';
import BaseButton from "@components/baseComponents/BaseButton.vue"; import BaseButton from "@components/baseComponents/BaseButton.vue";
import BaseSVG from "@components/baseComponents/BaseSVG.vue"; import BaseSVG from "@components/baseComponents/BaseSVG.vue";
import BaseInput from "@components/baseComponents/BaseInput.vue"; import BaseInput from "@components/baseComponents/BaseInput.vue";
const username = ref(''); const username = ref('');
const password = ref(''); const password = ref('');
const {login} = useLogin(); const {login} = useLogin();
const showPassword = ref(false); const showPassword = ref(false);
</script> </script>
<template> <template>
@ -34,12 +35,10 @@ const showPassword = ref(false);
<button type="button" class="input-group-text border-start-0" <button type="button" class="input-group-text border-start-0"
style="cursor:pointer;" style="cursor:pointer;"
@click="showPassword = !showPassword"> @click="showPassword = !showPassword">
<BaseSVG svg-url="/src/assets/svg/eye.svg" :width="20" :height="20" <BaseSVG :svg="eye" :width="20" :height="20"
view-box="0 0 28 28" view-box="0 0 28 28" v-if="!showPassword"/>
v-recolor-svg v-if="!showPassword"/> <BaseSVG :svg="eyeSlash" :width="20" :height="20"
<BaseSVG svg-url="/src/assets/svg/eye-slash.svg" :width="20" :height="20" view-box="0 0 28 28" v-else/>
view-box="0 0 28 28"
v-recolor-svg v-else/>
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,32 +1,20 @@
// server/app.js // server/app.js
console.log("Starting Server...");
import express from 'express'; import express from 'express';
import cors from 'cors'; import cors from 'cors';
// import multer from 'multer'; // import multer from 'multer';
// import path from 'path'; // import path from 'path';
import db from './models/db.js'; import db from './models/db.js';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import emailController from "./controllers/emailController.js";
dotenv.config(); dotenv.config();
/**@typedef {import("../client/src/DBRecord.ts").DBRecord} DBRecord */
console.log("Starting Server...");
// test db connection // test db connection
try { try {
await db.query("SELECT 1") await db.query("SELECT 1")
console.log("DB connection successful") console.log("DB connection successful")
await db.get('users', {id: 1}); await db.get('users', {id: 1});
console.log("Users table exists"); console.log("Users table exists");
await emailController.withTimeout(emailController.transporter.verify(), 15000)
.then(() => {
console.log("Transporter is ready to send emails");
})
.catch((error) => {
console.error("Error verifying transporter:", error);
process.exit(1);
});
} catch (err) { } catch (err) {
console.error(err); console.error(err);
process.exit(1); process.exit(1);
@ -52,11 +40,11 @@ const upload = multer({storage});*/
// user routes // user routes
import userRouter from './controllers/userController.js'; import userRouter from './controllers/userController.js';
app.use('/api/users', userRouter); app.use('/api/users', userRouter);
// email routes // email routes
app.use('/api/email', emailController.router); import emailRouter from "./controllers/emailController.js";
app.use('/api/email', emailRouter);
// other routes // other routes

View File

@ -144,9 +144,5 @@ async function withTimeout(promise, ms) {
} }
export default { export default router;
withTimeout,
transporter,
router
};

View File

@ -41,12 +41,12 @@ router.get("/",
*/ */
async (req, res, next) => { async (req, res, next) => {
if (!req.body || !req.body.username || !req.body.password) { if (!req.body || !req.body.username || !req.body.password) {
return res.status(400).json({data: null, error: 'Username and password are required'}); return res.status(400).json({data: null, message: 'Username and password are required'});
} }
await users.addNewUser(req.body.username, req.body.password) await users.addNewUser(req.body.username, req.body.password)
.then((user) => { .then((user) => {
if (!user) { if (!user) {
return res.status(500).json({data: null, error: 'User not found after insertion'}); return res.status(500).json({data: null, message: 'User not found after insertion'});
} }
res.status(200).json({data: user, message: 'User added successfully'}); res.status(200).json({data: user, message: 'User added successfully'});
}) })
@ -64,21 +64,21 @@ router.get("/:identifier",
*/ */
async (req, res, next) => { async (req, res, next) => {
const userIdentifier = req.params.identifier; const userIdentifier = req.params.identifier;
if (!userIdentifier) return res.status(400).json({data: null, error: 'User identifier is required'}); if (!userIdentifier) return res.status(400).json({data: null, message: 'User identifier is required'});
await users.getUser(userIdentifier) await users.getUser(userIdentifier)
.then(user => { .then(user => {
if (!user) return res.status(404).json({data: null, error: 'User not found'}); if (!user) return res.status(404).json({data: null, message: 'User not found'});
res.status(200).json({data: user}); res.status(200).json({data: user});
}) })
.catch((err) => { .catch((err) => {
if (err instanceof Error) { if (err instanceof Error) {
if (err.message === 'User not found') { if (err.message === 'User not found') {
return res.status(404).json({data: null, error: 'User not found'}); return res.status(404).json({data: null, error: err});
} }
if (err.message === 'Multiple users found with the same identifier, something has gone wrong') { if (err.message === 'Multiple users found with the same identifier, something has gone wrong') {
res.status(500).json({ res.status(500).json({
data: null, data: null,
error: 'Multiple users found with the same identifier, something has gone wrong' error: err
}); });
} else next(err); } else next(err);
} else { } else {
@ -100,16 +100,16 @@ router.post("/login",
const {username, password} = req.body; const {username, password} = req.body;
if (!username || !password) return res.status(400).json({ if (!username || !password) return res.status(400).json({
data: null, data: null,
error: 'Username and password are required' message: 'Username and password are required'
}); });
await users.login(username, password).then(data => { await users.login(username, password).then(data => {
if (!data) return res.status(401).json({data: null, error: 'Invalid username or password'}); if (!data) return res.status(401).json({data: null, message: 'Invalid username or password'});
res.status(200).json({data: data, message: 'Login successful'}); res.status(200).json({data: data, message: 'Login successful'});
}) })
.catch((err) => { .catch((err) => {
if (err instanceof Error) { if (err instanceof Error) {
if (err.message === 'Invalid username or password') { if (err.message === 'Invalid username or password') {
res.status(401).json({data: null, error: 'Invalid username or password'}); res.status(401).json({data: null, error: err});
} else next(err); } else next(err);
} else { } else {
next(new Error('An unhandled error occurred while login')); next(new Error('An unhandled error occurred while login'));
@ -127,12 +127,10 @@ router.patch("/:identifier", authHandler.authenticateUser,
*/ */
async (req, res, next) => { async (req, res, next) => {
const userIdentifier = req.params.id; const userIdentifier = req.params.id;
if (!userIdentifier) { if (!userIdentifier) return res.status(400).json({data: null, message: 'User Identifier is required'});
return res.status(400).json({data: null, error: 'User Identifier is required'});
}
await users.updateUser(req.body) await users.updateUser(req.body)
.then(user => { .then(user => {
if (!user) res.status(404).json({data: null, error: 'User not found'}); if (!user) res.status(404).json({data: null, message: 'User not found'});
else res.status(200).json({data: user, message: 'User updated successfully'}); else res.status(200).json({data: user, message: 'User updated successfully'});
}) })
.catch((err) => next(err)) .catch((err) => next(err))
@ -150,10 +148,10 @@ router.delete("/:identifier", authHandler.authenticateUser,
*/ */
async (req, res, next) => { async (req, res, next) => {
const userIdentifier = req.params.identifier; const userIdentifier = req.params.identifier;
if (!userIdentifier) return res.status(400).json({data: null, error: 'User identifier is required'}); if (!userIdentifier) return res.status(400).json({data: null, message: 'User identifier is required'});
await users.deleteUser(userIdentifier) await users.deleteUser(userIdentifier)
.then(user => { .then(user => {
if (!user) return res.status(404).json({data: null, error: 'User not found'}); if (!user) return res.status(404).json({data: null, message: 'User not found'});
res.status(200).json({data: user, message: 'User deleted successfully'}); res.status(200).json({data: user, message: 'User deleted successfully'});
}) })
.catch((err) => next(err)); .catch((err) => next(err));