Merge pull request 'Role-Management-Overhaul' (#122) from Role-Management-Overhaul into main
All checks were successful
Continuous Integration / Update Development (push) Successful in 1m52s

Reviewed-on: #122
This commit was merged in pull request #122.
This commit is contained in:
2025-12-30 20:54:50 -06:00
15 changed files with 473 additions and 216 deletions

View File

@@ -5,7 +5,8 @@ const ur = express.Router();
import { MemberState } from '@app/shared/types/member';
import pool from '../db';
import { requireLogin, requireMemberState, requireRole } from '../middleware/auth';
import { assignUserGroup, createGroup } from '../services/rolesService';
import { assignUserGroup, createGroup, getAllRoles, getRole, getUsersWithRole } from '../services/rolesService';
import { Request, Response } from 'express';
r.use(requireLogin)
ur.use(requireLogin)
@@ -15,10 +16,16 @@ ur.post('/', [requireMemberState(MemberState.Member), requireRole("17th Administ
try {
const body = req.body;
assignUserGroup(body.member_id, body.role_id);
await assignUserGroup(body.member_id, body.role_id);
res.sendStatus(201);
} catch (err) {
if (err?.code === 'ER_DUP_ENTRY') {
return res.status(400).json({
error: 'Member already has this role',
});
}
console.error('Insert failed:', err);
res.status(500).json({ error: 'Failed to add to group' });
}
@@ -44,45 +51,39 @@ ur.delete('/', [requireMemberState(MemberState.Member), requireRole("17th Admini
//get all roles
r.get('/', [requireMemberState(MemberState.Member)], async (req, res) => {
try {
var con = await pool.getConnection();
// Get all roles
const roles = await con.query('SELECT * FROM roles;');
// Get all members for each role
const membersRoles = await con.query(`
SELECT mr.role_id, v.*
FROM members_roles mr
JOIN view_member_rank_unit_status_latest v ON mr.member_id = v.member_id
`);
// Group members by role_id
const roleIdToMembers = {};
for (const row of membersRoles) {
if (!roleIdToMembers[row.role_id]) roleIdToMembers[row.role_id] = [];
// Remove role_id from member object
const { role_id, ...member } = row;
roleIdToMembers[role_id].push(member);
}
// Attach members to each role
const result = roles.map(role => ({
...role,
members: roleIdToMembers[role.id] || []
}));
res.json(result);
const roles = await getAllRoles();
res.status(200).json(roles);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Internal server error' });
} finally {
con.release();
res.sendStatus(500);
}
});
r.get('/:id/members', [requireMemberState(MemberState.Member)], async (req: Request, res: Response) => {
try {
const members = await getUsersWithRole(Number(req.params.id));
res.status(200).json(members);
} catch (err) {
console.error(err);
res.sendStatus(500);
}
})
r.get('/:id', [requireMemberState(MemberState.Member)], async (req: Request, res: Response) => {
try {
const role = await getRole(Number(req.params.id));
res.status(200).json(role);
} catch (err) {
console.error(err);
res.sendStatus(500);
}
})
//create a new role
r.post('/', [requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req, res) => {
r.post('/', [requireMemberState(MemberState.Member), requireRole("dev")], async (req, res) => {
try {
const { name, color, description } = req.body;
if (!name || !color) {
@@ -103,7 +104,7 @@ r.post('/', [requireMemberState(MemberState.Member), requireRole("17th Administr
}
})
r.delete('/:id', [requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req, res) => {
r.delete('/:id', [requireMemberState(MemberState.Member), requireRole("dev")], async (req, res) => {
try {
const id = req.params.id;

View File

@@ -1,5 +1,6 @@
import { Role } from "@app/shared/types/roles";
import pool from "../db";
import { Member, MemberLight, memberSettings, MemberState } from '@app/shared/types/member'
import { Member, MemberCardDetails, MemberLight, memberSettings, MemberState } from '@app/shared/types/member'
export async function getUserData(userID: number): Promise<Member> {
const sql = `SELECT * FROM view_member_rank_unit_status_latest WHERE member_id = ?`;
@@ -60,10 +61,50 @@ export async function getAllMembersLite(): Promise<MemberLight[]> {
return res;
}
export async function getMembersFull(ids: number[]): Promise<Member[]> {
const sql = `SELECT * FROM view_member_rank_unit_status_latest WHERE member_id IN (?);`;
const res: Member[] = await pool.query(sql, [ids]);
return res;
export async function getMembersFull(ids: number[]): Promise<MemberCardDetails[]> {
const sql = `
SELECT m.*,
COALESCE(
JSON_ARRAYAGG(
CASE
WHEN r.id IS NOT NULL THEN JSON_OBJECT(
'id', r.id,
'name', r.name,
'color', r.color,
'description', r.description
)
END
),
JSON_ARRAY()
) AS roles
FROM view_member_rank_unit_status_latest m
LEFT JOIN members_roles mr ON m.member_id = mr.member_id
LEFT JOIN roles r ON mr.role_id = r.id
WHERE m.member_id IN (?)
GROUP BY m.member_id;
`;
const rows: any[] = await pool.query(sql, [ids]);
return rows.map(row => {
const member: Member = {
member_id: row.member_id,
member_name: row.member_name,
displayName: row.displayName,
rank: row.rank,
rank_date: row.rank_date,
unit: row.unit,
unit_date: row.unit_date,
status: row.status,
status_date: row.status_date,
loa_until: row.loa_until ? new Date(row.loa_until) : undefined,
};
// roles comes as array of strings; parse each one
const roles: Role[] = JSON.parse(row.roles).map((r: string) => JSON.parse(r));
return { member, roles };
});
}
export async function mapDiscordtoID(id: number): Promise<number | null> {

View File

@@ -1,8 +1,8 @@
import { MemberLight } from '@app/shared/types/member';
import pool from '../db';
import { Role } from '@app/shared/types/roles'
import { Role, RoleSummary } from '@app/shared/types/roles'
export async function assignUserGroup(userID: number, roleID: number) {
const sql = `INSERT INTO members_roles (member_id, role_id) VALUES (?, ?);`;
const params = [userID, roleID];
@@ -24,4 +24,34 @@ export async function getUserRoles(userID: number): Promise<Role[]> {
WHERE mr.member_id = ?;`;
return await pool.query(sql, [userID]);
}
export async function getRole(id: number): Promise<Role> {
let res = await pool.query(`SELECT * FROM roles WHERE id = ?`, [id])
return res[0] as Role;
}
export async function getAllRoles(): Promise<RoleSummary> {
return await pool.query(`SELECT id, name, color FROM roles`);
}
export async function getUsersWithRole(roleId: number): Promise<MemberLight[]> {
const out = await pool.query(
`
SELECT
m.member_id AS id,
m.member_name AS username,
m.displayName,
u.color
FROM members_roles mr
JOIN view_member_rank_unit_status_latest m
ON m.member_id = mr.member_id
LEFT JOIN units u
ON u.name = m.unit
WHERE mr.role_id = ?
`,
[roleId]
)
return out as MemberLight[]
}