mirror of
https://github.com/YuzuZensai/Minikura.git
synced 2026-03-30 21:27:36 +00:00
✨ feat: initial prototype
This commit is contained in:
26
apps/backend/src/infrastructure/api-key-generator.ts
Normal file
26
apps/backend/src/infrastructure/api-key-generator.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export interface ApiKeyGenerator {
|
||||
generateServerApiKey(): string;
|
||||
generateReverseProxyApiKey(): string;
|
||||
}
|
||||
|
||||
export class ApiKeyGeneratorImpl implements ApiKeyGenerator {
|
||||
private readonly SERVER_PREFIX = "minikura_srv_";
|
||||
private readonly REVERSE_PROXY_PREFIX = "minikura_proxy_";
|
||||
private readonly TOKEN_LENGTH = 32;
|
||||
|
||||
generateServerApiKey(): string {
|
||||
const token = Buffer.from(crypto.randomUUID())
|
||||
.toString("base64")
|
||||
.replace(/[^a-zA-Z0-9]/g, "")
|
||||
.substring(0, this.TOKEN_LENGTH);
|
||||
return `${this.SERVER_PREFIX}${token}`;
|
||||
}
|
||||
|
||||
generateReverseProxyApiKey(): string {
|
||||
const token = Buffer.from(crypto.randomUUID())
|
||||
.toString("base64")
|
||||
.replace(/[^a-zA-Z0-9]/g, "")
|
||||
.substring(0, this.TOKEN_LENGTH);
|
||||
return `${this.REVERSE_PROXY_PREFIX}${token}`;
|
||||
}
|
||||
}
|
||||
45
apps/backend/src/infrastructure/event-bus.ts
Normal file
45
apps/backend/src/infrastructure/event-bus.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { DomainEvent } from "../domain/events/domain-event";
|
||||
|
||||
type EventHandler<T extends DomainEvent = DomainEvent> = (event: T) => void | Promise<void>;
|
||||
|
||||
export class EventBus {
|
||||
private handlers = new Map<string, Set<EventHandler>>();
|
||||
private eventHistory: DomainEvent[] = [];
|
||||
|
||||
subscribe<T extends DomainEvent>(
|
||||
eventClass: { new (...args: any[]): T },
|
||||
handler: EventHandler<T>
|
||||
): () => void {
|
||||
const eventName = eventClass.name;
|
||||
if (!this.handlers.has(eventName)) {
|
||||
this.handlers.set(eventName, new Set());
|
||||
}
|
||||
this.handlers.get(eventName)!.add(handler as EventHandler);
|
||||
return () => {
|
||||
this.handlers.get(eventName)?.delete(handler as EventHandler);
|
||||
};
|
||||
}
|
||||
|
||||
async publish<T extends DomainEvent>(event: T): Promise<void> {
|
||||
this.eventHistory.push(event);
|
||||
const eventName = event.constructor.name;
|
||||
const handlers = this.handlers.get(eventName) || [];
|
||||
for (const handler of handlers) {
|
||||
try {
|
||||
await handler(event);
|
||||
} catch (error) {
|
||||
console.error(`[EventBus] Error in handler for ${eventName}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getHistory(): DomainEvent[] {
|
||||
return [...this.eventHistory];
|
||||
}
|
||||
|
||||
clearHistory(): void {
|
||||
this.eventHistory = [];
|
||||
}
|
||||
}
|
||||
|
||||
export const eventBus = new EventBus();
|
||||
4
apps/backend/src/infrastructure/event-handlers/index.ts
Normal file
4
apps/backend/src/infrastructure/event-handlers/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import "./server-event.handler";
|
||||
import "./user-event.handler";
|
||||
|
||||
console.log("[EventBus] All event handlers registered");
|
||||
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
ServerCreatedEvent,
|
||||
ServerDeletedEvent,
|
||||
ServerUpdatedEvent,
|
||||
} from "../../domain/events/server-lifecycle.events";
|
||||
import { eventBus } from "../event-bus";
|
||||
import { wsService } from "../../application/di-container";
|
||||
|
||||
eventBus.subscribe(ServerCreatedEvent, async (event) => {
|
||||
console.log(`[Event] Server created: ${event.serverId} (${event.serverType})`);
|
||||
wsService.broadcast("create", event.serverType, event.serverId);
|
||||
});
|
||||
|
||||
eventBus.subscribe(ServerUpdatedEvent, async (event) => {
|
||||
console.log(`[Event] Server updated: ${event.serverId}`);
|
||||
wsService.broadcast("update", "server", event.serverId);
|
||||
});
|
||||
|
||||
eventBus.subscribe(ServerDeletedEvent, async (event) => {
|
||||
console.log(`[Event] Server deleted: ${event.serverId}`);
|
||||
wsService.broadcast("delete", "server", event.serverId);
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
UserSuspendedEvent,
|
||||
UserUnsuspendedEvent,
|
||||
} from "../../domain/events/server-lifecycle.events";
|
||||
import { eventBus } from "../event-bus";
|
||||
|
||||
eventBus.subscribe(UserSuspendedEvent, async (event) => {
|
||||
if (event.suspendedUntil) {
|
||||
console.log(
|
||||
`[Event] User suspended: ${event.userId} until ${event.suspendedUntil.toISOString()}`
|
||||
);
|
||||
} else {
|
||||
console.log(`[Event] User suspended: ${event.userId} indefinitely`);
|
||||
}
|
||||
});
|
||||
|
||||
eventBus.subscribe(UserUnsuspendedEvent, async (event) => {
|
||||
console.log(`[Event] User unsuspended: ${event.userId}`);
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
import { type EnvVariable, prisma, type ReverseProxyWithEnvVars } from "@minikura/db";
|
||||
import { ConflictError, NotFoundError } from "../../../domain/errors/base.error";
|
||||
import type {
|
||||
ReverseProxyCreateInput,
|
||||
ReverseProxyRepository,
|
||||
ReverseProxyUpdateInput,
|
||||
} from "../../../domain/repositories/reverse-proxy.repository";
|
||||
import { ApiKeyGeneratorImpl } from "../../api-key-generator";
|
||||
|
||||
export class PrismaReverseProxyRepository implements ReverseProxyRepository {
|
||||
private apiKeyGenerator = new ApiKeyGeneratorImpl();
|
||||
|
||||
async findById(id: string, omitSensitive = false): Promise<ReverseProxyWithEnvVars | null> {
|
||||
const proxy = await prisma.reverseProxyServer.findUnique({
|
||||
where: { id },
|
||||
include: { env_variables: true },
|
||||
});
|
||||
|
||||
if (!proxy) return null;
|
||||
|
||||
if (omitSensitive) {
|
||||
const { api_key, ...rest } = proxy;
|
||||
return { ...rest, api_key: "" } as ReverseProxyWithEnvVars;
|
||||
}
|
||||
|
||||
return proxy;
|
||||
}
|
||||
|
||||
async findAll(omitSensitive = false): Promise<ReverseProxyWithEnvVars[]> {
|
||||
const proxies = await prisma.reverseProxyServer.findMany({
|
||||
include: { env_variables: true },
|
||||
});
|
||||
|
||||
if (omitSensitive) {
|
||||
return proxies.map((proxy) => {
|
||||
const { api_key, ...rest } = proxy;
|
||||
return { ...rest, api_key: "" } as ReverseProxyWithEnvVars;
|
||||
});
|
||||
}
|
||||
|
||||
return proxies;
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<boolean> {
|
||||
const proxy = await prisma.reverseProxyServer.findUnique({
|
||||
where: { id },
|
||||
select: { id: true },
|
||||
});
|
||||
return proxy !== null;
|
||||
}
|
||||
|
||||
async create(input: ReverseProxyCreateInput): Promise<ReverseProxyWithEnvVars> {
|
||||
const existing = await this.exists(input.id);
|
||||
if (existing) {
|
||||
throw new ConflictError("ReverseProxyServer", input.id);
|
||||
}
|
||||
|
||||
const token = this.apiKeyGenerator.generateReverseProxyApiKey();
|
||||
|
||||
const proxy = await prisma.reverseProxyServer.create({
|
||||
data: {
|
||||
id: input.id,
|
||||
type: input.type ?? "VELOCITY",
|
||||
description: input.description ?? null,
|
||||
external_address: input.external_address,
|
||||
external_port: input.external_port,
|
||||
listen_port: input.listen_port ?? 25577,
|
||||
service_type: input.service_type ?? "LOAD_BALANCER",
|
||||
node_port: input.node_port ?? null,
|
||||
memory: input.memory ?? 512,
|
||||
cpu_request: input.cpu_request ?? "100m",
|
||||
cpu_limit: input.cpu_limit ?? "200m",
|
||||
api_key: token,
|
||||
env_variables: input.env_variables
|
||||
? {
|
||||
create: input.env_variables.map((ev) => ({
|
||||
key: ev.key,
|
||||
value: ev.value,
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
include: { env_variables: true },
|
||||
});
|
||||
|
||||
return proxy;
|
||||
}
|
||||
|
||||
async update(id: string, input: ReverseProxyUpdateInput): Promise<ReverseProxyWithEnvVars> {
|
||||
const proxy = await prisma.reverseProxyServer.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!proxy) {
|
||||
throw new NotFoundError("ReverseProxyServer", id);
|
||||
}
|
||||
|
||||
// Update proxy fields
|
||||
const updated = await prisma.reverseProxyServer.update({
|
||||
where: { id },
|
||||
data: {
|
||||
description: input.description,
|
||||
external_address: input.external_address,
|
||||
external_port: input.external_port,
|
||||
listen_port: input.listen_port,
|
||||
type: input.type,
|
||||
service_type: input.service_type,
|
||||
node_port: input.node_port,
|
||||
memory: input.memory,
|
||||
cpu_request: input.cpu_request,
|
||||
cpu_limit: input.cpu_limit,
|
||||
},
|
||||
include: { env_variables: true },
|
||||
});
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await prisma.reverseProxyServer.delete({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
async setEnvVariable(proxyId: string, key: string, value: string): Promise<void> {
|
||||
await prisma.customEnvironmentVariable.upsert({
|
||||
where: {
|
||||
key_reverse_proxy_id: {
|
||||
key,
|
||||
reverse_proxy_id: proxyId,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
value,
|
||||
},
|
||||
create: {
|
||||
key,
|
||||
value,
|
||||
reverse_proxy_id: proxyId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getEnvVariables(proxyId: string): Promise<EnvVariable[]> {
|
||||
const proxy = await prisma.reverseProxyServer.findUnique({
|
||||
where: { id: proxyId },
|
||||
include: { env_variables: true },
|
||||
});
|
||||
|
||||
if (!proxy) {
|
||||
throw new NotFoundError("ReverseProxyServer", proxyId);
|
||||
}
|
||||
|
||||
return proxy.env_variables.map((ev) => ({
|
||||
key: ev.key,
|
||||
value: ev.value,
|
||||
}));
|
||||
}
|
||||
|
||||
async deleteEnvVariable(proxyId: string, key: string): Promise<void> {
|
||||
await prisma.customEnvironmentVariable.deleteMany({
|
||||
where: {
|
||||
key,
|
||||
reverse_proxy_id: proxyId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async replaceEnvVariables(proxyId: string, envVars: EnvVariable[]): Promise<void> {
|
||||
await prisma.customEnvironmentVariable.deleteMany({
|
||||
where: { reverse_proxy_id: proxyId },
|
||||
});
|
||||
|
||||
if (envVars.length > 0) {
|
||||
await prisma.customEnvironmentVariable.createMany({
|
||||
data: envVars.map((envVar) => ({
|
||||
key: envVar.key,
|
||||
value: envVar.value,
|
||||
reverse_proxy_id: proxyId,
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
import { type EnvVariable, prisma, type ServerWithEnvVars } from "@minikura/db";
|
||||
import { ConflictError, NotFoundError } from "../../../domain/errors/base.error";
|
||||
import type {
|
||||
ServerCreateInput,
|
||||
ServerRepository,
|
||||
ServerUpdateInput,
|
||||
} from "../../../domain/repositories/server.repository";
|
||||
import { ApiKeyGeneratorImpl } from "../../api-key-generator";
|
||||
|
||||
export class PrismaServerRepository implements ServerRepository {
|
||||
private apiKeyGenerator = new ApiKeyGeneratorImpl();
|
||||
|
||||
async findById(id: string, omitSensitive = false): Promise<ServerWithEnvVars | null> {
|
||||
const server = await prisma.server.findUnique({
|
||||
where: { id },
|
||||
include: { env_variables: true },
|
||||
});
|
||||
|
||||
if (!server) return null;
|
||||
|
||||
if (omitSensitive) {
|
||||
const { api_key, ...rest } = server;
|
||||
return { ...rest, api_key: "" } as ServerWithEnvVars;
|
||||
}
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
async findAll(omitSensitive = false): Promise<ServerWithEnvVars[]> {
|
||||
const servers = await prisma.server.findMany({
|
||||
include: { env_variables: true },
|
||||
});
|
||||
|
||||
if (omitSensitive) {
|
||||
return servers.map((server) => {
|
||||
const { api_key, ...rest } = server;
|
||||
return { ...rest, api_key: "" } as ServerWithEnvVars;
|
||||
});
|
||||
}
|
||||
|
||||
return servers;
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<boolean> {
|
||||
const server = await prisma.server.findUnique({
|
||||
where: { id },
|
||||
select: { id: true },
|
||||
});
|
||||
return server !== null;
|
||||
}
|
||||
|
||||
async create(input: ServerCreateInput): Promise<ServerWithEnvVars> {
|
||||
const existing = await this.exists(input.id);
|
||||
if (existing) {
|
||||
throw new ConflictError("Server", input.id);
|
||||
}
|
||||
|
||||
const token = this.apiKeyGenerator.generateServerApiKey();
|
||||
|
||||
const server = await prisma.server.create({
|
||||
data: {
|
||||
id: input.id,
|
||||
type: input.type,
|
||||
description: input.description ?? null,
|
||||
listen_port: input.listen_port,
|
||||
service_type: input.service_type ?? "CLUSTER_IP",
|
||||
node_port: input.node_port ?? null,
|
||||
memory: input.memory ?? 2048,
|
||||
memory_request: input.memory_request ?? 1024,
|
||||
cpu_request: input.cpu_request ?? "250m",
|
||||
cpu_limit: input.cpu_limit ?? "500m",
|
||||
jar_type: input.jar_type ?? "PAPER",
|
||||
minecraft_version: input.minecraft_version ?? "LATEST",
|
||||
jvm_opts: input.jvm_opts ?? null,
|
||||
use_aikar_flags: input.use_aikar_flags ?? true,
|
||||
use_meowice_flags: input.use_meowice_flags ?? false,
|
||||
difficulty: input.difficulty ?? "EASY",
|
||||
game_mode: input.game_mode ?? "SURVIVAL",
|
||||
max_players: input.max_players ?? 20,
|
||||
pvp: input.pvp ?? true,
|
||||
online_mode: input.online_mode ?? true,
|
||||
motd: input.motd ?? null,
|
||||
level_seed: input.level_seed ?? null,
|
||||
level_type: input.level_type ?? null,
|
||||
api_key: token,
|
||||
env_variables: input.env_variables
|
||||
? {
|
||||
create: input.env_variables.map((ev) => ({
|
||||
key: ev.key,
|
||||
value: ev.value,
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
include: { env_variables: true },
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
async update(id: string, input: ServerUpdateInput): Promise<ServerWithEnvVars> {
|
||||
const server = await prisma.server.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!server) {
|
||||
throw new NotFoundError("Server", id);
|
||||
}
|
||||
|
||||
// Handle env variables separately
|
||||
if (input.env_variables !== undefined) {
|
||||
await this.replaceEnvVariables(id, input.env_variables);
|
||||
}
|
||||
|
||||
// Update server fields
|
||||
const updated = await prisma.server.update({
|
||||
where: { id },
|
||||
data: {
|
||||
description: input.description,
|
||||
listen_port: input.listen_port,
|
||||
service_type: input.service_type,
|
||||
node_port: input.node_port,
|
||||
memory: input.memory,
|
||||
memory_request: input.memory_request,
|
||||
cpu_request: input.cpu_request,
|
||||
cpu_limit: input.cpu_limit,
|
||||
jar_type: input.jar_type,
|
||||
minecraft_version: input.minecraft_version,
|
||||
jvm_opts: input.jvm_opts,
|
||||
use_aikar_flags: input.use_aikar_flags,
|
||||
use_meowice_flags: input.use_meowice_flags,
|
||||
difficulty: input.difficulty,
|
||||
game_mode: input.game_mode,
|
||||
max_players: input.max_players,
|
||||
pvp: input.pvp,
|
||||
online_mode: input.online_mode,
|
||||
motd: input.motd,
|
||||
level_seed: input.level_seed,
|
||||
level_type: input.level_type,
|
||||
},
|
||||
include: { env_variables: true },
|
||||
});
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await prisma.server.delete({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
async setEnvVariable(serverId: string, key: string, value: string): Promise<void> {
|
||||
await prisma.customEnvironmentVariable.upsert({
|
||||
where: {
|
||||
key_server_id: {
|
||||
key,
|
||||
server_id: serverId,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
value,
|
||||
},
|
||||
create: {
|
||||
key,
|
||||
value,
|
||||
server_id: serverId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getEnvVariables(serverId: string): Promise<EnvVariable[]> {
|
||||
const server = await prisma.server.findUnique({
|
||||
where: { id: serverId },
|
||||
include: { env_variables: true },
|
||||
});
|
||||
|
||||
if (!server) {
|
||||
throw new NotFoundError("Server", serverId);
|
||||
}
|
||||
|
||||
return server.env_variables.map((ev) => ({
|
||||
key: ev.key,
|
||||
value: ev.value,
|
||||
}));
|
||||
}
|
||||
|
||||
async deleteEnvVariable(serverId: string, key: string): Promise<void> {
|
||||
await prisma.customEnvironmentVariable.deleteMany({
|
||||
where: {
|
||||
key,
|
||||
server_id: serverId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async replaceEnvVariables(serverId: string, envVars: EnvVariable[]): Promise<void> {
|
||||
await prisma.customEnvironmentVariable.deleteMany({
|
||||
where: { server_id: serverId },
|
||||
});
|
||||
|
||||
if (envVars.length > 0) {
|
||||
await prisma.customEnvironmentVariable.createMany({
|
||||
data: envVars.map((envVar) => ({
|
||||
key: envVar.key,
|
||||
value: envVar.value,
|
||||
server_id: serverId,
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { prisma, type UpdateSuspensionInput, type UpdateUserInput, type User } from "@minikura/db";
|
||||
import { UserRole } from "../../../domain/entities/enums";
|
||||
import type { UserRepository } from "../../../domain/repositories/user.repository";
|
||||
|
||||
export class PrismaUserRepository implements UserRepository {
|
||||
async findById(id: string): Promise<User | null> {
|
||||
return await prisma.user.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
async findByEmail(email: string): Promise<User | null> {
|
||||
return await prisma.user.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
}
|
||||
|
||||
async findAll(): Promise<User[]> {
|
||||
return await prisma.user.findMany({
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
}
|
||||
|
||||
async update(id: string, input: UpdateUserInput): Promise<User> {
|
||||
return await prisma.user.update({
|
||||
where: { id },
|
||||
data: input,
|
||||
});
|
||||
}
|
||||
|
||||
async updateSuspension(id: string, input: UpdateSuspensionInput): Promise<User> {
|
||||
return await prisma.user.update({
|
||||
where: { id },
|
||||
data: input,
|
||||
});
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await prisma.user.delete({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
async count(): Promise<number> {
|
||||
return await prisma.user.count();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user