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
4 changes: 4 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import { AccessControlModule } from './access-control/access-control.module';
import { WaitlistModule } from './waitlist/waitlist.module';
import { EventsModule } from './events/events.module';
import { MembershipPlansModule } from './membership-plans/membership-plans.module';
import { InventoryModule } from './inventory/inventory.module';
import { AuditLogModule } from './audit-log/audit-log.module';

@Module({
imports: [
Expand Down Expand Up @@ -114,6 +116,8 @@ import { MembershipPlansModule } from './membership-plans/membership-plans.modul
WaitlistModule,
EventsModule,
MembershipPlansModule,
InventoryModule,
AuditLogModule,
],
controllers: [AppController],
providers: [
Expand Down
35 changes: 35 additions & 0 deletions backend/src/audit-log/audit-log.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { AuditLogService } from './audit-log.service';
import { JwtAuthGuard } from '../auth/guard/jwt.auth.guard';
import { RolesGuard } from '../auth/guard/roles.guard';
import { Roles } from '../auth/decorators/roles.decorators';
import { UserRole } from '../users/enums/userRoles.enum';

@ApiTags('Audit Log')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RolesGuard)
@Controller('audit-log')
export class AuditLogController {
constructor(private readonly service: AuditLogService) {}

@Get()
@Roles(UserRole.SUPER_ADMIN)
async findAll(
@Query('actorId') actorId?: string,
@Query('resourceType') resourceType?: string,
@Query('startDate') startDate?: string,
@Query('endDate') endDate?: string,
@Query('page') page?: string,
@Query('limit') limit?: string,
) {
return this.service.findAll({
actorId,
resourceType,
startDate,
endDate,
page: page ? +page : 1,
limit: limit ? +limit : 20,
});
}
}
13 changes: 13 additions & 0 deletions backend/src/audit-log/audit-log.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuditLog } from './entities/audit-log.entity';
import { AuditLogService } from './audit-log.service';
import { AuditLogController } from './audit-log.controller';

@Module({
imports: [TypeOrmModule.forFeature([AuditLog])],
controllers: [AuditLogController],
providers: [AuditLogService],
exports: [AuditLogService],
})
export class AuditLogModule {}
49 changes: 49 additions & 0 deletions backend/src/audit-log/audit-log.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AuditLog } from './entities/audit-log.entity';

@Injectable()
export class AuditLogService {
constructor(
@InjectRepository(AuditLog)
private readonly repo: Repository<AuditLog>,
) {}

async log(
actorId: string,
action: string,
resourceType: string,
resourceId: string,
metadata?: Record<string, unknown>,
ipAddress?: string,
): Promise<void> {
await this.repo.save(
this.repo.create({ actorUserId: actorId, action, resourceType, resourceId, metadata, ipAddress: ipAddress ?? null }),
);
}

async findAll(filters: {
actorId?: string;
resourceType?: string;
startDate?: string;
endDate?: string;
page?: number;
limit?: number;
}) {
const { page = 1, limit = 20, actorId, resourceType, startDate, endDate } = filters;
const qb = this.repo.createQueryBuilder('a')
.leftJoinAndSelect('a.actor', 'actor')
.orderBy('a.createdAt', 'DESC')
.skip((page - 1) * limit)
.take(limit);

if (actorId) qb.andWhere('a.actorUserId = :actorId', { actorId });
if (resourceType) qb.andWhere('a.resourceType = :resourceType', { resourceType });
if (startDate) qb.andWhere('a.createdAt >= :startDate', { startDate });
if (endDate) qb.andWhere('a.createdAt <= :endDate', { endDate });

const [items, total] = await qb.getManyAndCount();
return { items, total, page, limit, totalPages: Math.ceil(total / limit) };
}
}
41 changes: 41 additions & 0 deletions backend/src/audit-log/entities/audit-log.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
JoinColumn,
CreateDateColumn,
} from 'typeorm';
import { User } from '../../users/entities/user.entity';
import { AuditAction } from '../enums/audit-action.enum';

@Entity('audit_logs')
export class AuditLog {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column('uuid')
actorUserId: string;

@ManyToOne(() => User, { onDelete: 'RESTRICT' })
@JoinColumn({ name: 'actorUserId' })
actor: User;

@Column({ type: 'varchar' })
action: string;

@Column({ type: 'varchar' })
resourceType: string;

@Column({ type: 'varchar' })
resourceId: string;

@Column({ type: 'jsonb', nullable: true })
metadata: Record<string, unknown>;

@Column({ nullable: true })
ipAddress: string | null;

@CreateDateColumn()
createdAt: Date;
}
12 changes: 12 additions & 0 deletions backend/src/audit-log/enums/audit-action.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export enum AuditAction {
MEMBER_SUSPENDED = 'MEMBER_SUSPENDED',
MEMBER_ACTIVATED = 'MEMBER_ACTIVATED',
BOOKING_CANCELLED = 'BOOKING_CANCELLED',
BOOKING_STATUS_OVERRIDE = 'BOOKING_STATUS_OVERRIDE',
PAYMENT_REFUNDED = 'PAYMENT_REFUNDED',
WORKSPACE_CREATED = 'WORKSPACE_CREATED',
WORKSPACE_UPDATED = 'WORKSPACE_UPDATED',
WORKSPACE_DELETED = 'WORKSPACE_DELETED',
PROMO_CODE_CREATED = 'PROMO_CODE_CREATED',
PROMO_CODE_DISABLED = 'PROMO_CODE_DISABLED',
}
6 changes: 6 additions & 0 deletions backend/src/inventory/dto/assign-inventory-item.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { IsUUID } from 'class-validator';

