Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"babel-jest": "^29.7.0",
"eslint": "^9.20.1",
"globals": "^15.15.0",
"jest": "^29.7.0",
"jest": "^30.2.0",
"mongodb-memory-server": "^10.1.3",
"nodemon": "^3.1.9",
"prettier": "3.5.1"
Expand Down
114 changes: 114 additions & 0 deletions backend/src/db/daos/accessDao.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import Access from "../models/access.js";
import User from "../models/user.js";

/**
*
* @param {string} scenarioId
* @returns the access list of the given scenarioId ordered
*/
const getAccessList = async (scenarioId) => {
if (!scenarioId) return null;
const list = await Access.findOne({ scenarioId: scenarioId }).sort({
name: -1,
});
return list;
};

/**
*
* @param {string} scenarioId
* @param {string} name scenario name
* @param {string} userId
* @returns created database access object
*/
const createAccessList = async (scenarioId, name, userId) => {
// Will need better checking and synergy with the creation to ensure that a scenario isnt created if an access list isn't created.
const uInfo = await User.findOne({ uid: userId }).select("name email -_id");
if (!uInfo) return null;
const dbAccess = new Access({
scenarioId: scenarioId,
name: name,
ownerId: userId,
users: {
[userId]: { name: uInfo.name, email: uInfo.email, date: new Date() },
},
});
await dbAccess.save();
return dbAccess;
};

const deleteAccessList = async (scenarioId, ownerId) => {
try {
const res = await Access.findOneAndDelete({
scenarioId: scenarioId,
ownerId: ownerId,
});

if (res) {
return true;
}
return false;
} catch {
return false;
}
};

/**
*
* @param {string} scenarioId
* @param {string} userId
* @returns updated access list
*/
const grantAccess = async (scenarioId, userId) => {
if (!scenarioId || !userId) return null;
const uInfo = await User.findOne({ uid: userId }).select("name email -_id");
if (!uInfo) return null;
const updateObj = {
[`users.${userId}`]: {
name: uInfo.name,
email: uInfo.email,
date: new Date(),
},
};

const updatedList = await Access.findOneAndUpdate(
{ scenarioId: scenarioId },
{ $set: updateObj },
{ new: true }
);
return updatedList;
};

/**
*
* @param {string} scenarioId
* @param {String} userId
* @returns
*/
const revokeAccess = async (scenarioId, userId) => {
if (!scenarioId || !userId) return false;

const doc = await Access.findOne({ scenarioId }).select("ownerId").lean();
const isOwner = doc?.ownerId == userId;

if (isOwner) return { status: 403, message: "Protected" };

const updated = await Access.findOneAndUpdate(
{ scenarioId: scenarioId },
{ $unset: { [`users.${userId}`]: "" } },
{ new: true }
);
const stillContains = updated.users.has(userId);

if (stillContains) return { status: 304, message: "No found or removed" };

return { status: 200, message: "Revoked" };
};

export {
getAccessList,
createAccessList,
deleteAccessList,
grantAccess,
revokeAccess,
};
27 changes: 27 additions & 0 deletions backend/src/db/daos/scenarioDao.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Access from "../models/access.js";
import Scenario from "../models/scenario.js";
import Scene from "../models/scene.js";
import { v4 as uuidv4 } from "uuid";
Expand Down Expand Up @@ -44,6 +45,31 @@ const addThumbs = async (scenarios) => {
return scenarioData;
};

const retrieveAccessibleScenarios = async (uid) => {
if (!uid) return [];

//Get all access list where the user is on the list but not owner
const access = await Access.find({
ownerId: { $ne: uid },
[`users.${uid}`]: { $exists: true },
})
.sort({ _id: 1 })
.select("scenarioId -_id")
.lean();

const scenarioIds = [...access.map((s) => s.scenarioId)];
if (scenarioIds.length == 0) return [];

const scenarios = await Scenario.find(
{ _id: { $in: scenarioIds } },
{ name: 1, scenes: { $slice: 1 } }
)
.sort({ _id: 1 })
.lean();

return addThumbs(scenarios);
};

/**
* Retrieves all scenarios authored by particular user
* @param {String} uid ID of user
Expand Down Expand Up @@ -263,6 +289,7 @@ const deleteStateVariable = async (scenarioId, stateVariableIdentifier) => {
};

export {
retrieveAccessibleScenarios,
createScenario,
deleteScenario,
retrieveRoleList,
Expand Down
9 changes: 9 additions & 0 deletions backend/src/db/daos/userDao.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ const retrieveAllUser = async () => {
return User.find();
};

/**
* Retrieves minified all users
* @returns uid, name, and email of all users
*/
const retrieveAllUserMinAsc = async () => {
return User.find().select("uid name email -_id").sort({ name: 1 });
};

