Skip to content
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
48 changes: 48 additions & 0 deletions api/db/migrations/20260811151000_create_live_event_table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Knex } from 'knex';

const tableName = 'live_event';
const COLLATE = 'utf8mb4_unicode_ci';

function applyCommon(table: Knex.CreateTableBuilder) {
table.collate(COLLATE);
table.increments('id').primary();
table.timestamps(false, true);
}

export async function up(knex: Knex): Promise<void> {
if (!await knex.schema.hasTable(tableName)) {
await knex.schema.createTable(tableName, table => {
console.log(`Creating ${tableName} table...`);
applyCommon(table);

table.integer('place_id')
.unsigned()
.nullable();

table.foreign('place_id')
.references('place.id');

table.boolean('enabled')
.notNullable()
.defaultTo(false);

table.integer('updated_by')
.unsigned()
.nullable();

table.foreign('updated_by')
.references('member.id');
});

await knex(tableName).insert({
enabled: false,
});
}
}

export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(tableName)) {
console.log(`Dropping ${tableName} table`);
await knex.schema.dropTable(tableName);
}
}
2 changes: 2 additions & 0 deletions api/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
homeRoutes,
hoodRoutes,
inboxRoutes,
liveEventRoutes,
mallRoutes,
memberRoutes,
messageRoutes,
Expand Down Expand Up @@ -63,6 +64,7 @@ app.use('/api/home', homeRoutes);
app.use('/api/messageboard', messageboardRoutes);
app.use('/api/admin', adminRoutes);
app.use('/api/inbox', inboxRoutes);
app.use('/api/live-event', liveEventRoutes);
app.use('/api/mall', mallRoutes);
app.use('/api/blackmarket', blackmarketRoutes);
app.use('/api/fleamarket', fleamarketRoutes);
Expand Down
1 change: 1 addition & 0 deletions api/src/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export * from './object-instance.controller';
export * from './object.controller';
export * from './place.controller';
export * from './vote.controller';
export * from './live-event.controller';
71 changes: 71 additions & 0 deletions api/src/controllers/live-event.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Request, Response } from 'express';
import { Container, Service } from 'typedi';

import { LiveEventService, MemberService } from '../services';

@Service()
export class LiveEventController {
constructor(
private liveEventService: LiveEventService,
private memberService: MemberService,
) {}

public async getCurrent(request: Request, response: Response): Promise<void> {
const session = this.memberService.decryptSession(request, response);
if (!session) return;

try {
const liveEvent = await this.liveEventService.getCurrent();
response.status(200).json({ liveEvent });
} catch (error) {
console.log(error);
response.status(400).json({ error });
}
}

public async update(request: Request, response: Response): Promise<void> {
const session = this.memberService.decryptSession(request, response);
if (!session) return;

const canManage = await this.liveEventService.canManage(session.id);

if (!canManage) {
response.status(403).json({ message: 'Access Denied' });
return;
}

const placeId = request.body.placeId === null
? null
: Number.parseInt(request.body.placeId);

const enabled = request.body.enabled === true;

if (enabled && !placeId) {
response.status(400).json({
message: 'A destination is required when Live Event is enabled.',
});
return;
}

try {
await this.liveEventService.update(
session.id,
placeId,
enabled,
);

response.status(200).json({ status: 'success' });
} catch (error) {
console.log(error);
response.status(400).json({ error });
}
}
}

const liveEventService = Container.get(LiveEventService);
const memberService = Container.get(MemberService);

export const liveEventController = new LiveEventController(
liveEventService,
memberService,
);
12 changes: 12 additions & 0 deletions api/src/controllers/place.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,18 @@ class PlaceController {
}
}

public async getLiveEventDestinations(request: Request, response: Response,): Promise<void> {
const session = this.memberService.decryptSession(request, response);
if (!session) return;
try {
const destinations = await this.placeService.getLiveEventDestinations();
response.status(200).json({ destinations });
} catch (error) {
console.log(error);
response.status(400).json({ error });
}
}

public async addStorage(request: Request, response: Response): Promise<void> {
const session = this.memberService.decryptSession(request, response);
if(!session) return;
Expand Down
1 change: 1 addition & 0 deletions api/src/repositories/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ export * from './wallet/wallet.repository';
export * from './messageboard/messageboard.repository';
export * from './inbox/inbox.repository';
export * from './virtual_pet/virtual-pet.repository';
export * from './live-event/live-event.repository';
29 changes: 29 additions & 0 deletions api/src/repositories/live-event/live-event.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Service } from 'typedi';

import { Db } from '../../db/db.class';

@Service()
export class LiveEventRepository {
constructor(private db: Db) {}

public async getCurrent(): Promise<any> {
return this.db.knex('live_event')
.where({ id: 1 })
.first();
}

public async update(
placeId: number | null,
enabled: boolean,
updatedBy: number,
): Promise<void> {
await this.db.knex('live_event')
.where({ id: 1 })
.update({
place_id: placeId,
enabled,
updated_by: updatedBy,
updated_at: new Date(),
});
}
}
14 changes: 14 additions & 0 deletions api/src/repositories/place/place.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,20 @@ export class PlaceRepository {
.orderBy(orderBy, 'asc');
}

public async findLiveEventDestinations(): Promise<any[]> {
return this.db.place
.select(
'place.id',
'place.name',
'place.slug',
'place.type',
)
.whereIn('place.type', ['colony', 'public'])
.where('place.status', 1)
.orderBy('place.type', 'asc')
.orderBy('place.name', 'asc');
}

