Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New admin schedule #61

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
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
Binary file modified .DS_Store
Binary file not shown.
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"express": "^4.19.2",
"express-validator": "^7.2.1",
"jsonwebtoken": "^9.0.2",
"knex": "^3.1.0",
"morgan": "^1.10.0",
"multer": "^1.4.5-lts.1",
"mysql": "^2.18.1",
Expand Down
33 changes: 22 additions & 11 deletions backend/src/common/databaseModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import { RowDataPacket } from "mysql2";
* This file was generated by a tool.
* Rerun sql-ts to regenerate this file.
*/
export interface AdminDB extends RowDataPacket {
'admin_id': string;
'f_name': string;
'fk_user_id'?: string | null;
'l_name': string;
export interface AbsenceRequestDB extends RowDataPacket {
'approved'?: any;
'category': 'emergency' | 'health' | 'conflict' | 'transportation' | 'other';
'comments'?: string | null;
'covered_by'?: string | null;
'details': string;
'fk_shift_id': number;
'request_id'?: number;
}
export interface AvailabilityDB extends RowDataPacket {
'availability_id'?: number;
Expand All @@ -34,17 +37,28 @@ export interface ClassPreferenceDB extends RowDataPacket {
'fk_schedule_id'?: number | null;
'fk_volunteer_id'?: string | null;
}
export interface CoverageRequestDB extends RowDataPacket {
'request_id': number;
'volunteer_id': string;
}
export interface ImageDB extends RowDataPacket {
'image': Buffer;
'image_id': string;
}
export interface InstructorDB extends RowDataPacket {
'email': string;
'f_name': string;
'fk_user_id'?: string | null;
'instructor_id': string;
'l_name': string;
}
export interface LogDB extends RowDataPacket {
'created_at'?: Date;
'description': string;
'fk_class_id'?: number | null;
'fk_volunteer_id'?: string | null;
'request_id'?: number;
'signoff': string;
}
export interface PendingShiftCoverageDB extends RowDataPacket {
'pending_volunteer': string;
'request_id': number;
Expand All @@ -54,6 +68,7 @@ export interface ScheduleDB extends RowDataPacket {
'day': number;
'end_time': string;
'fk_class_id': number;
'frequency'?: string;
'schedule_id'?: number;
'start_time': string;
}
Expand All @@ -77,8 +92,7 @@ export interface UserDB extends RowDataPacket {
'fk_image_id'?: string | null;
'l_name': string;
'password': string;
'role': string;
'role2': 'volunteer' | 'admin' | 'instructor';
'role': 'volunteer' | 'admin' | 'instructor';
'user_id': string;
}
export interface VolunteerClassDB extends RowDataPacket {
Expand All @@ -93,10 +107,7 @@ export interface VolunteerDB extends RowDataPacket {
'active'?: any;
'bio'?: string | null;
'city'?: string | null;
'email': string;
'f_name': string;
'fk_user_id'?: string | null;
'l_name': string;
'p_name'?: string | null;
'p_time_ctmt'?: number;
'phone_number'?: string | null;
Expand Down
16 changes: 11 additions & 5 deletions backend/src/common/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
export enum Role {
admin = 'admin',
volunteer = 'volunteer',
instructor = 'instructor'
};
import { createStringEnum } from "./typeUtils.js";

export const Role = createStringEnum(['admin', 'volunteer', 'instructor'] as const);
export type Role = typeof Role.values[number];

export const ShiftStatus = createStringEnum(['absence-pending', 'open', 'coverage-pending', 'resolved'] as const);
export type ShiftStatus = typeof ShiftStatus.values[number];

export const ShiftQueryType = createStringEnum(['coverage', 'absence'] as const);

export type ShiftQueryType = typeof ShiftQueryType.values[number];
7 changes: 7 additions & 0 deletions backend/src/common/typeUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function createStringEnum<const T extends readonly string[]>(values: T) {
const record = Object.fromEntries(
values.map(v => [v, v])
) as { [K in T[number]]: K };

return Object.assign(record, { values });
}
20 changes: 8 additions & 12 deletions backend/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,9 @@ import { ValidationChain } from "express-validator";
import { UserDB } from "./databaseModels.js";

export type AuthenticatedRequest = Request & {
user?: RequestUser
user: UserDB
}

/**
* UserDB with password excluded
*/
export type RequestUser = Pick<UserDB, Exclude<keyof UserDB, "password">>;

/**
* Interface for the decoded data from a JWT
*/
Expand All @@ -20,20 +15,21 @@ export interface DecodedJwtPayload {

export type HTTPMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options' | 'head';

export interface RouteGroup {
export interface RouteGroup<T extends Request = Request> {
path: string;
validation?: ValidationChain[];
middleware?: Array<(req: Request, res: Response, next: NextFunction) => void>;
middleware?: Array<(req: T, res: Response, next: NextFunction) => void>;
children: RouteDefinition[];
}

export interface RouteEndpoint {
export interface RouteEndpoint<T extends Request = Request> {
path: string;
validation?: ValidationChain[];
middleware?: Array<(req: Request, res: Response, next: NextFunction) => void>;
middleware?: Array<(req: T, res: Response, next: NextFunction) => void>;
method: HTTPMethod;
action: (req: Request, res: Response, next: NextFunction) => Promise<any>;
action: (req: T, res: Response, next: NextFunction) => Promise<any>;
}

export type RouteDefinition = RouteGroup | RouteEndpoint;
/* We know the correct Request subtype, this prevents ts from getting mad */
export type RouteDefinition = RouteGroup<any> | RouteEndpoint<any>;

13 changes: 5 additions & 8 deletions backend/src/config/authCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,16 @@ import jwt from "jsonwebtoken";
import { Role } from "../common/interfaces.js";
import {
AuthenticatedRequest,
DecodedJwtPayload,
RequestUser
DecodedJwtPayload
} from "../common/types.js";
import UserModel from "../models/userModel.js";
import { userModel } from "./models.js";

// Load environment variables
dotenv.config();

// Define environment variables
const TOKEN_SECRET = process.env.TOKEN_SECRET;

const userModel = new UserModel();

async function isAuthorized(
req: Request,
res: Response,
Expand Down Expand Up @@ -48,10 +45,10 @@ async function isAuthorized(
const result = await userModel.getUserById(decoded.user_id);

// Attach the user to the request
(req as AuthenticatedRequest).user = result as RequestUser;
(req as AuthenticatedRequest).user = result;

// Call the next function
next();
return next();
} catch (err) {
return res.status(401).json({
error: "The token is either invalid or has expired",
Expand All @@ -64,7 +61,7 @@ async function isAdmin(
res: Response,
next: NextFunction
): Promise<any> {
if (req.user!.role !== Role.admin) {
if (req.user.role !== Role.admin) {
return res.status(403).json({
error: "Forbidden",
});
Expand Down
17 changes: 17 additions & 0 deletions backend/src/config/models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import AvailabilityModel from "../models/availabilityModel.js";
import ClassesModel from "../models/classModel.js";
import ImageModel from "../models/imageModel.js";
import InstructorModel from "../models/instructorModel.js";
import ScheduleModel from "../models/scheduleModel.js";
import ShiftModel from "../models/shiftModel.js";
import UserModel from "../models/userModel.js";
import VolunteerModel from "../models/volunteerModel.js";

export const availabilityModel = new AvailabilityModel();
export const classesModel = new ClassesModel();
export const imageModel = new ImageModel();
export const instructorModel = new InstructorModel();
export const scheduleModel = new ScheduleModel();
export const shiftModel = new ShiftModel();
export const userModel = new UserModel();
export const volunteerModel = new VolunteerModel();
5 changes: 5 additions & 0 deletions backend/src/config/queryBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import knex from "knex";

const queryBuilder = knex({ client: 'mysql' });

export default queryBuilder;
22 changes: 19 additions & 3 deletions backend/src/controllers/adminController.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { Response } from "express";
import { VolunteerDB } from "../common/databaseModels.js";
import { AuthenticatedRequest } from "../common/types.js";
import VolunteerModel from "../models/volunteerModel.js";
import { volunteerModel } from "../config/models.js";

const volunteerModel = new VolunteerModel();

async function getUnverifiedVolunteers(
req: AuthenticatedRequest,
Expand Down Expand Up @@ -33,4 +32,21 @@ async function verifyVolunteer(
});
}

export { getUnverifiedVolunteers, verifyVolunteer };
async function deactivateVolunteer(
req: AuthenticatedRequest,
res: Response
): Promise<any> {
// Get the token from the request parameters
const volunteer_id = req.body.volunteer_id;

// Update the user's active status
await volunteerModel.updateVolunteer(volunteer_id, {
active: false,
} as VolunteerDB);

return res.status(200).json({
message: "User deactivated successfully",
});
}

export { getUnverifiedVolunteers, verifyVolunteer, deactivateVolunteer };
19 changes: 8 additions & 11 deletions backend/src/controllers/authController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ import nodemailer from "nodemailer";
import { v4 as uuidv4 } from "uuid";
import { UserDB, VolunteerDB } from "../common/databaseModels.js";
import { Role } from "../common/interfaces.js";
import { AuthenticatedRequest, RequestUser } from "../common/types.js";
import { AuthenticatedRequest } from "../common/types.js";
import connectionPool from "../config/database.js";
import UserModel from "../models/userModel.js";
import VolunteerModel from "../models/volunteerModel.js";
import { userModel, volunteerModel } from "../config/models.js";

// Load environment variables
dotenv.config();
Expand All @@ -28,8 +27,6 @@ const transporter = nodemailer.createTransport({
},
});

const userModel = new UserModel();
const volunteerModel = new VolunteerModel();

async function checkAuthorization(
req: AuthenticatedRequest,
Expand Down Expand Up @@ -71,7 +68,7 @@ async function registerUser(
role = role.trim();

// Hash Password with salt
const digest = bcrypt.hashSync(password, 10);
const digest = await bcrypt.hash(password, 10);

// User Id
const user_id = uuidv4();
Expand Down Expand Up @@ -143,7 +140,7 @@ async function loginUser(req: Request, res: Response): Promise<any> {
const user = await userModel.getUserByEmail(email, true);

// If the password is incorrect, return an error
if (!bcrypt.compareSync(password, user.password)) {
if (!(await bcrypt.compare(password, user.password))) {
return res.status(403).json({
error: "Incorrect password",
});
Expand Down Expand Up @@ -281,7 +278,7 @@ async function resetPassword(req: Request, res: Response): Promise<any> {
await verifyUserWithIdAndToken(id, token);

// Hash the new password
const hashedPassword = bcrypt.hashSync(password, 10);
const hashedPassword = await bcrypt.hash(password, 10);

await userModel.updateUserPassword(id, hashedPassword);

Expand All @@ -298,18 +295,18 @@ async function updatePassword(
const { currentPassword, newPassword } = req.body;

// User coming from the isAuthorized middleware, query auth info
const user = req.user as RequestUser;
const user = req.user;
const authInfo = await userModel.getUserByEmail(user.email, true);

// If the password is incorrect, return an error
if (!bcrypt.compareSync(currentPassword, authInfo.password)) {
if (!(await bcrypt.compare(currentPassword, authInfo.password))) {
return res.status(403).json({
error: "Incorrect password",
});
}

// Hash the new password and update
const hashedPassword = bcrypt.hashSync(newPassword, 10);
const hashedPassword = await bcrypt.hash(newPassword, 10);
await userModel.updateUserPassword(user.user_id, hashedPassword);

return res.status(200).json({
Expand Down
4 changes: 1 addition & 3 deletions backend/src/controllers/availabilityController.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { Response } from 'express';
import { AvailabilityDB } from '../common/databaseModels.js';
import { AuthenticatedRequest } from '../common/types.js';
import AvailabilityModel from '../models/availabilityModel.js';

const availabilityModel = new AvailabilityModel();
import { availabilityModel } from '../config/models.js';

async function getAvailabilities(req: AuthenticatedRequest, res: Response) {
const availabilities = await availabilityModel.getAvailabilities();
Expand Down
4 changes: 1 addition & 3 deletions backend/src/controllers/classController.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { Response } from 'express';
import { ClassDB } from '../common/databaseModels.js';
import { AuthenticatedRequest } from '../common/types.js';
import ClassesModel from '../models/classModel.js';

const classesModel = new ClassesModel();
import { classesModel } from '../config/models.js';

async function getAllClasses(req: AuthenticatedRequest, res: Response) {
const classes = await classesModel.getClasses();
Expand Down
6 changes: 2 additions & 4 deletions backend/src/controllers/imageController.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { Response } from "express";
import { AuthenticatedRequest } from "../common/types.js";
import ImageModel from "../models/imageModel.js";

const imageService = new ImageModel();
import { imageModel } from "../config/models.js";

export async function getImage(req: AuthenticatedRequest, res: Response) {
const { image_id } = req.params;

const image = await imageService.getImage(image_id);
const image = await imageModel.getImage(image_id);

res.type('image/png').send(image.image);
}
Loading