mirror of
https://github.com/YuzuZensai/Minikura.git
synced 2026-09-13 10:49:21 +00:00
♻️ refactor: remove unused code
This commit is contained in:
@@ -2,11 +2,8 @@ import type { UpdateSuspensionInput, UpdateUserInput, User } from "@minikura/db"
|
|||||||
|
|
||||||
export interface IUserService {
|
export interface IUserService {
|
||||||
getUserById(id: string): Promise<User>;
|
getUserById(id: string): Promise<User>;
|
||||||
getUserByEmail(email: string): Promise<User | null>;
|
|
||||||
getAllUsers(): Promise<User[]>;
|
getAllUsers(): Promise<User[]>;
|
||||||
updateUser(id: string, input: UpdateUserInput): Promise<User>;
|
updateUser(id: string, input: UpdateUserInput): Promise<User>;
|
||||||
updateSuspension(id: string, input: UpdateSuspensionInput): Promise<User>;
|
updateSuspension(id: string, input: UpdateSuspensionInput): Promise<User>;
|
||||||
suspendUser(id: string, suspendedUntil?: Date | null): Promise<User>;
|
|
||||||
unsuspendUser(id: string): Promise<User>;
|
|
||||||
deleteUser(requestingUserId: string, targetUserId: string): Promise<void>;
|
deleteUser(requestingUserId: string, targetUserId: string): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { BusinessRuleError, NotFoundError } from "../../domain/errors/base.error
|
|||||||
import {
|
import {
|
||||||
UserSuspendedEvent,
|
UserSuspendedEvent,
|
||||||
UserUnsuspendedEvent,
|
UserUnsuspendedEvent,
|
||||||
} from "../../domain/events/server-lifecycle.events";
|
} from "../../domain/events/user-lifecycle.events";
|
||||||
import type { UserRepository } from "../../domain/repositories/user.repository";
|
import type { UserRepository } from "../../domain/repositories/user.repository";
|
||||||
import { eventBus } from "../../infrastructure/event-bus";
|
import { eventBus } from "../../infrastructure/event-bus";
|
||||||
import type { IUserService } from "../interfaces/user.service.interface";
|
import type { IUserService } from "../interfaces/user.service.interface";
|
||||||
@@ -19,10 +19,6 @@ export class UserService implements IUserService {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserByEmail(email: string): Promise<User | null> {
|
|
||||||
return this.userRepo.findByEmail(email);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAllUsers(): Promise<User[]> {
|
async getAllUsers(): Promise<User[]> {
|
||||||
return this.userRepo.findAll();
|
return this.userRepo.findAll();
|
||||||
}
|
}
|
||||||
@@ -42,20 +38,6 @@ export class UserService implements IUserService {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
async suspendUser(id: string, suspendedUntil?: Date | null): Promise<User> {
|
|
||||||
return this.updateSuspension(id, {
|
|
||||||
isSuspended: true,
|
|
||||||
suspendedUntil: suspendedUntil ?? null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async unsuspendUser(id: string): Promise<User> {
|
|
||||||
return this.updateSuspension(id, {
|
|
||||||
isSuspended: false,
|
|
||||||
suspendedUntil: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteUser(requestingUserId: string, targetUserId: string): Promise<void> {
|
async deleteUser(requestingUserId: string, targetUserId: string): Promise<void> {
|
||||||
if (requestingUserId === targetUserId) {
|
if (requestingUserId === targetUserId) {
|
||||||
throw new BusinessRuleError("Cannot delete yourself");
|
throw new BusinessRuleError("Cannot delete yourself");
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
export const API_KEY_PREFIXES = {
|
|
||||||
SERVER: "minikura_server_api_key_",
|
|
||||||
REVERSE_PROXY: "minikura_reverse_proxy_server_api_key_",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const DEFAULT_PORTS = {
|
|
||||||
MINECRAFT: 25565,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const DEFAULT_MEMORY = {
|
|
||||||
SERVER: 2048,
|
|
||||||
REVERSE_PROXY: 512,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const DEFAULT_MEMORY_REQUEST = {
|
|
||||||
SERVER: 1024,
|
|
||||||
REVERSE_PROXY: 512,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const DEFAULT_CPU = {
|
|
||||||
SERVER: {
|
|
||||||
REQUEST: "500m",
|
|
||||||
LIMIT: "2",
|
|
||||||
},
|
|
||||||
REVERSE_PROXY: {
|
|
||||||
REQUEST: "250m",
|
|
||||||
LIMIT: "500m",
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const VALIDATION = {
|
|
||||||
ID_PATTERN: /^[a-zA-Z0-9-_]+$/,
|
|
||||||
ID_ERROR_MESSAGE: "ID must be alphanumeric with - or _",
|
|
||||||
PORT_MIN: 1,
|
|
||||||
PORT_MAX: 65535,
|
|
||||||
} as const;
|
|
||||||
@@ -26,18 +26,3 @@ export class ServerDeletedEvent extends DomainEvent {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserSuspendedEvent extends DomainEvent {
|
|
||||||
constructor(
|
|
||||||
public readonly userId: string,
|
|
||||||
public readonly suspendedUntil: Date | null
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UserUnsuspendedEvent extends DomainEvent {
|
|
||||||
constructor(public readonly userId: string) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { DomainEvent } from "./domain-event";
|
||||||
|
|
||||||
|
export class UserSuspendedEvent extends DomainEvent {
|
||||||
|
constructor(
|
||||||
|
public readonly userId: string,
|
||||||
|
public readonly suspendedUntil: Date | null
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserUnsuspendedEvent extends DomainEvent {
|
||||||
|
constructor(public readonly userId: string) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,10 +2,8 @@ import type { UpdateSuspensionInput, UpdateUserInput, User } from "@minikura/db"
|
|||||||
|
|
||||||
export interface UserRepository {
|
export interface UserRepository {
|
||||||
findById(id: string): Promise<User | null>;
|
findById(id: string): Promise<User | null>;
|
||||||
findByEmail(email: string): Promise<User | null>;
|
|
||||||
findAll(): Promise<User[]>;
|
findAll(): Promise<User[]>;
|
||||||
update(id: string, input: UpdateUserInput): Promise<User>;
|
update(id: string, input: UpdateUserInput): Promise<User>;
|
||||||
updateSuspension(id: string, input: UpdateSuspensionInput): Promise<User>;
|
updateSuspension(id: string, input: UpdateSuspensionInput): Promise<User>;
|
||||||
delete(id: string): Promise<void>;
|
delete(id: string): Promise<void>;
|
||||||
count(): Promise<number>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
export class ApiKey {
|
|
||||||
private static readonly SERVER_PREFIX = "minikura_srv_";
|
|
||||||
private static readonly REVERSE_PROXY_PREFIX = "minikura_proxy_";
|
|
||||||
private static readonly TOKEN_BYTES = 32;
|
|
||||||
|
|
||||||
private constructor(private readonly value: string) {}
|
|
||||||
|
|
||||||
static generate(type: "server" | "reverse-proxy"): ApiKey {
|
|
||||||
const prefix = type === "server" ? ApiKey.SERVER_PREFIX : ApiKey.REVERSE_PROXY_PREFIX;
|
|
||||||
const token = Buffer.from(crypto.randomUUID())
|
|
||||||
.toString("base64")
|
|
||||||
.replace(/[^a-zA-Z0-9]/g, "")
|
|
||||||
.substring(0, ApiKey.TOKEN_BYTES);
|
|
||||||
return new ApiKey(`${prefix}${token}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
static validate(value: string): boolean {
|
|
||||||
const patterns = [
|
|
||||||
new RegExp(`^${ApiKey.SERVER_PREFIX}[a-zA-Z0-9]{32}$`),
|
|
||||||
new RegExp(`^${ApiKey.REVERSE_PROXY_PREFIX}[a-zA-Z0-9]{32}$`),
|
|
||||||
];
|
|
||||||
return patterns.some((pattern) => pattern.test(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
toString(): string {
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
getType(): "server" | "reverse-proxy" {
|
|
||||||
if (this.value.startsWith(ApiKey.SERVER_PREFIX)) return "server";
|
|
||||||
return "reverse-proxy";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
export class K8sConnectionInfo {
|
|
||||||
constructor(
|
|
||||||
public readonly host: string,
|
|
||||||
public readonly port: number,
|
|
||||||
public readonly namespace: string
|
|
||||||
) {}
|
|
||||||
|
|
||||||
toUrl(): string {
|
|
||||||
return `${this.host}:${this.port}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
toConnectionString(): string {
|
|
||||||
return `Host: ${this.host}, Port: ${this.port}, Namespace: ${this.namespace}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
export class ServerConfig {
|
|
||||||
constructor(
|
|
||||||
public readonly memory: number,
|
|
||||||
public readonly memoryRequest: number,
|
|
||||||
public readonly cpuRequest: string,
|
|
||||||
public readonly cpuLimit: string,
|
|
||||||
public readonly jvmOpts: string | null
|
|
||||||
) {}
|
|
||||||
|
|
||||||
static fromDefaults(): ServerConfig {
|
|
||||||
return new ServerConfig(2048, 1024, "250m", "500m", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromInput(input: {
|
|
||||||
memory?: number;
|
|
||||||
memoryRequest?: number;
|
|
||||||
cpuRequest?: string;
|
|
||||||
cpuLimit?: string;
|
|
||||||
jvmOpts?: string;
|
|
||||||
}): ServerConfig {
|
|
||||||
return new ServerConfig(
|
|
||||||
input.memory ?? 2048,
|
|
||||||
input.memoryRequest ?? 1024,
|
|
||||||
input.cpuRequest ?? "250m",
|
|
||||||
input.cpuLimit ?? "500m",
|
|
||||||
input.jvmOpts ?? null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getJvmArgs(): string {
|
|
||||||
const args: string[] = [`-Xmx${this.memory}M`];
|
|
||||||
|
|
||||||
if (this.jvmOpts) {
|
|
||||||
args.push(this.jvmOpts);
|
|
||||||
}
|
|
||||||
|
|
||||||
return args.join(" ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,6 @@ type EventHandler<T extends DomainEvent = DomainEvent> = (event: T) => void | Pr
|
|||||||
|
|
||||||
export class EventBus {
|
export class EventBus {
|
||||||
private handlers = new Map<string, Set<EventHandler>>();
|
private handlers = new Map<string, Set<EventHandler>>();
|
||||||
private eventHistory: DomainEvent[] = [];
|
|
||||||
|
|
||||||
subscribe<T extends DomainEvent>(
|
subscribe<T extends DomainEvent>(
|
||||||
eventClass: { new (...args: any[]): T },
|
eventClass: { new (...args: any[]): T },
|
||||||
@@ -22,7 +21,6 @@ export class EventBus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async publish<T extends DomainEvent>(event: T): Promise<void> {
|
async publish<T extends DomainEvent>(event: T): Promise<void> {
|
||||||
this.eventHistory.push(event);
|
|
||||||
const eventName = event.constructor.name;
|
const eventName = event.constructor.name;
|
||||||
const handlers = this.handlers.get(eventName) || [];
|
const handlers = this.handlers.get(eventName) || [];
|
||||||
for (const handler of handlers) {
|
for (const handler of handlers) {
|
||||||
@@ -33,14 +31,6 @@ export class EventBus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getHistory(): DomainEvent[] {
|
|
||||||
return [...this.eventHistory];
|
|
||||||
}
|
|
||||||
|
|
||||||
clearHistory(): void {
|
|
||||||
this.eventHistory = [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const eventBus = new EventBus();
|
export const eventBus = new EventBus();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
UserSuspendedEvent,
|
UserSuspendedEvent,
|
||||||
UserUnsuspendedEvent,
|
UserUnsuspendedEvent,
|
||||||
} from "../../domain/events/server-lifecycle.events";
|
} from "../../domain/events/user-lifecycle.events";
|
||||||
import { eventBus } from "../event-bus";
|
import { eventBus } from "../event-bus";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,6 @@ export class PrismaUserRepository implements UserRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByEmail(email: string): Promise<User | null> {
|
|
||||||
return await prisma.user.findUnique({
|
|
||||||
where: { email },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async findAll(): Promise<User[]> {
|
async findAll(): Promise<User[]> {
|
||||||
return await prisma.user.findMany({
|
return await prisma.user.findMany({
|
||||||
orderBy: { createdAt: "desc" },
|
orderBy: { createdAt: "desc" },
|
||||||
@@ -39,8 +33,4 @@ export class PrismaUserRepository implements UserRepository {
|
|||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async count(): Promise<number> {
|
|
||||||
return await prisma.user.count();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,41 @@
|
|||||||
import type { User } from "@minikura/db";
|
import type { User } from "@minikura/db";
|
||||||
import type { Elysia } from "elysia";
|
import type { Elysia } from "elysia";
|
||||||
import { ForbiddenError, UnauthorizedError } from "../domain/errors/base.error";
|
import { ForbiddenError, UnauthorizedError } from "../domain/errors/base.error";
|
||||||
|
import { bearerToken, findApiKeyOwner } from "./api-key";
|
||||||
|
|
||||||
|
function authenticatedUser(ctx: { user?: User | null; isSuspended?: boolean }) {
|
||||||
|
const { user, isSuspended } = ctx;
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
}
|
||||||
|
if (isSuspended) {
|
||||||
|
throw new ForbiddenError("Account is suspended");
|
||||||
|
}
|
||||||
|
return { user };
|
||||||
|
}
|
||||||
|
|
||||||
export const requireAuth = (app: Elysia) => {
|
export const requireAuth = (app: Elysia) => {
|
||||||
return app.derive((ctx: any) => {
|
return app.derive((ctx: any) => authenticatedUser(ctx));
|
||||||
const { user, isSuspended } = ctx as {
|
|
||||||
user: User | null;
|
|
||||||
isSuspended: boolean;
|
|
||||||
};
|
|
||||||
if (!user) {
|
|
||||||
throw new UnauthorizedError();
|
|
||||||
}
|
|
||||||
if (isSuspended) {
|
|
||||||
throw new ForbiddenError("Account is suspended");
|
|
||||||
}
|
|
||||||
return { user };
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const requireAdmin = (app: Elysia) => {
|
|
||||||
return app.derive((ctx: any) => {
|
|
||||||
const { user, isSuspended } = ctx as {
|
|
||||||
user: User | null;
|
|
||||||
isSuspended: boolean;
|
|
||||||
};
|
|
||||||
if (!user) {
|
|
||||||
throw new UnauthorizedError();
|
|
||||||
}
|
|
||||||
if (isSuspended) {
|
|
||||||
throw new ForbiddenError("Account is suspended");
|
|
||||||
}
|
|
||||||
if (user.role !== "admin") {
|
|
||||||
throw new ForbiddenError("Admin access required");
|
|
||||||
}
|
|
||||||
return { user };
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const requireRole = (role: string) => (app: Elysia) => {
|
export const requireRole = (role: string) => (app: Elysia) => {
|
||||||
return app.derive((ctx: any) => {
|
return app.derive((ctx: any) => {
|
||||||
const { user, isSuspended } = ctx as {
|
const { user } = authenticatedUser(ctx);
|
||||||
user: User | null;
|
|
||||||
isSuspended: boolean;
|
|
||||||
};
|
|
||||||
if (!user) {
|
|
||||||
throw new UnauthorizedError();
|
|
||||||
}
|
|
||||||
if (isSuspended) {
|
|
||||||
throw new ForbiddenError("Account is suspended");
|
|
||||||
}
|
|
||||||
if (user.role !== role) {
|
if (user.role !== role) {
|
||||||
throw new ForbiddenError(`${role} access required`);
|
throw new ForbiddenError(`${role} access required`);
|
||||||
}
|
}
|
||||||
return { user };
|
return { user };
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const requireAdmin = requireRole("admin");
|
||||||
|
|
||||||
|
export const requirePluginApiKey = (app: Elysia) => {
|
||||||
|
return app.derive(async ({ request }) => {
|
||||||
|
const owner = await findApiKeyOwner(bearerToken(request.headers.get("authorization")));
|
||||||
|
if (!owner) {
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
}
|
||||||
|
return { pluginAuth: owner };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import type { z } from "zod";
|
|
||||||
|
|
||||||
type ErrorHandler = (code: number, value: unknown) => never;
|
|
||||||
|
|
||||||
export function validateBody<T extends z.ZodType>(
|
|
||||||
schema: T,
|
|
||||||
body: unknown,
|
|
||||||
error: ErrorHandler
|
|
||||||
): z.infer<T> {
|
|
||||||
const result = schema.safeParse(body);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
const firstError = result.error.issues[0];
|
|
||||||
const message = `${firstError.path.join(".")}: ${firstError.message}`;
|
|
||||||
throw error(400, { message });
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function zodValidate<T extends z.ZodType>(schema: T) {
|
|
||||||
return (context: { body: unknown; error: ErrorHandler }) => {
|
|
||||||
return validateBody(schema, context.body, context.error);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -2,10 +2,8 @@ import { labelKeys } from "@minikura/api";
|
|||||||
import { Elysia } from "elysia";
|
import { Elysia } from "elysia";
|
||||||
import { k8sService } from "../application/di-container";
|
import { k8sService } from "../application/di-container";
|
||||||
import { requireAuth } from "../middleware/auth-guards";
|
import { requireAuth } from "../middleware/auth-guards";
|
||||||
import { authPlugin } from "../middleware/auth-plugin";
|
|
||||||
|
|
||||||
export const k8sRoutes = new Elysia({ prefix: "/k8s" })
|
export const k8sRoutes = new Elysia({ prefix: "/k8s" })
|
||||||
.use(authPlugin)
|
|
||||||
.use(requireAuth)
|
.use(requireAuth)
|
||||||
.get("/status", async () => {
|
.get("/status", async () => {
|
||||||
return k8sService.getConnectionInfo();
|
return k8sService.getConnectionInfo();
|
||||||
|
|||||||
@@ -54,49 +54,7 @@ export const createServerSchema = z.object({
|
|||||||
level_type: z.string().optional(),
|
level_type: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateServerSchema = z.object({
|
export const updateServerSchema = createServerSchema.omit({ id: true, type: true }).partial();
|
||||||
description: z.string().nullable().optional(),
|
|
||||||
listen_port: z.number().int().min(1).max(65535).optional(),
|
|
||||||
service_type: z.nativeEnum(ServiceType).optional(),
|
|
||||||
node_port: z
|
|
||||||
.union([
|
|
||||||
z
|
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.min(30000, "Node port must be at least 30000")
|
|
||||||
.max(32767, "Node port must be at most 32767"),
|
|
||||||
z.null(),
|
|
||||||
])
|
|
||||||
.optional(),
|
|
||||||
env_variables: z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
key: z.string().min(1),
|
|
||||||
value: z.string(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
memory: z.number().int().min(256).optional(),
|
|
||||||
memory_request: z.number().int().min(256).optional(),
|
|
||||||
cpu_request: z.string().optional(),
|
|
||||||
cpu_limit: z.string().optional(),
|
|
||||||
|
|
||||||
jar_type: z.nativeEnum(MinecraftServerJarType).optional(),
|
|
||||||
minecraft_version: z.string().optional(),
|
|
||||||
|
|
||||||
jvm_opts: z.string().optional(),
|
|
||||||
use_aikar_flags: z.boolean().optional(),
|
|
||||||
use_meowice_flags: z.boolean().optional(),
|
|
||||||
|
|
||||||
difficulty: z.nativeEnum(ServerDifficulty).optional(),
|
|
||||||
game_mode: z.nativeEnum(GameMode).optional(),
|
|
||||||
max_players: z.number().int().min(1).max(1000).optional(),
|
|
||||||
pvp: z.boolean().optional(),
|
|
||||||
online_mode: z.boolean().optional(),
|
|
||||||
motd: z.string().optional(),
|
|
||||||
level_seed: z.string().optional(),
|
|
||||||
level_type: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createReverseProxySchema = z.object({
|
export const createReverseProxySchema = z.object({
|
||||||
id: z
|
id: z
|
||||||
@@ -123,18 +81,9 @@ export const createReverseProxySchema = z.object({
|
|||||||
cpu_limit: z.string().optional(),
|
cpu_limit: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateReverseProxySchema = z.object({
|
export const updateReverseProxySchema = createReverseProxySchema
|
||||||
description: z.string().nullable().optional(),
|
.omit({ id: true, env_variables: true })
|
||||||
external_address: z.string().optional(),
|
.partial();
|
||||||
external_port: z.number().int().min(1).max(65535).optional(),
|
|
||||||
listen_port: z.number().int().min(1).max(65535).optional(),
|
|
||||||
type: z.nativeEnum(ReverseProxyServerType).optional(),
|
|
||||||
service_type: z.nativeEnum(ServiceType).optional(),
|
|
||||||
node_port: z.union([z.number().int().min(30000).max(32767), z.null()]).optional(),
|
|
||||||
memory: z.number().int().min(256).optional(),
|
|
||||||
cpu_request: z.string().optional(),
|
|
||||||
cpu_limit: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const envVariableSchema = z.object({
|
export const envVariableSchema = z.object({
|
||||||
key: z.string().min(1, "Key is required"),
|
key: z.string().min(1, "Key is required"),
|
||||||
|
|||||||
@@ -10,10 +10,5 @@ export const updateSuspensionSchema = z.object({
|
|||||||
suspendedUntil: z.string().nullable().optional(),
|
suspendedUntil: z.string().nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const suspendUserSchema = z.object({
|
|
||||||
suspendedUntil: z.string().nullable().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type UpdateUserInput = z.infer<typeof updateUserSchema>;
|
export type UpdateUserInput = z.infer<typeof updateUserSchema>;
|
||||||
export type UpdateSuspensionInput = z.infer<typeof updateSuspensionSchema>;
|
export type UpdateSuspensionInput = z.infer<typeof updateSuspensionSchema>;
|
||||||
export type SuspendUserInput = z.infer<typeof suspendUserSchema>;
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export class K8sService implements IK8sService {
|
|||||||
|
|
||||||
private initializeOperations(): void {
|
private initializeOperations(): void {
|
||||||
this.podOps = new PodOperations(this.coreApi, this.namespace);
|
this.podOps = new PodOperations(this.coreApi, this.namespace);
|
||||||
this.clusterOps = new ClusterOperations(this.coreApi, this.customObjectsApi, this.namespace);
|
this.clusterOps = new ClusterOperations(this.customObjectsApi, this.namespace);
|
||||||
this.customResourceOps = new CustomResourceOperations(this.customObjectsApi, this.namespace);
|
this.customResourceOps = new CustomResourceOperations(this.customObjectsApi, this.namespace);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,20 +3,12 @@ import { BaseK8sOperations } from "./base.operations";
|
|||||||
|
|
||||||
export class ClusterOperations extends BaseK8sOperations {
|
export class ClusterOperations extends BaseK8sOperations {
|
||||||
constructor(
|
constructor(
|
||||||
private coreApi: k8s.CoreV1Api,
|
|
||||||
private customObjectsApi: k8s.CustomObjectsApi,
|
private customObjectsApi: k8s.CustomObjectsApi,
|
||||||
namespace: string
|
namespace: string
|
||||||
) {
|
) {
|
||||||
super(namespace);
|
super(namespace);
|
||||||
}
|
}
|
||||||
|
|
||||||
async listNodes() {
|
|
||||||
return this.executeOperation(
|
|
||||||
() => this.coreApi.listNode().then((r) => r.items),
|
|
||||||
"Failed to fetch nodes"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getNodeMetrics() {
|
async getNodeMetrics() {
|
||||||
return this.executeOperation(
|
return this.executeOperation(
|
||||||
() =>
|
() =>
|
||||||
@@ -28,12 +20,4 @@ export class ClusterOperations extends BaseK8sOperations {
|
|||||||
"Failed to fetch node metrics"
|
"Failed to fetch node metrics"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async listConfigMaps(namespace?: string) {
|
|
||||||
const ns = namespace || this.namespace;
|
|
||||||
return this.executeOperation(
|
|
||||||
() => this.coreApi.listNamespacedConfigMap({ namespace: ns }).then((r) => r.items),
|
|
||||||
"Failed to fetch configmaps"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
export { BaseK8sOperations } from "./base.operations";
|
export { BaseK8sOperations } from "./base.operations";
|
||||||
export { ClusterOperations } from "./cluster.operations";
|
export { ClusterOperations } from "./cluster.operations";
|
||||||
export { CustomResourceOperations } from "./custom-resource.operations";
|
export { CustomResourceOperations } from "./custom-resource.operations";
|
||||||
export { NetworkOperations } from "./network.operations";
|
|
||||||
export { PodOperations } from "./pod.operations";
|
export { PodOperations } from "./pod.operations";
|
||||||
export { WorkloadOperations } from "./workload.operations";
|
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
import type * as k8s from "@kubernetes/client-node";
|
|
||||||
import { BaseK8sOperations } from "./base.operations";
|
|
||||||
|
|
||||||
export class NetworkOperations extends BaseK8sOperations {
|
|
||||||
constructor(
|
|
||||||
private coreApi: k8s.CoreV1Api,
|
|
||||||
private networkingApi: k8s.NetworkingV1Api,
|
|
||||||
namespace: string
|
|
||||||
) {
|
|
||||||
super(namespace);
|
|
||||||
}
|
|
||||||
|
|
||||||
async listServices() {
|
|
||||||
return this.executeOperation(
|
|
||||||
() => this.coreApi.listNamespacedService({ namespace: this.namespace }).then((r) => r.items),
|
|
||||||
"Failed to fetch services"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async listIngresses() {
|
|
||||||
return this.executeOperation(
|
|
||||||
() =>
|
|
||||||
this.networkingApi
|
|
||||||
.listNamespacedIngress({ namespace: this.namespace })
|
|
||||||
.then((r) => r.items),
|
|
||||||
"Failed to fetch ingresses"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getServiceInfo(serviceName: string) {
|
|
||||||
return this.executeOperation(
|
|
||||||
() => this.coreApi.readNamespacedService({ name: serviceName, namespace: this.namespace }),
|
|
||||||
`Failed to fetch service info for ${serviceName}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getServerConnectionInfo(serviceName: string) {
|
|
||||||
return this.executeOperation(async () => {
|
|
||||||
const service = await this.coreApi.readNamespacedService({
|
|
||||||
name: serviceName,
|
|
||||||
namespace: this.namespace,
|
|
||||||
});
|
|
||||||
|
|
||||||
const serviceType = service.spec?.type || "ClusterIP";
|
|
||||||
const ports = service.spec?.ports || [];
|
|
||||||
|
|
||||||
let host: string;
|
|
||||||
let externalHost: string | undefined;
|
|
||||||
|
|
||||||
switch (serviceType) {
|
|
||||||
case "LoadBalancer": {
|
|
||||||
const ingress = service.status?.loadBalancer?.ingress?.[0];
|
|
||||||
host =
|
|
||||||
ingress?.hostname ||
|
|
||||||
ingress?.ip ||
|
|
||||||
`${serviceName}.${this.namespace}.svc.cluster.local`;
|
|
||||||
externalHost = ingress?.hostname || ingress?.ip;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "NodePort":
|
|
||||||
host = `${serviceName}.${this.namespace}.svc.cluster.local`;
|
|
||||||
externalHost = "<node-ip>";
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
host = `${serviceName}.${this.namespace}.svc.cluster.local`;
|
|
||||||
externalHost = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const portMappings = ports.map((port) => ({
|
|
||||||
name: port.name,
|
|
||||||
port: port.port,
|
|
||||||
targetPort: port.targetPort,
|
|
||||||
nodePort: port.nodePort,
|
|
||||||
protocol: port.protocol || "TCP",
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
serviceName,
|
|
||||||
namespace: this.namespace,
|
|
||||||
serviceType,
|
|
||||||
internalHost: host,
|
|
||||||
externalHost,
|
|
||||||
ports: portMappings,
|
|
||||||
};
|
|
||||||
}, `Failed to fetch connection info for service ${serviceName}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,30 +9,6 @@ export class PodOperations extends BaseK8sOperations {
|
|||||||
super(namespace);
|
super(namespace);
|
||||||
}
|
}
|
||||||
|
|
||||||
async listPods() {
|
|
||||||
return this.executeOperation(
|
|
||||||
() => this.coreApi.listNamespacedPod({ namespace: this.namespace }).then((r) => r.items),
|
|
||||||
"Failed to fetch pods"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async listPodsByLabel(labelSelector: string) {
|
|
||||||
return this.executeOperation(
|
|
||||||
() =>
|
|
||||||
this.coreApi
|
|
||||||
.listNamespacedPod({ namespace: this.namespace, labelSelector })
|
|
||||||
.then((r) => r.items),
|
|
||||||
"Failed to fetch pods by label"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPodInfo(podName: string) {
|
|
||||||
return this.executeOperation(
|
|
||||||
() => this.coreApi.readNamespacedPod({ name: podName, namespace: this.namespace }),
|
|
||||||
`Failed to fetch pod info for ${podName}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPodLogs(
|
async getPodLogs(
|
||||||
podName: string,
|
podName: string,
|
||||||
options?: {
|
options?: {
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import type * as k8s from "@kubernetes/client-node";
|
|
||||||
import { BaseK8sOperations } from "./base.operations";
|
|
||||||
|
|
||||||
export class WorkloadOperations extends BaseK8sOperations {
|
|
||||||
constructor(
|
|
||||||
private appsApi: k8s.AppsV1Api,
|
|
||||||
namespace: string
|
|
||||||
) {
|
|
||||||
super(namespace);
|
|
||||||
}
|
|
||||||
|
|
||||||
async listDeployments() {
|
|
||||||
return this.executeOperation(
|
|
||||||
() =>
|
|
||||||
this.appsApi.listNamespacedDeployment({ namespace: this.namespace }).then((r) => r.items),
|
|
||||||
"Failed to fetch deployments"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async listStatefulSets() {
|
|
||||||
return this.executeOperation(
|
|
||||||
() =>
|
|
||||||
this.appsApi.listNamespacedStatefulSet({ namespace: this.namespace }).then((r) => r.items),
|
|
||||||
"Failed to fetch statefulsets"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import * as k8s from "@kubernetes/client-node";
|
import * as k8s from "@kubernetes/client-node";
|
||||||
|
import { API_GROUP } from "@minikura/api";
|
||||||
import { prisma, type ReverseProxyWithEnvVars, type ServerWithEnvVars } from "@minikura/db";
|
import { prisma, type ReverseProxyWithEnvVars, type ServerWithEnvVars } from "@minikura/db";
|
||||||
import { buildKubeConfig } from "@minikura/shared/kube-auth";
|
import { buildKubeConfig } from "@minikura/shared/kube-auth";
|
||||||
import { logger } from "../infrastructure/logger";
|
import { logger } from "../infrastructure/logger";
|
||||||
|
|
||||||
const API_GROUP = "minikura.kirameki.cafe";
|
|
||||||
const API_VERSION = "v1alpha1";
|
const API_VERSION = "v1alpha1";
|
||||||
const FIELD_MANAGER = "minikura-backend";
|
const FIELD_MANAGER = "minikura-backend";
|
||||||
const SYNC_INTERVAL_MS = 30_000;
|
const SYNC_INTERVAL_MS = 30_000;
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
import type { TopologyEdge, TopologyNode } from "@/lib/topology-types";
|
|
||||||
|
|
||||||
const LAYOUT_CONFIG = {
|
|
||||||
TIER_SPACING: 300,
|
|
||||||
NODE_SPACING: 250,
|
|
||||||
START_X: 150,
|
|
||||||
START_Y: 100,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export function applyHierarchicalLayout(
|
|
||||||
nodes: TopologyNode[],
|
|
||||||
edges: TopologyEdge[]
|
|
||||||
): { nodes: TopologyNode[]; edges: TopologyEdge[] } {
|
|
||||||
const proxyNodes = nodes.filter((n) => n.data.type === "proxy");
|
|
||||||
const serverNodes = nodes.filter((n) => n.data.type === "server");
|
|
||||||
|
|
||||||
const layoutedNodes: TopologyNode[] = [];
|
|
||||||
|
|
||||||
const maxNodesInTier = Math.max(proxyNodes.length, serverNodes.length);
|
|
||||||
const tierWidth = maxNodesInTier * LAYOUT_CONFIG.NODE_SPACING;
|
|
||||||
|
|
||||||
const proxyOffsetX = (tierWidth - proxyNodes.length * LAYOUT_CONFIG.NODE_SPACING) / 2;
|
|
||||||
proxyNodes.forEach((node, index) => {
|
|
||||||
const x = LAYOUT_CONFIG.START_X + proxyOffsetX + index * LAYOUT_CONFIG.NODE_SPACING;
|
|
||||||
const y = LAYOUT_CONFIG.START_Y;
|
|
||||||
|
|
||||||
layoutedNodes.push({
|
|
||||||
...node,
|
|
||||||
position: { x, y },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const serverOffsetX = (tierWidth - serverNodes.length * LAYOUT_CONFIG.NODE_SPACING) / 2;
|
|
||||||
serverNodes.forEach((node, index) => {
|
|
||||||
const x = LAYOUT_CONFIG.START_X + serverOffsetX + index * LAYOUT_CONFIG.NODE_SPACING;
|
|
||||||
const y = LAYOUT_CONFIG.START_Y + LAYOUT_CONFIG.TIER_SPACING;
|
|
||||||
|
|
||||||
layoutedNodes.push({
|
|
||||||
...node,
|
|
||||||
position: { x, y },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
nodes: layoutedNodes,
|
|
||||||
edges,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function applyGridLayout(
|
|
||||||
nodes: TopologyNode[],
|
|
||||||
edges: TopologyEdge[]
|
|
||||||
): { nodes: TopologyNode[]; edges: TopologyEdge[] } {
|
|
||||||
const columns = Math.ceil(Math.sqrt(nodes.length));
|
|
||||||
|
|
||||||
const layoutedNodes = nodes.map((node, index) => ({
|
|
||||||
...node,
|
|
||||||
position: {
|
|
||||||
x: LAYOUT_CONFIG.START_X + (index % columns) * LAYOUT_CONFIG.NODE_SPACING,
|
|
||||||
y: LAYOUT_CONFIG.START_Y + Math.floor(index / columns) * LAYOUT_CONFIG.TIER_SPACING,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
nodes: layoutedNodes,
|
|
||||||
edges,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function layoutTopologyGraph(
|
|
||||||
nodes: TopologyNode[],
|
|
||||||
edges: TopologyEdge[]
|
|
||||||
): { nodes: TopologyNode[]; edges: TopologyEdge[] } {
|
|
||||||
if (nodes.length === 0) {
|
|
||||||
return { nodes: [], edges: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nodes.length < 20) {
|
|
||||||
return applyHierarchicalLayout(nodes, edges);
|
|
||||||
}
|
|
||||||
|
|
||||||
return applyGridLayout(nodes, edges);
|
|
||||||
}
|
|
||||||
@@ -9,24 +9,10 @@ import type {
|
|||||||
PodInfo,
|
PodInfo,
|
||||||
StatefulSetInfo,
|
StatefulSetInfo,
|
||||||
} from "@minikura/api";
|
} from "@minikura/api";
|
||||||
|
import { getErrorMessage } from "@minikura/shared/errors";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { api } from "@/lib/api-client";
|
import { api } from "@/lib/api-client";
|
||||||
|
|
||||||
function getErrorMessage(error: unknown) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
return error.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof error === "object" && error) {
|
|
||||||
const value = "value" in error ? error.value : error;
|
|
||||||
if (typeof value === "object" && value && "message" in value) {
|
|
||||||
return String(value.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "Failed to fetch Kubernetes resources";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useK8sResources() {
|
export function useK8sResources() {
|
||||||
const [status, setStatus] = useState<K8sStatus | null>(null);
|
const [status, setStatus] = useState<K8sStatus | null>(null);
|
||||||
const [pods, setPods] = useState<PodInfo[]>([]);
|
const [pods, setPods] = useState<PodInfo[]>([]);
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import type {
|
|||||||
ReverseProxyServer,
|
ReverseProxyServer,
|
||||||
} from "@minikura/api";
|
} from "@minikura/api";
|
||||||
import type { Edge, Node } from "@xyflow/react";
|
import type { Edge, Node } from "@xyflow/react";
|
||||||
|
import type { ResourceMetrics } from "./k8s-metrics";
|
||||||
|
|
||||||
|
export type { ResourceMetrics };
|
||||||
|
|
||||||
export type HealthStatus = "healthy" | "degraded" | "unhealthy" | "unknown";
|
export type HealthStatus = "healthy" | "degraded" | "unhealthy" | "unknown";
|
||||||
|
|
||||||
@@ -13,13 +16,6 @@ export type NodeType = "server" | "proxy" | "k8s-node";
|
|||||||
|
|
||||||
export type EdgeType = "proxy-to-server" | "pod-to-node";
|
export type EdgeType = "proxy-to-server" | "pod-to-node";
|
||||||
|
|
||||||
export interface ResourceMetrics {
|
|
||||||
cpuUsage?: string;
|
|
||||||
memoryUsage?: string;
|
|
||||||
cpuUsagePercent?: number;
|
|
||||||
memoryUsagePercent?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface K8sNodeMetadata {
|
export interface K8sNodeMetadata {
|
||||||
node: K8sNodeSummary;
|
node: K8sNodeSummary;
|
||||||
podCount: number;
|
podCount: number;
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import type { Config } from "drizzle-kit";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
schema: "./packages/db/src/schema.ts",
|
|
||||||
out: "./packages/db/src/drizzle",
|
|
||||||
dialect: "postgresql",
|
|
||||||
dbCredentials: {
|
|
||||||
url: process.env.DATABASE_URL || "",
|
|
||||||
},
|
|
||||||
} satisfies Config;
|
|
||||||
@@ -5,6 +5,12 @@ export function getErrorMessage(error: unknown): string {
|
|||||||
if (typeof error === "string") {
|
if (typeof error === "string") {
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
if (typeof error === "object" && error) {
|
||||||
|
const value = "value" in error ? error.value : error;
|
||||||
|
if (typeof value === "object" && value && "message" in value) {
|
||||||
|
return String(value.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
return String(error);
|
return String(error);
|
||||||
}
|
}
|
||||||
export function getAge(timestamp: Date | string | undefined): string {
|
export function getAge(timestamp: Date | string | undefined): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user