mirror of
https://github.com/mudabbir-ahmad/MAD-Treasure-Hunt.git
synced 2026-10-07 19:00:20 +00:00
527 lines
15 KiB
JavaScript
527 lines
15 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;
|
|
}
|
|
|
|
getCreatedPrivateGameByUser(uid) {
|
|
return this.db.groups.find(
|
|
(group) => group.CreatedByUid === Number(uid) && group.GameType === 'private',
|
|
) || 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 existing = this.getCreatedPrivateGameByUser(user.Uid);
|
|
if (existing) {
|
|
return existing;
|
|
}
|
|
|
|
const gid = this.nextId('group');
|
|
const isBusinessGroup = Boolean(isBusiness);
|
|
const group = new GroupModel({
|
|
Gid: gid,
|
|
GroupName: groupName,
|
|
BusinessOrSchoolName: businessOrSchoolName || groupName,
|
|
GameType: gameType,
|
|
AdminJoinCode: this.generateUniqueJoinCode(this.db.groups, 'AdminJoinCode'),
|
|
CreatedByUid: user.Uid,
|
|
MaxMemberSubgroups: isBusinessGroup ? 999 : 1,
|
|
TeamsEnabled: true,
|
|
IsBusinessGroup: isBusinessGroup,
|
|
ApprovedAdmins: isBusinessGroup ? [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,
|
|
IsGameStarted: false,
|
|
CacheTriggerMeters: 20,
|
|
});
|
|
|
|
if (!isBusinessGroup) {
|
|
this.createSubgroup({ gid: group.Gid, subgroupName: 'Members', cacheTriggerMeters: 20 });
|
|
}
|
|
|
|
const ownerSgid = isBusinessGroup ? 0 : 1;
|
|
const ownerIsAcceptedAdmin = isBusinessGroup;
|
|
|
|
this.ensureMembership(user.Uid, group.Gid, ownerSgid, ownerIsAcceptedAdmin);
|
|
user.Gid = group.Gid;
|
|
user.SGid = ownerSgid;
|
|
user.IsAcceptedAdmin = ownerIsAcceptedAdmin;
|
|
|
|
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,
|
|
IsGameStarted: false,
|
|
CacheTriggerMeters: cacheTriggerMeters,
|
|
};
|
|
|
|
this.db.subgroups.push(subgroup);
|
|
return subgroup;
|
|
}
|
|
|
|
getSubgroups(gid) {
|
|
return this.db.subgroups.filter((subgroup) => subgroup.Gid === Number(gid));
|
|
}
|
|
|
|
getVisibleSubgroups(gid) {
|
|
return this.getSubgroups(gid).filter((subgroup) => Number(subgroup.SGid) > 0);
|
|
}
|
|
|
|
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;
|
|
|