Explorar o código

feat(geolite): add GeoLite2 database checks and download functionality in self-check tasks

0xJacky hai 4 días
pai
achega
5b47ccf639

+ 2 - 1
.claude/settings.local.json

@@ -19,7 +19,8 @@
       "Bash(go generate:*)",
       "Bash(pnpm eslint:*)",
       "Read(//workspaces/cosy/settings/**)",
-      "Bash(go doc:*)"
+      "Bash(go doc:*)",
+      "Bash(pnpm exec eslint:*)"
     ],
     "deny": []
   }

+ 1 - 11
app/components.d.ts

@@ -11,7 +11,6 @@ declare module 'vue' {
     AAlert: typeof import('ant-design-vue/es')['Alert']
     AApp: typeof import('ant-design-vue/es')['App']
     AAutoComplete: typeof import('ant-design-vue/es')['AutoComplete']
-    AAvatar: typeof import('ant-design-vue/es')['Avatar']
     ABadge: typeof import('ant-design-vue/es')['Badge']
     ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']
     ABreadcrumbItem: typeof import('ant-design-vue/es')['BreadcrumbItem']
@@ -22,7 +21,6 @@ declare module 'vue' {
     ACol: typeof import('ant-design-vue/es')['Col']
     ACollapse: typeof import('ant-design-vue/es')['Collapse']
     ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel']
-    AComment: typeof import('ant-design-vue/es')['Comment']
     AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
     ADivider: typeof import('ant-design-vue/es')['Divider']
     ADrawer: typeof import('ant-design-vue/es')['Drawer']
@@ -32,8 +30,6 @@ declare module 'vue' {
     AFormItem: typeof import('ant-design-vue/es')['FormItem']
     AInput: typeof import('ant-design-vue/es')['Input']
     AInputGroup: typeof import('ant-design-vue/es')['InputGroup']
-    AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
-    AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
     ALayout: typeof import('ant-design-vue/es')['Layout']
     ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
     ALayoutFooter: typeof import('ant-design-vue/es')['LayoutFooter']
@@ -43,25 +39,19 @@ declare module 'vue' {
     AListItem: typeof import('ant-design-vue/es')['ListItem']
     AListItemMeta: typeof import('ant-design-vue/es')['ListItemMeta']
     AMenu: typeof import('ant-design-vue/es')['Menu']
-    AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
     AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
     AModal: typeof import('ant-design-vue/es')['Modal']
     APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
     APopover: typeof import('ant-design-vue/es')['Popover']
     AppProviderAppProvider: typeof import('./src/components/AppProvider/AppProvider.vue')['default']
     AProgress: typeof import('ant-design-vue/es')['Progress']
-    AQrcode: typeof import('ant-design-vue/es')['QRCode']
     ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
-    AResult: typeof import('ant-design-vue/es')['Result']
     ARow: typeof import('ant-design-vue/es')['Row']
     ASegmented: typeof import('ant-design-vue/es')['Segmented']
     ASelect: typeof import('ant-design-vue/es')['Select']
     ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
     ASpace: typeof import('ant-design-vue/es')['Space']
-    ASpin: typeof import('ant-design-vue/es')['Spin']
     AStatistic: typeof import('ant-design-vue/es')['Statistic']
-    AStep: typeof import('ant-design-vue/es')['Step']
-    ASteps: typeof import('ant-design-vue/es')['Steps']
     ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
     ASwitch: typeof import('ant-design-vue/es')['Switch']
     ATable: typeof import('ant-design-vue/es')['Table']
@@ -70,7 +60,6 @@ declare module 'vue' {
     ATag: typeof import('ant-design-vue/es')['Tag']
     ATextarea: typeof import('ant-design-vue/es')['Textarea']
     ATooltip: typeof import('ant-design-vue/es')['Tooltip']
-    ATypographyParagraph: typeof import('ant-design-vue/es')['TypographyParagraph']
     ATypographyText: typeof import('ant-design-vue/es')['TypographyText']
     ATypographyTitle: typeof import('ant-design-vue/es')['TypographyTitle']
     AutoCertFormAutoCertForm: typeof import('./src/components/AutoCertForm/AutoCertForm.vue')['default']
@@ -120,6 +109,7 @@ declare module 'vue' {
     ReactiveFromNowReactiveFromNow: typeof import('./src/components/ReactiveFromNow/ReactiveFromNow.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
+    SelfCheckAsyncErrorDisplay: typeof import('./src/components/SelfCheck/AsyncErrorDisplay.vue')['default']
     SelfCheckSelfCheck: typeof import('./src/components/SelfCheck/SelfCheck.vue')['default']
     SelfCheckSelfCheckHeaderBanner: typeof import('./src/components/SelfCheck/SelfCheckHeaderBanner.vue')['default']
     SensitiveStringSensitiveString: typeof import('./src/components/SensitiveString/SensitiveString.vue')['default']

+ 1 - 7
app/src/components/GeoLiteDownload/GeoLiteDownload.vue

@@ -156,10 +156,7 @@ defineExpose({
         <div class="space-y-2">
           <p>{{ $gettext('The GeoLite2 database is required for offline geographic IP analysis. Please download it to enable this feature.') }}</p>
           <p class="text-sm">
-            {{ $gettext('Alternatively, if you cannot download the database, you can manually place') }}
-            <code class="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded">GeoLite2-City.mmdb</code>
-            {{ $gettext('in the same directory as') }}
-            <code class="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded">app.ini</code>
+            {{ $gettext('Alternatively, if you cannot download the database, you can manually place GeoLite2-City.mmdb in the same directory as app.ini.') }}
           </p>
         </div>
       </template>
@@ -209,9 +206,6 @@ defineExpose({
           :percent="downloadProgressComputed"
           :status="downloadStatus"
         />
-        <ATypographyText v-if="downloadMessage" type="secondary" class="text-sm mt-2">
-          {{ downloadMessage }}
-        </ATypographyText>
       </div>
     </div>
   </div>

+ 17 - 0
app/src/components/SelfCheck/AsyncErrorDisplay.vue

@@ -0,0 +1,17 @@
+<script setup lang="ts">
+import type { CosyError } from '@/lib/http/types'
+import { translateError } from '@/lib/http/error'
+
+const props = defineProps<{
+  error: CosyError
+  status: 'warning' | 'error'
+}>()
+
+const translatedError = await translateError(props.error)
+</script>
+
+<template>
+  <ATag :color="status === 'warning' ? 'warning' : 'error'">
+    {{ translatedError }}
+  </ATag>
+</template>

+ 36 - 4
app/src/components/SelfCheck/SelfCheck.vue

@@ -1,11 +1,29 @@
 <script setup lang="ts">
 import { CheckCircleOutlined, CloseCircleOutlined, WarningOutlined } from '@ant-design/icons-vue'
+import GeoLiteDownload from '@/components/GeoLiteDownload'
+import AsyncErrorDisplay from './AsyncErrorDisplay.vue'
 import { useSelfCheckStore } from './store'
 
 const store = useSelfCheckStore()
 
 const { data, loading, fixing } = storeToRefs(store)
 
+const geoLiteModalVisible = ref(false)
+
+function handleFix(key: string) {
+  if (key === 'GeoLite-DB') {
+    geoLiteModalVisible.value = true
+  }
+  else {
+    store.fix(key)
+  }
+}
+
+function handleGeoLiteDownloadComplete() {
+  geoLiteModalVisible.value = false
+  store.check()
+}
+
 onMounted(() => {
   store.check()
 })
@@ -26,7 +44,7 @@ onMounted(() => {
     <AList>
       <AListItem v-for="(item, index) in data" :key="index">
         <template v-if="item.status === 'error' && item.fixable" #actions>
-          <AButton type="link" size="small" :loading="fixing[item.key]" @click="store.fix(item.key)">
+          <AButton type="link" size="small" :loading="fixing[item.key]" @click="handleFix(item.key)">
             {{ $gettext('Attempt to fix') }}
           </AButton>
         </template>
@@ -39,9 +57,14 @@ onMounted(() => {
               {{ item.description?.() }}
             </div>
             <div v-if="item.status !== 'success' && item.err?.message" class="mt-1">
-              <ATag :color="item.status === 'warning' ? 'warning' : 'error'">
-                {{ $gettext(item.err?.message) }}
-              </ATag>
+              <Suspense>
+                <AsyncErrorDisplay :error="item.err" :status="item.status" />
+                <template #fallback>
+                  <ATag :color="item.status === 'warning' ? 'warning' : 'error'">
+                    {{ item.err.message }}
+                  </ATag>
+                </template>
+              </Suspense>
             </div>
           </template>
           <template #avatar>
@@ -54,6 +77,15 @@ onMounted(() => {
         </AListItemMeta>
       </AListItem>
     </AList>
+
+    <AModal
+      v-model:open="geoLiteModalVisible"
+      :title="$gettext('Download GeoLite2 Database')"
+      :footer="null"
+      width="600px"
+    >
+      <GeoLiteDownload @download-complete="handleGeoLiteDownloadComplete" />
+    </AModal>
   </ACard>
 </template>
 

+ 13 - 0
app/src/constants/errors/geolite.ts

@@ -0,0 +1,13 @@
+export default {
+  60000: () => $gettext('Failed to download GeoLite2 database: {0}'),
+  60001: () => $gettext('Failed to decompress GeoLite2 database: {0}'),
+  60002: () => $gettext('GeoLite2 database not found at {0}'),
+  60003: () => $gettext('Failed to get file size: {0}'),
+  60004: () => $gettext('Failed to create file: {0}'),
+  60005: () => $gettext('Failed to save downloaded file: {0}'),
+  60006: () => $gettext('Failed to open file: {0}'),
+  60007: () => $gettext('Failed to create xz reader: {0}'),
+  60008: () => $gettext('Failed to write decompressed data: {0}'),
+  60009: () => $gettext('Failed to read compressed data: {0}'),
+  60010: () => $gettext('Decompression succeeded but failed to delete compressed file: {0}'),
+}

+ 1 - 0
app/src/constants/errors/self_check.ts

@@ -21,4 +21,5 @@ export default {
   40417: () => $gettext('Access log path not exist'),
   40418: () => $gettext('Error log path not exist'),
   40419: () => $gettext('Conf.d directory not exists'),
+  40420: () => $gettext('GeoLite2 database not found at {0}. Log indexing requires GeoLite2 database for geographic IP analysis'),
 }

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 218 - 112
app/src/language/ar/app.po


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 220 - 110
app/src/language/de_DE/app.po


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 211 - 113
app/src/language/en/app.po


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 217 - 110
app/src/language/es/app.po


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 221 - 110
app/src/language/fr_FR/app.po


+ 2 - 0
app/src/language/generate.ts

@@ -4,6 +4,7 @@ export const msg = [
   $gettext('Certificate not found: %{error}'),
   $gettext('Certificate revoked successfully'),
   $gettext('Check if /var/run/docker.sock exists. If you are using Nginx UI Official Docker Image, please make sure the docker socket is mounted like this: `-v /var/run/docker.sock:/var/run/docker.sock`. Nginx UI official image uses /var/run/docker.sock to communicate with the host Docker Engine via Docker Client API. This feature is used to control Nginx in another container and perform container replacement rather than binary replacement during OTA upgrades of Nginx UI to ensure container dependencies are also upgraded. If you don\'t need this feature, please add the environment variable NGINX_UI_IGNORE_DOCKER_SOCKET=true to the container.'),
+  $gettext('Check if the GeoLite2 database is available when log indexing is enabled. The GeoLite2 database is required for geographic IP analysis in log indexing. You can download it from the Preference page or manually place GeoLite2-City.mmdb in the same directory as app.ini'),
   $gettext('Check if the conf.d directory is under the nginx configuration directory'),
   $gettext('Check if the nginx PID path exists. By default, this path is obtained from \'nginx -V\'. If it cannot be obtained, an error will be reported. In this case, you need to modify the configuration file to specify the Nginx PID path.Refer to the docs for more details: https://nginxui.com/zh_CN/guide/config-nginx.html#pidpath'),
   $gettext('Check if the nginx access log path exists. By default, this path is obtained from \'nginx -V\'. If it cannot be obtained or the obtained path does not point to a valid, existing file, an error will be reported. In this case, you need to modify the configuration file to specify the access log path.Refer to the docs for more details: https://nginxui.com/zh_CN/guide/config-nginx.html#accesslogpath'),
@@ -20,6 +21,7 @@ export const msg = [
   $gettext('Docker socket exists'),
   $gettext('Failed to delete certificate from database: %{error}'),
   $gettext('Failed to revoke certificate: %{error}'),
+  $gettext('GeoLite2 database available'),
   $gettext('Log file %{log_path} is not a regular file. If you are using nginx-ui in docker container, please refer to https://nginxui.com/zh_CN/guide/config-nginx-log.html for more information.'),
   $gettext('Nginx PID path exists'),
   $gettext('Nginx access log path exists'),

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 217 - 112
app/src/language/ja_JP/app.po


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 217 - 112
app/src/language/ko_KR/app.po


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 209 - 114
app/src/language/messages.pot


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 217 - 110
app/src/language/pt_PT/app.po


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 218 - 112
app/src/language/ru_RU/app.po


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 217 - 110
app/src/language/tr_TR/app.po


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 216 - 110
app/src/language/uk_UA/app.po


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 218 - 112
app/src/language/vi_VN/app.po


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 213 - 113
app/src/language/zh_CN/app.po


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 213 - 113
app/src/language/zh_TW/app.po


+ 1 - 0
internal/self_check/errors.go

@@ -26,4 +26,5 @@ var (
 	ErrAccessLogPathNotExist            = e.New(40417, "Access log path not exist")
 	ErrErrorLogPathNotExist             = e.New(40418, "Error log path not exist")
 	ErrConfdNotExists                   = e.New(40419, "Conf.d directory not exists")
+	ErrGeoLiteDBNotFound                = e.New(40420, "GeoLite2 database not found at {0}. Log indexing requires GeoLite2 database for geographic IP analysis")
 )

+ 26 - 0
internal/self_check/geolite.go

@@ -0,0 +1,26 @@
+package self_check
+
+import (
+	"github.com/0xJacky/Nginx-UI/internal/geolite"
+	"github.com/0xJacky/Nginx-UI/settings"
+	"github.com/uozi-tech/cosy"
+)
+
+func CheckGeoLiteDB() error {
+	// Only check if log indexing is enabled
+	if !settings.NginxLogSettings.IndexingEnabled {
+		return nil
+	}
+
+	if !geolite.DBExists() {
+		return cosy.WrapErrorWithParams(ErrGeoLiteDBNotFound, geolite.GetDBPath())
+	}
+
+	return nil
+}
+
+func FixGeoLiteDB() error {
+	// This is a placeholder function to mark the task as fixable
+	// The actual fix is handled by the frontend modal
+	return ErrTaskNotFixable
+}

+ 13 - 0
internal/self_check/tasks.go

@@ -4,6 +4,7 @@ import (
 	"github.com/0xJacky/Nginx-UI/internal/helper"
 	"github.com/0xJacky/Nginx-UI/internal/nginx"
 	"github.com/0xJacky/Nginx-UI/internal/translation"
+	"github.com/0xJacky/Nginx-UI/settings"
 	"github.com/elliotchance/orderedmap/v3"
 	"github.com/uozi-tech/cosy"
 )
@@ -152,6 +153,18 @@ func Init() {
 		})
 	}
 
+	if settings.NginxLogSettings.IndexingEnabled {
+		selfCheckTasks = append(selfCheckTasks, &Task{
+			Key:  "GeoLite-DB",
+			Name: translation.C("GeoLite2 database available"),
+			Description: translation.C("Check if the GeoLite2 database is available when log indexing is enabled. " +
+				"The GeoLite2 database is required for geographic IP analysis in log indexing. " +
+				"You can download it from the Preference page or manually place GeoLite2-City.mmdb in the same directory as app.ini"),
+			CheckFunc: CheckGeoLiteDB,
+			FixFunc:   FixGeoLiteDB,
+		})
+	}
+
 	for _, task := range selfCheckTasks {
 		selfCheckTaskMap.Set(task.Key, task)
 	}

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio