improve app registration and component service ports
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -2456,6 +2456,7 @@ type ComponentPort = {
|
|||||||
name: string;
|
name: string;
|
||||||
port: number;
|
port: number;
|
||||||
targetPort: number;
|
targetPort: number;
|
||||||
|
nodePort?: number;
|
||||||
protocol: string;
|
protocol: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2464,6 +2465,7 @@ function normalizeComponentPorts(
|
|||||||
name?: string;
|
name?: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
targetPort?: number;
|
targetPort?: number;
|
||||||
|
nodePort?: number;
|
||||||
protocol?: string;
|
protocol?: string;
|
||||||
}> | null,
|
}> | null,
|
||||||
fallbackPort?: number | null,
|
fallbackPort?: number | null,
|
||||||
@@ -2496,6 +2498,7 @@ function normalizeComponentPort(
|
|||||||
name?: string;
|
name?: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
targetPort?: number;
|
targetPort?: number;
|
||||||
|
nodePort?: number;
|
||||||
protocol?: string;
|
protocol?: string;
|
||||||
},
|
},
|
||||||
index: number,
|
index: number,
|
||||||
@@ -2512,11 +2515,16 @@ function normalizeComponentPort(
|
|||||||
|
|
||||||
const protocol = port.protocol?.toUpperCase() === 'UDP' ? 'UDP' : 'TCP';
|
const protocol = port.protocol?.toUpperCase() === 'UDP' ? 'UDP' : 'TCP';
|
||||||
const name = sanitizePortName(port.name || (index === 0 ? 'http' : `port-${servicePort}`));
|
const name = sanitizePortName(port.name || (index === 0 ? 'http' : `port-${servicePort}`));
|
||||||
|
const nodePort = port.nodePort ? Number(port.nodePort) : undefined;
|
||||||
|
if (nodePort && (!Number.isInteger(nodePort) || nodePort < 30000 || nodePort > 32767)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
port: servicePort,
|
port: servicePort,
|
||||||
targetPort,
|
targetPort,
|
||||||
|
...(nodePort ? { nodePort } : {}),
|
||||||
protocol,
|
protocol,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -2537,6 +2545,7 @@ function getComponentPorts(component?: {
|
|||||||
name: typeof item.name === 'string' ? item.name : undefined,
|
name: typeof item.name === 'string' ? item.name : undefined,
|
||||||
port: typeof item.port === 'number' ? item.port : Number(item.port),
|
port: typeof item.port === 'number' ? item.port : Number(item.port),
|
||||||
targetPort: typeof item.targetPort === 'number' ? item.targetPort : Number(item.targetPort),
|
targetPort: typeof item.targetPort === 'number' ? item.targetPort : Number(item.targetPort),
|
||||||
|
nodePort: typeof item.nodePort === 'number' ? item.nodePort : Number(item.nodePort),
|
||||||
protocol: typeof item.protocol === 'string' ? item.protocol : undefined,
|
protocol: typeof item.protocol === 'string' ? item.protocol : undefined,
|
||||||
}, index);
|
}, index);
|
||||||
})
|
})
|
||||||
@@ -2950,6 +2959,7 @@ function createServiceManifest(input: {
|
|||||||
ports: ComponentPort[];
|
ports: ComponentPort[];
|
||||||
serviceType?: string | null;
|
serviceType?: string | null;
|
||||||
}) {
|
}) {
|
||||||
|
const serviceType = normalizeServiceType(input.serviceType);
|
||||||
return {
|
return {
|
||||||
apiVersion: 'v1',
|
apiVersion: 'v1',
|
||||||
kind: 'Service',
|
kind: 'Service',
|
||||||
@@ -2959,7 +2969,7 @@ function createServiceManifest(input: {
|
|||||||
labels: getManagedLabels(input.name, 'service'),
|
labels: getManagedLabels(input.name, 'service'),
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
type: normalizeServiceType(input.serviceType),
|
type: serviceType,
|
||||||
selector: {
|
selector: {
|
||||||
'app.kubernetes.io/name': input.name,
|
'app.kubernetes.io/name': input.name,
|
||||||
},
|
},
|
||||||
@@ -2968,6 +2978,7 @@ function createServiceManifest(input: {
|
|||||||
port: port.port,
|
port: port.port,
|
||||||
targetPort: port.targetPort,
|
targetPort: port.targetPort,
|
||||||
protocol: port.protocol,
|
protocol: port.protocol,
|
||||||
|
...(serviceType === 'NodePort' && port.nodePort ? { nodePort: port.nodePort } : {}),
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ export class CreateAppComponentDto {
|
|||||||
name?: string;
|
name?: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
targetPort?: number;
|
targetPort?: number;
|
||||||
|
nodePort?: number;
|
||||||
protocol?: string;
|
protocol?: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-1
@@ -1,4 +1,4 @@
|
|||||||
import { IsOptional, IsString } from 'class-validator';
|
import { IsEmail, IsIn, IsOptional, IsString, IsUUID, MinLength } from 'class-validator';
|
||||||
|
|
||||||
export class ListUserSessionsQuery {
|
export class ListUserSessionsQuery {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@@ -9,3 +9,31 @@ export class ListUserSessionsQuery {
|
|||||||
@IsString()
|
@IsString()
|
||||||
state?: string;
|
state?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class CreateManagedUserDto {
|
||||||
|
@IsEmail()
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@MinLength(8)
|
||||||
|
password!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
organizationId?: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
department?: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
position?: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(['FREE', 'EDU', 'PRO'])
|
||||||
|
licenseCode?: 'FREE' | 'EDU' | 'PRO';
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Controller, Delete, Get, Param, Patch, Query } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common';
|
||||||
|
|
||||||
import { ListUserSessionsQuery } from './dto';
|
import { CreateManagedUserDto, ListUserSessionsQuery } from './dto';
|
||||||
import { UsersService } from './users.service';
|
import { UsersService } from './users.service';
|
||||||
|
|
||||||
@Controller('users')
|
@Controller('users')
|
||||||
@@ -12,6 +12,11 @@ export class UsersController {
|
|||||||
return this.usersService.listSessions(query);
|
return this.usersService.listSessions(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
createManagedUser(@Body() body: CreateManagedUserDto) {
|
||||||
|
return this.usersService.createManagedUser(body);
|
||||||
|
}
|
||||||
|
|
||||||
@Patch(':userId/lock')
|
@Patch(':userId/lock')
|
||||||
lockUser(@Param('userId') userId: string) {
|
lockUser(@Param('userId') userId: string) {
|
||||||
return this.usersService.lockUser(userId);
|
return this.usersService.lockUser(userId);
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { randomBytes, scrypt as scryptCallback } from 'node:crypto';
|
||||||
|
import { promisify } from 'node:util';
|
||||||
|
|
||||||
import { AuthPrismaService } from '../auth-prisma/auth-prisma.service';
|
import { AuthPrismaService } from '../auth-prisma/auth-prisma.service';
|
||||||
import { ListUserSessionsQuery } from './dto';
|
import { CreateManagedUserDto, ListUserSessionsQuery } from './dto';
|
||||||
|
|
||||||
|
const scrypt = promisify(scryptCallback);
|
||||||
|
const passwordKeyLength = 64;
|
||||||
|
|
||||||
type UserSessionRow = {
|
type UserSessionRow = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -144,6 +149,66 @@ export class UsersService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createManagedUser(body: CreateManagedUserDto) {
|
||||||
|
const email = body.email.trim().toLowerCase();
|
||||||
|
const existing = await this.authPrisma.user.findUnique({
|
||||||
|
where: { email },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
throw new ConflictException('이미 등록된 이메일입니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const license = await this.authPrisma.license.findUnique({
|
||||||
|
where: { code: body.licenseCode ?? 'EDU' },
|
||||||
|
select: { id: true, defaultDurationDays: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!license) {
|
||||||
|
throw new NotFoundException('라이센스를 찾을 수 없습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const startsAt = new Date();
|
||||||
|
const expiresAt = calculateLicenseExpiresAt(
|
||||||
|
startsAt,
|
||||||
|
license.defaultDurationDays,
|
||||||
|
);
|
||||||
|
|
||||||
|
const user = await this.authPrisma.user.create({
|
||||||
|
data: {
|
||||||
|
email,
|
||||||
|
passwordHash: await hashPassword(body.password),
|
||||||
|
name: body.name.trim(),
|
||||||
|
role: 'USER',
|
||||||
|
affiliations: body.organizationId
|
||||||
|
? {
|
||||||
|
create: {
|
||||||
|
organizationId: body.organizationId,
|
||||||
|
department: body.department?.trim() || null,
|
||||||
|
position: body.position?.trim() || null,
|
||||||
|
isPrimary: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
userLicenses: {
|
||||||
|
create: {
|
||||||
|
licenseId: license.id,
|
||||||
|
startsAt,
|
||||||
|
expiresAt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { user };
|
||||||
|
}
|
||||||
|
|
||||||
async lockUser(userId: string) {
|
async lockUser(userId: string) {
|
||||||
const user = await this.authPrisma.user.findUnique({
|
const user = await this.authPrisma.user.findUnique({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
@@ -209,6 +274,23 @@ export class UsersService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function hashPassword(password: string) {
|
||||||
|
const salt = randomBytes(16).toString('hex');
|
||||||
|
const derivedKey = (await scrypt(password, salt, passwordKeyLength)) as Buffer;
|
||||||
|
return `scrypt:${salt}:${derivedKey.toString('hex')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateLicenseExpiresAt(
|
||||||
|
startsAt: Date,
|
||||||
|
durationDays: number | null | undefined,
|
||||||
|
) {
|
||||||
|
if (!durationDays) return null;
|
||||||
|
|
||||||
|
const expiresAt = new Date(startsAt);
|
||||||
|
expiresAt.setDate(expiresAt.getDate() + durationDays);
|
||||||
|
return expiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
function getUserAvatarUrl(
|
function getUserAvatarUrl(
|
||||||
userId: string,
|
userId: string,
|
||||||
mimeType?: string | null,
|
mimeType?: string | null,
|
||||||
|
|||||||
Reference in New Issue
Block a user