public async removePlace(id: number): Promise<any> {
await this.db.place
.where('id', id)
Expand Down
1 change: 1 addition & 0 deletions api/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ export * from './object.routes';
export * from './place.routes';
export * from './messageboard.routes';
export * from './inbox.routes';
export * from './live-event.routes';
export * from './vote.routes';
13 changes: 13 additions & 0 deletions api/src/routes/live-event.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Router from 'express';

import { liveEventController } from '../controllers';

const liveEventRoutes = Router();

liveEventRoutes.get('/', (request, response) =>
liveEventController.getCurrent(request, response));

liveEventRoutes.post('/', (request, response) =>
liveEventController.update(request, response));

export { liveEventRoutes };
4 changes: 3 additions & 1 deletion api/src/routes/place.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ placeRoutes.get('/getAccessInfo/:slug/:id?',
(request, response) => placeController.getAccessInfoByUsername(request, response));
placeRoutes.get('/getSecurityInfo',
(request, response) => placeController.getSecurityInfo(request, response));
placeRoutes.get('/:placeId/object_instance',
placeRoutes.get('/live-event-destinations',
(request, response) => placeController.getLiveEventDestinations(request, response),);
placeRoutes.get('/:placeId/object_instance',
(request, response) => placeController.getPlaceObjects(request, response));
placeRoutes.get('/:slug',
(request, response) => placeController.getPlace(request, response));
Expand Down
1 change: 1 addition & 0 deletions api/src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ export * from './wallet/wallet.service';
export * from './messageboard/messageboard.service';
export * from './inbox/inbox.service';
export * from './vote/vote.service';
export * from './live-event/live-event.service';
61 changes: 61 additions & 0 deletions api/src/services/live-event/live-event.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Service } from 'typedi';

import {
LiveEventRepository,
PlaceRepository,
RoleAssignmentRepository,
RoleRepository,
} from '../../repositories';

@Service()
export class LiveEventService {
constructor(
private liveEventRepository: LiveEventRepository,
private placeRepository: PlaceRepository,
private roleAssignmentRepository: RoleAssignmentRepository,
private roleRepository: RoleRepository,
) {}

public async getCurrent(): Promise<any> {
const liveEvent = await this.liveEventRepository.getCurrent();

if (!liveEvent || !liveEvent.place_id) {
return {
enabled: false,
place: null,
};
}

const place = await this.placeRepository.findById(liveEvent.place_id);

return {
enabled: liveEvent.enabled,
place,
updatedBy: liveEvent.updated_by,
updatedAt: liveEvent.updated_at,
};
}

public async canManage(memberId: number): Promise<boolean> {
const roleAssignments = await this.roleAssignmentRepository.getByMemberId(memberId);

const allowedRoles = [
this.roleRepository.roleMap.Admin,
this.roleRepository.roleMap.CityMayor,
this.roleRepository.roleMap.PlacesChief,
this.roleRepository.roleMap.ColonyRepresentative,
];

return !!roleAssignments.find(
assignment => allowedRoles.includes(assignment.role_id),
);
}

public async update(
memberId: number,
placeId: number | null,
enabled: boolean,
): Promise<void> {
await this.liveEventRepository.update(placeId, enabled, memberId);
}
}
19 changes: 19 additions & 0 deletions api/src/services/member/member.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,21 @@ export class MemberService {
return !!roleAssignments.find(assignment => LEADER_ROLES.includes(assignment.role_id));
}

public async canManageLiveEvent(memberId: number): Promise<boolean> {
const roleAssignments = await this.roleAssignmentRepository.getByMemberId(memberId);

const LIVE_EVENT_ROLES = [
this.roleRepository.roleMap.Admin,
this.roleRepository.roleMap.CityMayor,
this.roleRepository.roleMap.PlacesChief,
this.roleRepository.roleMap.ColonyRepresentative,
];

return !!roleAssignments.find(
assignment => LIVE_EVENT_ROLES.includes(assignment.role_id),
);
}

public async canStaff(memberId: number): Promise<boolean> {
const roleAssignments = await this.roleAssignmentRepository.getByMemberId(memberId);
// Extracted staff roles into a constant for easy management
Expand Down Expand Up @@ -105,6 +120,7 @@ export class MemberService {
const security = await this.canAdmin(memberId);
const roleAssignments = await this.roleAssignmentRepository.getByMemberId(memberId);
const leader = await this.canLeader(memberId);
const liveEvent = await this.canManageLiveEvent(memberId);
const admin = !!roleAssignments.find(
assignment => assignment.role_id === this.roleRepository.roleMap.Admin,
);
Expand All @@ -118,6 +134,9 @@ export class MemberService {
if (leader) {
accessLevel.push('leader');
}
if (liveEvent) {
accessLevel.push('live-event');
}
return accessLevel;
}

Expand Down
4 changes: 4 additions & 0 deletions api/src/services/place/place.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ export class PlaceService {
return await this.placeRepository.findByUserId(userId);
}

public async getLiveEventDestinations(): Promise<any[]> {
return this.placeRepository.findLiveEventDestinations();
}

public async removeVirtualPet(id: number): Promise<any> {
await this.virtualPetRepository.removeVirtualPet(id);
}
Expand Down
Binary file added spa/assets/img/live-event.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading