Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/acmucsd/membership-portal
Browse files Browse the repository at this point in the history
…into feature/automate-database-migrations

merging from master
  • Loading branch information
nik-dange committed Mar 9, 2024
2 parents 43e27b4 + db9d7db commit 779dd84
Show file tree
Hide file tree
Showing 42 changed files with 1,537 additions and 90 deletions.
22 changes: 19 additions & 3 deletions api/controllers/AttendanceController.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
import { JsonController, Get, Post, UseBefore, Params, ForbiddenError, Body } from 'routing-controllers';
import EmailService from '../../services/EmailService';
import { UserAuthentication } from '../middleware/UserAuthentication';
import { AuthenticatedUser } from '../decorators/AuthenticatedUser';
import { AttendEventRequest } from '../validators/AttendanceControllerRequests';
import { AttendEventRequest, AttendViaExpressCheckinRequest } from '../validators/AttendanceControllerRequests';
import { UserModel } from '../../models/UserModel';
import AttendanceService from '../../services/AttendanceService';
import PermissionsService from '../../services/PermissionsService';
import { GetAttendancesForEventResponse, GetAttendancesForUserResponse, AttendEventResponse } from '../../types';
import { UuidParam } from '../validators/GenericRequests';

@UseBefore(UserAuthentication)
@JsonController('/attendance')
export class AttendanceController {
private attendanceService: AttendanceService;

constructor(attendanceService: AttendanceService) {
private emailService: EmailService;

constructor(attendanceService: AttendanceService, emailService: EmailService) {
this.attendanceService = attendanceService;
this.emailService = emailService;
}

@UseBefore(UserAuthentication)
@Get('/:uuid')
async getAttendancesForEvent(@Params() params: UuidParam,
@AuthenticatedUser() user: UserModel): Promise<GetAttendancesForEventResponse> {
Expand All @@ -25,12 +29,14 @@ export class AttendanceController {
return { error: null, attendances };
}

@UseBefore(UserAuthentication)
@Get()
async getAttendancesForCurrentUser(@AuthenticatedUser() user: UserModel): Promise<GetAttendancesForUserResponse> {
const attendances = await this.attendanceService.getAttendancesForCurrentUser(user);
return { error: null, attendances };
}

@UseBefore(UserAuthentication)
@Get('/user/:uuid')
async getAttendancesForUser(@Params() params: UuidParam,
@AuthenticatedUser() currentUser: UserModel): Promise<GetAttendancesForEventResponse> {
Expand All @@ -41,10 +47,20 @@ export class AttendanceController {
return { error: null, attendances };
}

@UseBefore(UserAuthentication)
@Post()
async attendEvent(@Body() body: AttendEventRequest,
@AuthenticatedUser() user: UserModel): Promise<AttendEventResponse> {
const { event } = await this.attendanceService.attendEvent(user, body.attendanceCode, body.asStaff);
return { error: null, event };
}

@Post('/expressCheckin')
async attendViaExpressCheckin(@Body() body: AttendViaExpressCheckinRequest): Promise<AttendEventResponse> {
body.email = body.email.toLowerCase();
const { email, attendanceCode } = body;
const { event } = await this.attendanceService.attendViaExpressCheckin(attendanceCode, email);
await this.emailService.sendExpressCheckinConfirmation(email, event.title, event.pointValue);
return { error: null, event };
}
}
37 changes: 37 additions & 0 deletions api/controllers/MerchStoreController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
GetAllMerchCollectionsResponse,
CreateMerchCollectionResponse,
EditMerchCollectionResponse,
CreateCollectionPhotoResponse,
DeleteCollectionPhotoResponse,
GetOneMerchItemResponse,
DeleteMerchCollectionResponse,
CreateMerchItemResponse,
Expand Down Expand Up @@ -55,6 +57,7 @@ import MerchStoreService from '../../services/MerchStoreService';
import {
CreateMerchCollectionRequest,
EditMerchCollectionRequest,
CreateCollectionPhotoRequest,
CreateMerchItemRequest,
EditMerchItemRequest,
PlaceMerchOrderRequest,
Expand Down Expand Up @@ -124,6 +127,40 @@ export class MerchStoreController {
return { error: null };
}

@UseBefore(UserAuthentication)
@Post('/collection/picture/:uuid')
async createMerchCollectionPhoto(@UploadedFile('image',
{ options: StorageService.getFileOptions(MediaType.MERCH_PHOTO) }) file: File,
@Params() params: UuidParam,
@Body() createCollectionRequest: CreateCollectionPhotoRequest,
@AuthenticatedUser() user: UserModel): Promise<CreateCollectionPhotoResponse> {
if (!PermissionsService.canEditMerchStore(user)) throw new ForbiddenError();

// generate a random string for the uploaded photo url
const position = parseInt(createCollectionRequest.position, 10);
if (Number.isNaN(position)) throw new BadRequestError('Position must be a number');
const uniqueFileName = uuid();
const uploadedPhoto = await this.storageService.uploadToFolder(
file, MediaType.MERCH_PHOTO, uniqueFileName, params.uuid,
);
const collectionPhoto = await this.merchStoreService.createCollectionPhoto(
params.uuid, { uploadedPhoto, position },
);

return { error: null, collectionPhoto };
}

@UseBefore(UserAuthentication)
@Delete('/collection/picture/:uuid')
async deleteMerchCollectionPhoto(@Params() params: UuidParam, @AuthenticatedUser() user: UserModel):
Promise<DeleteCollectionPhotoResponse> {
if (!PermissionsService.canEditMerchStore(user)) throw new ForbiddenError();
const photoToDelete = await this.merchStoreService.getCollectionPhotoForDeletion(params.uuid);
await this.storageService.deleteAtUrl(photoToDelete.uploadedPhoto);
await this.merchStoreService.deleteCollectionPhoto(photoToDelete);
return { error: null };
}

@Get('/item/:uuid')
async getOneMerchItem(@Params() params: UuidParam,
@AuthenticatedUser() user: UserModel): Promise<GetOneMerchItemResponse> {
Expand Down
17 changes: 15 additions & 2 deletions api/validators/AttendanceControllerRequests.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { IsDefined, IsNotEmpty, Allow } from 'class-validator';
import { AttendEventRequest as IAttendEventRequest } from '../../types';
import { IsDefined, IsNotEmpty, Allow, IsEmail } from 'class-validator';
import {
AttendEventRequest as IAttendEventRequest,
AttendViaExpressCheckinRequest as IAttendViaExpressCheckinRequest,
} from '../../types';

export class AttendEventRequest implements IAttendEventRequest {
@IsDefined()
Expand All @@ -9,3 +12,13 @@ export class AttendEventRequest implements IAttendEventRequest {
@Allow()
asStaff?: boolean;
}

export class AttendViaExpressCheckinRequest implements IAttendViaExpressCheckinRequest {
@IsDefined()
@IsNotEmpty()
attendanceCode: string;

@IsDefined()
@IsEmail()
email: string;
}
32 changes: 32 additions & 0 deletions api/validators/MerchStoreRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { Type } from 'class-transformer';
import {
CreateMerchCollectionRequest as ICreateMerchCollectionRequest,
EditMerchCollectionRequest as IEditMerchCollectionRequest,
CreateCollectionPhotoRequest as ICreateCollectionPhotoRequest,
CreateMerchItemRequest as ICreateMerchItemRequest,
EditMerchItemRequest as IEditMerchItemRequest,
CreateMerchItemOptionRequest as ICreateMerchItemOptionRequest,
Expand All @@ -32,6 +33,8 @@ import {
OrderItemFulfillmentUpdate as IOrderItemFulfillmentUpdate,
MerchCollection as IMerchCollection,
MerchCollectionEdit as IMerchCollectionEdit,
MerchCollectionPhoto as IMerchCollectionPhoto,
MerchCollectionPhotoEdit as IMerchCollectionPhotoEdit,
MerchItem as IMerchItem,
MerchItemEdit as IMerchItemEdit,
MerchItemOption as IMerchItemOption,
Expand Down Expand Up @@ -60,6 +63,9 @@ export class MerchCollection implements IMerchCollection {

@Allow()
archived?: boolean;

@Allow()
collectionPhotos: MerchCollectionPhoto[];
}

export class MerchCollectionEdit implements IMerchCollectionEdit {
Expand All @@ -78,6 +84,26 @@ export class MerchCollectionEdit implements IMerchCollectionEdit {
@Min(0)
@Max(100)
discountPercentage?: number;

@Allow()
collectionPhotos?: MerchCollectionPhotoEdit[];
}

export class MerchCollectionPhoto implements IMerchCollectionPhoto {
@Allow()
uploadedPhoto: string;

@Allow()
position: number;
}

export class MerchCollectionPhotoEdit implements IMerchCollectionPhotoEdit {
@IsDefined()
@IsUUID()
uuid: string;

@Allow()
position: number;
}

export class MerchItemOptionMetadata implements IMerchItemOptionMetadata {
Expand Down Expand Up @@ -305,6 +331,12 @@ export class EditMerchCollectionRequest implements IEditMerchCollectionRequest {
collection: MerchCollectionEdit;
}

export class CreateCollectionPhotoRequest implements ICreateCollectionPhotoRequest {
@IsDefined()
@IsNumberString()
position: string;
}

export class CreateMerchItemRequest implements ICreateMerchItemRequest {
@Type(() => MerchItem)
@ValidateNested()
Expand Down
1 change: 1 addition & 0 deletions config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const Config = {
MAX_EVENT_COVER_FILE_SIZE: Number(process.env.MAX_EVENT_COVER_FILE_SIZE) * BYTES_PER_KILOBYTE,
MAX_BANNER_FILE_SIZE: Number(process.env.MAX_BANNER_FILE_SIZE) * BYTES_PER_KILOBYTE,
MAX_MERCH_PHOTO_FILE_SIZE: Number(process.env.MAX_MERCH_PHOTO_FILE_SIZE) * BYTES_PER_KILOBYTE,
MAX_COLLECTION_PHOTO_FILE_SIZE: Number(process.env.MAX_MERCH_PHOTO_FILE_SIZE) * BYTES_PER_KILOBYTE,
MAX_RESUME_FILE_SIZE: Number(process.env.MAX_RESUME_FILE_SIZE) * BYTES_PER_KILOBYTE,
PROFILE_PICTURE_UPLOAD_PATH: process.env.BASE_UPLOAD_PATH + process.env.PROFILE_PICTURE_UPLOAD_PATH,
EVENT_COVER_UPLOAD_PATH: process.env.BASE_UPLOAD_PATH + process.env.EVENT_COVER_UPLOAD_PATH,
Expand Down
58 changes: 58 additions & 0 deletions migrations/0039-add-collection-image-table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm';

const TABLE_NAME = 'MerchCollectionPhotos';
const COLLECTION_TABLE_NAME = 'MerchandiseCollections';

export class AddCollectionImageTable1696990832868 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(new Table({
name: TABLE_NAME,
columns: [
{
name: 'uuid',
type: 'uuid',
isGenerated: true,
isPrimary: true,
generationStrategy: 'uuid',
},
{
name: 'merchCollection',
type: 'uuid',
},
{
name: 'uploadedPhoto',
type: 'varchar(255)',
},
{
name: 'uploadedAt',
type: 'timestamptz',
default: 'CURRENT_TIMESTAMP(6)',
},
{
name: 'position',
type: 'integer',
},
],
// cascade delete
foreignKeys: [
{
columnNames: ['merchCollection'],
referencedTableName: COLLECTION_TABLE_NAME,
referencedColumnNames: ['uuid'],
onDelete: 'CASCADE',
},
],
}));

await queryRunner.createIndices(TABLE_NAME, [
new TableIndex({
name: 'images_by_collection_index',
columnNames: ['merchCollection'],
}),
]);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable(TABLE_NAME);
}
}
38 changes: 38 additions & 0 deletions migrations/0041-add-expressCheckins-table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { MigrationInterface, QueryRunner, Table } from 'typeorm';

const TABLE_NAME = 'ExpressCheckins';

export class AddExpressCheckinsTable1708807643314 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(new Table({
name: TABLE_NAME,
columns: [
{
name: 'uuid',
type: 'uuid',
isPrimary: true,
isGenerated: true,
generationStrategy: 'uuid',
},
{
name: 'email',
type: 'varchar(255)',
isUnique: true,
},
{
name: 'event',
type: 'uuid',
},
{
name: 'timestamp',
type: 'timestamptz',
default: 'CURRENT_TIMESTAMP(6)',
},
],
}));
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable(TABLE_NAME);
}
}
4 changes: 4 additions & 0 deletions models/EventModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as moment from 'moment';
import { BaseEntity, Column, Entity, Index, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
import { PublicEvent, Uuid } from '../types';
import { AttendanceModel } from './AttendanceModel';
import { ExpressCheckinModel } from './ExpressCheckinModel';

@Entity('Events')
@Index('event_start_end_index', ['start', 'end'])
Expand Down Expand Up @@ -59,6 +60,9 @@ export class EventModel extends BaseEntity {
@OneToMany((type) => AttendanceModel, (attendance) => attendance.event, { cascade: true })
attendances: AttendanceModel[];

@OneToMany((type) => ExpressCheckinModel, (expressCheckin) => expressCheckin.event, { cascade: true })
expressCheckins: ExpressCheckinModel[];

public getPublicEvent(canSeeAttendanceCode = false): PublicEvent {
const publicEvent: PublicEvent = {
uuid: this.uuid,
Expand Down
28 changes: 28 additions & 0 deletions models/ExpressCheckinModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { BaseEntity, Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { PublicExpressCheckin, Uuid } from '../types';
import { EventModel } from './EventModel';

@Entity('ExpressCheckins')
export class ExpressCheckinModel extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
uuid: Uuid;

@Column()
@Index({ unique: true })
email: string;

@ManyToOne((type) => EventModel, (event) => event.expressCheckins, { nullable: false })
@JoinColumn({ name: 'event' })
event: EventModel;

@Column('timestamptz', { default: () => 'CURRENT_TIMESTAMP(6)' })
timestamp: Date;

public getPublicExpressCheckin(): PublicExpressCheckin {
return {
email: this.email,
event: this.event.getPublicEvent(),
timestamp: this.timestamp,
};
}
}
Loading

0 comments on commit 779dd84

Please sign in to comment.