Files
MAD-Treasure-Hunt/app/Model/DbController.js
T

503 lines
14 KiB
JavaScript

import usersSeed from '../../DB/users.json';
import groupsSeed from '../../DB/groups.json';
import subgroupsSeed from '../../DB/subgroups.json';
import subgroupMembershipsSeed from '../../DB/subgroup-memberships.json';
import adminWaitlistSeed from '../../DB/admin-waitlist.json';
import teamsSeed from '../../DB/teams.json';
import teamMembersSeed from '../../DB/team-members.json';
import gameDataSeed from '../../DB/game-data.json';
import gameTypesSeed from '../../DB/game-types.json';
import UserModel from './UserModel';
import GroupModel from './GroupModel';
import TeamModel from './TeamModel';
const buildSeedState = () => ({
users: usersSeed,
groups: groupsSeed,
subgroups: subgroupsSeed,
subgroup_memberships: subgroupMembershipsSeed,
admin_waitlist: adminWaitlistSeed,
teams: teamsSeed,
team_members: teamMembersSeed,
game_data: gameDataSeed,
game_types: gameTypesSeed,
});
class DbController {
constructor(seed = buildSeedState()) {
this.seed = JSON.parse(JSON.stringify(seed));
this.reset();
}
reset() {
this.db = JSON.parse(JSON.stringify(this.seed));
}
hashPassword(value) {
let hash = 0;
for (let i = 0; i < value.length; i += 1) {
hash = (hash << 5) - hash + value.charCodeAt(i);
hash |= 0;
}
return `H_${Math.abs(hash)}`;
}
toMinuteIso(date = new Date()) {
return `${date.toISOString().slice(0, 16)}Z`;
}
generateJoinCode(length = 5) {
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let code = '';
for (let i = 0; i < length; i += 1) {
code += charset[Math.floor(Math.random() * charset.length)];
}
return code;
}
generateUniqueJoinCode(collection, key) {
let code = this.generateJoinCode(Math.random() > 0.5 ? 5 : 6);
while (collection.some((item) => item[key] === code)) {
code = this.generateJoinCode(Math.random() > 0.5 ? 5 : 6);
}
return code;
}
nextId(counterKey) {
const keyToCollection = {
user: { collection: 'users', field: 'Uid' },
group: { collection: 'groups', field: 'Gid' },
team: { collection: 'teams', field: 'Tid' },
};
const config = keyToCollection[counterKey];
if (!config) {
throw new Error(`Unsupported counter key: ${counterKey}`);
}
const rows = this.db[config.collection] || [];
const maxId = rows.reduce((max, row) => {
const value = Number(row[config.field] || 0);
return value > max ? value : max;
}, 0);
return maxId + 1;
}
getUserByEmail(email) {
return this.db.users.find((u) => u.email.toLowerCase() === email.toLowerCase()) || null;
}
getUserById(uid) {
return this.db.users.find((u) => u.Uid === Number(uid)) || null;
}
getGroupById(gid) {
return this.db.groups.find((group) => group.Gid === Number(gid)) || null;
}
register({ username, email, password, confirmPassword, accountType }) {
if (!username || !email || !password || !confirmPassword) {
throw new Error('Missing required registration fields');
}
if (password !== confirmPassword) {
throw new Error('Passwords do not match');
}
if (this.getUserByEmail(email)) {
throw new Error('Email already in use');
}
const uid = this.nextId('user');
const user = new UserModel({
Uid: uid,
username,
email,
passwordHash: this.hashPassword(password),
isBusiness: accountType === 'Business/School',
IsAcceptedAdmin: false,
Gid: null,
SGid: null,
TGid: null,
});
this.db.users.push(user.toJSON());
return user.toJSON();
}
login({ email, password }) {
const user = this.getUserByEmail(email);
if (!user) {
throw new Error('Email not found');
}
const incomingHash = this.hashPassword(password);
if (incomingHash !== user.passwordHash) {
throw new Error('Invalid password');
}
return user;
}
getGameTypes() {
return this.db.game_types;
}
getSubgroupByJoinCode(joinCode) {
return this.db.subgroups.find(
(subgroup) => subgroup.JoinCode && subgroup.JoinCode === joinCode.toUpperCase(),
) || null;
}
ensureMembership(uid, gid, sgid, isAcceptedAdmin) {
const existing = this.db.subgroup_memberships.find(
(membership) => membership.Uid === uid && membership.Gid === gid,
);
if (existing) {
existing.SGid = sgid;
existing.IsAcceptedAdmin = isAcceptedAdmin;
return existing;
}
const membership = {
Uid: uid,
Gid: gid,
SGid: sgid,
IsAcceptedAdmin: isAcceptedAdmin,
};
this.db.subgroup_memberships.push(membership);
return membership;
}
ensureGroupGameData(gid) {
const existing = this.db.game_data.find((entry) => entry.Gid === gid);
if (existing) {
return existing;
}
const payload = { Gid: gid, GeoCaches: [] };
this.db.game_data.push(payload);
return payload;
}
hasActiveAdmin(gid) {
return this.db.subgroup_memberships.some(
(membership) => membership.Gid === gid && membership.SGid === 0 && membership.IsAcceptedAdmin,
);
}
canJoinTeamForSubgroup(group, sgid) {
return group.TeamsEnabled && sgid > 0;
}
joinPrivateGame({ userId, joinCode }) {
const user = this.getUserById(Number(userId));
const subgroup = this.getSubgroupByJoinCode(joinCode);
if (!user || !subgroup || subgroup.IsAdminGroup) {
throw new Error('Invalid subgroup join code');
}
const group = this.getGroupById(subgroup.Gid);
if (!group) {
throw new Error('Group not found');
}
this.ensureMembership(user.Uid, subgroup.Gid, subgroup.SGid, false);
user.Gid = group.Gid;
user.SGid = subgroup.SGid;
user.IsAcceptedAdmin = false;
return group;
}
createPrivateGame({ userId, groupName, businessOrSchoolName, gameType = 'private', isBusiness = false }) {
const user = this.getUserById(Number(userId));
if (!user) {
throw new Error('User not found');
}
const gid = this.nextId('group');
const group = new GroupModel({
Gid: gid,
GroupName: groupName,
BusinessOrSchoolName: businessOrSchoolName || groupName,
GameType: gameType,
AdminJoinCode: this.generateUniqueJoinCode(this.db.groups, 'AdminJoinCode'),
CreatedByUid: user.Uid,
MaxMemberSubgroups: isBusiness ? 999 : 1,
TeamsEnabled: true,
ApprovedAdmins: [user.Uid],
CreatedAt: this.toMinuteIso(),
});
this.db.groups.push(group.toJSON());
this.ensureGroupGameData(gid);
this.db.subgroups.push({
SGid: 0,
SubGroupName: 'Admins',
Gid: group.Gid,
JoinCode: null,
IsAdminGroup: true,
CacheTriggerMeters: 20,
});
this.createSubgroup({ gid: group.Gid, subgroupName: 'Members', cacheTriggerMeters: 20 });
this.ensureMembership(user.Uid, group.Gid, 0, true);
user.Gid = group.Gid;
user.SGid = 0;
user.IsAcceptedAdmin = true;
return group.toJSON();
}
joinAsAdmin({ userId, businessOrSchoolName }) {
const user = this.getUserById(Number(userId));
const group = this.db.groups.find(
(entry) => entry.BusinessOrSchoolName.toLowerCase() === businessOrSchoolName.trim().toLowerCase(),
);
if (!user || !group) {
throw new Error('User or business/school not found');
}
this.ensureMembership(user.Uid, group.Gid, 0, false);
user.Gid = group.Gid;
user.SGid = 0;
user.IsAcceptedAdmin = false;
const activeAdminExists = this.hasActiveAdmin(group.Gid);
if (!activeAdminExists) {
return this.promoteAdmin({ uid: user.Uid, gid: group.Gid, approvedByUid: user.Uid });
}
const alreadyQueued = this.db.admin_waitlist.some(
(entry) => entry.Uid === user.Uid && entry.Gid === group.Gid && entry.Status === 'pending',
);
if (!alreadyQueued) {
this.db.admin_waitlist.push({
RequestId: `${group.Gid}_${user.Uid}`,
Uid: user.Uid,
Gid: group.Gid,
RequestedAt: this.toMinuteIso(),
Status: 'pending',
});
}
return { queued: true, Gid: group.Gid, Uid: user.Uid };
}
promoteAdmin({ uid, gid, approvedByUid }) {
const user = this.getUserById(Number(uid));
const group = this.getGroupById(Number(gid));
if (!user || !group) {
throw new Error('User or group not found');
}
this.ensureMembership(user.Uid, group.Gid, 0, true);
user.Gid = group.Gid;
user.SGid = 0;
user.IsAcceptedAdmin = true;
if (!group.ApprovedAdmins.includes(user.Uid)) {
group.ApprovedAdmins.push(user.Uid);
}
if (!group.ApprovedAdmins.includes(Number(approvedByUid))) {
group.ApprovedAdmins.push(Number(approvedByUid));
}
this.db.admin_waitlist = this.db.admin_waitlist.map((entry) => {
if (entry.Uid === user.Uid && entry.Gid === group.Gid && entry.Status === 'pending') {
return { ...entry, Status: 'approved' };
}
return entry;
});
return user;
}
requestAdminApproval({ userId, groupId }) {
const group = this.getGroupById(Number(groupId));
if (!group) {
throw new Error('Group not found');
}
return this.joinAsAdmin({ userId, businessOrSchoolName: group.BusinessOrSchoolName });
}
approveAdmin({ approverUserId, userId, groupId }) {
const approverMembership = this.db.subgroup_memberships.find(
(membership) => membership.Uid === Number(approverUserId)
&& membership.Gid === Number(groupId)
&& membership.SGid === 0
&& membership.IsAcceptedAdmin,
);
if (!approverMembership) {
throw new Error('Approver is not an admin for this group');
}
return this.promoteAdmin({ uid: Number(userId), gid: Number(groupId), approvedByUid: Number(approverUserId) });
}
createSubgroup({ gid, subgroupName, cacheTriggerMeters = 20 }) {
const group = this.getGroupById(Number(gid));
if (!group) {
throw new Error('Group not found');
}
const nonAdminCount = this.db.subgroups.filter((entry) => entry.Gid === group.Gid && !entry.IsAdminGroup).length;
if (nonAdminCount >= group.MaxMemberSubgroups) {
throw new Error('Maximum subgroup count reached');
}
const nextSgid = this.db.subgroups
.filter((entry) => entry.Gid === group.Gid && !entry.IsAdminGroup)
.reduce((max, entry) => Math.max(max, entry.SGid), 0) + 1;
const subgroup = {
SGid: nextSgid,
SubGroupName: subgroupName,
Gid: group.Gid,
JoinCode: this.generateUniqueJoinCode(this.db.subgroups.filter((entry) => !entry.IsAdminGroup), 'JoinCode'),
IsAdminGroup: false,
CacheTriggerMeters: cacheTriggerMeters,
};
this.db.subgroups.push(subgroup);
return subgroup;
}
getSubgroups(gid) {
return this.db.subgroups.filter((subgroup) => subgroup.Gid === Number(gid));
}
updateSubgroupCacheSettings({ gid, sgid, triggerMeters }) {
const subgroup = this.db.subgroups.find(
(entry) => entry.Gid === Number(gid) && entry.SGid === Number(sgid),
);
if (!subgroup) {
throw new Error('Subgroup not found');
}
subgroup.CacheTriggerMeters = Number(triggerMeters);
return subgroup;
}
createTeam({ userId, gid, teamName }) {
const user = this.getUserById(Number(userId));
if (!user) {
throw new Error('User not found');
}
const group = this.getGroupById(Number(gid));
if (!group) {
throw new Error('Group not found');
}
if (!this.canJoinTeamForSubgroup(group, user.SGid || 0)) {
throw new Error('Teams are disabled or user is in admin subgroup');
}
const tid = this.nextId('team');
const team = new TeamModel({
Tid: tid,
TeamName: teamName,
Gid: group.Gid,
JoinCode: this.generateUniqueJoinCode(this.db.teams, 'JoinCode'),
CreatorUid: user.Uid,
SGid: user.SGid,
CreatedAt: this.toMinuteIso(),
});
this.db.teams.push(team.toJSON());
this.db.team_members.push({
Tid: team.Tid,
Uid: user.Uid,
IsTeamCreator: true,
});
user.TGid = team.Tid;
return team.toJSON();
}
joinTeamByCode({ userId, teamCode }) {
const user = this.getUserById(Number(userId));
const team = this.db.teams.find((t) => t.JoinCode === teamCode.toUpperCase());
if (!user || !team) {
throw new Error('Invalid user or team code');
}
const group = this.getGroupById(team.Gid);
if (!group || !this.canJoinTeamForSubgroup(group, user.SGid || 0)) {
throw new Error('Teams are disabled or user is in admin subgroup');
}
if (user.Gid !== team.Gid) {
throw new Error('User is not in this game group');
}
const alreadyInTeam = this.db.team_members.some((tm) => tm.Tid === team.Tid && tm.Uid === user.Uid);
if (!alreadyInTeam) {
this.db.team_members.push({ Tid: team.Tid, Uid: user.Uid, IsTeamCreator: false });
}
user.TGid = team.Tid;
return team;
}
getLobby(gid) {
const group = this.getGroupById(Number(gid));
if (!group) {
return null;
}
const members = this.db.subgroup_memberships
.filter((membership) => membership.Gid === group.Gid)
.map((membership) => this.getUserById(membership.Uid))
.filter(Boolean);
return {
group,
members,
subgroups: this.getSubgroups(group.Gid),
teams: this.db.teams.filter((team) => team.Gid === group.Gid),
};
}
getTeam(tid) {
const team = this.db.teams.find((entry) => entry.Tid === Number(tid));
if (!team) {
return null;
}
const members = this.db.team_members
.filter((member) => member.Tid === team.Tid)
.map((member) => this.getUserById(member.Uid))
.filter(Boolean);
return { team, members };
}
getMapPoints(gid, sgid = null) {
const gameData = this.db.game_data.find((entry) => entry.Gid === Number(gid));
if (!gameData) {
return [];
}
if (sgid === null || sgid === undefined) {
return gameData.GeoCaches;
}
return gameData.GeoCaches.filter((cache) => cache.SGid === Number(sgid));
}
updateGroupSettings({ gid, teamsEnabled }) {
const group = this.getGroupById(Number(gid));
if (!group) {
throw new Error('Group not found');
}
group.TeamsEnabled = Boolean(teamsEnabled);
return group;
}
getState() {
return this.db;
}
}
const dbController = new DbController();
export { DbController };
export default dbController;