Enhance API with OIDC support, including login and callback endpoints. Update environment variables for OIDC configuration in .env.example. Add new features to the catalog service for listing software families, Minecraft versions, and deployment regions. Implement server management actions such as kill, delete, and update in the servers module. Integrate feature flags for maintenance mode in server operations. Update pnpm-lock.yaml with new dependencies and versions.
This commit is contained in:
74
packages/catalog/src/curseforge.ts
Normal file
74
packages/catalog/src/curseforge.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
export interface CurseForgeProject {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
summary: string;
|
||||
downloadCount: number;
|
||||
logo?: { thumbnailUrl?: string };
|
||||
}
|
||||
|
||||
export interface CurseForgeSearchResponse {
|
||||
data: CurseForgeProject[];
|
||||
pagination: { totalCount: number };
|
||||
}
|
||||
|
||||
export interface CurseForgeFile {
|
||||
id: number;
|
||||
displayName: string;
|
||||
fileName: string;
|
||||
downloadUrl: string;
|
||||
fileLength: number;
|
||||
gameVersions: string[];
|
||||
modLoader?: number;
|
||||
}
|
||||
|
||||
const CURSEFORGE_API = 'https://api.curseforge.com/v1';
|
||||
|
||||
function apiKey(): string {
|
||||
const key = process.env['CURSEFORGE_API_KEY'];
|
||||
if (!key) {
|
||||
throw new Error('CURSEFORGE_API_KEY is not configured');
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
async function curseforgeFetch<T>(path: string): Promise<T> {
|
||||
const response = await fetch(`${CURSEFORGE_API}${path}`, {
|
||||
headers: {
|
||||
'x-api-key': apiKey(),
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`CurseForge API error: ${response.status}`);
|
||||
}
|
||||
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
export async function searchCurseForgeMods(input: {
|
||||
gameId?: number;
|
||||
searchFilter: string;
|
||||
pageSize?: number;
|
||||
classId?: number;
|
||||
}): Promise<CurseForgeSearchResponse> {
|
||||
const params = new URLSearchParams({
|
||||
gameId: String(input.gameId ?? 432),
|
||||
searchFilter: input.searchFilter,
|
||||
pageSize: String(input.pageSize ?? 20),
|
||||
});
|
||||
if (input.classId !== undefined) {
|
||||
params.set('classId', String(input.classId));
|
||||
}
|
||||
|
||||
return curseforgeFetch<CurseForgeSearchResponse>(`/mods/search?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getCurseForgeModFiles(modId: number): Promise<{ data: CurseForgeFile[] }> {
|
||||
return curseforgeFetch<{ data: CurseForgeFile[] }>(`/mods/${modId}/files`);
|
||||
}
|
||||
|
||||
export async function getCurseForgeMod(modId: number): Promise<{ data: CurseForgeProject }> {
|
||||
return curseforgeFetch<{ data: CurseForgeProject }>(`/mods/${modId}`);
|
||||
}
|
||||
@@ -11,6 +11,15 @@ export {
|
||||
type SoftwareFamily,
|
||||
} from './modrinth';
|
||||
|
||||
export {
|
||||
searchCurseForgeMods,
|
||||
getCurseForgeModFiles,
|
||||
getCurseForgeMod,
|
||||
type CurseForgeProject,
|
||||
type CurseForgeSearchResponse,
|
||||
type CurseForgeFile,
|
||||
} from './curseforge';
|
||||
|
||||
export {
|
||||
resolveModrinthLoader,
|
||||
resolveProjectType,
|
||||
|
||||
42
packages/contracts/src/abuse.ts
Normal file
42
packages/contracts/src/abuse.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const abuseReportStatusSchema = z.enum([
|
||||
'OPEN',
|
||||
'INVESTIGATING',
|
||||
'RESOLVED',
|
||||
'DISMISSED',
|
||||
]);
|
||||
|
||||
export const createAbuseReportSchema = z.object({
|
||||
reason: z.string().min(1).max(256),
|
||||
details: z.string().max(4096).optional(),
|
||||
targetUserId: z.string().uuid().optional(),
|
||||
serverId: z.string().uuid().optional(),
|
||||
});
|
||||
|
||||
export const abuseReportSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
reporterId: z.string().uuid().nullable(),
|
||||
targetUserId: z.string().uuid().nullable(),
|
||||
serverId: z.string().uuid().nullable(),
|
||||
status: abuseReportStatusSchema,
|
||||
reason: z.string(),
|
||||
details: z.string().nullable(),
|
||||
resolution: z.string().nullable(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
});
|
||||
|
||||
export const abuseReportListResponseSchema = z.object({
|
||||
reports: z.array(abuseReportSchema),
|
||||
});
|
||||
|
||||
export const updateAbuseReportSchema = z.object({
|
||||
status: abuseReportStatusSchema.optional(),
|
||||
resolution: z.string().max(4096).optional(),
|
||||
});
|
||||
|
||||
export type CreateAbuseReport = z.infer<typeof createAbuseReportSchema>;
|
||||
export type AbuseReport = z.infer<typeof abuseReportSchema>;
|
||||
export type AbuseReportListResponse = z.infer<typeof abuseReportListResponseSchema>;
|
||||
export type UpdateAbuseReport = z.infer<typeof updateAbuseReportSchema>;
|
||||
25
packages/contracts/src/compliance.ts
Normal file
25
packages/contracts/src/compliance.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const dataExportRequestSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
status: z.enum(['PENDING', 'PROCESSING', 'READY', 'FAILED', 'EXPIRED']),
|
||||
downloadUrl: z.string().url().nullable(),
|
||||
expiresAt: z.string().datetime().nullable(),
|
||||
completedAt: z.string().datetime().nullable(),
|
||||
createdAt: z.string().datetime(),
|
||||
});
|
||||
|
||||
export const accountDeletionRequestSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
status: z.enum(['PENDING', 'SCHEDULED', 'COMPLETED', 'CANCELLED']),
|
||||
scheduledFor: z.string().datetime(),
|
||||
completedAt: z.string().datetime().nullable(),
|
||||
createdAt: z.string().datetime(),
|
||||
});
|
||||
|
||||
export const requestAccountDeletionSchema = z.object({
|
||||
confirmEmail: z.string().email(),
|
||||
});
|
||||
|
||||
export type DataExportRequest = z.infer<typeof dataExportRequestSchema>;
|
||||
export type AccountDeletionRequest = z.infer<typeof accountDeletionRequestSchema>;
|
||||
@@ -21,6 +21,7 @@ export const fileContentSchema = z.object({
|
||||
path: z.string(),
|
||||
content: z.string(),
|
||||
encoding: z.enum(['utf-8', 'base64']).default('utf-8'),
|
||||
version: z.number().int().optional(),
|
||||
});
|
||||
|
||||
export const readFileContentQuerySchema = z.object({
|
||||
@@ -31,6 +32,27 @@ export const updateFileContentSchema = z.object({
|
||||
path: z.string().min(1),
|
||||
content: z.string(),
|
||||
encoding: z.enum(['utf-8', 'base64']).default('utf-8'),
|
||||
version: z.number().int().optional(),
|
||||
});
|
||||
|
||||
export const uploadFileSchema = z.object({
|
||||
path: z.string().min(1),
|
||||
content: z.string(),
|
||||
encoding: z.enum(['utf-8', 'base64']).default('base64'),
|
||||
});
|
||||
|
||||
export const filePathSchema = z.object({
|
||||
path: z.string().min(1),
|
||||
});
|
||||
|
||||
export const archiveFilesSchema = z.object({
|
||||
paths: z.array(z.string().min(1)).min(1).max(100),
|
||||
archiveName: z.string().min(1).max(128).optional(),
|
||||
});
|
||||
|
||||
export const unarchiveFileSchema = z.object({
|
||||
archivePath: z.string().min(1),
|
||||
destinationPath: z.string().default('.'),
|
||||
});
|
||||
|
||||
export type ListFilesQuery = z.infer<typeof listFilesQuerySchema>;
|
||||
@@ -39,3 +61,6 @@ export type FileListResponse = z.infer<typeof fileListResponseSchema>;
|
||||
export type FileContent = z.infer<typeof fileContentSchema>;
|
||||
export type ReadFileContentQuery = z.infer<typeof readFileContentQuerySchema>;
|
||||
export type UpdateFileContent = z.infer<typeof updateFileContentSchema>;
|
||||
export type UploadFile = z.infer<typeof uploadFileSchema>;
|
||||
export type ArchiveFiles = z.infer<typeof archiveFilesSchema>;
|
||||
export type UnarchiveFile = z.infer<typeof unarchiveFileSchema>;
|
||||
|
||||
@@ -61,6 +61,7 @@ export {
|
||||
serverEditionSchema,
|
||||
softwareFamilySchema,
|
||||
createServerRequestSchema,
|
||||
updateServerRequestSchema,
|
||||
serverResponseSchema,
|
||||
serverListResponseSchema,
|
||||
serverActionResponseSchema,
|
||||
@@ -68,6 +69,7 @@ export {
|
||||
type ServerEdition,
|
||||
type SoftwareFamily,
|
||||
type CreateServerRequest,
|
||||
type UpdateServerRequest,
|
||||
type ServerResponse,
|
||||
type ServerListResponse,
|
||||
type ServerActionResponse,
|
||||
@@ -80,12 +82,19 @@ export {
|
||||
fileContentSchema,
|
||||
readFileContentQuerySchema,
|
||||
updateFileContentSchema,
|
||||
uploadFileSchema,
|
||||
filePathSchema,
|
||||
archiveFilesSchema,
|
||||
unarchiveFileSchema,
|
||||
type ListFilesQuery,
|
||||
type FileEntry,
|
||||
type FileListResponse,
|
||||
type FileContent,
|
||||
type ReadFileContentQuery,
|
||||
type UpdateFileContent,
|
||||
type UploadFile,
|
||||
type ArchiveFiles,
|
||||
type UnarchiveFile,
|
||||
} from './files';
|
||||
|
||||
export {
|
||||
@@ -108,13 +117,54 @@ export {
|
||||
onlinePlayerSchema,
|
||||
whitelistEntrySchema,
|
||||
operatorEntrySchema,
|
||||
banEntrySchema,
|
||||
playerListResponseSchema,
|
||||
addWhitelistSchema,
|
||||
addOperatorSchema,
|
||||
removePlayerEntrySchema,
|
||||
addBanSchema,
|
||||
type OnlinePlayer,
|
||||
type WhitelistEntry,
|
||||
type OperatorEntry,
|
||||
type BanEntry,
|
||||
type PlayerListResponse,
|
||||
type AddWhitelist,
|
||||
type AddOperator,
|
||||
type RemovePlayerEntry,
|
||||
type AddBan,
|
||||
} from './players';
|
||||
|
||||
export {
|
||||
scheduleTaskTypeSchema,
|
||||
scheduleSchema,
|
||||
createScheduleSchema,
|
||||
updateScheduleSchema,
|
||||
scheduleListResponseSchema,
|
||||
type Schedule,
|
||||
type CreateSchedule,
|
||||
type UpdateSchedule,
|
||||
type ScheduleListResponse,
|
||||
} from './schedules';
|
||||
|
||||
export {
|
||||
notificationTypeSchema,
|
||||
notificationChannelSchema,
|
||||
notificationSchema,
|
||||
notificationListResponseSchema,
|
||||
notificationPreferenceSchema,
|
||||
updateNotificationPreferencesSchema,
|
||||
type Notification,
|
||||
type NotificationListResponse,
|
||||
type UpdateNotificationPreferences,
|
||||
} from './notifications';
|
||||
|
||||
export {
|
||||
reinstallServerSchema,
|
||||
reinstallServerResponseSchema,
|
||||
type ReinstallServer,
|
||||
type ReinstallServerResponse,
|
||||
} from './software';
|
||||
|
||||
export {
|
||||
backupStatusSchema,
|
||||
backupTypeSchema,
|
||||
@@ -273,3 +323,62 @@ export {
|
||||
type WhmcsMtlsStatusResponse,
|
||||
type WhmcsValidatePlanResponse,
|
||||
} from './whmcs';
|
||||
|
||||
export {
|
||||
dataExportRequestSchema,
|
||||
accountDeletionRequestSchema,
|
||||
requestAccountDeletionSchema,
|
||||
type DataExportRequest,
|
||||
type AccountDeletionRequest,
|
||||
} from './compliance';
|
||||
|
||||
export {
|
||||
whmcsServiceActionRequestSchema,
|
||||
whmcsServiceActionResponseSchema,
|
||||
type WhmcsServiceActionRequest,
|
||||
type WhmcsServiceActionResponse,
|
||||
} from './whmcs-actions';
|
||||
|
||||
export {
|
||||
organizationMemberRoleSchema,
|
||||
organizationSchema,
|
||||
organizationListResponseSchema,
|
||||
createOrganizationSchema,
|
||||
organizationMemberSchema,
|
||||
organizationMemberListResponseSchema,
|
||||
inviteOrganizationMemberSchema,
|
||||
organizationInviteResponseSchema,
|
||||
type Organization,
|
||||
type OrganizationListResponse,
|
||||
type CreateOrganization,
|
||||
type OrganizationMember,
|
||||
type OrganizationMemberListResponse,
|
||||
type InviteOrganizationMember,
|
||||
type OrganizationInviteResponse,
|
||||
} from './organizations';
|
||||
|
||||
export {
|
||||
supportTicketStatusSchema,
|
||||
supportTicketPrioritySchema,
|
||||
supportTicketMessageSchema,
|
||||
supportTicketSchema,
|
||||
supportTicketListResponseSchema,
|
||||
createSupportTicketSchema,
|
||||
addSupportTicketMessageSchema,
|
||||
type SupportTicket,
|
||||
type SupportTicketListResponse,
|
||||
type CreateSupportTicket,
|
||||
type AddSupportTicketMessage,
|
||||
} from './support';
|
||||
|
||||
export {
|
||||
abuseReportStatusSchema,
|
||||
createAbuseReportSchema,
|
||||
abuseReportSchema,
|
||||
abuseReportListResponseSchema,
|
||||
updateAbuseReportSchema,
|
||||
type CreateAbuseReport,
|
||||
type AbuseReport,
|
||||
type AbuseReportListResponse,
|
||||
type UpdateAbuseReport,
|
||||
} from './abuse';
|
||||
|
||||
51
packages/contracts/src/notifications.ts
Normal file
51
packages/contracts/src/notifications.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const notificationTypeSchema = z.enum([
|
||||
'SERVER_READY',
|
||||
'SERVER_START_FAILED',
|
||||
'SERVER_STOPPED',
|
||||
'IDLE_STOP_WARNING',
|
||||
'BACKUP_SUCCESS',
|
||||
'BACKUP_FAILED',
|
||||
'RESTORE_SUCCESS',
|
||||
'RESTORE_FAILED',
|
||||
'CREDITS_LOW',
|
||||
'BILLING_ISSUE',
|
||||
'INVITE',
|
||||
'NODE_INCIDENT',
|
||||
'MAINTENANCE',
|
||||
'SECURITY',
|
||||
'NEW_LOGIN',
|
||||
'TWO_FA_CHANGED',
|
||||
]);
|
||||
|
||||
export const notificationChannelSchema = z.enum(['IN_APP', 'EMAIL', 'WEBHOOK', 'DISCORD']);
|
||||
|
||||
export const notificationSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
type: notificationTypeSchema,
|
||||
channel: notificationChannelSchema,
|
||||
title: z.string(),
|
||||
body: z.string(),
|
||||
readAt: z.string().datetime().nullable(),
|
||||
createdAt: z.string().datetime(),
|
||||
});
|
||||
|
||||
export const notificationListResponseSchema = z.object({
|
||||
notifications: z.array(notificationSchema),
|
||||
unreadCount: z.number().int(),
|
||||
});
|
||||
|
||||
export const notificationPreferenceSchema = z.object({
|
||||
type: notificationTypeSchema,
|
||||
channel: notificationChannelSchema,
|
||||
enabled: z.boolean(),
|
||||
});
|
||||
|
||||
export const updateNotificationPreferencesSchema = z.object({
|
||||
preferences: z.array(notificationPreferenceSchema),
|
||||
});
|
||||
|
||||
export type Notification = z.infer<typeof notificationSchema>;
|
||||
export type NotificationListResponse = z.infer<typeof notificationListResponseSchema>;
|
||||
export type UpdateNotificationPreferences = z.infer<typeof updateNotificationPreferencesSchema>;
|
||||
56
packages/contracts/src/organizations.ts
Normal file
56
packages/contracts/src/organizations.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const organizationMemberRoleSchema = z.enum(['OWNER', 'ADMIN', 'MEMBER']);
|
||||
|
||||
export const organizationSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
role: organizationMemberRoleSchema,
|
||||
createdAt: z.string().datetime(),
|
||||
});
|
||||
|
||||
export const organizationListResponseSchema = z.object({
|
||||
organizations: z.array(organizationSchema),
|
||||
});
|
||||
|
||||
export const createOrganizationSchema = z.object({
|
||||
name: z.string().min(1).max(128),
|
||||
slug: z.string().min(2).max(64).regex(/^[a-z0-9-]+$/).optional(),
|
||||
});
|
||||
|
||||
export const organizationMemberSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
userId: z.string().uuid(),
|
||||
role: organizationMemberRoleSchema,
|
||||
email: z.string().email(),
|
||||
username: z.string(),
|
||||
displayName: z.string().nullable(),
|
||||
createdAt: z.string().datetime(),
|
||||
});
|
||||
|
||||
export const organizationMemberListResponseSchema = z.object({
|
||||
members: z.array(organizationMemberSchema),
|
||||
});
|
||||
|
||||
export const inviteOrganizationMemberSchema = z.object({
|
||||
email: z.string().email(),
|
||||
role: organizationMemberRoleSchema.default('MEMBER'),
|
||||
});
|
||||
|
||||
export const organizationInviteResponseSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
email: z.string().email(),
|
||||
role: organizationMemberRoleSchema,
|
||||
expiresAt: z.string().datetime(),
|
||||
});
|
||||
|
||||
export type Organization = z.infer<typeof organizationSchema>;
|
||||
export type OrganizationListResponse = z.infer<typeof organizationListResponseSchema>;
|
||||
export type CreateOrganization = z.infer<typeof createOrganizationSchema>;
|
||||
export type OrganizationMember = z.infer<typeof organizationMemberSchema>;
|
||||
export type OrganizationMemberListResponse = z.infer<
|
||||
typeof organizationMemberListResponseSchema
|
||||
>;
|
||||
export type InviteOrganizationMember = z.infer<typeof inviteOrganizationMemberSchema>;
|
||||
export type OrganizationInviteResponse = z.infer<typeof organizationInviteResponseSchema>;
|
||||
@@ -17,13 +17,48 @@ export const operatorEntrySchema = z.object({
|
||||
bypassesPlayerLimit: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const banEntrySchema = z.object({
|
||||
uuid: z.string().uuid().optional(),
|
||||
name: z.string(),
|
||||
reason: z.string().optional(),
|
||||
source: z.string().optional(),
|
||||
expires: z.string().optional(),
|
||||
});
|
||||
|
||||
export const playerListResponseSchema = z.object({
|
||||
online: z.array(onlinePlayerSchema),
|
||||
whitelist: z.array(whitelistEntrySchema),
|
||||
operators: z.array(operatorEntrySchema),
|
||||
bans: z.array(banEntrySchema).optional(),
|
||||
});
|
||||
|
||||
export const addWhitelistSchema = z.object({
|
||||
uuid: z.string().uuid(),
|
||||
name: z.string().min(1).max(16),
|
||||
});
|
||||
|
||||
export const addOperatorSchema = z.object({
|
||||
uuid: z.string().uuid(),
|
||||
name: z.string().min(1).max(16),
|
||||
level: z.number().int().min(0).max(4).default(4),
|
||||
bypassesPlayerLimit: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export const removePlayerEntrySchema = z.object({
|
||||
uuid: z.string().uuid(),
|
||||
});
|
||||
|
||||
export const addBanSchema = z.object({
|
||||
name: z.string().min(1).max(16),
|
||||
reason: z.string().max(256).optional(),
|
||||
});
|
||||
|
||||
export type OnlinePlayer = z.infer<typeof onlinePlayerSchema>;
|
||||
export type WhitelistEntry = z.infer<typeof whitelistEntrySchema>;
|
||||
export type OperatorEntry = z.infer<typeof operatorEntrySchema>;
|
||||
export type BanEntry = z.infer<typeof banEntrySchema>;
|
||||
export type PlayerListResponse = z.infer<typeof playerListResponseSchema>;
|
||||
export type AddWhitelist = z.infer<typeof addWhitelistSchema>;
|
||||
export type AddOperator = z.infer<typeof addOperatorSchema>;
|
||||
export type RemovePlayerEntry = z.infer<typeof removePlayerEntrySchema>;
|
||||
export type AddBan = z.infer<typeof addBanSchema>;
|
||||
|
||||
54
packages/contracts/src/schedules.ts
Normal file
54
packages/contracts/src/schedules.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const scheduleTaskTypeSchema = z.enum([
|
||||
'START',
|
||||
'STOP',
|
||||
'RESTART',
|
||||
'BACKUP',
|
||||
'CONSOLE_COMMAND',
|
||||
'UPDATE_CHECK',
|
||||
'MAINTENANCE',
|
||||
]);
|
||||
|
||||
export const scheduledTaskSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
taskType: scheduleTaskTypeSchema,
|
||||
payload: z.record(z.unknown()).nullable(),
|
||||
});
|
||||
|
||||
export const scheduleSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
timezone: z.string(),
|
||||
cronExpr: z.string(),
|
||||
enabled: z.boolean(),
|
||||
nextRunAt: z.string().datetime().nullable(),
|
||||
lastRunAt: z.string().datetime().nullable(),
|
||||
tasks: z.array(scheduledTaskSchema),
|
||||
});
|
||||
|
||||
export const createScheduleSchema = z.object({
|
||||
name: z.string().min(1).max(64),
|
||||
timezone: z.string().default('UTC'),
|
||||
cronExpr: z.string().min(5).max(128),
|
||||
enabled: z.boolean().default(true),
|
||||
tasks: z
|
||||
.array(
|
||||
z.object({
|
||||
taskType: scheduleTaskTypeSchema,
|
||||
payload: z.record(z.unknown()).optional(),
|
||||
}),
|
||||
)
|
||||
.min(1),
|
||||
});
|
||||
|
||||
export const updateScheduleSchema = createScheduleSchema.partial();
|
||||
|
||||
export const scheduleListResponseSchema = z.object({
|
||||
schedules: z.array(scheduleSchema),
|
||||
});
|
||||
|
||||
export type Schedule = z.infer<typeof scheduleSchema>;
|
||||
export type CreateSchedule = z.infer<typeof createScheduleSchema>;
|
||||
export type UpdateSchedule = z.infer<typeof updateScheduleSchema>;
|
||||
export type ScheduleListResponse = z.infer<typeof scheduleListResponseSchema>;
|
||||
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
||||
|
||||
export const gameServerStatusSchema = z.enum([
|
||||
'DRAFT',
|
||||
'ALLOCATING',
|
||||
'PROVISIONING',
|
||||
'INSTALLING',
|
||||
'STOPPED',
|
||||
@@ -11,6 +12,9 @@ export const gameServerStatusSchema = z.enum([
|
||||
'STOPPING',
|
||||
'BACKING_UP',
|
||||
'RESTORING',
|
||||
'MIGRATING',
|
||||
'SUSPENDED',
|
||||
'MAINTENANCE',
|
||||
'UNKNOWN',
|
||||
'ERROR',
|
||||
'DELETING',
|
||||
@@ -42,6 +46,11 @@ export const createServerRequestSchema = z.object({
|
||||
planId: z.string().uuid().optional(),
|
||||
});
|
||||
|
||||
export const updateServerRequestSchema = z.object({
|
||||
name: z.string().min(1).max(64).optional(),
|
||||
idleShutdownEnabled: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const serverResponseSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
userId: z.string().uuid(),
|
||||
@@ -82,6 +91,7 @@ export type GameServerStatus = z.infer<typeof gameServerStatusSchema>;
|
||||
export type ServerEdition = z.infer<typeof serverEditionSchema>;
|
||||
export type SoftwareFamily = z.infer<typeof softwareFamilySchema>;
|
||||
export type CreateServerRequest = z.infer<typeof createServerRequestSchema>;
|
||||
export type UpdateServerRequest = z.infer<typeof updateServerRequestSchema>;
|
||||
export type ServerResponse = z.infer<typeof serverResponseSchema>;
|
||||
export type ServerListResponse = z.infer<typeof serverListResponseSchema>;
|
||||
export type ServerActionResponse = z.infer<typeof serverActionResponseSchema>;
|
||||
|
||||
18
packages/contracts/src/software.ts
Normal file
18
packages/contracts/src/software.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { softwareFamilySchema } from './servers';
|
||||
|
||||
export const reinstallServerSchema = z.object({
|
||||
softwareFamily: softwareFamilySchema,
|
||||
minecraftVersion: z.string().min(1).max(32),
|
||||
createBackup: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export const reinstallServerResponseSchema = z.object({
|
||||
serverId: z.string().uuid(),
|
||||
jobId: z.string(),
|
||||
status: z.string(),
|
||||
});
|
||||
|
||||
export type ReinstallServer = z.infer<typeof reinstallServerSchema>;
|
||||
export type ReinstallServerResponse = z.infer<typeof reinstallServerResponseSchema>;
|
||||
50
packages/contracts/src/support.ts
Normal file
50
packages/contracts/src/support.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const supportTicketStatusSchema = z.enum([
|
||||
'OPEN',
|
||||
'IN_PROGRESS',
|
||||
'WAITING',
|
||||
'RESOLVED',
|
||||
'CLOSED',
|
||||
]);
|
||||
|
||||
export const supportTicketPrioritySchema = z.enum(['LOW', 'NORMAL', 'HIGH', 'URGENT']);
|
||||
|
||||
export const supportTicketMessageSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
authorId: z.string().uuid().nullable(),
|
||||
isInternal: z.boolean(),
|
||||
body: z.string(),
|
||||
createdAt: z.string().datetime(),
|
||||
});
|
||||
|
||||
export const supportTicketSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
subject: z.string(),
|
||||
status: supportTicketStatusSchema,
|
||||
priority: supportTicketPrioritySchema,
|
||||
serverId: z.string().uuid().nullable(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
messages: z.array(supportTicketMessageSchema).optional(),
|
||||
});
|
||||
|
||||
export const supportTicketListResponseSchema = z.object({
|
||||
tickets: z.array(supportTicketSchema),
|
||||
});
|
||||
|
||||
export const createSupportTicketSchema = z.object({
|
||||
subject: z.string().min(1).max(256),
|
||||
body: z.string().min(1).max(8192),
|
||||
priority: supportTicketPrioritySchema.default('NORMAL'),
|
||||
serverId: z.string().uuid().optional(),
|
||||
});
|
||||
|
||||
export const addSupportTicketMessageSchema = z.object({
|
||||
body: z.string().min(1).max(8192),
|
||||
});
|
||||
|
||||
export type SupportTicket = z.infer<typeof supportTicketSchema>;
|
||||
export type SupportTicketListResponse = z.infer<typeof supportTicketListResponseSchema>;
|
||||
export type CreateSupportTicket = z.infer<typeof createSupportTicketSchema>;
|
||||
export type AddSupportTicketMessage = z.infer<typeof addSupportTicketMessageSchema>;
|
||||
12
packages/contracts/src/whmcs-actions.ts
Normal file
12
packages/contracts/src/whmcs-actions.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { serverActionResponseSchema } from './servers';
|
||||
|
||||
export const whmcsServiceActionRequestSchema = z.object({
|
||||
reason: z.string().max(256).optional(),
|
||||
});
|
||||
|
||||
export const whmcsServiceActionResponseSchema = serverActionResponseSchema;
|
||||
|
||||
export type WhmcsServiceActionRequest = z.infer<typeof whmcsServiceActionRequestSchema>;
|
||||
export type WhmcsServiceActionResponse = z.infer<typeof whmcsServiceActionResponseSchema>;
|
||||
@@ -0,0 +1,18 @@
|
||||
-- Gap closure migration: permissions, catalog, compliance, WHMCS mappings, server states
|
||||
|
||||
-- AlterEnum
|
||||
ALTER TYPE "GameServerStatus" ADD VALUE IF NOT EXISTS 'ALLOCATING';
|
||||
ALTER TYPE "GameServerStatus" ADD VALUE IF NOT EXISTS 'MIGRATING';
|
||||
ALTER TYPE "GameServerStatus" ADD VALUE IF NOT EXISTS 'SUSPENDED';
|
||||
ALTER TYPE "GameServerStatus" ADD VALUE IF NOT EXISTS 'MAINTENANCE';
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "GameServerMemberRole" AS ENUM ('OWNER', 'ADMIN', 'OPERATOR', 'DEVELOPER', 'VIEWER');
|
||||
CREATE TYPE "OrganizationMemberRole" AS ENUM ('OWNER', 'ADMIN', 'MEMBER');
|
||||
CREATE TYPE "NotificationChannel" AS ENUM ('IN_APP', 'EMAIL', 'WEBHOOK', 'DISCORD');
|
||||
CREATE TYPE "NotificationType" AS ENUM ('SERVER_READY', 'SERVER_START_FAILED', 'SERVER_STOPPED', 'IDLE_STOP_WARNING', 'BACKUP_SUCCESS', 'BACKUP_FAILED', 'RESTORE_SUCCESS', 'RESTORE_FAILED', 'CREDITS_LOW', 'BILLING_ISSUE', 'INVITE', 'NODE_INCIDENT', 'MAINTENANCE', 'SECURITY', 'NEW_LOGIN', 'TWO_FA_CHANGED');
|
||||
CREATE TYPE "AbuseReportStatus" AS ENUM ('OPEN', 'INVESTIGATING', 'RESOLVED', 'DISMISSED');
|
||||
CREATE TYPE "SupportTicketStatus" AS ENUM ('OPEN', 'IN_PROGRESS', 'WAITING', 'RESOLVED', 'CLOSED');
|
||||
CREATE TYPE "SupportTicketPriority" AS ENUM ('LOW', 'NORMAL', 'HIGH', 'URGENT');
|
||||
CREATE TYPE "ScheduleTaskType" AS ENUM ('START', 'STOP', 'RESTART', 'BACKUP', 'CONSOLE_COMMAND', 'UPDATE_CHECK', 'MAINTENANCE');
|
||||
CREATE TYPE "CatalogProviderKind" AS ENUM ('MODRINTH', 'CURSEFORGE', 'MOJANG', 'PAPERMC', 'CUSTOM');
|
||||
@@ -0,0 +1,768 @@
|
||||
-- Gap closure tables: organizations, catalog, schedules, notifications, compliance, billing, admin
|
||||
|
||||
-- CreateEnum
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "JobStatus" AS ENUM ('PENDING', 'RUNNING', 'COMPLETED', 'FAILED', 'CANCELLED');
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE IF NOT EXISTS "organizations" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "organizations_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "organization_members" (
|
||||
"id" TEXT NOT NULL,
|
||||
"organizationId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"role" "OrganizationMemberRole" NOT NULL DEFAULT 'MEMBER',
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "organization_members_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "organization_invites" (
|
||||
"id" TEXT NOT NULL,
|
||||
"organizationId" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"role" "OrganizationMemberRole" NOT NULL DEFAULT 'MEMBER',
|
||||
"tokenHash" TEXT NOT NULL,
|
||||
"invitedById" TEXT NOT NULL,
|
||||
"expiresAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
"acceptedAt" TIMESTAMPTZ(3),
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "organization_invites_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "game_server_members" (
|
||||
"id" TEXT NOT NULL,
|
||||
"serverId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"role" "GameServerMemberRole" NOT NULL DEFAULT 'VIEWER',
|
||||
"permissions" JSONB,
|
||||
"invitedAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"acceptedAt" TIMESTAMPTZ(3),
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "game_server_members_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "game_server_properties" (
|
||||
"id" TEXT NOT NULL,
|
||||
"serverId" TEXT NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
"value" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "game_server_properties_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "game_server_environment_variables" (
|
||||
"id" TEXT NOT NULL,
|
||||
"serverId" TEXT NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
"value" TEXT NOT NULL,
|
||||
"isSecret" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "game_server_environment_variables_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "game_server_secrets" (
|
||||
"id" TEXT NOT NULL,
|
||||
"serverId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"valueCiphertext" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "game_server_secrets_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "game_node_capabilities" (
|
||||
"id" TEXT NOT NULL,
|
||||
"nodeId" TEXT NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
"value" TEXT,
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "game_node_capabilities_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "game_node_maintenance_windows" (
|
||||
"id" TEXT NOT NULL,
|
||||
"nodeId" TEXT NOT NULL,
|
||||
"startsAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
"endsAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
"drainFirst" BOOLEAN NOT NULL DEFAULT true,
|
||||
"message" TEXT,
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "game_node_maintenance_windows_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "minecraft_versions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"version" TEXT NOT NULL,
|
||||
"edition" "ServerEdition" NOT NULL DEFAULT 'JAVA',
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "minecraft_versions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "software_builds" (
|
||||
"id" TEXT NOT NULL,
|
||||
"family" "SoftwareFamily" NOT NULL,
|
||||
"minecraftVersionId" TEXT NOT NULL,
|
||||
"downloadUrl" TEXT,
|
||||
"sha256" TEXT,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "software_builds_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "runtime_images" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"imageRef" TEXT NOT NULL,
|
||||
"digest" TEXT,
|
||||
"javaVersion" TEXT,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "runtime_images_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "catalog_providers" (
|
||||
"id" TEXT NOT NULL,
|
||||
"kind" "CatalogProviderKind" NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"isEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"config" JSONB,
|
||||
"lastSyncAt" TIMESTAMPTZ(3),
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "catalog_providers_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "catalog_projects" (
|
||||
"id" TEXT NOT NULL,
|
||||
"providerId" TEXT NOT NULL,
|
||||
"externalId" TEXT NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"projectType" TEXT NOT NULL,
|
||||
"metadata" JSONB,
|
||||
"syncedAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "catalog_projects_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "catalog_versions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"projectId" TEXT NOT NULL,
|
||||
"externalId" TEXT NOT NULL,
|
||||
"versionNumber" TEXT NOT NULL,
|
||||
"gameVersions" JSONB,
|
||||
"loaders" JSONB,
|
||||
"sha512" TEXT,
|
||||
"downloadUrl" TEXT,
|
||||
"syncedAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "catalog_versions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "catalog_dependencies" (
|
||||
"id" TEXT NOT NULL,
|
||||
"versionId" TEXT NOT NULL,
|
||||
"dependencyId" TEXT NOT NULL,
|
||||
"dependencyType" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "catalog_dependencies_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "worlds" (
|
||||
"id" TEXT NOT NULL,
|
||||
"serverId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT false,
|
||||
"sizeBytes" BIGINT,
|
||||
"hasLevelDat" BOOLEAN NOT NULL DEFAULT false,
|
||||
"seed" TEXT,
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "worlds_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "world_archives" (
|
||||
"id" TEXT NOT NULL,
|
||||
"worldId" TEXT NOT NULL,
|
||||
"s3Key" TEXT NOT NULL,
|
||||
"sha256" TEXT,
|
||||
"sizeBytes" BIGINT,
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "world_archives_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "schedules" (
|
||||
"id" TEXT NOT NULL,
|
||||
"serverId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"timezone" TEXT NOT NULL DEFAULT 'UTC',
|
||||
"cronExpr" TEXT NOT NULL,
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"nextRunAt" TIMESTAMPTZ(3),
|
||||
"lastRunAt" TIMESTAMPTZ(3),
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "schedules_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "scheduled_tasks" (
|
||||
"id" TEXT NOT NULL,
|
||||
"scheduleId" TEXT NOT NULL,
|
||||
"taskType" "ScheduleTaskType" NOT NULL,
|
||||
"payload" JSONB,
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "scheduled_tasks_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "notifications" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"type" "NotificationType" NOT NULL,
|
||||
"channel" "NotificationChannel" NOT NULL DEFAULT 'IN_APP',
|
||||
"title" TEXT NOT NULL,
|
||||
"body" TEXT NOT NULL,
|
||||
"metadata" JSONB,
|
||||
"readAt" TIMESTAMPTZ(3),
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "notifications_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "notification_preferences" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"type" "NotificationType" NOT NULL,
|
||||
"channel" "NotificationChannel" NOT NULL,
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "notification_preferences_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "abuse_reports" (
|
||||
"id" TEXT NOT NULL,
|
||||
"reporterId" TEXT,
|
||||
"targetUserId" TEXT,
|
||||
"serverId" TEXT,
|
||||
"status" "AbuseReportStatus" NOT NULL DEFAULT 'OPEN',
|
||||
"reason" TEXT NOT NULL,
|
||||
"details" TEXT,
|
||||
"resolution" TEXT,
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "abuse_reports_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "support_tickets" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"serverId" TEXT,
|
||||
"subject" TEXT NOT NULL,
|
||||
"status" "SupportTicketStatus" NOT NULL DEFAULT 'OPEN',
|
||||
"priority" "SupportTicketPriority" NOT NULL DEFAULT 'NORMAL',
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "support_tickets_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "support_ticket_messages" (
|
||||
"id" TEXT NOT NULL,
|
||||
"ticketId" TEXT NOT NULL,
|
||||
"authorId" TEXT,
|
||||
"isInternal" BOOLEAN NOT NULL DEFAULT false,
|
||||
"body" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "support_ticket_messages_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "subscriptions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"planId" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL,
|
||||
"startsAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
"endsAt" TIMESTAMPTZ(3),
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "subscriptions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "invoices" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"subscriptionId" TEXT,
|
||||
"amountCents" INTEGER NOT NULL,
|
||||
"currency" TEXT NOT NULL DEFAULT 'EUR',
|
||||
"status" TEXT NOT NULL,
|
||||
"externalRef" TEXT,
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "invoices_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "payments" (
|
||||
"id" TEXT NOT NULL,
|
||||
"invoiceId" TEXT NOT NULL,
|
||||
"amountCents" INTEGER NOT NULL,
|
||||
"provider" TEXT NOT NULL,
|
||||
"externalRef" TEXT,
|
||||
"status" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "payments_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "resource_quotas" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT,
|
||||
"planId" TEXT,
|
||||
"key" TEXT NOT NULL,
|
||||
"limit" INTEGER NOT NULL,
|
||||
"used" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "resource_quotas_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "security_change_logs" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"action" TEXT NOT NULL,
|
||||
"metadata" JSONB,
|
||||
"ipAddress" TEXT,
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "security_change_logs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "data_export_requests" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'PENDING',
|
||||
"downloadUrl" TEXT,
|
||||
"expiresAt" TIMESTAMPTZ(3),
|
||||
"completedAt" TIMESTAMPTZ(3),
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "data_export_requests_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "account_deletion_requests" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'PENDING',
|
||||
"scheduledFor" TIMESTAMPTZ(3) NOT NULL,
|
||||
"completedAt" TIMESTAMPTZ(3),
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "account_deletion_requests_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "whmcs_user_links" (
|
||||
"id" TEXT NOT NULL,
|
||||
"installationId" TEXT NOT NULL,
|
||||
"externalUserId" TEXT NOT NULL,
|
||||
"externalClientId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "whmcs_user_links_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "whmcs_product_mappings" (
|
||||
"id" TEXT NOT NULL,
|
||||
"installationId" TEXT NOT NULL,
|
||||
"externalProductId" TEXT NOT NULL,
|
||||
"planSlug" TEXT NOT NULL,
|
||||
"defaultEdition" "ServerEdition" NOT NULL DEFAULT 'JAVA',
|
||||
"defaultSoftware" "SoftwareFamily" NOT NULL DEFAULT 'VANILLA',
|
||||
"defaultVersion" TEXT NOT NULL DEFAULT '1.21.1',
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "whmcs_product_mappings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "whmcs_option_mappings" (
|
||||
"id" TEXT NOT NULL,
|
||||
"installationId" TEXT NOT NULL,
|
||||
"externalOptionId" TEXT NOT NULL,
|
||||
"externalValueId" TEXT NOT NULL,
|
||||
"resourceKey" TEXT NOT NULL,
|
||||
"normalizedValue" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "whmcs_option_mappings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "whmcs_addon_mappings" (
|
||||
"id" TEXT NOT NULL,
|
||||
"installationId" TEXT NOT NULL,
|
||||
"externalAddonId" TEXT NOT NULL,
|
||||
"featureKey" TEXT NOT NULL,
|
||||
"deltaValue" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "whmcs_addon_mappings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "feature_flags" (
|
||||
"id" TEXT NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"metadata" JSONB,
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "feature_flags_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "system_settings" (
|
||||
"id" TEXT NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
"value" JSONB NOT NULL,
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "system_settings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "job_records" (
|
||||
"id" TEXT NOT NULL,
|
||||
"queue" TEXT NOT NULL,
|
||||
"jobName" TEXT NOT NULL,
|
||||
"status" "JobStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"payload" JSONB,
|
||||
"result" JSONB,
|
||||
"error" TEXT,
|
||||
"attempts" INTEGER NOT NULL DEFAULT 0,
|
||||
"startedAt" TIMESTAMPTZ(3),
|
||||
"completedAt" TIMESTAMPTZ(3),
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "job_records_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "audit_events" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT,
|
||||
"action" TEXT NOT NULL,
|
||||
"entityType" TEXT,
|
||||
"entityId" TEXT,
|
||||
"metadata" JSONB,
|
||||
"ipAddress" TEXT,
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "audit_events_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "organizations_slug_key" ON "organizations"("slug");
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "organization_members_organizationId_userId_key" ON "organization_members"("organizationId", "userId");
|
||||
CREATE INDEX IF NOT EXISTS "organization_members_userId_idx" ON "organization_members"("userId");
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "organization_invites_tokenHash_key" ON "organization_invites"("tokenHash");
|
||||
CREATE INDEX IF NOT EXISTS "organization_invites_organizationId_idx" ON "organization_invites"("organizationId");
|
||||
CREATE INDEX IF NOT EXISTS "organization_invites_email_idx" ON "organization_invites"("email");
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "game_server_members_serverId_userId_key" ON "game_server_members"("serverId", "userId");
|
||||
CREATE INDEX IF NOT EXISTS "game_server_members_userId_idx" ON "game_server_members"("userId");
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "game_server_properties_serverId_key_key" ON "game_server_properties"("serverId", "key");
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "game_server_environment_variables_serverId_key_key" ON "game_server_environment_variables"("serverId", "key");
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "game_server_secrets_serverId_name_key" ON "game_server_secrets"("serverId", "name");
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "game_node_capabilities_nodeId_key_key" ON "game_node_capabilities"("nodeId", "key");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "game_node_maintenance_windows_nodeId_startsAt_idx" ON "game_node_maintenance_windows"("nodeId", "startsAt");
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "minecraft_versions_version_key" ON "minecraft_versions"("version");
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "software_builds_family_minecraftVersionId_key" ON "software_builds"("family", "minecraftVersionId");
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "runtime_images_name_key" ON "runtime_images"("name");
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "catalog_providers_kind_key" ON "catalog_providers"("kind");
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "catalog_projects_providerId_externalId_key" ON "catalog_projects"("providerId", "externalId");
|
||||
CREATE INDEX IF NOT EXISTS "catalog_projects_slug_idx" ON "catalog_projects"("slug");
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "catalog_versions_projectId_externalId_key" ON "catalog_versions"("projectId", "externalId");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "catalog_dependencies_versionId_idx" ON "catalog_dependencies"("versionId");
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "worlds_serverId_name_key" ON "worlds"("serverId", "name");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "world_archives_worldId_idx" ON "world_archives"("worldId");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "schedules_serverId_idx" ON "schedules"("serverId");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "scheduled_tasks_scheduleId_idx" ON "scheduled_tasks"("scheduleId");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "notifications_userId_readAt_idx" ON "notifications"("userId", "readAt");
|
||||
CREATE INDEX IF NOT EXISTS "notifications_createdAt_idx" ON "notifications"("createdAt");
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "notification_preferences_userId_type_channel_key" ON "notification_preferences"("userId", "type", "channel");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "abuse_reports_status_idx" ON "abuse_reports"("status");
|
||||
CREATE INDEX IF NOT EXISTS "abuse_reports_serverId_idx" ON "abuse_reports"("serverId");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "support_tickets_userId_idx" ON "support_tickets"("userId");
|
||||
CREATE INDEX IF NOT EXISTS "support_tickets_status_idx" ON "support_tickets"("status");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "support_ticket_messages_ticketId_idx" ON "support_ticket_messages"("ticketId");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "subscriptions_userId_idx" ON "subscriptions"("userId");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "invoices_userId_idx" ON "invoices"("userId");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "payments_invoiceId_idx" ON "payments"("invoiceId");
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "resource_quotas_userId_planId_key_key" ON "resource_quotas"("userId", "planId", "key");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "security_change_logs_userId_createdAt_idx" ON "security_change_logs"("userId", "createdAt");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "data_export_requests_userId_idx" ON "data_export_requests"("userId");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "account_deletion_requests_userId_idx" ON "account_deletion_requests"("userId");
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "whmcs_user_links_installationId_externalUserId_key" ON "whmcs_user_links"("installationId", "externalUserId");
|
||||
CREATE INDEX IF NOT EXISTS "whmcs_user_links_userId_idx" ON "whmcs_user_links"("userId");
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "whmcs_product_mappings_installationId_externalProductId_key" ON "whmcs_product_mappings"("installationId", "externalProductId");
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "whmcs_option_mappings_installationId_externalOptionId_externalValueId_key" ON "whmcs_option_mappings"("installationId", "externalOptionId", "externalValueId");
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "whmcs_addon_mappings_installationId_externalAddonId_key" ON "whmcs_addon_mappings"("installationId", "externalAddonId");
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "feature_flags_key_key" ON "feature_flags"("key");
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "system_settings_key_key" ON "system_settings"("key");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "job_records_queue_status_idx" ON "job_records"("queue", "status");
|
||||
CREATE INDEX IF NOT EXISTS "job_records_createdAt_idx" ON "job_records"("createdAt");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "audit_events_userId_idx" ON "audit_events"("userId");
|
||||
CREATE INDEX IF NOT EXISTS "audit_events_action_idx" ON "audit_events"("action");
|
||||
CREATE INDEX IF NOT EXISTS "audit_events_entityType_entityId_idx" ON "audit_events"("entityType", "entityId");
|
||||
CREATE INDEX IF NOT EXISTS "audit_events_createdAt_idx" ON "audit_events"("createdAt");
|
||||
|
||||
-- AddForeignKey
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "organization_members" ADD CONSTRAINT "organization_members_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "organization_members" ADD CONSTRAINT "organization_members_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "organization_invites" ADD CONSTRAINT "organization_invites_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "organization_invites" ADD CONSTRAINT "organization_invites_invitedById_fkey" FOREIGN KEY ("invitedById") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "game_server_members" ADD CONSTRAINT "game_server_members_serverId_fkey" FOREIGN KEY ("serverId") REFERENCES "game_servers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "game_server_members" ADD CONSTRAINT "game_server_members_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "game_server_properties" ADD CONSTRAINT "game_server_properties_serverId_fkey" FOREIGN KEY ("serverId") REFERENCES "game_servers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "game_server_environment_variables" ADD CONSTRAINT "game_server_environment_variables_serverId_fkey" FOREIGN KEY ("serverId") REFERENCES "game_servers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "game_server_secrets" ADD CONSTRAINT "game_server_secrets_serverId_fkey" FOREIGN KEY ("serverId") REFERENCES "game_servers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "game_node_capabilities" ADD CONSTRAINT "game_node_capabilities_nodeId_fkey" FOREIGN KEY ("nodeId") REFERENCES "game_nodes"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "game_node_maintenance_windows" ADD CONSTRAINT "game_node_maintenance_windows_nodeId_fkey" FOREIGN KEY ("nodeId") REFERENCES "game_nodes"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "software_builds" ADD CONSTRAINT "software_builds_minecraftVersionId_fkey" FOREIGN KEY ("minecraftVersionId") REFERENCES "minecraft_versions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "catalog_projects" ADD CONSTRAINT "catalog_projects_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "catalog_providers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "catalog_versions" ADD CONSTRAINT "catalog_versions_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "catalog_projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "catalog_dependencies" ADD CONSTRAINT "catalog_dependencies_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "catalog_versions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "worlds" ADD CONSTRAINT "worlds_serverId_fkey" FOREIGN KEY ("serverId") REFERENCES "game_servers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "world_archives" ADD CONSTRAINT "world_archives_worldId_fkey" FOREIGN KEY ("worldId") REFERENCES "worlds"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "schedules" ADD CONSTRAINT "schedules_serverId_fkey" FOREIGN KEY ("serverId") REFERENCES "game_servers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "scheduled_tasks" ADD CONSTRAINT "scheduled_tasks_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "schedules"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "notifications" ADD CONSTRAINT "notifications_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "notification_preferences" ADD CONSTRAINT "notification_preferences_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "abuse_reports" ADD CONSTRAINT "abuse_reports_reporterId_fkey" FOREIGN KEY ("reporterId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "support_tickets" ADD CONSTRAINT "support_tickets_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "support_ticket_messages" ADD CONSTRAINT "support_ticket_messages_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "support_tickets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "payments" ADD CONSTRAINT "payments_invoiceId_fkey" FOREIGN KEY ("invoiceId") REFERENCES "invoices"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "security_change_logs" ADD CONSTRAINT "security_change_logs_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "data_export_requests" ADD CONSTRAINT "data_export_requests_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "account_deletion_requests" ADD CONSTRAINT "account_deletion_requests_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "whmcs_user_links" ADD CONSTRAINT "whmcs_user_links_installationId_fkey" FOREIGN KEY ("installationId") REFERENCES "whmcs_installations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "whmcs_user_links" ADD CONSTRAINT "whmcs_user_links_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "whmcs_product_mappings" ADD CONSTRAINT "whmcs_product_mappings_installationId_fkey" FOREIGN KEY ("installationId") REFERENCES "whmcs_installations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "whmcs_option_mappings" ADD CONSTRAINT "whmcs_option_mappings_installationId_fkey" FOREIGN KEY ("installationId") REFERENCES "whmcs_installations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "whmcs_addon_mappings" ADD CONSTRAINT "whmcs_addon_mappings_installationId_fkey" FOREIGN KEY ("installationId") REFERENCES "whmcs_installations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "audit_events" ADD CONSTRAINT "audit_events_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
@@ -31,6 +31,7 @@ enum SoftwareFamily {
|
||||
|
||||
enum GameServerStatus {
|
||||
DRAFT
|
||||
ALLOCATING
|
||||
PROVISIONING
|
||||
INSTALLING
|
||||
STOPPED
|
||||
@@ -40,12 +41,95 @@ enum GameServerStatus {
|
||||
STOPPING
|
||||
BACKING_UP
|
||||
RESTORING
|
||||
MIGRATING
|
||||
SUSPENDED
|
||||
MAINTENANCE
|
||||
UNKNOWN
|
||||
ERROR
|
||||
DELETING
|
||||
DELETED
|
||||
}
|
||||
|
||||
enum GameServerMemberRole {
|
||||
OWNER
|
||||
ADMIN
|
||||
OPERATOR
|
||||
DEVELOPER
|
||||
VIEWER
|
||||
}
|
||||
|
||||
enum OrganizationMemberRole {
|
||||
OWNER
|
||||
ADMIN
|
||||
MEMBER
|
||||
}
|
||||
|
||||
enum NotificationChannel {
|
||||
IN_APP
|
||||
EMAIL
|
||||
WEBHOOK
|
||||
DISCORD
|
||||
}
|
||||
|
||||
enum NotificationType {
|
||||
SERVER_READY
|
||||
SERVER_START_FAILED
|
||||
SERVER_STOPPED
|
||||
IDLE_STOP_WARNING
|
||||
BACKUP_SUCCESS
|
||||
BACKUP_FAILED
|
||||
RESTORE_SUCCESS
|
||||
RESTORE_FAILED
|
||||
CREDITS_LOW
|
||||
BILLING_ISSUE
|
||||
INVITE
|
||||
NODE_INCIDENT
|
||||
MAINTENANCE
|
||||
SECURITY
|
||||
NEW_LOGIN
|
||||
TWO_FA_CHANGED
|
||||
}
|
||||
|
||||
enum AbuseReportStatus {
|
||||
OPEN
|
||||
INVESTIGATING
|
||||
RESOLVED
|
||||
DISMISSED
|
||||
}
|
||||
|
||||
enum SupportTicketStatus {
|
||||
OPEN
|
||||
IN_PROGRESS
|
||||
WAITING
|
||||
RESOLVED
|
||||
CLOSED
|
||||
}
|
||||
|
||||
enum SupportTicketPriority {
|
||||
LOW
|
||||
NORMAL
|
||||
HIGH
|
||||
URGENT
|
||||
}
|
||||
|
||||
enum ScheduleTaskType {
|
||||
START
|
||||
STOP
|
||||
RESTART
|
||||
BACKUP
|
||||
CONSOLE_COMMAND
|
||||
UPDATE_CHECK
|
||||
MAINTENANCE
|
||||
}
|
||||
|
||||
enum CatalogProviderKind {
|
||||
MODRINTH
|
||||
CURSEFORGE
|
||||
MOJANG
|
||||
PAPERMC
|
||||
CUSTOM
|
||||
}
|
||||
|
||||
enum GameNodeStatus {
|
||||
ONLINE
|
||||
OFFLINE
|
||||
@@ -194,7 +278,18 @@ model User {
|
||||
totpCredential TotpCredential?
|
||||
creditWallet CreditWallet?
|
||||
whmcsClientLink WhmcsClientLink?
|
||||
whmcsUserLinks WhmcsUserLink[]
|
||||
ssoTickets SsoTicket[]
|
||||
organizationMembers OrganizationMember[]
|
||||
organizationInvites OrganizationInvite[] @relation("OrganizationInviteInviter")
|
||||
gameServerMembers GameServerMember[]
|
||||
notifications Notification[]
|
||||
notificationPreferences NotificationPreference[]
|
||||
abuseReports AbuseReport[]
|
||||
supportTickets SupportTicket[]
|
||||
securityChangeLogs SecurityChangeLog[]
|
||||
dataExportRequests DataExportRequest[]
|
||||
accountDeletionRequests AccountDeletionRequest[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
@@ -361,6 +456,12 @@ model GameServer {
|
||||
dnsRecords ServerDnsRecord[]
|
||||
whmcsServiceLink WhmcsServiceLink?
|
||||
ssoTickets SsoTicket[]
|
||||
members GameServerMember[]
|
||||
worlds World[]
|
||||
schedules Schedule[]
|
||||
properties GameServerProperty[]
|
||||
environmentVars GameServerEnvironmentVariable[]
|
||||
secrets GameServerSecret[]
|
||||
|
||||
@@index([userId])
|
||||
@@index([nodeId])
|
||||
@@ -430,6 +531,8 @@ model GameNode {
|
||||
heartbeats GameNodeHeartbeat[]
|
||||
allocations GameServerAllocation[]
|
||||
startQueueEntries ServerStartQueueEntry[]
|
||||
capabilities GameNodeCapability[]
|
||||
maintenanceWindows GameNodeMaintenanceWindow[]
|
||||
|
||||
@@map("game_nodes")
|
||||
}
|
||||
@@ -743,7 +846,11 @@ model WhmcsInstallation {
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
clientLinks WhmcsClientLink[]
|
||||
userLinks WhmcsUserLink[]
|
||||
serviceLinks WhmcsServiceLink[]
|
||||
productMappings WhmcsProductMapping[]
|
||||
optionMappings WhmcsOptionMapping[]
|
||||
addonMappings WhmcsAddonMapping[]
|
||||
operations IntegrationOperation[]
|
||||
ssoTickets SsoTicket[]
|
||||
events IntegrationEvent[]
|
||||
@@ -907,3 +1014,562 @@ model WhmcsUsageExport {
|
||||
@@index([installationId, periodEnd])
|
||||
@@map("whmcs_usage_exports")
|
||||
}
|
||||
|
||||
model Organization {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
slug String @unique
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
members OrganizationMember[]
|
||||
invites OrganizationInvite[]
|
||||
|
||||
@@map("organizations")
|
||||
}
|
||||
|
||||
model OrganizationMember {
|
||||
id String @id @default(uuid())
|
||||
organizationId String
|
||||
userId String
|
||||
role OrganizationMemberRole @default(MEMBER)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([organizationId, userId])
|
||||
@@index([userId])
|
||||
@@map("organization_members")
|
||||
}
|
||||
|
||||
model OrganizationInvite {
|
||||
id String @id @default(uuid())
|
||||
organizationId String
|
||||
email String
|
||||
role OrganizationMemberRole @default(MEMBER)
|
||||
tokenHash String @unique
|
||||
invitedById String
|
||||
expiresAt DateTime @db.Timestamptz(3)
|
||||
acceptedAt DateTime? @db.Timestamptz(3)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
invitedBy User @relation("OrganizationInviteInviter", fields: [invitedById], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([organizationId])
|
||||
@@index([email])
|
||||
@@map("organization_invites")
|
||||
}
|
||||
|
||||
model GameServerMember {
|
||||
id String @id @default(uuid())
|
||||
serverId String
|
||||
userId String
|
||||
role GameServerMemberRole @default(VIEWER)
|
||||
permissions Json?
|
||||
invitedAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
acceptedAt DateTime? @db.Timestamptz(3)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
server GameServer @relation(fields: [serverId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([serverId, userId])
|
||||
@@index([userId])
|
||||
@@map("game_server_members")
|
||||
}
|
||||
|
||||
model GameServerProperty {
|
||||
id String @id @default(uuid())
|
||||
serverId String
|
||||
key String
|
||||
value String
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
server GameServer @relation(fields: [serverId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([serverId, key])
|
||||
@@map("game_server_properties")
|
||||
}
|
||||
|
||||
model GameServerEnvironmentVariable {
|
||||
id String @id @default(uuid())
|
||||
serverId String
|
||||
key String
|
||||
value String
|
||||
isSecret Boolean @default(false)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
server GameServer @relation(fields: [serverId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([serverId, key])
|
||||
@@map("game_server_environment_variables")
|
||||
}
|
||||
|
||||
model GameServerSecret {
|
||||
id String @id @default(uuid())
|
||||
serverId String
|
||||
name String
|
||||
valueCiphertext String
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
server GameServer @relation(fields: [serverId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([serverId, name])
|
||||
@@map("game_server_secrets")
|
||||
}
|
||||
|
||||
model GameNodeCapability {
|
||||
id String @id @default(uuid())
|
||||
nodeId String
|
||||
key String
|
||||
value String?
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
node GameNode @relation(fields: [nodeId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([nodeId, key])
|
||||
@@map("game_node_capabilities")
|
||||
}
|
||||
|
||||
model GameNodeMaintenanceWindow {
|
||||
id String @id @default(uuid())
|
||||
nodeId String
|
||||
startsAt DateTime @db.Timestamptz(3)
|
||||
endsAt DateTime @db.Timestamptz(3)
|
||||
drainFirst Boolean @default(true)
|
||||
message String?
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
node GameNode @relation(fields: [nodeId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([nodeId, startsAt])
|
||||
@@map("game_node_maintenance_windows")
|
||||
}
|
||||
|
||||
model MinecraftVersion {
|
||||
id String @id @default(uuid())
|
||||
version String @unique
|
||||
edition ServerEdition @default(JAVA)
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
softwareBuilds SoftwareBuild[]
|
||||
|
||||
@@map("minecraft_versions")
|
||||
}
|
||||
|
||||
model SoftwareBuild {
|
||||
id String @id @default(uuid())
|
||||
family SoftwareFamily
|
||||
minecraftVersionId String
|
||||
downloadUrl String?
|
||||
sha256 String?
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
minecraftVersion MinecraftVersion @relation(fields: [minecraftVersionId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([family, minecraftVersionId])
|
||||
@@map("software_builds")
|
||||
}
|
||||
|
||||
model RuntimeImage {
|
||||
id String @id @default(uuid())
|
||||
name String @unique
|
||||
imageRef String
|
||||
digest String?
|
||||
javaVersion String?
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
@@map("runtime_images")
|
||||
}
|
||||
|
||||
model CatalogProvider {
|
||||
id String @id @default(uuid())
|
||||
kind CatalogProviderKind @unique
|
||||
name String
|
||||
isEnabled Boolean @default(false)
|
||||
config Json?
|
||||
lastSyncAt DateTime? @db.Timestamptz(3)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
projects CatalogProject[]
|
||||
|
||||
@@map("catalog_providers")
|
||||
}
|
||||
|
||||
model CatalogProject {
|
||||
id String @id @default(uuid())
|
||||
providerId String
|
||||
externalId String
|
||||
slug String
|
||||
name String
|
||||
projectType String
|
||||
metadata Json?
|
||||
syncedAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
provider CatalogProvider @relation(fields: [providerId], references: [id], onDelete: Cascade)
|
||||
versions CatalogVersion[]
|
||||
|
||||
@@unique([providerId, externalId])
|
||||
@@index([slug])
|
||||
@@map("catalog_projects")
|
||||
}
|
||||
|
||||
model CatalogVersion {
|
||||
id String @id @default(uuid())
|
||||
projectId String
|
||||
externalId String
|
||||
versionNumber String
|
||||
gameVersions Json?
|
||||
loaders Json?
|
||||
sha512 String?
|
||||
downloadUrl String?
|
||||
syncedAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
project CatalogProject @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
dependencies CatalogDependency[] @relation("VersionDependencies")
|
||||
|
||||
@@unique([projectId, externalId])
|
||||
@@map("catalog_versions")
|
||||
}
|
||||
|
||||
model CatalogDependency {
|
||||
id String @id @default(uuid())
|
||||
versionId String
|
||||
dependencyId String
|
||||
dependencyType String
|
||||
|
||||
version CatalogVersion @relation("VersionDependencies", fields: [versionId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([versionId])
|
||||
@@map("catalog_dependencies")
|
||||
}
|
||||
|
||||
model World {
|
||||
id String @id @default(uuid())
|
||||
serverId String
|
||||
name String
|
||||
isActive Boolean @default(false)
|
||||
sizeBytes BigInt?
|
||||
hasLevelDat Boolean @default(false)
|
||||
seed String?
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
server GameServer @relation(fields: [serverId], references: [id], onDelete: Cascade)
|
||||
archives WorldArchive[]
|
||||
|
||||
@@unique([serverId, name])
|
||||
@@map("worlds")
|
||||
}
|
||||
|
||||
model WorldArchive {
|
||||
id String @id @default(uuid())
|
||||
worldId String
|
||||
s3Key String
|
||||
sha256 String?
|
||||
sizeBytes BigInt?
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
world World @relation(fields: [worldId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([worldId])
|
||||
@@map("world_archives")
|
||||
}
|
||||
|
||||
model Schedule {
|
||||
id String @id @default(uuid())
|
||||
serverId String
|
||||
name String
|
||||
timezone String @default("UTC")
|
||||
cronExpr String
|
||||
enabled Boolean @default(true)
|
||||
nextRunAt DateTime? @db.Timestamptz(3)
|
||||
lastRunAt DateTime? @db.Timestamptz(3)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
server GameServer @relation(fields: [serverId], references: [id], onDelete: Cascade)
|
||||
tasks ScheduledTask[]
|
||||
|
||||
@@index([serverId])
|
||||
@@map("schedules")
|
||||
}
|
||||
|
||||
model ScheduledTask {
|
||||
id String @id @default(uuid())
|
||||
scheduleId String
|
||||
taskType ScheduleTaskType
|
||||
payload Json?
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
schedule Schedule @relation(fields: [scheduleId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([scheduleId])
|
||||
@@map("scheduled_tasks")
|
||||
}
|
||||
|
||||
model Notification {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
type NotificationType
|
||||
channel NotificationChannel @default(IN_APP)
|
||||
title String
|
||||
body String
|
||||
metadata Json?
|
||||
readAt DateTime? @db.Timestamptz(3)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId, readAt])
|
||||
@@index([createdAt])
|
||||
@@map("notifications")
|
||||
}
|
||||
|
||||
model NotificationPreference {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
type NotificationType
|
||||
channel NotificationChannel
|
||||
enabled Boolean @default(true)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([userId, type, channel])
|
||||
@@map("notification_preferences")
|
||||
}
|
||||
|
||||
model AbuseReport {
|
||||
id String @id @default(uuid())
|
||||
reporterId String?
|
||||
targetUserId String?
|
||||
serverId String?
|
||||
status AbuseReportStatus @default(OPEN)
|
||||
reason String
|
||||
details String?
|
||||
resolution String?
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
reporter User? @relation(fields: [reporterId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([status])
|
||||
@@index([serverId])
|
||||
@@map("abuse_reports")
|
||||
}
|
||||
|
||||
model SupportTicket {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
serverId String?
|
||||
subject String
|
||||
status SupportTicketStatus @default(OPEN)
|
||||
priority SupportTicketPriority @default(NORMAL)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
messages SupportTicketMessage[]
|
||||
|
||||
@@index([userId])
|
||||
@@index([status])
|
||||
@@map("support_tickets")
|
||||
}
|
||||
|
||||
model SupportTicketMessage {
|
||||
id String @id @default(uuid())
|
||||
ticketId String
|
||||
authorId String?
|
||||
isInternal Boolean @default(false)
|
||||
body String
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
ticket SupportTicket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([ticketId])
|
||||
@@map("support_ticket_messages")
|
||||
}
|
||||
|
||||
model Subscription {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
planId String
|
||||
status String
|
||||
startsAt DateTime @db.Timestamptz(3)
|
||||
endsAt DateTime? @db.Timestamptz(3)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
@@index([userId])
|
||||
@@map("subscriptions")
|
||||
}
|
||||
|
||||
model Invoice {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
subscriptionId String?
|
||||
amountCents Int
|
||||
currency String @default("EUR")
|
||||
status String
|
||||
externalRef String?
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
payments Payment[]
|
||||
|
||||
@@index([userId])
|
||||
@@map("invoices")
|
||||
}
|
||||
|
||||
model Payment {
|
||||
id String @id @default(uuid())
|
||||
invoiceId String
|
||||
amountCents Int
|
||||
provider String
|
||||
externalRef String?
|
||||
status String
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
invoice Invoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([invoiceId])
|
||||
@@map("payments")
|
||||
}
|
||||
|
||||
model ResourceQuota {
|
||||
id String @id @default(uuid())
|
||||
userId String?
|
||||
planId String?
|
||||
key String
|
||||
limit Int
|
||||
used Int @default(0)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
@@unique([userId, planId, key])
|
||||
@@map("resource_quotas")
|
||||
}
|
||||
|
||||
model SecurityChangeLog {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
action String
|
||||
metadata Json?
|
||||
ipAddress String?
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId, createdAt])
|
||||
@@map("security_change_logs")
|
||||
}
|
||||
|
||||
model DataExportRequest {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
status String @default("PENDING")
|
||||
downloadUrl String?
|
||||
expiresAt DateTime? @db.Timestamptz(3)
|
||||
completedAt DateTime? @db.Timestamptz(3)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@map("data_export_requests")
|
||||
}
|
||||
|
||||
model AccountDeletionRequest {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
status String @default("PENDING")
|
||||
scheduledFor DateTime @db.Timestamptz(3)
|
||||
completedAt DateTime? @db.Timestamptz(3)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@map("account_deletion_requests")
|
||||
}
|
||||
|
||||
model WhmcsUserLink {
|
||||
id String @id @default(uuid())
|
||||
installationId String
|
||||
externalUserId String
|
||||
externalClientId String
|
||||
userId String
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
installation WhmcsInstallation @relation(fields: [installationId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([installationId, externalUserId])
|
||||
@@index([userId])
|
||||
@@map("whmcs_user_links")
|
||||
}
|
||||
|
||||
model WhmcsProductMapping {
|
||||
id String @id @default(uuid())
|
||||
installationId String
|
||||
externalProductId String
|
||||
planSlug String
|
||||
defaultEdition ServerEdition @default(JAVA)
|
||||
defaultSoftware SoftwareFamily @default(VANILLA)
|
||||
defaultVersion String @default("1.21.1")
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
installation WhmcsInstallation @relation(fields: [installationId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([installationId, externalProductId])
|
||||
@@map("whmcs_product_mappings")
|
||||
}
|
||||
|
||||
model WhmcsOptionMapping {
|
||||
id String @id @default(uuid())
|
||||
installationId String
|
||||
externalOptionId String
|
||||
externalValueId String
|
||||
resourceKey String
|
||||
normalizedValue String
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
installation WhmcsInstallation @relation(fields: [installationId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([installationId, externalOptionId, externalValueId])
|
||||
@@map("whmcs_option_mappings")
|
||||
}
|
||||
|
||||
model WhmcsAddonMapping {
|
||||
id String @id @default(uuid())
|
||||
installationId String
|
||||
externalAddonId String
|
||||
featureKey String
|
||||
deltaValue String
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
installation WhmcsInstallation @relation(fields: [installationId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([installationId, externalAddonId])
|
||||
@@map("whmcs_addon_mappings")
|
||||
}
|
||||
|
||||
@@ -38,6 +38,19 @@ export type {
|
||||
SystemSetting,
|
||||
AuditEvent,
|
||||
JobRecord,
|
||||
Schedule,
|
||||
ScheduledTask,
|
||||
Notification,
|
||||
NotificationPreference,
|
||||
DataExportRequest,
|
||||
AccountDeletionRequest,
|
||||
Organization,
|
||||
OrganizationMember,
|
||||
OrganizationMemberRole,
|
||||
OrganizationInvite,
|
||||
SupportTicket,
|
||||
SupportTicketMessage,
|
||||
AbuseReport,
|
||||
} from '@prisma/client';
|
||||
|
||||
import { PrismaClient, Prisma } from '@prisma/client';
|
||||
|
||||
17
packages/email-templates/package.json
Normal file
17
packages/email-templates/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@hexahost/email-templates",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
102
packages/email-templates/src/index.ts
Normal file
102
packages/email-templates/src/index.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
export type EmailLocale = 'de' | 'en';
|
||||
|
||||
export interface EmailTemplateInput {
|
||||
appName: string;
|
||||
locale?: EmailLocale;
|
||||
}
|
||||
|
||||
export interface LinkEmailInput extends EmailTemplateInput {
|
||||
url: string;
|
||||
}
|
||||
|
||||
const copy = {
|
||||
de: {
|
||||
verifySubject: (app: string) => `E-Mail bestätigen – ${app}`,
|
||||
verifyHtml: (app: string, url: string) =>
|
||||
`<p>Willkommen bei ${app}!</p><p><a href="${url}">E-Mail-Adresse bestätigen</a></p><p>Der Link ist 24 Stunden gültig.</p>`,
|
||||
resetSubject: (app: string) => `Passwort zurücksetzen – ${app}`,
|
||||
resetHtml: (app: string, url: string) =>
|
||||
`<p>Du hast ein Passwort-Reset für ${app} angefordert.</p><p><a href="${url}">Neues Passwort setzen</a></p><p>Falls du das nicht warst, ignoriere diese E-Mail.</p>`,
|
||||
serverReadySubject: (app: string, name: string) => `Server bereit – ${name} – ${app}`,
|
||||
serverReadyHtml: (app: string, name: string) =>
|
||||
`<p>Dein Server <strong>${name}</strong> ist bereit.</p><p>Öffne das Panel in ${app}, um zu starten.</p>`,
|
||||
backupSuccessSubject: (app: string, name: string) => `Backup erfolgreich – ${name}`,
|
||||
backupSuccessHtml: (app: string, name: string) =>
|
||||
`<p>Das Backup für <strong>${name}</strong> wurde erfolgreich erstellt.</p>`,
|
||||
backupFailedSubject: (app: string, name: string) => `Backup fehlgeschlagen – ${name}`,
|
||||
backupFailedHtml: (app: string, name: string) =>
|
||||
`<p>Das Backup für <strong>${name}</strong> ist fehlgeschlagen. Bitte prüfe das Panel in ${app}.</p>`,
|
||||
},
|
||||
en: {
|
||||
verifySubject: (app: string) => `Verify your email – ${app}`,
|
||||
verifyHtml: (app: string, url: string) =>
|
||||
`<p>Welcome to ${app}!</p><p><a href="${url}">Verify your email address</a></p><p>This link expires in 24 hours.</p>`,
|
||||
resetSubject: (app: string) => `Reset your password – ${app}`,
|
||||
resetHtml: (app: string, url: string) =>
|
||||
`<p>You requested a password reset for ${app}.</p><p><a href="${url}">Set a new password</a></p><p>If you did not request this, ignore this email.</p>`,
|
||||
serverReadySubject: (app: string, name: string) => `Server ready – ${name} – ${app}`,
|
||||
serverReadyHtml: (app: string, name: string) =>
|
||||
`<p>Your server <strong>${name}</strong> is ready.</p><p>Open the panel in ${app} to get started.</p>`,
|
||||
backupSuccessSubject: (app: string, name: string) => `Backup successful – ${name}`,
|
||||
backupSuccessHtml: (app: string, name: string) =>
|
||||
`<p>The backup for <strong>${name}</strong> completed successfully.</p>`,
|
||||
backupFailedSubject: (app: string, name: string) => `Backup failed – ${name}`,
|
||||
backupFailedHtml: (app: string, name: string) =>
|
||||
`<p>The backup for <strong>${name}</strong> failed. Please check the panel in ${app}.</p>`,
|
||||
},
|
||||
} as const;
|
||||
|
||||
function localeOf(input: EmailTemplateInput): EmailLocale {
|
||||
return input.locale === 'de' ? 'de' : 'en';
|
||||
}
|
||||
|
||||
export function renderVerifyEmail(input: LinkEmailInput): { subject: string; html: string } {
|
||||
const locale = localeOf(input);
|
||||
const t = copy[locale];
|
||||
return {
|
||||
subject: t.verifySubject(input.appName),
|
||||
html: t.verifyHtml(input.appName, input.url),
|
||||
};
|
||||
}
|
||||
|
||||
export function renderResetEmail(input: LinkEmailInput): { subject: string; html: string } {
|
||||
const locale = localeOf(input);
|
||||
const t = copy[locale];
|
||||
return {
|
||||
subject: t.resetSubject(input.appName),
|
||||
html: t.resetHtml(input.appName, input.url),
|
||||
};
|
||||
}
|
||||
|
||||
export function renderServerReadyEmail(
|
||||
input: EmailTemplateInput & { serverName: string },
|
||||
): { subject: string; html: string } {
|
||||
const locale = localeOf(input);
|
||||
const t = copy[locale];
|
||||
return {
|
||||
subject: t.serverReadySubject(input.appName, input.serverName),
|
||||
html: t.serverReadyHtml(input.appName, input.serverName),
|
||||
};
|
||||
}
|
||||
|
||||
export function renderBackupSuccessEmail(
|
||||
input: EmailTemplateInput & { serverName: string },
|
||||
): { subject: string; html: string } {
|
||||
const locale = localeOf(input);
|
||||
const t = copy[locale];
|
||||
return {
|
||||
subject: t.backupSuccessSubject(input.appName, input.serverName),
|
||||
html: t.backupSuccessHtml(input.appName, input.serverName),
|
||||
};
|
||||
}
|
||||
|
||||
export function renderBackupFailedEmail(
|
||||
input: EmailTemplateInput & { serverName: string },
|
||||
): { subject: string; html: string } {
|
||||
const locale = localeOf(input);
|
||||
const t = copy[locale];
|
||||
return {
|
||||
subject: t.backupFailedSubject(input.appName, input.serverName),
|
||||
html: t.backupFailedHtml(input.appName, input.serverName),
|
||||
};
|
||||
}
|
||||
8
packages/email-templates/tsconfig.json
Normal file
8
packages/email-templates/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
22
packages/feature-flags/package.json
Normal file
22
packages/feature-flags/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@hexahost/feature-flags",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"lint": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hexahost/database": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^3.0.5"
|
||||
}
|
||||
}
|
||||
57
packages/feature-flags/src/index.ts
Normal file
57
packages/feature-flags/src/index.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { PrismaClient } from '@hexahost/database';
|
||||
|
||||
export interface FeatureFlagSnapshot {
|
||||
key: string;
|
||||
enabled: boolean;
|
||||
metadata: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export class FeatureFlagService {
|
||||
private cache = new Map<string, FeatureFlagSnapshot>();
|
||||
private loadedAt = 0;
|
||||
private readonly ttlMs: number;
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaClient,
|
||||
ttlMs = 30_000,
|
||||
) {
|
||||
this.ttlMs = ttlMs;
|
||||
}
|
||||
|
||||
async refresh(): Promise<void> {
|
||||
const flags = await this.prisma.featureFlag.findMany();
|
||||
this.cache.clear();
|
||||
for (const flag of flags) {
|
||||
this.cache.set(flag.key, {
|
||||
key: flag.key,
|
||||
enabled: flag.enabled,
|
||||
metadata:
|
||||
flag.metadata && typeof flag.metadata === 'object' && !Array.isArray(flag.metadata)
|
||||
? (flag.metadata as Record<string, unknown>)
|
||||
: null,
|
||||
});
|
||||
}
|
||||
this.loadedAt = Date.now();
|
||||
}
|
||||
|
||||
private async ensureLoaded(): Promise<void> {
|
||||
if (Date.now() - this.loadedAt > this.ttlMs || this.cache.size === 0) {
|
||||
await this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
async isEnabled(key: string, defaultValue = false): Promise<boolean> {
|
||||
await this.ensureLoaded();
|
||||
return this.cache.get(key)?.enabled ?? defaultValue;
|
||||
}
|
||||
|
||||
async getFlag(key: string): Promise<FeatureFlagSnapshot | null> {
|
||||
await this.ensureLoaded();
|
||||
return this.cache.get(key) ?? null;
|
||||
}
|
||||
|
||||
async listFlags(): Promise<FeatureFlagSnapshot[]> {
|
||||
await this.ensureLoaded();
|
||||
return [...this.cache.values()];
|
||||
}
|
||||
}
|
||||
8
packages/feature-flags/tsconfig.json
Normal file
8
packages/feature-flags/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
24
packages/otel/package.json
Normal file
24
packages/otel/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@hexahost/otel",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.57.1",
|
||||
"@opentelemetry/resources": "^1.30.1",
|
||||
"@opentelemetry/sdk-node": "^0.57.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.30.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
37
packages/otel/src/index.ts
Normal file
37
packages/otel/src/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { diag, DiagConsoleLogger, DiagLogLevel } from '@opentelemetry/api';
|
||||
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
||||
import { Resource } from '@opentelemetry/resources';
|
||||
import { NodeSDK } from '@opentelemetry/sdk-node';
|
||||
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
|
||||
|
||||
let sdk: NodeSDK | null = null;
|
||||
|
||||
export function initOpenTelemetry(serviceName: string): NodeSDK | null {
|
||||
const endpoint = process.env['OTEL_EXPORTER_OTLP_ENDPOINT'];
|
||||
if (!endpoint) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (process.env['OTEL_LOG_LEVEL'] === 'debug') {
|
||||
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);
|
||||
}
|
||||
|
||||
sdk = new NodeSDK({
|
||||
resource: new Resource({
|
||||
[ATTR_SERVICE_NAME]: serviceName,
|
||||
}),
|
||||
traceExporter: new OTLPTraceExporter({
|
||||
url: endpoint.endsWith('/v1/traces') ? endpoint : `${endpoint.replace(/\/$/, '')}/v1/traces`,
|
||||
}),
|
||||
});
|
||||
|
||||
sdk.start();
|
||||
return sdk;
|
||||
}
|
||||
|
||||
export async function shutdownOpenTelemetry(): Promise<void> {
|
||||
if (sdk) {
|
||||
await sdk.shutdown();
|
||||
sdk = null;
|
||||
}
|
||||
}
|
||||
8
packages/otel/tsconfig.json
Normal file
8
packages/otel/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
17
packages/permissions/package.json
Normal file
17
packages/permissions/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@hexahost/permissions",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hexahost/typescript-config": "workspace:*",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^3.0.5"
|
||||
}
|
||||
}
|
||||
134
packages/permissions/src/index.ts
Normal file
134
packages/permissions/src/index.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
export type ServerPermission =
|
||||
| 'server.view'
|
||||
| 'server.start'
|
||||
| 'server.stop'
|
||||
| 'server.restart'
|
||||
| 'server.kill'
|
||||
| 'server.console.view'
|
||||
| 'server.console.command'
|
||||
| 'server.files.view'
|
||||
| 'server.files.edit'
|
||||
| 'server.files.upload'
|
||||
| 'server.files.download'
|
||||
| 'server.files.delete'
|
||||
| 'server.software.change'
|
||||
| 'server.addons.manage'
|
||||
| 'server.worlds.manage'
|
||||
| 'server.players.manage'
|
||||
| 'server.backups.view'
|
||||
| 'server.backups.create'
|
||||
| 'server.backups.restore'
|
||||
| 'server.backups.delete'
|
||||
| 'server.schedules.manage'
|
||||
| 'server.members.manage'
|
||||
| 'server.billing.view'
|
||||
| 'server.delete';
|
||||
|
||||
export type ServerMemberRole =
|
||||
| 'OWNER'
|
||||
| 'ADMIN'
|
||||
| 'OPERATOR'
|
||||
| 'DEVELOPER'
|
||||
| 'VIEWER';
|
||||
|
||||
const ROLE_PERMISSIONS: Record<ServerMemberRole, ServerPermission[]> = {
|
||||
OWNER: [
|
||||
'server.view',
|
||||
'server.start',
|
||||
'server.stop',
|
||||
'server.restart',
|
||||
'server.kill',
|
||||
'server.console.view',
|
||||
'server.console.command',
|
||||
'server.files.view',
|
||||
'server.files.edit',
|
||||
'server.files.upload',
|
||||
'server.files.download',
|
||||
'server.files.delete',
|
||||
'server.software.change',
|
||||
'server.addons.manage',
|
||||
'server.worlds.manage',
|
||||
'server.players.manage',
|
||||
'server.backups.view',
|
||||
'server.backups.create',
|
||||
'server.backups.restore',
|
||||
'server.backups.delete',
|
||||
'server.schedules.manage',
|
||||
'server.members.manage',
|
||||
'server.billing.view',
|
||||
'server.delete',
|
||||
],
|
||||
ADMIN: [
|
||||
'server.view',
|
||||
'server.start',
|
||||
'server.stop',
|
||||
'server.restart',
|
||||
'server.kill',
|
||||
'server.console.view',
|
||||
'server.console.command',
|
||||
'server.files.view',
|
||||
'server.files.edit',
|
||||
'server.files.upload',
|
||||
'server.files.download',
|
||||
'server.files.delete',
|
||||
'server.software.change',
|
||||
'server.addons.manage',
|
||||
'server.worlds.manage',
|
||||
'server.players.manage',
|
||||
'server.backups.view',
|
||||
'server.backups.create',
|
||||
'server.backups.restore',
|
||||
'server.members.manage',
|
||||
'server.billing.view',
|
||||
],
|
||||
OPERATOR: [
|
||||
'server.view',
|
||||
'server.start',
|
||||
'server.stop',
|
||||
'server.restart',
|
||||
'server.console.view',
|
||||
'server.console.command',
|
||||
'server.players.manage',
|
||||
'server.backups.view',
|
||||
],
|
||||
DEVELOPER: [
|
||||
'server.view',
|
||||
'server.console.view',
|
||||
'server.console.command',
|
||||
'server.files.view',
|
||||
'server.files.edit',
|
||||
'server.files.upload',
|
||||
'server.files.download',
|
||||
'server.addons.manage',
|
||||
'server.worlds.manage',
|
||||
'server.backups.view',
|
||||
'server.backups.create',
|
||||
],
|
||||
VIEWER: ['server.view', 'server.console.view', 'server.files.view', 'server.backups.view'],
|
||||
};
|
||||
|
||||
export function permissionsForRole(role: ServerMemberRole): ServerPermission[] {
|
||||
return ROLE_PERMISSIONS[role];
|
||||
}
|
||||
|
||||
export function hasPermission(
|
||||
role: ServerMemberRole,
|
||||
permission: ServerPermission,
|
||||
overrides?: ServerPermission[] | null,
|
||||
): boolean {
|
||||
if (overrides?.includes(permission)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return ROLE_PERMISSIONS[role].includes(permission);
|
||||
}
|
||||
|
||||
export function assertPermission(
|
||||
role: ServerMemberRole,
|
||||
permission: ServerPermission,
|
||||
overrides?: ServerPermission[] | null,
|
||||
): void {
|
||||
if (!hasPermission(role, permission, overrides)) {
|
||||
throw new Error(`Missing permission: ${permission}`);
|
||||
}
|
||||
}
|
||||
19
packages/permissions/src/permissions.test.ts
Normal file
19
packages/permissions/src/permissions.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { hasPermission, permissionsForRole } from './index';
|
||||
|
||||
describe('permissions', () => {
|
||||
it('grants owner full access', () => {
|
||||
expect(hasPermission('OWNER', 'server.delete')).toBe(true);
|
||||
expect(permissionsForRole('OWNER')).toContain('server.kill');
|
||||
});
|
||||
|
||||
it('restricts viewer to read-only', () => {
|
||||
expect(hasPermission('VIEWER', 'server.start')).toBe(false);
|
||||
expect(hasPermission('VIEWER', 'server.view')).toBe(true);
|
||||
});
|
||||
|
||||
it('honors explicit overrides', () => {
|
||||
expect(hasPermission('VIEWER', 'server.start', ['server.start'])).toBe(true);
|
||||
});
|
||||
});
|
||||
8
packages/permissions/tsconfig.json
Normal file
8
packages/permissions/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@hexahost/typescript-config/library.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
17
packages/server-state/package.json
Normal file
17
packages/server-state/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@hexahost/server-state",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hexahost/typescript-config": "workspace:*",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^3.0.5"
|
||||
}
|
||||
}
|
||||
107
packages/server-state/src/index.ts
Normal file
107
packages/server-state/src/index.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
export type GameServerStatus =
|
||||
| 'DRAFT'
|
||||
| 'ALLOCATING'
|
||||
| 'PROVISIONING'
|
||||
| 'INSTALLING'
|
||||
| 'STOPPED'
|
||||
| 'QUEUED'
|
||||
| 'STARTING'
|
||||
| 'RUNNING'
|
||||
| 'STOPPING'
|
||||
| 'BACKING_UP'
|
||||
| 'RESTORING'
|
||||
| 'MIGRATING'
|
||||
| 'SUSPENDED'
|
||||
| 'MAINTENANCE'
|
||||
| 'UNKNOWN'
|
||||
| 'ERROR'
|
||||
| 'DELETING'
|
||||
| 'DELETED';
|
||||
|
||||
export const ALLOWED_TRANSITIONS: Record<GameServerStatus, GameServerStatus[]> = {
|
||||
DRAFT: ['ALLOCATING', 'PROVISIONING', 'DELETING'],
|
||||
ALLOCATING: ['PROVISIONING', 'ERROR', 'DELETING'],
|
||||
PROVISIONING: ['INSTALLING', 'ERROR', 'DELETING'],
|
||||
INSTALLING: ['STOPPED', 'ERROR', 'DELETING'],
|
||||
STOPPED: [
|
||||
'STARTING',
|
||||
'QUEUED',
|
||||
'PROVISIONING',
|
||||
'BACKING_UP',
|
||||
'RESTORING',
|
||||
'MIGRATING',
|
||||
'SUSPENDED',
|
||||
'MAINTENANCE',
|
||||
'DELETING',
|
||||
],
|
||||
QUEUED: ['STARTING', 'STOPPED', 'DELETING'],
|
||||
STARTING: ['RUNNING', 'STOPPED', 'ERROR', 'UNKNOWN', 'DELETING'],
|
||||
RUNNING: ['STOPPING', 'BACKING_UP', 'ERROR', 'UNKNOWN', 'DELETING'],
|
||||
STOPPING: ['STOPPED', 'ERROR', 'UNKNOWN', 'DELETING'],
|
||||
BACKING_UP: ['RUNNING', 'STOPPED', 'ERROR'],
|
||||
RESTORING: ['STOPPED', 'ERROR'],
|
||||
MIGRATING: ['STOPPED', 'ERROR'],
|
||||
SUSPENDED: ['STOPPED', 'DELETING'],
|
||||
MAINTENANCE: ['STOPPED', 'DELETING'],
|
||||
UNKNOWN: ['STOPPED', 'STARTING', 'ERROR', 'DELETING'],
|
||||
ERROR: ['STOPPED', 'STARTING', 'QUEUED', 'PROVISIONING', 'DELETING'],
|
||||
DELETING: ['DELETED'],
|
||||
DELETED: [],
|
||||
};
|
||||
|
||||
export const IN_PROGRESS_STATUSES: GameServerStatus[] = [
|
||||
'ALLOCATING',
|
||||
'PROVISIONING',
|
||||
'INSTALLING',
|
||||
'STARTING',
|
||||
'STOPPING',
|
||||
'BACKING_UP',
|
||||
'RESTORING',
|
||||
'MIGRATING',
|
||||
'DELETING',
|
||||
];
|
||||
|
||||
export function assertTransition(
|
||||
fromStatus: GameServerStatus,
|
||||
toStatus: GameServerStatus,
|
||||
): void {
|
||||
const allowed = ALLOWED_TRANSITIONS[fromStatus];
|
||||
|
||||
if (!allowed.includes(toStatus)) {
|
||||
throw new Error(`Invalid state transition from ${fromStatus} to ${toStatus}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function isInProgress(status: GameServerStatus): boolean {
|
||||
return IN_PROGRESS_STATUSES.includes(status);
|
||||
}
|
||||
|
||||
export function resolveStartTarget(status: GameServerStatus): GameServerStatus {
|
||||
switch (status) {
|
||||
case 'DRAFT':
|
||||
case 'ERROR':
|
||||
return 'PROVISIONING';
|
||||
case 'STOPPED':
|
||||
case 'UNKNOWN':
|
||||
case 'QUEUED':
|
||||
return 'STARTING';
|
||||
default:
|
||||
throw new Error(`Cannot start server while in status ${status}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveStopTarget(status: GameServerStatus): GameServerStatus {
|
||||
if (status !== 'RUNNING') {
|
||||
throw new Error(`Cannot stop server while in status ${status}`);
|
||||
}
|
||||
|
||||
return 'STOPPING';
|
||||
}
|
||||
|
||||
export function resolveKillTarget(status: GameServerStatus): GameServerStatus {
|
||||
if (status !== 'RUNNING' && status !== 'STARTING' && status !== 'STOPPING') {
|
||||
throw new Error(`Cannot kill server while in status ${status}`);
|
||||
}
|
||||
|
||||
return 'STOPPED';
|
||||
}
|
||||
28
packages/server-state/src/state.test.ts
Normal file
28
packages/server-state/src/state.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
assertTransition,
|
||||
isInProgress,
|
||||
resolveKillTarget,
|
||||
resolveStartTarget,
|
||||
} from './index';
|
||||
|
||||
describe('server state machine', () => {
|
||||
it('allows draft to provisioning', () => {
|
||||
expect(() => assertTransition('DRAFT', 'PROVISIONING')).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects invalid transition', () => {
|
||||
expect(() => assertTransition('DELETED', 'RUNNING')).toThrow();
|
||||
});
|
||||
|
||||
it('detects in-progress statuses', () => {
|
||||
expect(isInProgress('STARTING')).toBe(true);
|
||||
expect(isInProgress('STOPPED')).toBe(false);
|
||||
});
|
||||
|
||||
it('resolves start and kill targets', () => {
|
||||
expect(resolveStartTarget('STOPPED')).toBe('STARTING');
|
||||
expect(resolveKillTarget('RUNNING')).toBe('STOPPED');
|
||||
});
|
||||
});
|
||||
8
packages/server-state/tsconfig.json
Normal file
8
packages/server-state/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@hexahost/typescript-config/library.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
@@ -12,6 +12,10 @@
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
|
||||
112
packages/ui/src/components/Dialog.tsx
Normal file
112
packages/ui/src/components/Dialog.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { forwardRef, type ComponentPropsWithoutRef, type ElementRef, type HTMLAttributes } from "react";
|
||||
import { cn } from "../lib/cn";
|
||||
|
||||
export const Dialog = DialogPrimitive.Root;
|
||||
export const DialogTrigger = DialogPrimitive.Trigger;
|
||||
export const DialogPortal = DialogPrimitive.Portal;
|
||||
export const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
export const DialogOverlay = forwardRef<
|
||||
ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-zinc-950/60 backdrop-blur-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
export const DialogContent = forwardRef<
|
||||
ElementRef<typeof DialogPrimitive.Content>,
|
||||
ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2",
|
||||
"rounded-lg border border-zinc-200 bg-white p-6 shadow-lg",
|
||||
"dark:border-zinc-800 dark:bg-zinc-900",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500 focus-visible:ring-offset-2",
|
||||
"dark:focus-visible:ring-offset-zinc-950",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close
|
||||
className={cn(
|
||||
"absolute right-4 top-4 rounded-md p-1 text-zinc-500 transition-colors",
|
||||
"hover:bg-zinc-100 hover:text-zinc-900",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500 focus-visible:ring-offset-2",
|
||||
"dark:hover:bg-zinc-800 dark:hover:text-zinc-100 dark:focus-visible:ring-offset-zinc-950",
|
||||
)}
|
||||
aria-label="Close"
|
||||
>
|
||||
<span aria-hidden="true" className="text-lg leading-none">
|
||||
×
|
||||
</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
export interface DialogHeaderProps extends HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
export const DialogHeader = forwardRef<HTMLDivElement, DialogHeaderProps>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex flex-col gap-1.5 pr-8 text-left", className)} {...props} />
|
||||
),
|
||||
);
|
||||
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
export interface DialogFooterProps extends HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
export const DialogFooter = forwardRef<HTMLDivElement, DialogFooterProps>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
|
||||
export const DialogTitle = forwardRef<
|
||||
ElementRef<typeof DialogPrimitive.Title>,
|
||||
ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-base font-semibold tracking-tight text-zinc-900 dark:text-zinc-50", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
export const DialogDescription = forwardRef<
|
||||
ElementRef<typeof DialogPrimitive.Description>,
|
||||
ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-zinc-600 dark:text-zinc-400", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
199
packages/ui/src/components/DropdownMenu.tsx
Normal file
199
packages/ui/src/components/DropdownMenu.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { forwardRef, type ComponentPropsWithoutRef, type ElementRef, type HTMLAttributes } from "react";
|
||||
import { cn } from "../lib/cn";
|
||||
|
||||
export const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
export const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
export const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
export const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
export const DropdownMenuSubTrigger = forwardRef<
|
||||
ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none",
|
||||
"text-zinc-700 focus:bg-zinc-100 data-[state=open]:bg-zinc-100",
|
||||
"dark:text-zinc-300 dark:focus:bg-zinc-800 dark:data-[state=open]:bg-zinc-800",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<span className="ml-auto text-xs text-zinc-500 dark:text-zinc-400" aria-hidden="true">
|
||||
›
|
||||
</span>
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
|
||||
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
export const DropdownMenuSubContent = forwardRef<
|
||||
ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-zinc-200 bg-white p-1 shadow-md",
|
||||
"dark:border-zinc-800 dark:bg-zinc-900",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
export const DropdownMenuContent = forwardRef<
|
||||
ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-zinc-200 bg-white p-1 shadow-md",
|
||||
"dark:border-zinc-800 dark:bg-zinc-900",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
export const DropdownMenuItem = forwardRef<
|
||||
ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none",
|
||||
"text-zinc-700 focus:bg-zinc-100 focus:text-zinc-900",
|
||||
"dark:text-zinc-300 dark:focus:bg-zinc-800 dark:focus:text-zinc-50",
|
||||
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
export const DropdownMenuCheckboxItem = forwardRef<
|
||||
ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none",
|
||||
"text-zinc-700 focus:bg-zinc-100 focus:text-zinc-900",
|
||||
"dark:text-zinc-300 dark:focus:bg-zinc-800 dark:focus:text-zinc-50",
|
||||
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<span className="text-xs" aria-hidden="true">
|
||||
✓
|
||||
</span>
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
|
||||
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
export const DropdownMenuRadioItem = forwardRef<
|
||||
ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none",
|
||||
"text-zinc-700 focus:bg-zinc-100 focus:text-zinc-900",
|
||||
"dark:text-zinc-300 dark:focus:bg-zinc-800 dark:focus:text-zinc-50",
|
||||
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<span className="size-1.5 rounded-full bg-current" aria-hidden="true" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
));
|
||||
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
export interface DropdownMenuLabelProps
|
||||
extends ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> {
|
||||
inset?: boolean;
|
||||
}
|
||||
|
||||
export const DropdownMenuLabel = forwardRef<
|
||||
ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
DropdownMenuLabelProps
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
|
||||
export const DropdownMenuSeparator = forwardRef<
|
||||
ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-zinc-200 dark:bg-zinc-800", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
export interface DropdownMenuShortcutProps extends HTMLAttributes<HTMLSpanElement> {}
|
||||
|
||||
export const DropdownMenuShortcut = forwardRef<HTMLSpanElement, DropdownMenuShortcutProps>(
|
||||
({ className, ...props }, ref) => (
|
||||
<span
|
||||
ref={ref}
|
||||
className={cn("ml-auto text-xs tracking-widest text-zinc-500 dark:text-zinc-400", className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||
@@ -13,4 +13,37 @@ export {
|
||||
type CardProps,
|
||||
type CardTitleProps,
|
||||
} from "./components/Card";
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
type DialogFooterProps,
|
||||
type DialogHeaderProps,
|
||||
} from "./components/Dialog";
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
type DropdownMenuLabelProps,
|
||||
type DropdownMenuShortcutProps,
|
||||
} from "./components/DropdownMenu";
|
||||
export { cn } from "./lib/cn";
|
||||
|
||||
Reference in New Issue
Block a user