export class AssignInventoryItemDto {
@IsUUID()
userId: string;
}
32 changes: 32 additions & 0 deletions backend/src/inventory/dto/create-inventory-item.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { IsDateString, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { ItemCondition } from '../enums/item-condition.enum';

export class CreateInventoryItemDto {
@IsString()
@IsNotEmpty()
name: string;

@IsString()
@IsNotEmpty()
category: string;

@IsOptional()
@IsString()
serialNumber?: string;

@IsString()
@IsNotEmpty()
location: string;

@IsOptional()
@IsEnum(ItemCondition)
condition?: ItemCondition;

@IsOptional()
@IsDateString()
purchasedAt?: string;

@IsOptional()
@IsString()
notes?: string;
}
57 changes: 57 additions & 0 deletions backend/src/inventory/entities/inventory-item.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
JoinColumn,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { User } from '../../users/entities/user.entity';
import { ItemCondition } from '../enums/item-condition.enum';

@Entity('inventory_items')
export class InventoryItem {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
name: string;

@Column()
category: string;

@Column({ nullable: true, unique: true })
serialNumber: string | null;

@Column()
location: string;

@Column({ type: 'enum', enum: ItemCondition, default: ItemCondition.GOOD })
condition: ItemCondition;

@Column('uuid', { nullable: true })
assignedToUserId: string | null;

@ManyToOne(() => User, { nullable: true, onDelete: 'SET NULL' })
@JoinColumn({ name: 'assignedToUserId' })
assignedTo: User;

@Column({ type: 'timestamptz', nullable: true })
assignedAt: Date | null;

@Column({ type: 'date', nullable: true })
purchasedAt: string | null;

@Column({ type: 'text', nullable: true })
notes: string | null;

@Column({ default: false })
isDeleted: boolean;

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}
6 changes: 6 additions & 0 deletions backend/src/inventory/enums/item-condition.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum ItemCondition {
GOOD = 'GOOD',
FAIR = 'FAIR',
NEEDS_REPAIR = 'NEEDS_REPAIR',
RETIRED = 'RETIRED',
}
85 changes: 85 additions & 0 deletions backend/src/inventory/inventory.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {
Controller,
Get,
Post,
Patch,
Delete,
Param,
Body,
Query,
UseGuards,
ParseUUIDPipe,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { InventoryService } from './inventory.service';
import { CreateInventoryItemDto } from './dto/create-inventory-item.dto';
import { AssignInventoryItemDto } from './dto/assign-inventory-item.dto';
import { JwtAuthGuard } from '../auth/guard/jwt.auth.guard';
import { RolesGuard } from '../auth/guard/roles.guard';
import { Roles } from '../auth/decorators/roles.decorators';
import { UserRole } from '../users/enums/userRoles.enum';

@ApiTags('Inventory')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RolesGuard)
@Controller('inventory')
export class InventoryController {
constructor(private readonly service: InventoryService) {}

@Post()
@Roles(UserRole.ADMIN)
async create(@Body() dto: CreateInventoryItemDto) {
const data = await this.service.create(dto);
return { message: 'Item created', data };
}

@Get()
@Roles(UserRole.ADMIN, UserRole.STAFF)
async findAll(
@Query('category') category?: string,
@Query('condition') condition?: string,
@Query('location') location?: string,
@Query('assignedToUserId') assignedToUserId?: string,
) {
const data = await this.service.findAll({ category, condition, location, assignedToUserId });
return { data };
}

@Get(':id')
@Roles(UserRole.ADMIN, UserRole.STAFF)
async findOne(@Param('id', ParseUUIDPipe) id: string) {
const data = await this.service.findOne(id);
return { data };
}

@Patch(':id')
@Roles(UserRole.ADMIN)
async update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: Partial<CreateInventoryItemDto>) {
const data = await this.service.update(id, dto);
return { message: 'Item updated', data };
}

@Post(':id/assign')
@Roles(UserRole.ADMIN)
async assign(@Param('id', ParseUUIDPipe) id: string, @Body() dto: AssignInventoryItemDto) {
const data = await this.service.assign(id, dto);
return { message: 'Item assigned', data };
}

@Post(':id/unassign')
@Roles(UserRole.ADMIN)
async unassign(@Param('id', ParseUUIDPipe) id: string) {
const data = await this.service.unassign(id);
return { message: 'Item unassigned', data };
}

@Delete(':id')
@Roles(UserRole.ADMIN)
@HttpCode(HttpStatus.OK)
async remove(@Param('id', ParseUUIDPipe) id: string) {
await this.service.softDelete(id);
return { message: 'Item retired' };
}
}
13 changes: 13 additions & 0 deletions backend/src/inventory/inventory.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { InventoryItem } from './entities/inventory-item.entity';
import { InventoryService } from './inventory.service';
import { InventoryController } from './inventory.controller';

@Module({
imports: [TypeOrmModule.forFeature([InventoryItem])],
controllers: [InventoryController],
providers: [InventoryService],
exports: [InventoryService],
})
export class InventoryModule {}
Loading
Loading