Skip to content
Merged
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
21 changes: 18 additions & 3 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,23 @@ REDIS_PORT=6379
#
SENDGRID_API_KEY=your_sendgrid_api_key

# Email Provider Webhook Secrets (Optional)
SENDGRID_WEBHOOK_SECRET=your_sendgrid_webhook_secret
MAILGUN_WEBHOOK_SECRET=your_mailgun_webhook_secret
SES_SNS_TOPIC_ARN=your_ses_sns_topic_arn

# Access Control
DEVICE_WEBHOOK_API_KEY=your_device_webhook_api_key_here

# ─── Database Migrations ────────────────────────────────────────────────────
# synchronize is set to FALSE in app.module.ts — schema changes are managed
# via TypeORM migrations only. After every deploy, run:
#
# npm run migration:run
#
# ==> OTHER
# To generate a migration after changing entities:
# npm run migration:generate -- src/migrations/DescriptiveMigrationName
#
COMPANY_NAME=ManageHub
SUPPORT_EMAIL=support@yourdomain.com
# To revert the most recent migration:
# npm run migration:revert
DATABASE_SSL=false
9 changes: 5 additions & 4 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"typeorm": "typeorm-ts-node-commonjs",
"typeorm:run-migrations": "npm run typeorm migration:run -- -d ./src/config/typeorm.config.ts",
"typeorm:generate-migration": "npm run typeorm -- migration:generate ./src/migrations/$npm_config_name -d ./src/config/typeorm.config.ts",
"typeorm:create-migration": "npm run typeorm -- migration:create ./src/migrations/$npm_config_name",
"typeorm:revert-migration": "npm run typeorm -- migration:revert -d ./src/config/typeorm.config.ts",
"migration:generate": "npm run typeorm -- migration:generate -d src/data-source.ts",
"migration:run": "npm run typeorm -- migration:run -d src/data-source.ts",
"migration:revert": "npm run typeorm -- migration:revert -d src/data-source.ts",
"migration:create": "npm run typeorm -- migration:create",
"migration:show": "npm run typeorm -- migration:show -d src/data-source.ts",
"demo:seed": "node scripts/demo-data.js seed",
"demo:clear": "node scripts/demo-data.js clear",
"demo:info": "node scripts/demo-data.js info"
Expand Down
7 changes: 5 additions & 2 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { ResourcesModule } from './resources/resources.module';
import { NpsModule } from './nps/nps.module';
import { SearchModule } from './search/search.module';
import { DoorAccessModule } from './integrations/access-control/door-access.module';
import { CommunityModule } from './community/community.module';

import { validationSchema } from './config/env.validation';

