feat: connection manager — hosts + groups CRUD with search
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3b9a0118b5
commit
41411b0cb8
12
backend/src/connections/connections.module.ts
Normal file
12
backend/src/connections/connections.module.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HostsService } from './hosts.service';
|
||||
import { HostsController } from './hosts.controller';
|
||||
import { GroupsService } from './groups.service';
|
||||
import { GroupsController } from './groups.controller';
|
||||
|
||||
@Module({
|
||||
providers: [HostsService, GroupsService],
|
||||
controllers: [HostsController, GroupsController],
|
||||
exports: [HostsService],
|
||||
})
|
||||
export class ConnectionsModule {}
|
||||
14
backend/src/connections/dto/create-group.dto.ts
Normal file
14
backend/src/connections/dto/create-group.dto.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { IsString, IsInt, IsOptional } from 'class-validator';
|
||||
|
||||
export class CreateGroupDto {
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
parentId?: number;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
sortOrder?: number;
|
||||
}
|
||||
41
backend/src/connections/dto/create-host.dto.ts
Normal file
41
backend/src/connections/dto/create-host.dto.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { IsString, IsInt, IsOptional, IsEnum, IsArray, Min, Max } from 'class-validator';
|
||||
import { Protocol } from '@prisma/client';
|
||||
|
||||
export class CreateHostDto {
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
hostname: string;
|
||||
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(65535)
|
||||
@IsOptional()
|
||||
port?: number;
|
||||
|
||||
@IsEnum(Protocol)
|
||||
@IsOptional()
|
||||
protocol?: Protocol;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
groupId?: number;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
credentialId?: number;
|
||||
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@IsOptional()
|
||||
tags?: string[];
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
notes?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
color?: string;
|
||||
}
|
||||
4
backend/src/connections/dto/update-group.dto.ts
Normal file
4
backend/src/connections/dto/update-group.dto.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateGroupDto } from './create-group.dto';
|
||||
|
||||
export class UpdateGroupDto extends PartialType(CreateGroupDto) {}
|
||||
4
backend/src/connections/dto/update-host.dto.ts
Normal file
4
backend/src/connections/dto/update-host.dto.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateHostDto } from './create-host.dto';
|
||||
|
||||
export class UpdateHostDto extends PartialType(CreateHostDto) {}
|
||||
41
backend/src/connections/groups.controller.ts
Normal file
41
backend/src/connections/groups.controller.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { Controller, Get, Post, Put, Delete, Param, Body, UseGuards, ParseIntPipe } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { GroupsService } from './groups.service';
|
||||
import { CreateGroupDto } from './dto/create-group.dto';
|
||||
import { UpdateGroupDto } from './dto/update-group.dto';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('groups')
|
||||
export class GroupsController {
|
||||
constructor(private groups: GroupsService) {}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.groups.findAll();
|
||||
}
|
||||
|
||||
@Get('tree')
|
||||
findTree() {
|
||||
return this.groups.findTree();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.groups.findOne(id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() dto: CreateGroupDto) {
|
||||
return this.groups.create(dto);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateGroupDto) {
|
||||
return this.groups.update(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.groups.remove(id);
|
||||
}
|
||||
}
|
||||
60
backend/src/connections/groups.service.ts
Normal file
60
backend/src/connections/groups.service.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateGroupDto } from './dto/create-group.dto';
|
||||
import { UpdateGroupDto } from './dto/update-group.dto';
|
||||
|
||||
@Injectable()
|
||||
export class GroupsService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
findAll() {
|
||||
return this.prisma.hostGroup.findMany({
|
||||
include: { children: true, hosts: { select: { id: true, name: true, protocol: true } } },
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
findTree() {
|
||||
return this.prisma.hostGroup.findMany({
|
||||
where: { parentId: null },
|
||||
include: {
|
||||
hosts: { orderBy: { sortOrder: 'asc' } },
|
||||
children: {
|
||||
include: {
|
||||
hosts: { orderBy: { sortOrder: 'asc' } },
|
||||
children: {
|
||||
include: {
|
||||
hosts: { orderBy: { sortOrder: 'asc' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
},
|
||||
},
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: number) {
|
||||
const group = await this.prisma.hostGroup.findUnique({
|
||||
where: { id },
|
||||
include: { hosts: true, children: true },
|
||||
});
|
||||
if (!group) throw new NotFoundException(`Group ${id} not found`);
|
||||
return group;
|
||||
}
|
||||
|
||||
create(dto: CreateGroupDto) {
|
||||
return this.prisma.hostGroup.create({ data: dto });
|
||||
}
|
||||
|
||||
async update(id: number, dto: UpdateGroupDto) {
|
||||
await this.findOne(id);
|
||||
return this.prisma.hostGroup.update({ where: { id }, data: dto });
|
||||
}
|
||||
|
||||
async remove(id: number) {
|
||||
await this.findOne(id);
|
||||
return this.prisma.hostGroup.delete({ where: { id } });
|
||||
}
|
||||
}
|
||||
41
backend/src/connections/hosts.controller.ts
Normal file
41
backend/src/connections/hosts.controller.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { Controller, Get, Post, Put, Delete, Param, Body, Query, UseGuards, ParseIntPipe } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { HostsService } from './hosts.service';
|
||||
import { CreateHostDto } from './dto/create-host.dto';
|
||||
import { UpdateHostDto } from './dto/update-host.dto';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('hosts')
|
||||
export class HostsController {
|
||||
constructor(private hosts: HostsService) {}
|
||||
|
||||
@Get()
|
||||
findAll(@Query('search') search?: string) {
|
||||
return this.hosts.findAll(search);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.hosts.findOne(id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() dto: CreateHostDto) {
|
||||
return this.hosts.create(dto);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateHostDto) {
|
||||
return this.hosts.update(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.hosts.remove(id);
|
||||
}
|
||||
|
||||
@Post('reorder')
|
||||
reorder(@Body() body: { ids: number[] }) {
|
||||
return this.hosts.reorder(body.ids);
|
||||
}
|
||||
}
|
||||
76
backend/src/connections/hosts.service.ts
Normal file
76
backend/src/connections/hosts.service.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateHostDto } from './dto/create-host.dto';
|
||||
import { UpdateHostDto } from './dto/update-host.dto';
|
||||
|
||||
@Injectable()
|
||||
export class HostsService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
findAll(search?: string) {
|
||||
const where = search
|
||||
? {
|
||||
OR: [
|
||||
{ name: { contains: search, mode: 'insensitive' as const } },
|
||||
{ hostname: { contains: search, mode: 'insensitive' as const } },
|
||||
{ tags: { has: search } },
|
||||
],
|
||||
}
|
||||
: {};
|
||||
return this.prisma.host.findMany({
|
||||
where,
|
||||
include: { group: true, credential: { select: { id: true, name: true, type: true } } },
|
||||
orderBy: [{ lastConnectedAt: { sort: 'desc', nulls: 'last' } }, { sortOrder: 'asc' }],
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: number) {
|
||||
const host = await this.prisma.host.findUnique({
|
||||
where: { id },
|
||||
include: { group: true, credential: true },
|
||||
});
|
||||
if (!host) throw new NotFoundException(`Host ${id} not found`);
|
||||
return host;
|
||||
}
|
||||
|
||||
create(dto: CreateHostDto) {
|
||||
return this.prisma.host.create({
|
||||
data: {
|
||||
name: dto.name,
|
||||
hostname: dto.hostname,
|
||||
port: dto.port ?? (dto.protocol === 'rdp' ? 3389 : 22),
|
||||
protocol: dto.protocol ?? 'ssh',
|
||||
groupId: dto.groupId,
|
||||
credentialId: dto.credentialId,
|
||||
tags: dto.tags ?? [],
|
||||
notes: dto.notes,
|
||||
color: dto.color,
|
||||
},
|
||||
include: { group: true },
|
||||
});
|
||||
}
|
||||
|
||||
async update(id: number, dto: UpdateHostDto) {
|
||||
await this.findOne(id); // throws if not found
|
||||
return this.prisma.host.update({ where: { id }, data: dto });
|
||||
}
|
||||
|
||||
async remove(id: number) {
|
||||
await this.findOne(id);
|
||||
return this.prisma.host.delete({ where: { id } });
|
||||
}
|
||||
|
||||
async touchLastConnected(id: number) {
|
||||
return this.prisma.host.update({
|
||||
where: { id },
|
||||
data: { lastConnectedAt: new Date() },
|
||||
});
|
||||
}
|
||||
|
||||
async reorder(ids: number[]) {
|
||||
const updates = ids.map((id, index) =>
|
||||
this.prisma.host.update({ where: { id }, data: { sortOrder: index } }),
|
||||
);
|
||||
return this.prisma.$transaction(updates);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user