Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
3aec94e
Add relevant sql tables for onboarding assessment
lavanyagarg112 Jul 8, 2025
d7145c7
Add endpoints for onboarding form
lavanyagarg112 Jul 8, 2025
0eac909
Add onboarding form as questionaire for onboarding
lavanyagarg112 Jul 9, 2025
ca2223f
Make tags and form organisation specific
lavanyagarg112 Jul 9, 2025
ed33dda
Merge pull request #30 from lavanyagarg112/lavanya/onboarding-assessment
lavanyagarg112 Jul 9, 2025
a4d2820
Update schema
lavanyagarg112 Jul 9, 2025
b02452c
Add roadmap endpoints
lavanyagarg112 Jul 9, 2025
d876967
Merge pull request #31 from lavanyagarg112/lavanya/roadmap
lavanyagarg112 Jul 9, 2025
c7dbe02
Add endpoints for member settings
lavanyagarg112 Jul 9, 2025
b847442
Merge pull request #32 from lavanyagarg112/lavanya/membersettings
lavanyagarg112 Jul 9, 2025
a514b63
Make a more complicated roadmap creation
lavanyagarg112 Jul 9, 2025
6c007f6
Fix roadmap issues and clean code
lavanyagarg112 Jul 9, 2025
f6d620b
Merge pull request #33 from lavanyagarg112/lavanya/tags-refactor
lavanyagarg112 Jul 9, 2025
5ce866b
add new table for user activity
lavanyagarg112 Jul 10, 2025
867fc23
Update logging for authentication
lavanyagarg112 Jul 10, 2025
b8722b0
Update logging for courses
lavanyagarg112 Jul 10, 2025
362cf8e
Update logging for onboarding
lavanyagarg112 Jul 10, 2025
8c3f6ef
Update logging for orgs
lavanyagarg112 Jul 10, 2025
7fff30a
Update logging for roadmap
lavanyagarg112 Jul 10, 2025
eed3164
Update logging for users
lavanyagarg112 Jul 10, 2025
97c9aa8
add basic endpoint for history
lavanyagarg112 Jul 10, 2025
2f76f50
Update to add missing fields
lavanyagarg112 Jul 10, 2025
fe7f45a
Update activity_logs table
lavanyagarg112 Jul 10, 2025
3c96d91
Update backend response
lavanyagarg112 Jul 10, 2025
df81f1a
Update activity logger
lavanyagarg112 Jul 10, 2025
b6dfb33
Add display meta for courses
lavanyagarg112 Jul 10, 2025
24aec24
Update display meta data for courses
lavanyagarg112 Jul 10, 2025
2084a5e
Update display meta data for orgs
lavanyagarg112 Jul 10, 2025
596f48b
Update display meta data for roadmap
lavanyagarg112 Jul 10, 2025
b61703b
Update display meta data for users
lavanyagarg112 Jul 10, 2025
29663d6
Fix history display bugs
lavanyagarg112 Jul 10, 2025
43ba33d
Merge pull request #34 from lavanyagarg112/lavanya/history
lavanyagarg112 Jul 10, 2025
126951f
Add working admin dashboard endpoint
lavanyagarg112 Jul 10, 2025
7bb5b23
ass working user dashboard endpoint
lavanyagarg112 Jul 10, 2025
ac3bfc8
Update latest code logic
lavanyagarg112 Jul 10, 2025
4c5e18e
Add global stats
lavanyagarg112 Jul 10, 2025
4f2786b
Merge pull request #35 from lavanyagarg112/lavanya/dashboard
lavanyagarg112 Jul 10, 2025
e057dc0
Fix bug and clean code
lavanyagarg112 Jul 10, 2025
99ece51
Merge pull request #36 from lavanyagarg112/lavanya/clean-code
lavanyagarg112 Jul 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 109 additions & 44 deletions database/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,12 @@ CREATE TABLE materials (
);

