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