feat: bootstrap NestJS backend with auth, stations, users, health-check, now-playing

This commit is contained in:
nk
2026-06-02 13:54:00 +03:00
commit 8aadd62e3c
47 changed files with 13234 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
import { IsInt, IsString, IsOptional, IsArray } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class CreateStationDto {
@ApiProperty()
@IsInt()
stationId: number;
@ApiProperty()
@IsString()
name: string;
@ApiProperty()
@IsString()
prefix: string;
@ApiProperty()
@IsString()
streamUrl: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
coverUrl?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
genre?: string;
@ApiProperty({ required: false, type: [String] })
@IsOptional()
@IsArray()
@IsString({ each: true })
tags?: string[];
@ApiProperty()
@IsInt()
sortOrder: number;
@ApiProperty()
@IsString()
source: string;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateStationDto } from './create-station.dto';
export class UpdateStationDto extends PartialType(CreateStationDto) {}

View File

@@ -0,0 +1,70 @@
import {
Controller,
Get,
Post,
Patch,
Delete,
Body,
Param,
Query,
UseGuards,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { StationsService } from './stations.service';
import { CreateStationDto } from './dto/create-station.dto';
import { UpdateStationDto } from './dto/update-station.dto';
import { AuthGuard } from '../auth/auth.guard';
@ApiTags('stations')
@Controller('stations')
export class StationsController {
constructor(private readonly stationsService: StationsService) {}
@Get()
@ApiOperation({ summary: 'List all stations' })
async findAll(
@Query('search') search?: string,
@Query('source') source?: string,
@Query('online') online?: string,
) {
return this.stationsService.findAll({
search,
source,
online: online === 'true' ? true : online === 'false' ? false : undefined,
});
}
@Get(':id')
@ApiOperation({ summary: 'Get station by ID' })
async findOne(@Param('id') id: string) {
return this.stationsService.findOne(id);
}
@Post()
@UseGuards(AuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Create station (admin)' })
@HttpCode(HttpStatus.CREATED)
async create(@Body() dto: CreateStationDto) {
return this.stationsService.create(dto);
}
@Patch(':id')
@UseGuards(AuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Update station (admin)' })
async update(@Param('id') id: string, @Body() dto: UpdateStationDto) {
return this.stationsService.update(id, dto);
}
@Delete(':id')
@UseGuards(AuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Delete station (admin)' })
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id') id: string) {
await this.stationsService.remove(id);
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { AuthModule } from '../auth/auth.module';
import { StationsService } from './stations.service';
import { StationsController } from './stations.controller';
@Module({
imports: [AuthModule],
controllers: [StationsController],
providers: [StationsService],
exports: [StationsService],
})
export class StationsModule {}

View File

@@ -0,0 +1,114 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateStationDto } from './dto/create-station.dto';
import { UpdateStationDto } from './dto/update-station.dto';
@Injectable()
export class StationsService {
constructor(private readonly prisma: PrismaService) {}
async findAll(filters: {
search?: string;
source?: string;
online?: boolean;
}) {
const where: any = {};
if (filters.source) {
where.source = filters.source;
}
if (filters.online !== undefined) {
where.isOnline = filters.online;
}
if (filters.search) {
where.OR = [
{ name: { contains: filters.search, mode: 'insensitive' } },
{ genre: { contains: filters.search, mode: 'insensitive' } },
{ tags: { has: filters.search } },
];
}
return this.prisma.station.findMany({
where,
orderBy: { sortOrder: 'asc' },
include: { nowPlaying: true },
});
}
async findOne(id: string) {
const station = await this.prisma.station.findUnique({
where: { id },
include: { nowPlaying: true },
});
if (!station) throw new NotFoundException('Station not found');
return station;
}
async findByStationId(stationId: number) {
return this.prisma.station.findUnique({
where: { stationId },
});
}
async create(dto: CreateStationDto) {
return this.prisma.station.create({
data: {
stationId: dto.stationId,
name: dto.name,
prefix: dto.prefix,
streamUrl: dto.streamUrl,
coverUrl: dto.coverUrl,
genre: dto.genre,
tags: dto.tags ?? [],
sortOrder: dto.sortOrder,
source: dto.source,
},
});
}
async update(id: string, dto: UpdateStationDto) {
return this.prisma.station.update({
where: { id },
data: {
...dto,
tags: dto.tags,
},
});
}
async remove(id: string) {
return this.prisma.station.delete({ where: { id } });
}
async upsertMany(stations: CreateStationDto[]) {
const results = [];
for (const dto of stations) {
const result = await this.prisma.station.upsert({
where: { stationId: dto.stationId },
update: {
name: dto.name,
prefix: dto.prefix,
streamUrl: dto.streamUrl,
coverUrl: dto.coverUrl,
genre: dto.genre,
tags: dto.tags ?? [],
sortOrder: dto.sortOrder,
source: dto.source,
},
create: {
stationId: dto.stationId,
name: dto.name,
prefix: dto.prefix,
streamUrl: dto.streamUrl,
coverUrl: dto.coverUrl,
genre: dto.genre,
tags: dto.tags ?? [],
sortOrder: dto.sortOrder,
source: dto.source,
},
});
results.push(result);
}
return results;
}
}