CREATE TABLE skills (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT,
organisation_id INTEGER NOT NULL REFERENCES organisations(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(name, organisation_id)
);

CREATE TABLE material_skills (
Expand Down Expand Up @@ -175,50 +179,41 @@ CREATE TABLE quiz_answers (
);


-- 8. TAGS
CREATE TABLE tags (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE
);

CREATE TABLE course_tags (
course_id INTEGER NOT NULL
REFERENCES courses(id) ON DELETE CASCADE,
tag_id INTEGER NOT NULL
REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY(course_id, tag_id)
);

CREATE TABLE module_tags (
module_id INTEGER NOT NULL
REFERENCES modules(id) ON DELETE CASCADE,
tag_id INTEGER NOT NULL
REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY(module_id, tag_id)
-- 8. CHANNELS, LEVELS & SKILLS
-- Channels (topics) for courses
CREATE TABLE channels (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT,
organisation_id INTEGER NOT NULL REFERENCES organisations(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(name, organisation_id)
);

CREATE TABLE revision_tags (
revision_id INTEGER NOT NULL
REFERENCES revisions(id) ON DELETE CASCADE,
tag_id INTEGER NOT NULL
REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY(revision_id, tag_id)
-- Levels (difficulty) for courses
CREATE TABLE levels (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL,
description TEXT,
sort_order INTEGER NOT NULL DEFAULT 0,
organisation_id INTEGER NOT NULL REFERENCES organisations(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(name, organisation_id)
);

CREATE TABLE quiz_tags (
quiz_id INTEGER NOT NULL
REFERENCES quizzes(id) ON DELETE CASCADE,
tag_id INTEGER NOT NULL
REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY(quiz_id, tag_id)
-- Course-channel-level associations
CREATE TABLE course_channels (
course_id INTEGER NOT NULL REFERENCES courses(id) ON DELETE CASCADE,
channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
level_id INTEGER NOT NULL REFERENCES levels(id) ON DELETE CASCADE,
PRIMARY KEY(course_id, channel_id, level_id)
);

CREATE TABLE question_tags (
question_id INTEGER NOT NULL
REFERENCES questions(id) ON DELETE CASCADE,
tag_id INTEGER NOT NULL
REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY(question_id, tag_id)
-- Module-skills associations
CREATE TABLE module_skills (
module_id INTEGER NOT NULL REFERENCES modules(id) ON DELETE CASCADE,
skill_id INTEGER NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
PRIMARY KEY(module_id, skill_id)
);


Expand Down Expand Up @@ -263,10 +258,80 @@ CREATE TABLE roadmaps (
CREATE TABLE roadmap_items (
roadmap_id INTEGER NOT NULL
REFERENCES roadmaps(id) ON DELETE CASCADE,
material_id INTEGER NOT NULL
REFERENCES materials(id) ON DELETE CASCADE,
module_id INTEGER NOT NULL
REFERENCES modules(id) ON DELETE CASCADE,
position INTEGER NOT NULL,
PRIMARY KEY(roadmap_id, material_id)
PRIMARY KEY(roadmap_id, module_id)
);

---

-- Table to store onboarding questions
CREATE TABLE onboarding_questions (
id SERIAL PRIMARY KEY,
question_text TEXT NOT NULL,
position INTEGER NOT NULL DEFAULT 0,
organisation_id INTEGER NOT NULL REFERENCES organisations(id) ON DELETE CASCADE
);

-- Table to store answer options for each question
CREATE TABLE onboarding_question_options (
id SERIAL PRIMARY KEY,
question_id INTEGER NOT NULL REFERENCES onboarding_questions(id) ON DELETE CASCADE,
option_text TEXT NOT NULL,
skill_id INTEGER REFERENCES skills(id) ON DELETE CASCADE,
channel_id INTEGER REFERENCES channels(id) ON DELETE CASCADE,
level_id INTEGER REFERENCES levels(id) ON DELETE CASCADE,
);

-- Table to store user responses
CREATE TABLE onboarding_responses (
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
option_id INTEGER NOT NULL REFERENCES onboarding_question_options(id) ON DELETE CASCADE,
PRIMARY KEY(user_id, option_id)
);

-- USER SKILLS
CREATE TABLE user_skills (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
skill_id INTEGER NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
level VARCHAR(50) NOT NULL DEFAULT 'beginner', -- 'beginner', 'intermediate', 'advanced', 'expert'
acquired_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(user_id, skill_id)
);

-- Table for user channel preferences
CREATE TABLE user_channels (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
preference_rank INTEGER NOT NULL DEFAULT 1,
added_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(user_id, channel_id)
);

COMMIT;
-- Table for user level preferences
CREATE TABLE user_levels (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
level_id INTEGER NOT NULL REFERENCES levels(id) ON DELETE CASCADE,
preference_rank INTEGER NOT NULL DEFAULT 1,
added_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(user_id, level_id)
);

CREATE TABLE activity_logs (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
organisation_id INTEGER REFERENCES organisations(id) ON DELETE SET NULL,
action VARCHAR(50) NOT NULL,
metadata JSONB DEFAULT '{}' NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
display_metadata JSONB NOT NULL DEFAULT '{}'
);



COMMIT;
49 changes: 49 additions & 0 deletions routes/activity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const express = require("express");
const pool = require("../database/db");
const router = express.Router();

function getAuthUser(req) {
const { auth } = req.cookies;
if (!auth) return null;
try {
return JSON.parse(auth);
} catch {
return null;
}
}

router.get("/", async (req, res) => {
const user = getAuthUser(req);
if (!user || !user.isLoggedIn) {
return res.status(401).json({ message: "Not logged in" });
}
const organisationId = user.organisation?.id;
if (!organisationId) {
return res.status(400).json({ message: "Organization required" });
}

try {
const { rows } = await pool.query(
`
SELECT
id,
user_id,
action,
display_metadata as metadata,
created_at
FROM activity_logs
WHERE organisation_id = $1 AND
user_id = $2
ORDER BY created_at DESC
LIMIT 100
`,
[organisationId, user.userId]
);
res.json({ logs: rows });
} catch (err) {
console.error("Error fetching activity logs:", err);
res.status(500).json({ message: "Server error" });
}
});

module.exports = router;
24 changes: 24 additions & 0 deletions routes/activityLogger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const pool = require("../database/db");

async function logActivity({
userId,
organisationId,
action,
metadata = {},
displayMetadata = {},
}) {
const sql = `
INSERT INTO activity_logs
(user_id, organisation_id, action, metadata, display_metadata)
VALUES ($1, $2, $3, $4, $5)
`;
await pool.query(sql, [
userId,
organisationId,
action,
metadata,
displayMetadata,
]);
}

module.exports = logActivity;
59 changes: 48 additions & 11 deletions routes/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const express = require("express");
const bcrypt = require("bcrypt");
const pool = require("../database/db");
const router = express.Router();
const logActivity = require("./activityLogger");

function setAuthCookie(res, payload) {
res.cookie("auth", JSON.stringify(payload), {
Expand All @@ -13,7 +14,6 @@ function setAuthCookie(res, payload) {
});
}

// SIGN UP → POST /api/signup
router.post("/signup", async (req, res) => {
const { email, password, firstname, lastname } = req.body;
try {
Expand Down Expand Up @@ -45,7 +45,6 @@ router.post("/signup", async (req, res) => {
}
});

// LOG IN → POST /api/login
router.post("/login", async (req, res) => {
const { email, password } = req.body;
try {
Expand Down Expand Up @@ -83,19 +82,30 @@ router.post("/login", async (req, res) => {
hasCompletedOnboarding: u.has_completed_onboarding,
organisation,
});
await logActivity({
userId: u.id,
organisationId: organisation ? organisation.id : null,
action: "login",
metadata: { email },
displayMetadata: { email },
});
return res.json({ success: true });
} catch (err) {
console.error(err);
return res.status(500).json({ message: "Server error" });
}
});

// LOGOUT → POST /api/logout
router.post("/logout", (req, res) => {
router.post("/logout", async (req, res) => {
const session = JSON.parse(req.cookies.auth || "{}");
res.clearCookie("auth", { path: "/" }).json({ success: true });
await logActivity({
userId: session.userId,
organisationId: session.organisation ? session.organisation.id : null,
action: "logout",
});
});

// WHOAMI → GET /api/me
router.get("/me", (req, res) => {
const { auth } = req.cookies;
if (!auth) return res.json({ isLoggedIn: false });
Expand All @@ -118,11 +128,6 @@ router.post("/complete-onboarding", async (req, res) => {
}

try {
await pool.query(
`UPDATE users SET has_completed_onboarding = true WHERE id = $1`,
[user.userId]
);

const mem = await pool.query(
`SELECT
o.id AS id,
Expand All @@ -137,13 +142,45 @@ router.post("/complete-onboarding", async (req, res) => {

const organisation = mem.rows[0] || null;

// Regenerate auth cookie
if (organisation && organisation.role === "employee") {
const questionCheck = await pool.query(
`SELECT COUNT(*) as question_count FROM onboarding_questions WHERE organisation_id = $1`,
[organisation.id]
);

const hasQuestions = parseInt(questionCheck.rows[0].question_count) > 0;

if (hasQuestions) {
const responseCheck = await pool.query(
`SELECT COUNT(*) as response_count FROM onboarding_responses WHERE user_id = $1`,
[user.userId]
);

if (parseInt(responseCheck.rows[0].response_count) === 0) {
return res.status(400).json({
message: "Onboarding questionnaire must be completed first",
});
}
}
}

await pool.query(
`UPDATE users SET has_completed_onboarding = true WHERE id = $1`,
[user.userId]
);

setAuthCookie(res, {
...user,
hasCompletedOnboarding: true,
organisation: organisation,
});

await logActivity({
userId: user.userId,
organisationId: organisation ? organisation.id : null,
action: "complete_onboarding",
});

res.json({ success: true });
} catch (err) {
console.error(err);
Expand Down
Loading