/**
* Retrieves user based on given uid
* @param {String} uid unique id of user
Expand Down Expand Up @@ -181,6 +189,7 @@ const setUserStateVariables = async (userId, scenarioId, stateVariables) => {

export {
retrieveAllUser,
retrieveAllUserMinAsc,
createUser,
retrieveUser,
retrieveUserByEmail,
Expand Down
40 changes: 40 additions & 0 deletions backend/src/db/models/access.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import mongoose from "mongoose";

const { Schema } = mongoose;

const userInfoSchema = new Schema(
{
name: { type: String },
email: { type: String },
addedAt: { type: Date, default: new Date() },
},
{ _id: false }
);

const accessSchema = new Schema(
{
scenarioId: {
type: String,
required: true,
unique: true,
index: true,
},
name: {
type: String,
},
ownerId: {
type: String,
required: true,
},
users: {
type: Map,
of: userInfoSchema,
defaul: {},
},
},
{ timestamps: true }
);

const Access = mongoose.model("Access", accessSchema, "access");

export default Access;
2 changes: 1 addition & 1 deletion backend/src/middleware/__tests__/scenarioAuth.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ describe("Scenario Auth Middleware tests", () => {
const res = mockResponse();
await scenarioAuth(req, res, nextFunction);

expect(nextFunction).toBeCalledTimes(1);
expect(nextFunction).toHaveBeenCalledTimes(1);
});

it("fails unauthorised user for scenario", async () => {
Expand Down
53 changes: 53 additions & 0 deletions backend/src/middleware/dashboardAuth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { getAccessList } from "../db/daos/accessDao.js";
import { getGroup } from "../db/daos/groupDao.js";
import { retrieveScenario } from "../db/daos/scenarioDao.js";

const HTTP_UNAUTHORISED = 401;
const HTTP_NOT_FOUND = 404;

export default async function dashboardAuth(req, res, next) {
try {
let sId = req.params?.scenarioId || null;
// No sId could mean group so get it from group
if (!sId) {
const group = await getGroup(req.params.groupId).catch(() => null);
sId = group.scenarioId;
}

// If still doesnt exists return the user with 404
if (!sId) {
return res.sendStatus(HTTP_NOT_FOUND);
}

const accessList = await getAccessList(sId).catch(() => null);
const scenario = await retrieveScenario(sId).catch(() => null);

// If both dont exists return 404
if (!accessList && !scenario) {
res.sendStatus(HTTP_NOT_FOUND);
return;
}
let isOnList = false;
let isOwner = false;
if (accessList && accessList.users) {
isOnList =
accessList.users.has(req.body.uid) ||
accessList.ownerId == req.body.uid;
} else {
// (Legacy Support) fallback for no accesslist
isOwner = scenario?.uid == req.body.uid;
}

if (isOnList || isOwner) {
next();
return;
}
// If it reaches the end just unauthorize
return res.sendStatus(HTTP_UNAUTHORISED);
} catch (err) {
res
.status(500)
.json({ error: "Internal server error", message: err.message });
return;
}
}
16 changes: 16 additions & 0 deletions backend/src/routes/api/__tests__/scenarioApi.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import Scenario from "../../../db/models/scenario.js";
import Scene from "../../../db/models/scene.js";
import auth from "../../../middleware/firebaseAuth.js";
import scenarioAuth from "../../../middleware/scenarioAuth.js";
import Access from "../../../db/models/access.js";

jest.mock("../../../middleware/firebaseAuth");
jest.mock("../../../middleware/scenarioAuth");
Expand Down Expand Up @@ -70,6 +71,20 @@ describe("Scenario API tests", () => {
uid: "user1",
};

const access1 = {
scenarioId: new mongoose.mongo.ObjectId("000000000000000000000001"),
name: "Scenario 1",
ownerId: "user1",
users: {},
};

const access2 = {
scenarioId: new mongoose.mongo.ObjectId("000000000000000000000002"),
name: "Scenario 2",
ownerId: "user1",
users: {},
};

// setup in-memory mongodb and express API
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
Expand All @@ -88,6 +103,7 @@ describe("Scenario API tests", () => {
beforeEach(async () => {
// Add scenario to database
await Scenario.create([scenario1, scenario2]);
await Access.create([access1, access2]);
await Scene.create([scene1, scene2]);
});

Expand Down
Loading