diff --git a/.github/workflows/docker-package-publish.yml b/.github/workflows/docker-package-publish.yml
index ad751c72..8e10ac38 100644
--- a/.github/workflows/docker-package-publish.yml
+++ b/.github/workflows/docker-package-publish.yml
@@ -7,11 +7,11 @@ name: Docker
on:
push:
- branches: [ 'master', 'develop' ]
+ branches: [ 'master', 'develop', 'feature/*' ]
# Publish semver tags as releases.
tags: [ 'v*.*', 'v*.*.*', 'v*.*.*-beta*' ]
pull_request:
- branches: [ 'master', 'develop' ]
+ branches: [ 'master', 'develop', 'feature/*' ]
env:
# Use docker.io for Docker Hub if empty
@@ -21,25 +21,43 @@ env:
jobs:
build:
- runs-on: ubuntu-latest
+ name: Build (${{ matrix.platform }})
+ runs-on: ${{ matrix.runner }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - platform: linux/amd64
+ runner: ubuntu-22.04
+ - platform: linux/arm64
+ runner: ubuntu-22.04-arm
permissions:
contents: read
packages: write
- # This is used to complete the identity challenge
- # with sigstore/fulcio when running outside of PRs.
id-token: write
+ outputs:
+ image-name: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
steps:
- name: Checkout repository
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- # Install the cosign tool except on PR
- # https://github.com/sigstore/cosign-installer
- - name: Install cosign
- if: github.event_name != 'pull_request'
- uses: sigstore/cosign-installer@6e04d228eb30da1757ee4e1dd75a0ec73a653e06 #v3.1.1
- with:
- cosign-release: 'v2.1.1'
+ # Ensure repository/image name is lowercase for Docker registry
+ - name: Set lowercase image name
+ id: image
+ run: |
+ image="${IMAGE_NAME}"
+ echo "lower=${image,,}" >> "$GITHUB_OUTPUT"
+ echo "IMAGE_NAME_LC=${image,,}" >> "$GITHUB_ENV"
+ env:
+ IMAGE_NAME: ${{ env.IMAGE_NAME }}
+
+ - name: Prepare platform name
+ id: platform
+ run: |
+ platform=${{ matrix.platform }}
+ echo "name=${platform//\//-}" >> $GITHUB_OUTPUT
# Set up BuildKit Docker container builder to be able to build
# multi-platform images and export cache
@@ -63,17 +81,118 @@ jobs:
id: meta
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
with:
- images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ images: ${{ env.REGISTRY }}/${{ steps.image.outputs.lower }}
+ flavor: |
+ latest=false
+ tags: |
+ type=raw,value=${{ github.sha }}-${{ steps.platform.outputs.name }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- - name: Build and push Docker image
- id: build-and-push
+ - name: Build and push Docker image by digest
+ id: build
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
with:
context: .
- push: ${{ github.event_name != 'pull_request' }}
- tags: ${{ steps.meta.outputs.tags }}
+ platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
- cache-from: type=gha
- cache-to: type=gha,mode=max
+ outputs: type=image,name=${{ env.REGISTRY }}/${{ steps.image.outputs.lower }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
+ cache-from: type=gha,scope=build-${{ steps.platform.outputs.name }}
+ cache-to: type=gha,mode=max,scope=build-${{ steps.platform.outputs.name }}
+ provenance: false
+ sbom: false
+
+ - name: Export digest
+ if: github.event_name != 'pull_request'
+ run: |
+ mkdir -p /tmp/digests
+ digest="${{ steps.build.outputs.digest }}"
+ touch "/tmp/digests/${digest#sha256:}"
+
+ - name: Upload digest
+ if: github.event_name != 'pull_request'
+ uses: actions/upload-artifact@v4
+ with:
+ name: digests-${{ steps.platform.outputs.name }}
+ path: /tmp/digests/*
+ if-no-files-found: error
+ retention-days: 1
+
+ merge:
+ name: Create multi-arch manifest
+ runs-on: ubuntu-22.04
+ needs: build
+ if: github.event_name != 'pull_request'
+ permissions:
+ contents: read
+ packages: write
+ id-token: write
+
+ steps:
+ - name: Download digests
+ uses: actions/download-artifact@v4
+ with:
+ pattern: digests-*
+ path: /tmp/digests
+ merge-multiple: true
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
+
+ - name: Log into registry ${{ env.REGISTRY }}
+ uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ # Ensure repository/image name is lowercase for Docker registry
+ - name: Set lowercase image name
+ id: image
+ run: |
+ image="${IMAGE_NAME}"
+ echo "lower=${image,,}" >> "$GITHUB_OUTPUT"
+ echo "IMAGE_NAME_LC=${image,,}" >> "$GITHUB_ENV"
+ env:
+ IMAGE_NAME: ${{ env.IMAGE_NAME }}
+
+ # Extract metadata (tags, labels) for Docker
+ # https://github.com/docker/metadata-action
+ - name: Extract Docker metadata
+ id: meta
+ uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
+ with:
+ images: ${{ env.REGISTRY }}/${{ steps.image.outputs.lower }}
+
+ - name: Create manifest list and push
+ id: create-manifest
+ working-directory: /tmp/digests
+ run: |
+ docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
+ $(printf '${{ env.REGISTRY }}/${{ steps.image.outputs.lower }}@sha256:%s ' *)
+
+ # Get the digest of the manifest
+ manifest_digest=$(docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ steps.image.outputs.lower }}:${{ steps.meta.outputs.version }} --format '{{json .Manifest}}' | jq -r '.digest // .Digest // empty')
+ if [ -z "$manifest_digest" ]; then
+ manifest_digest=$(docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ steps.image.outputs.lower }}:${{ steps.meta.outputs.version }} --format '{{.Digest}}')
+ fi
+ echo "digest=${manifest_digest}" >> $GITHUB_OUTPUT
+
+ - name: Inspect image
+ run: |
+ docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ steps.image.outputs.lower }}:${{ steps.meta.outputs.version }}
+
+ # Install cosign (before using it)
+ - name: Install cosign
+ uses: sigstore/cosign-installer@v3.7.0
+ with:
+ cosign-release: 'v2.2.4'
+
+ # Sign the pushed image by digest (skip on PRs)
+ - name: Sign image with cosign
+ if: github.event_name != 'pull_request'
+ env:
+ DIGEST: ${{ steps.create-manifest.outputs.digest }}
+ TAGS: ${{ steps.meta.outputs.tags }}
+ run: |
+ echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
diff --git a/package-lock.json b/package-lock.json
index dd122069..7f5faf83 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "gml-frontend",
- "version": "25.2",
+ "version": "25.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gml-frontend",
- "version": "25.2",
+ "version": "25.3",
"dependencies": {
"@faker-js/faker": "9.0.0",
"@hookform/resolvers": "3.9.0",
diff --git a/package.json b/package.json
index 74a62457..01a32ccd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "gml-frontend",
- "version": "25.2",
+ "version": "25.3",
"private": true,
"scripts": {
"dev": "next dev --experimental-https",
diff --git a/src/app/auth/signin/page.tsx b/src/app/auth/signin/page.tsx
index 0f53eb07..5bec5367 100644
--- a/src/app/auth/signin/page.tsx
+++ b/src/app/auth/signin/page.tsx
@@ -1,10 +1,8 @@
import Image from 'next/image';
-import Link from 'next/link';
import classes from './styles.module.css';
import { LoginPluginScriptViewer, SignInForm } from '@/features/auth-credentials-form';
-import { AUTH_PAGES } from '@/shared/routes';
import logo from '@/assets/logos/logo.svg';
export default function Page() {
@@ -22,12 +20,12 @@ export default function Page() {
-
diff --git a/src/app/auth/signup/page.tsx b/src/app/auth/signup/page.tsx
index 41b57520..9144aa00 100644
--- a/src/app/auth/signup/page.tsx
+++ b/src/app/auth/signup/page.tsx
@@ -1,38 +1,7 @@
-import Image from 'next/image';
-import Link from 'next/link';
+import { redirect } from 'next/navigation';
-import classes from './styles.module.css';
-
-import { SignUpForm } from '@/features/auth-credentials-form';
import { AUTH_PAGES } from '@/shared/routes';
-import logo from '@/assets/logos/logo.svg';
export default function Page() {
- return (
- <>
-
-
-
-
-
-
Регистрация
-
- Введите свой адрес электронной почты ниже, чтобы создать свою учетную запись
-
-
-
-
- Уже есть аккаунт?{' '}
-
- Войти
-
-
-
-
-
-
-
-
- >
- );
+ redirect(AUTH_PAGES.SIGN_IN);
}
diff --git a/src/app/mnt/page.tsx b/src/app/mnt/page.tsx
index 6bced766..64e17fc7 100644
--- a/src/app/mnt/page.tsx
+++ b/src/app/mnt/page.tsx
@@ -75,7 +75,7 @@ export default function MntPage() {
Расположение каталога данных:{' '}
- /data/GmlBackend
+ /srv/gml
diff --git a/src/app/page.tsx b/src/app/page.tsx
index ef98e3c9..9a262fc4 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,142 +1,113 @@
-import Link from 'next/link';
-import { CodeIcon, DesktopIcon, LockClosedIcon } from '@radix-ui/react-icons';
-import {
- AtomIcon,
- CloudUploadIcon,
- GamepadIcon,
- GlobeLockIcon,
- MessageSquareHeartIcon,
- PencilIcon,
- ServerIcon,
- SettingsIcon,
- ShieldIcon,
-} from 'lucide-react';
+import Image from 'next/image';
import React from 'react';
+import Link from 'next/link';
-import { cn } from '@/shared/lib/utils';
+import { config } from '@/core/configs';
+import logo from '@/assets/logos/logo.svg';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/shared/ui/card';
import { AUTH_PAGES } from '@/shared/routes';
+import { cn } from '@/shared/lib/utils';
import { buttonVariants } from '@/shared/ui/button';
-import WelcomeNavbar from '@/app/auth/welcome/welcomeNavbar';
-import { Card, CardDescription, CardHeader, CardTitle } from '@/shared/ui/card';
-
-const cardData = [
- {
- icon:
,
- title: 'Кроссплатформенность',
- description:
- 'Поддержка различных операционных систем для легкости развертывания и использования.',
- },
- {
- icon:
,
- title: 'Инфраструктура для Minecraft',
- description:
- 'Полный набор инструментов для создания и управления игровыми клиентами и профилями.',
- },
- {
- icon:
,
- title: 'Безопасность и защита',
- description:
- 'Встроенные механизмы безопасности и базовая защита для обеспечения безопасности проекта.',
- },
- {
- icon:
,
- title: 'Гибкость и расширяемость',
- description:
- 'Легкая адаптация под различные требования и возможность расширять функциональность.',
- },
- {
- icon:
,
- title: 'Установка на любую ОС',
- description:
- 'Простота и гибкость установки на Windows, Linux или macOS (сборка из исходного кода) с использованием Docker и Pterodactyl для удобства.',
- },
- {
- icon:
,
- title: 'Собственная авторизация',
- description:
- 'Возможность использования собственной, так и предустановленной системы авторизации.',
- },
- {
- icon:
,
- title: 'Современные технологии',
- description:
- 'Использование C#, .NET 8, Docker и других технологий для обеспечения высокой производительности.',
- },
- {
- icon:
,
- title: 'Поддержка серверов',
- description: 'Удобная настройка и управление игровыми серверами с полной поддержкой Docker.',
- },
- {
- icon:
,
- title: 'Редактирование лаунчера',
- description:
- 'Легкость в редактировании стилей, цветов и компонентов лаунчера для индивидуальных нужд.',
- },
- {
- icon:
,
- title: 'Публикация лаунчера',
- description: 'Простота публикации лаунчера через панель для различных операционных систем.',
- },
- {
- icon:
,
- title: 'Интеграция с популярными сервисами',
- description:
- 'Возможность подключения к сторонним API, таким как Discord, и другими платформами.',
- },
- {
- icon:
,
- title: 'Обновления и поддержка',
- description:
- 'Регулярные обновления с улучшениями и исправлениями, а также доступ к документации .',
- },
-];
export default function Home() {
return (
<>
-
-
-
-
-
-
Спасибо за выбор Gml
-
- Добро пожаловать в панель управления! Мы рады приветствовать вас в панели управления.
- Здесь вы можете настроить, управлять и следить за всеми аспектами вашего проекта.
-
-
-
+
+
+
+
+ {config.name}
+
+
+
+ Добро пожаловать
+
+ Здесь вы можете настроить, управлять и следить за всеми аспектами вашего проекта.
+
+
+
- Войти
-
-
- Регистрация
+ Продолжить
-
-
-
-
- {cardData.map((card) => (
-
-
-
- {card.icon}
- {card.title}
- {card.description}
-
-
-
- ))}
-
+
+
+
+ {/*
*/}
+ {/*
*/}
+ {/*
Добро пожаловать! */}
+
+ {/*
*/}
+ {/*
*/}
+ {/* */}
+ {/* Панель управления вашим игровым проектом */}
+ {/* */}
+ {/* Добро пожаловать в панель управления! Мы рады приветствовать вас в панели*/}
+ {/* управления. Здесь вы можете настроить, управлять и следить за всеми аспектами вашего*/}
+ {/* проекта.*/}
+ {/* */}
+ {/* */}
+ {/* */}
+ {/* */}
+ {/* Войти*/}
+ {/* */}
+ {/* */}
+ {/* */}
+
+ {/*
*/}
+ {/* {config.name} {config.version}*/}
+ {/*
*/}
+ {/*
*/}
+ {/*
*/}
+ {/*
*/}
+ {/*
*/}
+
+ {/*
*/}
+ {/*
*/}
+ {/*
Спасибо за выбор Gml */}
+ {/*
*/}
+ {/* Добро пожаловать в панель управления! Мы рады приветствовать вас в панели управления.*/}
+ {/* Здесь вы можете настроить, управлять и следить за всеми аспектами вашего проекта.*/}
+ {/*
*/}
+
+ {/*
*/}
+ {/* */}
+ {/* Войти*/}
+ {/* */}
+ {/* /!* *!/*/}
+ {/* /!* Регистрация*!/*/}
+ {/* /!**!/*/}
+ {/*
*/}
+ {/*
*/}
+
+ {/* /!*
*!/*/}
+ {/* /!* {cardData.map((card) => (*!/*/}
+ {/* /!*
*!/*/}
+ {/* /!* *!/*/}
+ {/* /!* *!/*/}
+ {/* /!* {card.icon}*!/*/}
+ {/* /!* {card.title} *!/*/}
+ {/* /!* {card.description} *!/*/}
+ {/* /!* *!/*/}
+ {/* /!* *!/*/}
+ {/* /!*
*!/*/}
+ {/* /!* ))}*!/*/}
+ {/* /!*
*!/*/}
+ {/*
*/}
+ {/*
*/}
>
);
}
diff --git a/src/features/applications/api/index.ts b/src/features/applications/api/index.ts
new file mode 100644
index 00000000..d337e568
--- /dev/null
+++ b/src/features/applications/api/index.ts
@@ -0,0 +1 @@
+export * from './useApplications';
diff --git a/src/features/applications/api/useApplications.ts b/src/features/applications/api/useApplications.ts
new file mode 100644
index 00000000..be8fac7f
--- /dev/null
+++ b/src/features/applications/api/useApplications.ts
@@ -0,0 +1,55 @@
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { toast } from 'sonner';
+
+import { applicationsApi } from '@/shared/api/applications';
+import { ExternalApplicationCreateDto } from '@/shared/api/contracts';
+
+// Query key for applications
+const APPLICATIONS_QUERY_KEY = ['applications'];
+
+/**
+ * Hook to fetch all applications
+ */
+export const useApplications = () => {
+ return useQuery({
+ queryKey: APPLICATIONS_QUERY_KEY,
+ queryFn: () => applicationsApi.getApplications(),
+ });
+};
+
+/**
+ * Hook to create a new application
+ */
+export const useCreateApplication = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (data: ExternalApplicationCreateDto) => applicationsApi.createApplication(data),
+ onSuccess: (data) => {
+ toast.success('Приложение успешно создано');
+ queryClient.invalidateQueries({ queryKey: APPLICATIONS_QUERY_KEY });
+ return data;
+ },
+ onError: (error: any) => {
+ toast.error(error?.message || 'Ошибка при создании приложения');
+ },
+ });
+};
+
+/**
+ * Hook to delete an application
+ */
+export const useDeleteApplication = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (id: string) => applicationsApi.deleteApplication(id),
+ onSuccess: () => {
+ toast.success('Приложение успешно удалено');
+ queryClient.invalidateQueries({ queryKey: APPLICATIONS_QUERY_KEY });
+ },
+ onError: (error: any) => {
+ toast.error(error?.message || 'Ошибка при удалении приложения');
+ },
+ });
+};
diff --git a/src/features/applications/index.ts b/src/features/applications/index.ts
new file mode 100644
index 00000000..308f5ae1
--- /dev/null
+++ b/src/features/applications/index.ts
@@ -0,0 +1 @@
+export * from './api';
\ No newline at end of file
diff --git a/src/features/authentication-azuriom-form/ui/AuthenticationFormAzuriom.tsx b/src/features/authentication-azuriom-form/ui/AuthenticationFormAzuriom.tsx
index de80b768..d71d96b2 100644
--- a/src/features/authentication-azuriom-form/ui/AuthenticationFormAzuriom.tsx
+++ b/src/features/authentication-azuriom-form/ui/AuthenticationFormAzuriom.tsx
@@ -66,7 +66,7 @@ export function AuthenticationFormAzuriom({ className, onOpenChange, ...props }:
в Azuriom есть проблема, которая не позволит заходить другим людям на ваши сервера!
-
+
Прочитайте эту статью
, чтобы узнать подробности.
diff --git a/src/middleware.ts b/src/middleware.ts
index 8eb6605a..e0da2f83 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -12,7 +12,7 @@ const protectedRoutes = [
DASHBOARD_PAGES.PROFILES,
DASHBOARD_PAGES.PROFILE,
];
-const publicRoutes = ['/', AUTH_PAGES.HOME, AUTH_PAGES.SIGN_IN, AUTH_PAGES.SIGN_UP];
+const publicRoutes = ['/', AUTH_PAGES.HOME, AUTH_PAGES.SIGN_IN];
export async function middleware(request: NextRequest) {
const {
diff --git a/src/shared/api/applications.ts b/src/shared/api/applications.ts
new file mode 100644
index 00000000..e1b72bf8
--- /dev/null
+++ b/src/shared/api/applications.ts
@@ -0,0 +1,34 @@
+import { $api } from '@/services/api.service';
+import {
+ ExternalApplicationCreateDto,
+ ExternalApplicationListDto,
+ ExternalApplicationReadDto,
+ TGetApplicationsResponse,
+ TPostApplicationResponse,
+ TDeleteApplicationResponse
+} from '@/shared/api/contracts';
+
+export const applicationsApi = {
+ /**
+ * Get all applications for the current user
+ */
+ async getApplications(): Promise {
+ const { data } = await $api.get('/applications');
+ return data.data;
+ },
+
+ /**
+ * Create a new external application
+ */
+ async createApplication(payload: ExternalApplicationCreateDto): Promise {
+ const { data } = await $api.post('/applications', payload);
+ return data.data;
+ },
+
+ /**
+ * Delete an external application
+ */
+ async deleteApplication(id: string): Promise {
+ await $api.delete(`/applications/${id}`);
+ }
+};
\ No newline at end of file
diff --git a/src/shared/api/contracts/applications/requests.ts b/src/shared/api/contracts/applications/requests.ts
new file mode 100644
index 00000000..e2569fe4
--- /dev/null
+++ b/src/shared/api/contracts/applications/requests.ts
@@ -0,0 +1,22 @@
+import {
+ ExternalApplicationListDto,
+ ExternalApplicationReadDto,
+ ExternalApplicationCreateDto
+} from './schemas';
+
+import { ResponseBaseEntity } from '@/shared/api/schemas';
+
+// GET /api/v1/applications
+export type TGetApplicationsResponse = ResponseBaseEntity & {
+ data: ExternalApplicationListDto[];
+};
+
+// POST /api/v1/applications
+export type TPostApplicationRequest = ExternalApplicationCreateDto;
+
+export type TPostApplicationResponse = ResponseBaseEntity & {
+ data: ExternalApplicationReadDto;
+};
+
+// DELETE /api/v1/applications/{id}
+export type TDeleteApplicationResponse = ResponseBaseEntity;
diff --git a/src/shared/api/contracts/applications/schemas.ts b/src/shared/api/contracts/applications/schemas.ts
new file mode 100644
index 00000000..bdc0c56f
--- /dev/null
+++ b/src/shared/api/contracts/applications/schemas.ts
@@ -0,0 +1,25 @@
+export interface PermissionDto {
+ id: number;
+ name: string;
+ description?: string;
+}
+
+export interface ExternalApplicationBaseEntity {
+ id: string;
+ name: string;
+ createdAtUtc: string;
+}
+
+export interface ExternalApplicationListDto extends ExternalApplicationBaseEntity {
+ permissions: PermissionDto[];
+}
+
+export interface ExternalApplicationReadDto extends ExternalApplicationBaseEntity {
+ token: string;
+ permissions: PermissionDto[];
+}
+
+export interface ExternalApplicationCreateDto {
+ name: string;
+ permissionIds: number[];
+}
diff --git a/src/shared/api/contracts/index.ts b/src/shared/api/contracts/index.ts
index 2dae0c3e..88872663 100644
--- a/src/shared/api/contracts/index.ts
+++ b/src/shared/api/contracts/index.ts
@@ -25,3 +25,7 @@ export * from '@/shared/api/contracts/gameservers/zod';
// Контракты для уведомлений
export * from '@/shared/api/contracts/notification/schemas';
export * from '@/shared/api/contracts/notification/requests';
+
+// Applications
+export * from '@/shared/api/contracts/applications/schemas';
+export * from '@/shared/api/contracts/applications/requests';
diff --git a/src/shared/api/contracts/user/UserBaseEntitry.ts b/src/shared/api/contracts/user/UserBaseEntitry.ts
index 10501ada..2e752324 100644
--- a/src/shared/api/contracts/user/UserBaseEntitry.ts
+++ b/src/shared/api/contracts/user/UserBaseEntitry.ts
@@ -31,6 +31,8 @@ export interface PlayerBaseEntity {
expiredDate: string;
textureSkinUrl: string;
textureCloakUrl: string;
+ externalTextureSkinUrl: string;
+ externalTextureCloakUrl: string;
textureSkinGuid: string;
textureCloakGuid: string;
fullSkinUrl?: any;
diff --git a/src/shared/constants/index.ts b/src/shared/constants/index.ts
index ec75bfff..0b06c8ce 100644
--- a/src/shared/constants/index.ts
+++ b/src/shared/constants/index.ts
@@ -7,7 +7,7 @@ export const HREF_GET_WEBMCR_RELOADED_AUTH_PHP =
export const HREF_GET_WORDPRESS_AUTH_PHP =
'https://github.com/GamerVII-NET/Gml.Modules.Auth/tree/master/WordPress';
export const HREF_DOCUMENTATION_CUSTOM_ENDPOINT =
- 'https://gml-launcher.github.io/Gml.Docs/integrations-auth-custom.html';
+ 'https://wiki.recloud.tech/docs/gml-launcher/backend/authorization/custom';
export const HREF_DISCORD = 'https://discord.com/invite/b5xgqfWgNt';
export const HREF_RECLOUD_PRO =
'https://market.recloud.tech/#:~:text=Gml%20Лаунчер%20%7C-,Тариф%20Pro,-(месяц)';
diff --git a/src/shared/routes/index.ts b/src/shared/routes/index.ts
index 4d4406bb..355a1447 100644
--- a/src/shared/routes/index.ts
+++ b/src/shared/routes/index.ts
@@ -1,7 +1,7 @@
export const AUTH_PAGES = {
HOME: '/auth',
SIGN_IN: '/auth/signin',
- SIGN_UP: '/auth/signup',
+ // SIGN_UP: '/auth/signup',
};
export const DASHBOARD_PAGES = {
diff --git a/src/views/settings/ui/ApplicationsTab.tsx b/src/views/settings/ui/ApplicationsTab.tsx
new file mode 100644
index 00000000..8efa718d
--- /dev/null
+++ b/src/views/settings/ui/ApplicationsTab.tsx
@@ -0,0 +1,403 @@
+'use client';
+
+import React, { useState, useEffect, useMemo } from 'react';
+import { MoreVertical, Plus, Trash, Copy, CheckCircle } from 'lucide-react';
+
+import {
+ useApplications,
+ useCreateApplication,
+ useDeleteApplication,
+} from '@/features/applications';
+import { ExternalApplicationCreateDto, ExternalApplicationReadDto } from '@/shared/api/contracts';
+import { rbacApi, PermissionDto } from '@/shared/api/rbac';
+import { Button } from '@/shared/ui/button';
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/shared/ui/table';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@/shared/ui/dropdown-menu';
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+} from '@/shared/ui/dialog';
+import { Label } from '@/shared/ui/label';
+import { Input } from '@/shared/ui/input';
+import { Icons } from '@/shared/ui/icons';
+import { MultiSelect } from '@/shared/ui/multi-select';
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from '@/shared/ui/alert-dialog';
+
+export const ApplicationsTab: React.FC = () => {
+ // Fetch applications data
+ const { data: applications = [], isLoading: isLoadingApplications, error } = useApplications();
+
+ // Create application mutation
+ const { mutateAsync: createApplication, isPending: isCreating } = useCreateApplication();
+
+ // Delete application mutation
+ const { mutateAsync: deleteApplication, isPending: isDeleting } = useDeleteApplication();
+
+ // State for permissions
+ const [permissions, setPermissions] = useState([]);
+ const [isLoadingPermissions, setIsLoadingPermissions] = useState(false);
+
+ const [createModalOpen, setCreateModalOpen] = useState(false);
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+ const [successDialogOpen, setSuccessDialogOpen] = useState(false);
+ const [createdApplication, setCreatedApplication] = useState(
+ null,
+ );
+ const [applicationToDelete, setApplicationToDelete] = useState<{
+ id: string;
+ name: string;
+ } | null>(null);
+ const [newApplication, setNewApplication] = useState({
+ name: '',
+ permissionIds: [],
+ });
+
+ // Fetch permissions when component mounts
+ useEffect(() => {
+ const fetchPermissions = async () => {
+ setIsLoadingPermissions(true);
+ try {
+ const perms = await rbacApi.getPermissions();
+ setPermissions(perms);
+ } catch (error) {
+ console.error('Failed to fetch permissions:', error);
+ } finally {
+ setIsLoadingPermissions(false);
+ }
+ };
+
+ fetchPermissions();
+ }, []);
+
+ // Group permissions by their prefix (before the dot in the permission name)
+ const permsByGroup = useMemo(() => {
+ const map = new Map();
+ for (const p of permissions) {
+ const name = p.name ?? '';
+ const group = name.includes('.') ? name.split('.')[0] : 'other';
+ const list = map.get(group) ?? [];
+ list.push(p);
+ map.set(group, list);
+ }
+ // sort groups alphabetically, and items within group by name
+ return Array.from(map.entries())
+ .sort((a, b) => a[0].localeCompare(b[0]))
+ .map(([g, list]) => [g, list.sort((x, y) => (x.name ?? '').localeCompare(y.name ?? ''))]) as [
+ string,
+ PermissionDto[],
+ ][];
+ }, [permissions]);
+
+ // Convert permissions to format expected by MultiSelect with groups
+ const permissionOptions = useMemo(() => {
+ return permsByGroup.flatMap(([group, perms]) => [
+ // Group header - using a custom component would be better, but we'll use a special label format
+ {
+ label: `▼ ${group.toUpperCase()} - Группа прав`,
+ value: `group-${group}`,
+ icon: undefined,
+ },
+ // Individual permissions with descriptions and indentation
+ ...perms.map((perm) => ({
+ label: perm.description ? ` • ${perm.name} - ${perm.description}` : ` • ${perm.name}`,
+ value: perm.id.toString(),
+ icon: undefined,
+ })),
+ ]);
+ }, [permsByGroup]);
+
+ // Handle permission selection changes
+ const handlePermissionChange = (selectedValues: string[]) => {
+ // Filter out group headers (values starting with "group-")
+ const filteredValues = selectedValues.filter((val) => !val.startsWith('group-'));
+
+ // Convert string IDs back to numbers
+ const permissionIds = filteredValues.map((val) => parseInt(val, 10)).filter((id) => !isNaN(id)); // Filter out any NaN values
+
+ setNewApplication((prev) => ({ ...prev, permissionIds }));
+ };
+
+ const handleCreateApplication = async () => {
+ const createdApp = await createApplication(newApplication);
+ setCreatedApplication(createdApp);
+ setNewApplication({ name: '', permissionIds: [] });
+ setCreateModalOpen(false);
+ setSuccessDialogOpen(true);
+ };
+
+ const handleDeleteApplication = async () => {
+ if (!applicationToDelete) return;
+
+ await deleteApplication(applicationToDelete.id);
+ setApplicationToDelete(null);
+ setDeleteDialogOpen(false);
+ };
+
+ return (
+
+
+
Внешние приложения
+
{
+ setNewApplication({ name: '', permissionIds: [] });
+ setCreateModalOpen(true);
+ }}
+ disabled={isLoadingApplications}
+ >
+
+ Создать приложение
+
+
+
+
+
+
+
+ ID
+ Название
+ Права
+ Дата создания
+ Действия
+
+
+
+ {isLoadingApplications ? (
+
+
+
+ Загрузка приложений...
+
+
+ ) : applications.length === 0 ? (
+
+
+ Нет созданных приложений
+
+
+ ) : (
+ applications.map((app) => (
+
+ {app.id.substring(0, 8)}...
+ {app.name}
+
+ {app.permissions.length > 0 ? (
+
+ {/* Group permissions by prefix */}
+ {(() => {
+ // Group permissions by prefix
+ const groupedPerms = new Map
();
+ for (const p of app.permissions) {
+ const name = p.name ?? '';
+ const group = name.includes('.') ? name.split('.')[0] : 'other';
+ const list = groupedPerms.get(group) ?? [];
+ list.push(p);
+ groupedPerms.set(group, list);
+ }
+
+ // Sort groups and permissions within groups
+ return Array.from(groupedPerms.entries())
+ .sort(([a], [b]) => a.localeCompare(b))
+ .map(([group, perms]) => (
+
+
+ {group.toUpperCase()}
+
+
+ {perms
+ .sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''))
+ .map((p) => (
+
+ {p.name}
+ {p.description && (
+
+ {' '}
+ {p.description}
+
+ )}
+
+ ))}
+
+
+ ));
+ })()}
+
+ ) : (
+ 'Нет прав'
+ )}
+
+ {new Date(app.createdAtUtc).toLocaleDateString()}
+
+
+
+
+ Открыть меню
+
+
+
+
+ {
+ setApplicationToDelete({ id: app.id, name: app.name });
+ setDeleteDialogOpen(true);
+ }}
+ >
+
+ Удалить
+
+
+
+
+
+ ))
+ )}
+
+
+
+
+ {/* Create Application Modal */}
+
+
+
+ Создать новое приложение
+
+
+
+ Название приложения
+ setNewApplication({ ...newApplication, name: e.target.value })}
+ placeholder="Введите название приложения"
+ />
+
+
+
Права доступа
+
+ {isLoadingPermissions && (
+
+
+ Загрузка прав доступа...
+
+ )}
+
+
+
+ setCreateModalOpen(false)}
+ disabled={isCreating}
+ >
+ Отмена
+
+
+ {isCreating && }
+ Создать
+
+
+
+
+
+ {/* Delete Confirmation Dialog */}
+
+
+
+ Удалить приложение
+
+ Вы уверены, что хотите удалить приложение "{applicationToDelete?.name}"? Это действие
+ нельзя отменить.
+
+
+
+ Отмена
+
+ {isDeleting && }
+ Удалить
+
+
+
+
+
+ {/* Success Dialog */}
+
+
+
+
+
+ Успех
+
+
+ Приложение "{createdApplication?.name}" успешно создано
+
+
+
+
+ Токен приложения:
+
+
+
+ {createdApplication?.token}
+ {
+ if (createdApplication?.token) {
+ navigator.clipboard.writeText(createdApplication.token);
+ }
+ }}
+ >
+
+ Копировать токен
+
+
+
+
+ Сохраните этот токен в надежном месте. Он будет показан только один раз.
+
+
+
+ setSuccessDialogOpen(false)}>Закрыть
+
+
+
+
+ );
+};
diff --git a/src/views/settings/ui/Settings.tsx b/src/views/settings/ui/Settings.tsx
index 83da8851..9f8f7e86 100644
--- a/src/views/settings/ui/Settings.tsx
+++ b/src/views/settings/ui/Settings.tsx
@@ -3,6 +3,7 @@
import React from 'react';
import { RolesPermissionsTab } from './RolesPermissionsTab';
+import { ApplicationsTab } from './ApplicationsTab';
import { ApiKeysTab } from './ApiKeysTab';
import { EditSettingsPlatformForm } from '@/features/edit-settings-platform-form';
@@ -31,10 +32,21 @@ export const SettingsPage = () => {
Основные
-
+
Роли и права
+
+
+ Приложения
- Beta
+
+ Beta
+
{/**/}
@@ -47,6 +59,9 @@ export const SettingsPage = () => {
+
+
+
diff --git a/src/widgets/players-table/lib/columns.tsx b/src/widgets/players-table/lib/columns.tsx
index 8b538d04..031cdc96 100644
--- a/src/widgets/players-table/lib/columns.tsx
+++ b/src/widgets/players-table/lib/columns.tsx
@@ -3,7 +3,18 @@
import React, { useMemo, useState } from 'react';
import { createColumnHelper } from '@tanstack/table-core';
import { format } from 'date-fns';
-import { Ban as BanIcon, GavelIcon, MoreVertical, ShieldCheck, Trash, User, Monitor, Laptop, Smartphone, Tablet } from 'lucide-react';
+import {
+ Ban as BanIcon,
+ GavelIcon,
+ Laptop,
+ Monitor,
+ MoreVertical,
+ ShieldCheck,
+ Smartphone,
+ Tablet,
+ Trash,
+ User,
+} from 'lucide-react';
import { DataTableColumnHeader } from '@/entities/Table';
import { PlayerBaseEntity } from '@/shared/api/contracts';
@@ -129,144 +140,153 @@ function PlayerDetailsDialog({
{/* Tabs layout for player card */}
-
- Обзор
- Текстуры
- Авторизации
- Сети/IP
- Сервер
-
+
+ Обзор
+ Текстуры
+ Авторизации
+ Сети/IP
+ Сервер
+
-
-
-
-
-
-
Статус
-
- {player.isBanned ? (
-
- Заблокирован
-
- ) : (
-
- Не заблокирован
-
+
+
+
+
+
+
Статус
+
+ {player.isBanned ? (
+
+ Заблокирован
+
+ ) : (
+
+ Не заблокирован
+
+ )}
+
+ {player.isLauncherStarted && (
+
+
+
+ Лаунчер запущен
+
+
+ )}
+
+
+
Сессия истекает
+
{sessionStr}
+
+
+
+ {addresses.length > 0 && (
+
+
IP адреса
+
{addresses.join(', ')}
+
)}
- {player.isLauncherStarted && (
-
-
- Лаунчер запущен
-
- )}
+
+
+
-
Сессия истекает
-
{sessionStr}
+
Текстуры
+
+ {player.externalTextureSkinUrl && (
+
+ )}
+ {player.externalTextureCloakUrl && (
+
+ )}
+ {!player.externalTextureSkinUrl && !player.externalTextureCloakUrl && (
+
Нет текстур.
+ )}
+
-
-
- {addresses.length > 0 && (
-
-
IP адреса
-
{addresses.join(', ')}
+
+
+
+
+
Авторизации
+
+ {(player.authHistory || []).map((h, idx) => (
+
+
{getDeviceIcon(h.device)}
+
+
+
+ {h.device || 'Неизвестное устройство'}
+
+ {h.hwid && (
+
+ HWID: {h.hwid}
+
+ )}
+
+
+ {h.address || '-'}
+
+
+ {timeAgo(h.date)}
+
+ {h.protocol}
+
+
+
+
+ ))}
+ {(!player.authHistory || player.authHistory.length === 0) && (
+
Нет данных.
+ )}
- )}
-
-
-
+
+
-
-
-
Текстуры
-
- {player.textureSkinUrl && (
-
- )}
- {player.textureCloakUrl && (
-
- )}
- {!player.textureSkinUrl && !player.textureCloakUrl && (
-
Нет текстур.
+
+ {addresses.length > 0 ? (
+
+ {addresses.map((a) => (
+
+ {a}
+
+ ))}
+
+ ) : (
+ Нет IP адресов.
)}
-
-
-
+
-
-
-
Авторизации
-
- {(player.authHistory || []).map((h, idx) => (
-
-
- {getDeviceIcon(h.device)}
-
-
-
-
- {h.device || 'Неизвестное устройство'}
-
- {h.hwid && (
-
- HWID: {h.hwid}
-
- )}
-
-
- {h.address || '-'}
-
-
- {timeAgo(h.date)}
- {h.protocol}
-
-
+
+ {Array.isArray((player as any).serverJoinHistory) ? (
+
+ {(player as any).serverJoinHistory?.length || 0} записей
- ))}
- {(!player.authHistory || player.authHistory.length === 0) && (
+ ) : (
Нет данных.
)}
-
+
-
-
-
- {addresses.length > 0 ? (
-
- {addresses.map((a) => (
-
- {a}
-
- ))}
-
- ) : (
- Нет IP адресов.
- )}
-
-
-
- {Array.isArray((player as any).serverJoinHistory) ? (
-
- {(player as any).serverJoinHistory?.length || 0} записей
-
- ) : (
- Нет данных.
- )}
-
-
-
+