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
37 changes: 37 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Dependencies
node_modules/
package-lock.json
yarn.lock

# Build outputs
dist/
build/
*.tsbuildinfo

# Environment
.env
.env.local
.env.*.local

# IDE
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store

# Uploads
uploads/

# Logs
logs/
*.log
npm-debug.log*

# Coverage
coverage/

# Misc
.cache/
.turbo/
8 changes: 8 additions & 0 deletions backend/contrib/src/assets/dto/bulk-delete-assets.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { IsArray, IsUUID, ArrayNotEmpty } from 'class-validator';

export class BulkDeleteAssetsDto {
@IsArray()
@ArrayNotEmpty()
@IsUUID('4', { each: true })
ids: string[];
}
11 changes: 11 additions & 0 deletions backend/contrib/src/assets/dto/bulk-transfer-department.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { IsArray, IsUUID, ArrayNotEmpty } from 'class-validator';

export class BulkTransferDepartmentDto {
@IsArray()
@ArrayNotEmpty()
@IsUUID('4', { each: true })
ids: string[];

@IsUUID('4')
departmentId: string;
}
12 changes: 12 additions & 0 deletions backend/contrib/src/assets/dto/bulk-update-status.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IsArray, IsEnum, IsUUID, ArrayNotEmpty } from 'class-validator';
import { AssetStatus } from '../enums';

export class BulkUpdateStatusDto {
@IsArray()
@ArrayNotEmpty()
@IsUUID('4', { each: true })
ids: string[];

@IsEnum(AssetStatus)
status: AssetStatus;
}
7 changes: 7 additions & 0 deletions backend/contrib/src/assets/dto/search-assets.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { IsOptional, IsString } from 'class-validator';

export class SearchAssetsDto {
@IsOptional()
@IsString()
q?: string;
}
7 changes: 7 additions & 0 deletions backend/contrib/src/assets/dto/update-asset-tags.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { IsArray, IsString } from 'class-validator';

export class UpdateAssetTagsDto {
@IsArray()
@IsString({ each: true })
tags: string[];
}
4 changes: 4 additions & 0 deletions backend/contrib/src/auth/decorators/roles.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
28 changes: 28 additions & 0 deletions backend/contrib/src/auth/guards/roles.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';

@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}

canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);

if (!requiredRoles || requiredRoles.length === 0) {
return true;
}

const request = context.switchToHttp().getRequest();
const userRole = String(request.user?.role || '').toLowerCase();

if (!requiredRoles.map((role) => role.toLowerCase()).includes(userRole)) {
throw new ForbiddenException('Insufficient role');
}

return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { IsBoolean, IsOptional } from 'class-validator';

export class UpdateNotificationPreferencesDto {
@IsOptional()
@IsBoolean()
assetCreated?: boolean;

@IsOptional()
@IsBoolean()
assetTransferred?: boolean;

@IsOptional()
@IsBoolean()
maintenanceDue?: boolean;

@IsOptional()
@IsBoolean()
warrantyExpiring?: boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
JoinColumn,
OneToOne,
} from 'typeorm';
import { User } from '../users/user.entity';

@Entity('notification_preferences')
export class NotificationPreference {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ unique: true })
userId: string;

@OneToOne(() => User, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'userId' })
user: User;

@Column({ default: true })
assetCreated: boolean;

@Column({ default: true })
assetTransferred: boolean;

@Column({ default: true })
maintenanceDue: boolean;

@Column({ default: true })
warrantyExpiring: boolean;

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Body, Controller, Get, Patch, Req, UnauthorizedException, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { NotificationPreferencesService } from './notification-preferences.service';
import { UpdateNotificationPreferencesDto } from './dto/update-notification-preferences.dto';

@ApiTags('Notifications')
@ApiBearerAuth('JWT-auth')
@UseGuards(JwtAuthGuard)
@Controller('notifications')
export class NotificationPreferencesController {
constructor(private readonly preferencesService: NotificationPreferencesService) {}

private getUserId(req: { user?: { id?: string } }): string {
const userId = req.user?.id;
if (!userId) {
throw new UnauthorizedException('User context is missing');
}
return userId;
}

@Get('preferences')
getPreferences(@Req() req: { user?: { id?: string } }) {
return this.preferencesService.getForUser(this.getUserId(req));
}

@Patch('preferences')
updatePreferences(
@Req() req: { user?: { id?: string } },
@Body() dto: UpdateNotificationPreferencesDto,
) {
return this.preferencesService.updateForUser(this.getUserId(req), dto);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { NotificationPreference } from './notification-preference.entity';
import { UpdateNotificationPreferencesDto } from './dto/update-notification-preferences.dto';

@Injectable()
export class NotificationPreferencesService {
constructor(
@InjectRepository(NotificationPreference)
private readonly preferencesRepo: Repository<NotificationPreference>,
) {}

async createDefaultsForUser(userId: string): Promise<NotificationPreference> {
const existing = await this.preferencesRepo.findOne({ where: { userId } });
if (existing) {
return existing;
}

return this.preferencesRepo.save(
this.preferencesRepo.create({
userId,
assetCreated: true,
assetTransferred: true,
maintenanceDue: true,
warrantyExpiring: true,
}),
);
}

async getForUser(userId: string): Promise<NotificationPreference> {
return this.createDefaultsForUser(userId);
}

async updateForUser(userId: string, dto: UpdateNotificationPreferencesDto): Promise<NotificationPreference> {
const current = await this.createDefaultsForUser(userId);
Object.assign(current, dto);
return this.preferencesRepo.save(current);
}
}
15 changes: 15 additions & 0 deletions backend/contrib/src/notifications/notifications.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { NotificationPreference } from './notification-preference.entity';
import { User } from '../users/user.entity';
import { NotificationPreferencesController } from './notification-preferences.controller';
import { NotificationPreferencesService } from './notification-preferences.service';
import { UserRegistrationSubscriber } from './user-registration.subscriber';

@Module({
imports: [TypeOrmModule.forFeature([NotificationPreference, User])],
controllers: [NotificationPreferencesController],
providers: [NotificationPreferencesService, UserRegistrationSubscriber],
exports: [NotificationPreferencesService],
})
export class NotificationsModule {}
36 changes: 36 additions & 0 deletions backend/contrib/src/notifications/user-registration.subscriber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { DataSource, EventSubscriber, EntitySubscriberInterface, InsertEvent } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { User } from '../users/user.entity';
import { NotificationPreference } from './notification-preference.entity';

@Injectable()
@EventSubscriber()
export class UserRegistrationSubscriber implements EntitySubscriberInterface<User> {
constructor(dataSource: DataSource) {
dataSource.subscribers.push(this);
}

listenTo() {
return User;
}

async afterInsert(event: InsertEvent<User>): Promise<void> {
if (!event.entity?.id) {
return;
}

const repo = event.manager.getRepository(NotificationPreference);
const existing = await repo.findOne({ where: { userId: event.entity.id } });
if (existing) {
return;
}

await repo.insert({
userId: event.entity.id,
assetCreated: true,
assetTransferred: true,
maintenanceDue: true,
warrantyExpiring: true,
});
}
}
Loading