diff --git a/package-lock.json b/package-lock.json
index 499c5ab..839a8c7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,6 +17,7 @@
"d3-array": "^3.2.4",
"d3-scale": "^4.0.2",
"d3-shape": "^3.2.0",
+ "debug": "^4.4.0",
"drizzle-orm": "^0.36.4",
"layerchart": "^0.59.1",
"mysql2": "^3.11.4"
@@ -29,6 +30,7 @@
"@types/d3-array": "^3.2.1",
"@types/d3-scale": "^4.0.8",
"@types/d3-shape": "^3.1.6",
+ "@types/debug": "^4.1.12",
"@types/eslint": "^9.6.0",
"autoprefixer": "^10.4.20",
"drizzle-kit": "^0.28.1",
@@ -2364,6 +2366,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/debug": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
+ "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
"node_modules/@types/eslint": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
@@ -2388,6 +2400,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/ms": {
+ "version": "0.7.34",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
+ "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/node": {
"version": "22.9.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
@@ -3425,10 +3444,9 @@
}
},
"node_modules/debug": {
- "version": "4.3.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
- "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
- "dev": true,
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+ "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -5347,7 +5365,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
"license": "MIT"
},
"node_modules/mysql2": {
diff --git a/package.json b/package.json
index b66096d..3d34c03 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"@types/d3-array": "^3.2.1",
"@types/d3-scale": "^4.0.8",
"@types/d3-shape": "^3.1.6",
+ "@types/debug": "^4.1.12",
"@types/eslint": "^9.6.0",
"autoprefixer": "^10.4.20",
"drizzle-kit": "^0.28.1",
@@ -46,6 +47,7 @@
"d3-array": "^3.2.4",
"d3-scale": "^4.0.2",
"d3-shape": "^3.2.0",
+ "debug": "^4.4.0",
"drizzle-orm": "^0.36.4",
"layerchart": "^0.59.1",
"mysql2": "^3.11.4"
diff --git a/src/hooks.server.ts b/src/hooks.server.ts
index 67bad01..2bc8bc7 100644
--- a/src/hooks.server.ts
+++ b/src/hooks.server.ts
@@ -2,10 +2,14 @@ import type { Handle } from "@sveltejs/kit";
import { deleteSessionTokenCookie, setSessionTokenCookie, validateSession } from "./lib/session/session";
+import debug from 'debug';
+
+const log = debug('hooks');
+
export const handle: Handle = async ({ event, resolve }) => {
const token = event.cookies.get("session") ?? null;
if (token === null) {
- console.log("No session available")
+ log("No session available")
event.locals.userId = null;
event.locals.session = null;
return resolve(event);
diff --git a/src/lib/components/Diagram.svelte b/src/lib/components/Diagram.svelte
index f0ca448..bcc5de3 100644
--- a/src/lib/components/Diagram.svelte
+++ b/src/lib/components/Diagram.svelte
@@ -1,6 +1,6 @@
@@ -51,13 +57,22 @@
{#each flatGroup(data, (d) => d.participant) as [participant, ratings], idx}
+
{/each}
-
-
+
+
diff --git a/src/lib/queries.ts b/src/lib/queries.ts
index c2d18f7..6835a43 100644
--- a/src/lib/queries.ts
+++ b/src/lib/queries.ts
@@ -22,7 +22,7 @@ export async function loadSurvey(id: number, ownerId: number) {
accessToken: participant.accessToken,
answers: answers.filter(answer => answer.participantId === participant.id).map(answer => ({
skillId: answer.skillId,
- rating: answer.rating
+ rating: answer.rating / 10 // convert back to original vallue. The DB stores integer values only to prevent rounding and matching errors
}))
})),
skills: skills.map(skill => ({
diff --git a/src/routes/(app)/survey/[surveyId]/+page.server.ts b/src/routes/(app)/survey/[surveyId]/+page.server.ts
index b5c781c..fe619d3 100644
--- a/src/routes/(app)/survey/[surveyId]/+page.server.ts
+++ b/src/routes/(app)/survey/[surveyId]/+page.server.ts
@@ -2,18 +2,25 @@ import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { loadSurvey } from '$lib/queries';
+import debug from 'debug';
+
+const log = debug('survey:admin');
+
export const load: PageServerLoad = async ({ params, locals }) => {
const surveyId = parseInt(params.surveyId);
if (isNaN(surveyId)) {
+ log('Invalid survey ID %s', params.surveyId);
error(400, 'Invalid survey ID');
}
if (!locals.userId) {
+ log('User is not logged in');
error(403, 'User is not logged in');
}
const surveyData = await loadSurvey(surveyId, locals.userId);
if (!surveyData) {
+ log('Survey not found or user (%s) does not have access: %s', locals.userId, params.surveyId);
error(404, 'Survey not found');
}
return surveyData;
diff --git a/src/routes/[accessToken]/+page.server.ts b/src/routes/[accessToken]/+page.server.ts
index eaf10ef..3c9de58 100644
--- a/src/routes/[accessToken]/+page.server.ts
+++ b/src/routes/[accessToken]/+page.server.ts
@@ -5,6 +5,11 @@ import type { PageServerLoad } from './$types';
import { eq } from 'drizzle-orm';
+import debug from 'debug';
+
+const log_load = debug('survey:load');
+const log_store = debug('survey:store');
+
export const load: PageServerLoad = async ({ params, url }) => {
const results = await db.select()
.from(surveyAccessTable)
@@ -13,10 +18,12 @@ export const load: PageServerLoad = async ({ params, url }) => {
.limit(1);
if (results.length === 0) {
+ log_load('Survey not found: %s', params.accessToken);
error(404, 'Survey not found');
}
if (await db.$count(surveyAnswersTable, eq(surveyAnswersTable.participantId, results[0].survey_access_table.id)) > 0) {
+ log_load('Answers already submitted: %s', params.accessToken);
error(400, 'Answers already submitted');
}
@@ -44,10 +51,12 @@ export const actions = {
.limit(1);
if (results.length === 0) {
+ log_store('Survey not found: %s', params.accessToken);
error(404, 'Survey not found');
}
if (await db.$count(surveyAnswersTable, eq(surveyAnswersTable.participantId, results[0].survey_access_table.id)) > 0) {
+ log_store('Answers already submitted: %s', params.accessToken);
error(400, 'Answers already submitted');
}
@@ -56,20 +65,29 @@ export const actions = {
const formData = await request.formData();
// validate that the form doesn't contain invalid skill IDs
- const skillEntries = [...formData.entries()].filter(([key, _]) => key.startsWith('disable-'));
+ const skillEntries = [...formData.entries()].filter(([key, _]) => !key.startsWith('disable-'));
const allIdsValid = skillEntries.every(([key, _]) => skills.some(skill => skill.id.toString() === key));
- if (!allIdsValid || skillEntries.length !== skills.length) {
+ const deselectedIds = [...formData.entries().filter(([key, _]) => key.startsWith('disable-')).map(([key, _]) => parseInt(key.replace('disable-', '')))];
+ if (!allIdsValid || skillEntries.length + deselectedIds.length !== skills.length) {
+ let missing_ids = skills.filter(skill => !skillEntries.some(([key, _]) => key === skill.id.toString()) && !deselectedIds.includes(skill.id));
+ let invalid_ids = skillEntries.filter(([key, _]) => !skills.some(skill => skill.id.toString() === key)).map(([key, _]) => key);
+ log_store.log("Invalid (%o) or missing (%o) skill IDs", invalid_ids, missing_ids);
error(400, 'Invalid skill ID');
}
const answers = skillEntries.map(([key, value]) => ({
participantId: results[0].survey_access_table.id,
skillId: parseInt(key),
- rating: parseFloat(value.toString())
+ rating: parseFloat(value.toString()) * 10 // convert to int to store in the database without rounding issues etc.
}));
- if (answers.some(answer => isNaN(answer.rating) || answer.rating < 0 || answer.rating > 3)) {
+ if (answers.some(answer => isNaN(answer.rating) || answer.rating < 0 || answer.rating > 30)) {
+ log_store(
+ "Invalid answer: %o",
+ answers.filter(answer => isNaN(answer.rating) || answer.rating < 0 || answer.rating > 30)
+ );
error(400, 'Invalid answer');
}
+
await db.insert(surveyAnswersTable).values([...answers]);
redirect(303, `/${params.accessToken}/thanks`);
diff --git a/src/routes/login/+page.server.ts b/src/routes/login/+page.server.ts
index d3c19b4..1210710 100644
--- a/src/routes/login/+page.server.ts
+++ b/src/routes/login/+page.server.ts
@@ -8,17 +8,21 @@ import { error } from '@sveltejs/kit';
import { verify } from '@node-rs/argon2';
import { generateRandomToken } from '$lib/randomToken';
+import debug from 'debug';
+
+let log = debug('login');
+
export const actions = {
default: async (event) => {
const formData = await event.request.formData();
const email = formData.get('email')?.toString();
const password = formData.get('password')?.toString();
if (!email || !password) {
+ log('Email and password are required');
error(400, 'Email and password are required');
}
const userCredentials = await db.select().from(usersTable).where(eq(usersTable.email, email));
- console.log(userCredentials, formData.get('email'));
if (userCredentials.length === 0) {
error(403, 'Invalid credentials');
}
@@ -38,6 +42,7 @@ export const actions = {
}
}
else {
+ log('Invalid credentials for %s', email);
error(403, 'Invalid credentials');
}
}