Expand Down Expand Up @@ -94,7 +95,8 @@ import { LoggerModule } from 'nestjs-pino';
},
{ name: 'newsletter', ttl: 60_000, limit: 10 },
{ name: 'contact', ttl: 60_000, limit: 5 },
{ name: 'feedback', ttl: 60_000, limit: 10 },
// otp: strict limit for OTP/2FA verification endpoints (5 per 10 minutes)
{ name: 'otp', ttl: 600_000, limit: 5 },
]),
BullModule.forRootAsync({
imports: [ConfigModule],
Expand Down Expand Up @@ -130,7 +132,7 @@ import { LoggerModule } from 'nestjs-pino';
port: +configService.get('DATABASE_PORT'),
host,
autoLoadEntities: true,
synchronize: true,
synchronize: false, // Never use synchronize in production — run migrations instead
ssl: sslRequired ? { rejectUnauthorized: false } : false,
};
},
Expand Down Expand Up @@ -181,6 +183,7 @@ import { LoggerModule } from 'nestjs-pino';
ResourcesModule,
NpsModule,
DoorAccessModule,
CommunityModule,
SearchModule,
],
controllers: [AppController],
Expand Down
13 changes: 13 additions & 0 deletions backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { Throttle, seconds } from '@nestjs/throttler';
import { AuthService } from './auth.service';
import { CreateUserDto } from './dto/create-user.dto';
import { LoginUserDto } from './dto/login-user.dto';
Expand Down Expand Up @@ -34,19 +35,23 @@ export class AuthController {
@Public()
@Post('register')
@HttpCode(HttpStatus.CREATED)
@Throttle({ short: { ttl: seconds(1), limit: 3 } })
create(@Body() createUserDto: CreateUserDto) {
return this.authService.createUser(createUserDto);
}

@Public()
@Post('verify-otp')
@HttpCode(HttpStatus.OK)
@Throttle({ otp: { ttl: seconds(600), limit: 5 } })
verifyOtp(@Body() verifyOtpDto: VerifyOtpDto) {
return this.authService.verifyOtp(verifyOtpDto);
}

@Public()
@Post('resend-verification-otp')
@HttpCode(HttpStatus.OK)
@Throttle({ otp: { ttl: seconds(600), limit: 5 } })
resendVerificationOtp(@Body() resendOtpDto: ResendOtpDto) {
return this.authService.resendVerificationOtp(resendOtpDto.email);
}
Expand All @@ -61,6 +66,7 @@ export class AuthController {
@Public()
@Post('login')
@HttpCode(HttpStatus.OK)
@Throttle({ short: { ttl: seconds(1), limit: 3 } })
login(@Body() loginUserDto: LoginUserDto) {
return this.authService.login(loginUserDto);
}
Expand All @@ -81,28 +87,33 @@ export class AuthController {
@Public()
@Post('forgot-password')
@HttpCode(HttpStatus.OK)
@Throttle({ short: { ttl: seconds(1), limit: 3 } })
forgotPassword(@Body() sendPasswordResetOtpDto: SendPasswordResetOtpDto) {
return this.authService.requestResetPasswordOtp(sendPasswordResetOtpDto);
}

@Public()
@Post('send-reset-password-otp')
@HttpCode(HttpStatus.OK)
@Throttle({ short: { ttl: seconds(1), limit: 3 } })
requestResetPasswordOtp(
@Body() sendPasswordResetOtpDto: SendPasswordResetOtpDto,
) {
return this.authService.requestResetPasswordOtp(sendPasswordResetOtpDto);
}

@Public()
@Post('resend-reset-password-otp')
@HttpCode(HttpStatus.OK)
@Throttle({ otp: { ttl: seconds(600), limit: 5 } })
resendResetPasswordVerificationOtp(@Body() resendOtpDto: ResendOtpDto) {
return this.authService.resendResetPasswordVerificationOtp(resendOtpDto);
}

@Public()
@Post('verify-reset-password-otp')
@HttpCode(HttpStatus.OK)
@Throttle({ otp: { ttl: seconds(600), limit: 5 } })
verifyResetPasswordOtp(@Body() verifyOtpDto: VerifyOtpDto) {
return this.authService.verifyResetPasswordOtp(verifyOtpDto);
}
Expand Down Expand Up @@ -131,13 +142,15 @@ export class AuthController {
@Public()
@Post('2fa/verify')
@HttpCode(HttpStatus.OK)
@Throttle({ otp: { ttl: seconds(600), limit: 5 } })
verifyTotpLogin(@Body() dto: VerifyTotpDto) {
return this.authService.verifyTotpLogin(dto);
}

@Public()
@Post('2fa/backup-code')
@HttpCode(HttpStatus.OK)
@Throttle({ otp: { ttl: seconds(600), limit: 5 } })
verifyBackupCode(@Body() dto: UseBackupCodeDto) {
return this.authService.verifyBackupCode(dto);
}
Expand Down
2 changes: 2 additions & 0 deletions backend/src/bookings/bookings.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
Header,
} from '@nestjs/common';
import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery } from '@nestjs/swagger';
import { Throttle, seconds } from '@nestjs/throttler';
import { BookingsService } from './bookings.service';
import { CreateBookingDto } from './dto/create-booking.dto';
import { CreateRecurringBookingDto } from './dto/create-recurring-booking.dto';
Expand Down Expand Up @@ -43,6 +44,7 @@ export class BookingsController {

@Post()
@ApiOperation({ summary: 'Create a booking' })
@Throttle({ long: { ttl: seconds(60), limit: 30 } })
async create(@Body() dto: CreateBookingDto, @GetCurrentUser('id') userId: string) {
const booking = await this.bookingsService.create(dto, userId);
return { message: 'Booking created successfully', data: booking };
Expand Down
96 changes: 96 additions & 0 deletions backend/src/community/community.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {
Controller,
Get,
Post,
Delete,
Patch,
Body,
Param,
Query,
HttpCode,
HttpStatus,
ParseUUIDPipe,
UseGuards,
} from '@nestjs/common';
import {
ApiBearerAuth,
ApiOperation,
ApiTags,
} from '@nestjs/swagger';
import { CommunityService } from './community.service';
import { CreatePostDto } from './dto/create-post.dto';
import { PostQueryDto } from './dto/post-query.dto';
import { GetCurrentUser } from '../auth/decorators/getCurrentUser.decorator';
import { Roles } from '../auth/decorators/roles.decorators';
import { RolesGuard } from '../auth/guard/roles.guard';
import { UserRole } from '../users/enums/userRoles.enum';

@ApiTags('Community Feed')
@ApiBearerAuth()
@UseGuards(RolesGuard)
@Controller('community/posts')
export class CommunityController {
constructor(private readonly communityService: CommunityService) {}

// POST /community/posts — any active authenticated member
@Post()
@Roles(UserRole.USER, UserRole.STAFF, UserRole.ADMIN, UserRole.SUPER_ADMIN)
@ApiOperation({ summary: 'Create a community post' })
async createPost(
@Body() dto: CreatePostDto,
@GetCurrentUser('id') userId: string,
) {
const post = await this.communityService.createPost(dto, userId);
return { message: 'Post created successfully', data: post };
}

// GET /community/posts — paginated feed; pinned first, then newest
@Get()
@Roles(UserRole.USER, UserRole.STAFF, UserRole.ADMIN, UserRole.SUPER_ADMIN)
@ApiOperation({ summary: 'Get paginated community feed (pinned first, then newest)' })
async getFeed(@Query() query: PostQueryDto) {
return this.communityService.getFeed(query);
}

// DELETE /community/posts/:id — soft-delete own post (member) or any post (admin)
@Delete(':id')
@Roles(UserRole.USER, UserRole.STAFF, UserRole.ADMIN, UserRole.SUPER_ADMIN)
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Soft-delete a post (own post for members, any post for admins)' })
async deletePost(
@Param('id', ParseUUIDPipe) id: string,
@GetCurrentUser('id') userId: string,
@GetCurrentUser('role') userRole: UserRole,
) {
await this.communityService.deletePost(id, userId, userRole);
return { message: 'Post deleted successfully' };
}

// POST /community/posts/:id/like — toggle like/unlike
@Post(':id/like')
@Roles(UserRole.USER, UserRole.STAFF, UserRole.ADMIN, UserRole.SUPER_ADMIN)
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Toggle like/unlike on a post' })
async toggleLike(
@Param('id', ParseUUIDPipe) id: string,
@GetCurrentUser('id') userId: string,
) {
const result = await this.communityService.toggleLike(id, userId);
return {
message: result.liked ? 'Post liked' : 'Post unliked',
data: result,
};
}

// PATCH /community/posts/:id/pin — toggle pin/unpin (admin only)
@Patch(':id/pin')
@Roles(UserRole.ADMIN, UserRole.SUPER_ADMIN)
@ApiOperation({ summary: 'Toggle pin/unpin on a post (admin only)' })
async togglePin(@Param('id', ParseUUIDPipe) id: string) {
const result = await this.communityService.togglePin(id);
return {
message: result.isPinned ? 'Post pinned' : 'Post unpinned',
data: result,
};
}
}
14 changes: 14 additions & 0 deletions backend/src/community/community.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CommunityPost } from './entities/community-post.entity';
import { CommunityPostLike } from './entities/community-post-like.entity';
import { CommunityService } from './community.service';
import { CommunityController } from './community.controller';

@Module({
imports: [TypeOrmModule.forFeature([CommunityPost, CommunityPostLike])],
controllers: [CommunityController],
providers: [CommunityService],
exports: [CommunityService],
})
export class CommunityModule {}
Loading
Loading