Merge pull request #2 from rustfs/base-route

feat: improve TypeScript type safety and code quality
This commit is contained in:
安正超
2025-07-10 23:20:55 +08:00
committed by GitHub
44 changed files with 14901 additions and 1026 deletions
+16 -1
View File
@@ -1,7 +1,22 @@
{
"permissions": {
"allow": [
"Bash(mkdir:*)"
"Bash(mkdir:*)",
"Bash(grep:*)",
"Bash(rg:*)",
"Bash(npm run dev:*)",
"Bash(pkill:*)",
"Bash(ls:*)",
"Bash(find:*)",
"Bash(npm install:*)",
"Bash(rm:*)",
"Bash(git checkout:*)",
"Bash(npm run type-check:*)",
"Bash(npm run lint)",
"Bash(mv:*)",
"Bash(cp:*)",
"Bash(npm run:*)",
"Bash(git add:*)"
],
"deny": []
}
+18
View File
@@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
[*.{json,yml,yaml}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
+3
View File
@@ -6,6 +6,9 @@ APP_DESCRIPTION="RustFS is a distributed file system written in Rust."
# Admin API base URL
API_BASE_URL=http://localhost:9000/rustfs/admin/v3
# Console base URL
BASE_URL=/rustfs/console/
# S3 API base URL
S3_ENDPOINT=http://localhost:9000
+7
View File
@@ -0,0 +1,7 @@
node_modules
dist
.nuxt
.output
.git
*.log
public
+12
View File
@@ -0,0 +1,12 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "es5",
"tabWidth": 2,
"useTabs": false,
"printWidth": 100,
"endOfLine": "lf",
"bracketSpacing": true,
"arrowParens": "avoid",
"vueIndentScriptAndStyle": false
}
+197 -17
View File
@@ -1,21 +1,201 @@
MIT License
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Copyright (c) 2024 rustfs
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
1. Definitions.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2024 Beijing Henghesha Technology Co., Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+316 -42
View File
@@ -1,75 +1,349 @@
# RustFS Console
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.8+-blue.svg)](https://www.typescriptlang.org/)
[![Vue.js](https://img.shields.io/badge/Vue.js-3.5+-green.svg)](https://vuejs.org/)
[![Nuxt](https://img.shields.io/badge/Nuxt-3.17+-green.svg)](https://nuxt.com/)
## Setup
A modern, responsive web management console for RustFS distributed file system, built with Vue 3, Nuxt 3, and TypeScript.
Make sure to install dependencies:
## ✨ Features
- 🚀 **Modern Stack**: Built with Vue 3, Nuxt 3, TypeScript, and Tailwind CSS
- 🎨 **Beautiful UI**: Clean and intuitive interface with dark mode support
- 🌍 **Internationalization**: Full i18n support with English and Chinese
- 📱 **Responsive Design**: Mobile-friendly design that works on all devices
-**High Performance**: Optimized with caching, lazy loading, and code splitting
- 🔒 **Enterprise Security**: Secure authentication and authorization
- 📊 **Rich Analytics**: Comprehensive monitoring and metrics dashboard
- 🛠 **Developer Experience**: Hot reload, TypeScript, ESLint, and Prettier
## 🎯 Core Functionalities
### Storage Management
- **Bucket Operations**: Create, delete, configure buckets
- **Object Management**: Upload, download, delete objects with batch operations
- **File Browser**: Intuitive file explorer with search and filtering
- **Access Control**: Granular permissions and bucket policies
### User & Access Management
- **User Management**: Create and manage users and groups
- **Access Keys**: Generate and manage API credentials
- **Policies**: Fine-grained access control policies
- **Authentication**: Multiple authentication methods (AccessKey, STS)
### System Monitoring
- **Performance Metrics**: Real-time system performance monitoring
- **Usage Analytics**: Storage usage and bandwidth statistics
- **Health Monitoring**: System health and status indicators
- **Audit Logs**: Comprehensive audit trail and logging
### Advanced Features
- **Lifecycle Management**: Automated data lifecycle policies
- **Replication**: Cross-region and site replication
- **Tiering**: Intelligent data tiering and archival
- **Encryption**: Data encryption at rest and in transit
- **Event Notifications**: Real-time event notifications
## 🛠 Technical Stack
- **Frontend Framework**: [Vue 3](https://vuejs.org/) with Composition API
- **Meta Framework**: [Nuxt 3](https://nuxt.com/) for SSR/SPA
- **Language**: [TypeScript](https://www.typescriptlang.org/) for type safety
- **Styling**: [Tailwind CSS](https://tailwindcss.com/) + [Naive UI](https://www.naiveui.com/)
- **State Management**: [Pinia](https://pinia.vuejs.org/) for reactive state
- **HTTP Client**: Custom AWS-compatible client with automatic signing
- **Build Tool**: [Vite](https://vitejs.dev/) for fast development and building
- **Package Manager**: npm/pnpm/yarn
## 🚀 Quick Start
### Prerequisites
- Node.js 18+
- npm/pnpm/yarn package manager
- RustFS backend server running
### Installation
```bash
# npm
# Clone the repository
git clone https://github.com/your-org/rustfs-console.git
cd rustfs-console
# Install dependencies
npm install
# pnpm
# or
pnpm install
# yarn
# or
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
### Development
```bash
# npm
# Start development server
npm run dev
# pnpm
# or
pnpm dev
# yarn
# or
yarn dev
# bun
bun run dev
```
## Production
The development server will start at `http://localhost:3000`.
Build the application for production:
### Environment Configuration
Create a `.env` file in the root directory:
```env
# Application Configuration
APP_NAME=RustFS
APP_DESCRIPTION=RustFS Console
BASE_URL=/rustfs/console/
# API Configuration
API_BASE_URL=http://localhost:9000/rustfs/admin/v3
SERVER_HOST=http://localhost:9000
# S3 Configuration
S3_REGION=us-east-1
S3_ENDPOINT=http://localhost:9000
# Session Configuration
SESSION_DURATION_SECONDS=43200
```
### Production Build
```bash
# npm
# Build for production
npm run build
# pnpm
# or
pnpm build
# yarn
# or
yarn build
# bun
bun run build
# Preview production build locally
npm run preview
```
Locally preview production build:
## 🔧 Development
### Project Structure
```
├── assets/ # Static assets (images, styles)
├── components/ # Vue components
│ ├── access-keys/ # Access key management
│ ├── buckets/ # Bucket operations
│ ├── object/ # Object management
│ ├── users/ # User management
│ └── ui/ # Reusable UI components
├── composables/ # Vue composables
├── layouts/ # Nuxt layouts
├── middleware/ # Route middleware
├── pages/ # File-based routing
├── plugins/ # Nuxt plugins
├── server/ # Server-side code
├── store/ # Pinia stores
├── types/ # TypeScript definitions
├── utils/ # Utility functions
└── nuxt.config.ts # Nuxt configuration
```
### Code Quality
We maintain high code quality standards with:
- **TypeScript**: Full type safety and better developer experience
- **ESLint**: Code linting and style enforcement
- **Prettier**: Automatic code formatting
- **Vue TSC**: Vue-specific TypeScript checking
```bash
# npm
npm run preview
# Run type checking
npm run type-check
# pnpm
pnpm preview
# Run linting
npm run lint
# yarn
yarn preview
# bun
bun run preview
# Fix linting issues
npm run lint:fix
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
### Key Architecture Patterns
- **Composables**: Reusable logic with Vue Composition API
- **Plugin System**: Modular configuration and service injection
- **Error Boundaries**: Comprehensive error handling and user feedback
- **Performance Optimization**: Smart caching, lazy loading, and code splitting
- **Security**: Input validation, XSS protection, and secure authentication
## 🎨 Customization
### Theming
The application supports light and dark themes. Customize themes in:
- `assets/css/tailwind.css` - Tailwind configuration
- `tailwind.config.ts` - Theme colors and design tokens
- `components/theme-switcher.vue` - Theme switching logic
### Internationalization
Add new languages by:
1. Creating locale files in `i18n/locales/`
2. Updating `nuxt.config.ts` locales configuration
3. Adding language switcher options
### Component Library
Built on Naive UI with custom components in `components/ui/`. Extend the design system by:
- Adding new components to `components/ui/`
- Following established naming conventions
- Including proper TypeScript definitions
## 🚀 Deployment
### Docker Deployment
```dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine AS runtime
WORKDIR /app
COPY --from=builder /app/.output ./.output
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]
```
### Environment Variables
Production deployment requires these environment variables:
- `API_BASE_URL`: RustFS backend API endpoint
- `SERVER_HOST`: RustFS server host
- `BASE_URL`: Application base path
- `NODE_ENV`: Set to `production`
### Build Optimization
For optimal production builds:
- Enable gzip/brotli compression
- Configure proper caching headers
- Use CDN for static assets
- Monitor bundle size and performance
## 🤝 Contributing
We welcome contributions! Please follow our contribution guidelines:
### Development Workflow
1. **Fork** the repository
2. **Clone** your fork locally
3. **Create** a feature branch: `git checkout -b feature/amazing-feature`
4. **Install** dependencies: `npm install`
5. **Make** your changes following our coding standards
6. **Test** your changes: `npm run type-check && npm run lint`
7. **Commit** your changes: `git commit -m 'feat: add amazing feature'`
8. **Push** to your branch: `git push origin feature/amazing-feature`
9. **Submit** a Pull Request
### Coding Standards
- **TypeScript**: Use TypeScript for all new code
- **Vue 3**: Use Composition API and `<script setup>` syntax
- **Naming**: Use kebab-case for files and components
- **Comments**: Add JSDoc comments for public APIs
- **Testing**: Include tests for new features
- **Performance**: Consider performance implications
### Code Style
We use automated code formatting:
```bash
# Format code
npm run lint:fix
# Check formatting
npm run lint
```
### Commit Convention
Follow [Conventional Commits](https://www.conventionalcommits.org/):
- `feat`: New features
- `fix`: Bug fixes
- `docs`: Documentation changes
- `style`: Code style changes
- `refactor`: Code refactoring
- `test`: Test additions/modifications
- `chore`: Build process or auxiliary tool changes
### Pull Request Guidelines
- **Title**: Use clear, descriptive titles
- **Description**: Explain what changes were made and why
- **Testing**: Describe how the changes were tested
- **Breaking Changes**: Clearly mark any breaking changes
- **Screenshots**: Include screenshots for UI changes
### Issue Reporting
When reporting issues:
1. **Search** existing issues first
2. **Use** the issue templates
3. **Provide** detailed reproduction steps
4. **Include** system information and error logs
5. **Add** relevant labels
### Development Setup
```bash
# Clone and setup
git clone https://github.com/your-username/rustfs-console.git
cd rustfs-console
npm install
# Create feature branch
git checkout -b feature/your-feature
# Start development
npm run dev
# Run checks before committing
npm run type-check
npm run lint
```
## 📄 License
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
## 🙏 Acknowledgments
- [Vue.js](https://vuejs.org/) - The progressive JavaScript framework
- [Nuxt](https://nuxt.com/) - The intuitive Vue framework
- [Naive UI](https://www.naiveui.com/) - A Vue 3 component library
- [Tailwind CSS](https://tailwindcss.com/) - A utility-first CSS framework
- [Iconify](https://iconify.design/) - Universal icon framework
## 📞 Support
- **Documentation**: [RustFS Docs](https://docs.rustfs.com)
- **Community**: [GitHub Discussions](https://github.com/your-org/rustfs-console/discussions)
- **Issues**: [GitHub Issues](https://github.com/your-org/rustfs-console/issues)
- **Email**: support@rustfs.com
---
Made with ❤️ by the RustFS team
+2 -1
View File
@@ -1,6 +1,7 @@
import navs from '~/config/navs'
import type { AppConfig } from '~/types/app-config'
export default defineAppConfig({
export default defineAppConfig<AppConfig>({
navs: navs,
name: process.env.APP_NAME || 'RustFS',
description: process.env.APP_DESCRIPTION || 'RustFS is a distributed file system written in Rust.',
+1 -1
View File
@@ -288,7 +288,7 @@ const getObjectLockConfig = async () => {
/**********object lock ***********************/
/******** policy ***********************/
import { setBucketPolicy, getBucketPolicy as getBucketPolicyFn } from "~/utils/bucketPolicy";
import { setBucketPolicy, getBucketPolicy as getBucketPolicyFn } from "~/utils/bucket-policy";
const bucketPolicy = ref("public");
const getbucketPolicy = async () => {
+12 -1
View File
@@ -125,7 +125,18 @@ import {
NInputNumber,
NSelect,
} from "naive-ui";
import { randomUUID } from "uncrypto";
// 使用 Web Crypto API 或 Node.js crypto 模块
const randomUUID = () => {
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID()
}
// 兜底方案
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0
const v = c === 'x' ? r : (r & 0x3 | 0x8)
return v.toString(16)
})
}
import { ref } from "vue";
import { useI18n } from "vue-i18n";
const { putBucketLifecycleConfiguration, getBucketVersioning, getBucketLifecycleConfiguration } = useBucket({});
+28 -44
View File
@@ -3,32 +3,39 @@ import { useI18n } from "vue-i18n";
import { RouterLink } from "vue-router";
import { useSidebarStore } from "~/store/sidebar";
import type { SiteConfig } from "~/types/config";
import type { AppConfig, NavItem } from "~/types/app-config";
const { t } = useI18n();
const appConfig = useAppConfig();
const siteConfig = useNuxtApp().$siteConfig as unknown as SiteConfig;
const appConfig = useAppConfig() as unknown as AppConfig;
const route = useRoute();
const sidebarStore = useSidebarStore();
const isCollapsed = computed(() => sidebarStore.isCollapsed);
// 安全地获取 siteConfig,如果失败则使用默认值
let siteConfig: SiteConfig;
try {
siteConfig = useNuxtApp().$siteConfig as SiteConfig;
} catch (error) {
console.warn('Failed to load siteConfig, using defaults:', error);
siteConfig = {
serverHost: window.location.origin,
api: { baseURL: `${window.location.origin}/rustfs/admin/v3` },
s3: {
endpoint: window.location.origin,
region: 'us-east-1',
accessKeyId: '',
secretAccessKey: ''
}
};
}
const toggleSidebar = () => {
sidebarStore.toggleSidebar();
};
const options = computed(() => {
return appConfig.navs.map(
(nav: {
label: string;
to?: string;
type?: string;
icon?: string;
target?: string;
children?: {
label: string;
to: string;
icon?: string;
}[];
}) => {
// 缓存导航配置的生成逻辑,避免每次重新计算
const navOptions = computed(() => {
return appConfig.navs.map((nav: NavItem) => {
let item: {
key: string;
type?: string;
@@ -59,7 +66,7 @@ const options = computed(() => {
return {
key: child.label,
type: "item",
label: () => h(RouterLink, { to: child.to }, { default: () => t(child.label) }),
label: () => h(RouterLink, { to: child.to || '/' }, { default: () => t(child.label) }),
icon: child.icon ? iconRender(child.icon) : undefined,
};
});
@@ -69,6 +76,9 @@ const options = computed(() => {
}
);
});
// 直接使用计算属性,不需要 readonly 包装
const menuOptions = navOptions;
</script>
<template>
<n-layout-sider
@@ -90,17 +100,6 @@ const options = computed(() => {
</n-avatar>
<h2 v-else class="text-center text-2xl flex">
<img src="~/assets/logo.svg" class="max-w-28" alt="" />
<span
v-if="siteConfig.license.name && siteConfig.license.expired != 0"
class="flex items-center justify-center text-[9px] leading-[9px] ml-1 p-1 bg-orange-600 text-white rounded rounded-bl-none">
{{ t("PRO") }}
</span>
<span
v-else
class="flex items-center justify-center text-[9px] leading-[9px] ml-1 p-1 bg-green-600 text-white rounded rounded-bl-none">
{{ t("OSS") }}
</span>
<span class="sr-only">{{ appConfig.name }}</span>
</h2>
</div>
@@ -115,7 +114,7 @@ const options = computed(() => {
:root-indent="12"
:collapsed-width="64"
:collapsed-icon-size="22"
:options="options"
:options="menuOptions"
default-expand-all
class="flex-1" />
</div>
@@ -124,21 +123,6 @@ const options = computed(() => {
<Icon name="ri:menu-unfold-fill" class="cursor-pointer text-xl" @click="toggleSidebar" :title="t('Expand')" />
</div>
<div v-if="!isCollapsed" class="flex flex-col p-4 text-gray-500">
<div class="flex items-center gap-2">
<Icon name="ri-server-line" />
<span>{{ t("Version") }}: {{ siteConfig.release.version }}</span>
</div>
<div class="flex items-center gap-2">
<Icon name="ri-calendar-line" />
<span>{{ t("Build Date") }}{{ siteConfig.release.date }}</span>
</div>
<div class="flex items-center gap-2">
<Icon name="ri-verified-badge-line" />
<span>{{ t("License") }}{{ siteConfig.license.name }}</span>
</div>
</div>
<div class="border-t dark:border-neutral-800 p-2" :class="{ 'flex justify-between items-center': !isCollapsed }">
<!-- 语言切换组件 -->
<div class="px-2 py-2">
+1 -1
View File
@@ -41,7 +41,7 @@
</template>
<script setup lang="ts">
import { download } from '@/utils/exportFile'
import { download } from '@/utils/export-file'
const visible = ref(false)
const accessKey = ref('')
+1 -1
View File
@@ -1,7 +1,7 @@
export const useAccessKeys = () => {
const { $api } = useNuxtApp()
const listUserServiceAccounts = async (params: object) => {
const listUserServiceAccounts = async (params: Record<string, string> = {}) => {
return await $api.get("/list-service-accounts", { params })
}
+3 -3
View File
@@ -17,7 +17,7 @@ export function useAuth() {
}
const getCredentials = () => {
if (!isValiedCredentials(store.value)) {
if (!isValidCredentials(store.value)) {
return
}
@@ -26,7 +26,7 @@ export function useAuth() {
const isExpired = (expiration: string) => expiration ? new Date(expiration) < new Date() : false
const isValiedCredentials = (credentials: Credentials) => {
const isValidCredentials = (credentials: Credentials) => {
return !!credentials?.AccessKeyId && !!credentials?.SecretAccessKey && !!credentials?.SessionToken && credentials?.Expiration && !isExpired(credentials.Expiration)
}
@@ -58,6 +58,6 @@ export function useAuth() {
logout,
logoutAndRedirect,
credentials: ref<Credentials | undefined>(getCredentials()),
isAuthenticated: ref(isValiedCredentials(store.value)),
isAuthenticated: ref(isValidCredentials(store.value)),
}
}
+4 -8
View File
@@ -1,4 +1,6 @@
// icons: https://icones.js.org/collection/ri
import type { NavItem } from '~/types/app-config'
export default [
{
label: "Browser",
@@ -73,15 +75,9 @@ export default [
key: "divider-2",
type: "divider",
},
{
label: "Settings",
to: "/settings",
icon: "ri:settings-3-line",
},
{
label: "License",
// to: 'https://github.com/rustfs/rustfs?tab=Apache-2.0-1-ov-file#readme',
to: "/licenseinfo",
to: "/license",
icon: "ri:copyright-line",
},
{
@@ -90,4 +86,4 @@ export default [
target: "_blank",
icon: "ri:file-list-3-line",
},
];
] as NavItem[];
+34 -17
View File
@@ -133,20 +133,9 @@
"Welcome Back": "Welcome Back",
"Login Problems": "Login Problems",
"Bucket Not Empty": "Bucket Not Empty",
"Server Configuration": "Server Configuration",
"Server URL": "Server URL",
"Enter complete server URL (e.g., http://localhost:9000)": "Enter complete server URL (e.g., http://localhost:9000)",
"Server URL is required": "Server URL is required",
"Only HTTP and HTTPS protocols are supported": "Only HTTP and HTTPS protocols are supported",
"Invalid hostname": "Invalid hostname",
"Invalid URL format": "Invalid URL format",
"Protocol": "Protocol",
"Host": "Host",
"Port": "Port",
"Server configuration saved": "Server configuration saved",
"Save Configuration": "Save Configuration",
"Clear Configuration": "Clear Configuration",
"Generated Configuration": "Generated Configuration",
"Current Configuration": "Current Configuration",
"Information": "Information",
"The system automatically uses the current host as the API base URL. No manual configuration is required.": "The system automatically uses the current host as the API base URL. No manual configuration is required.",
"Upload File": "Upload File",
"Folder": "Folder",
"Update Time": "Update Time",
@@ -371,7 +360,35 @@
"Please enter bandwidth": "Please enter bandwidth",
"Please enter storage type": "Please enter storage type",
"Add Replication Rule": "Add Replication Rule",
"No valid server configuration detected, please check server configuration": "No valid server configuration detected, please check server configuration",
"Login failed, please check server configuration": "Login failed, please check server configuration",
"Click to modify": "Click to modify"
"Click to modify": "Click to modify",
"Server Configuration": "Server Configuration",
"Please configure your RustFS server address": "Please configure your RustFS server address",
"Server Address": "Server Address",
"Please enter server address (e.g., http://localhost:9000)": "Please enter server address (e.g., http://localhost:9000)",
"Example: http://localhost:9000 or https://your-domain.com": "Example: http://localhost:9000 or https://your-domain.com",
"Save Configuration": "Save Configuration",
"Skip": "Skip",
"Need help?": "Need help?",
"View Documentation": "View Documentation",
"Need to configure server?": "Need to configure server?",
"Please enter server address": "Please enter server address",
"Server configuration saved successfully": "Server configuration saved successfully",
"Invalid server address format": "Invalid server address format",
"Settings": "Settings",
"Current Configuration": "Current Configuration",
"Not configured": "Not configured",
"API Base URL": "API Base URL",
"S3 Endpoint": "S3 Endpoint",
"S3 Region": "S3 Region",
"Reset to Default": "Reset to Default",
"Configuration Information": "Configuration Information",
"Configuration is saved locally in your browser": "Configuration is saved locally in your browser",
"Page will refresh automatically after saving configuration": "Page will refresh automatically after saving configuration",
"Make sure the server address is accessible from your network": "Make sure the server address is accessible from your network",
"Failed to load current configuration": "Failed to load current configuration",
"Configuration saved successfully": "Configuration saved successfully",
"Configuration reset successfully": "Configuration reset successfully",
"Reset to default successfully": "Reset to default successfully",
"Leave empty to use current host as default": "Leave empty to use current host as default"
}
+35 -17
View File
@@ -154,20 +154,9 @@
"Login Failed": "登录失败",
"Login Problems": "登录遇到问题?",
"Get Help": "获取帮助",
"Server Configuration": "服务器配置",
"Server URL": "服务器地址",
"Enter complete server URL (e.g., http://localhost:9000)": "请输入完整的服务器地址(例如:http://localhost:9000",
"Server URL is required": "服务器地址不能为空",
"Only HTTP and HTTPS protocols are supported": "仅支持 HTTP 和 HTTPS 协议",
"Invalid hostname": "无效的主机名",
"Invalid URL format": "无效的URL格式",
"Protocol": "协议",
"Host": "主机",
"Port": "端口",
"Server configuration saved": "服务器配置已保存",
"Save Configuration": "保存配置",
"Clear Configuration": "清除配置",
"Generated Configuration": "生成的配置",
"Current Configuration": "当前配置",
"Information": "信息",
"The system automatically uses the current host as the API base URL. No manual configuration is required.": "系统自动使用当前主机作为API基础URL,无需手动配置。",
"Create Bucket": "创建存储桶",
"Creation Date": "创建时间",
"Bucket Not Empty": "当前桶不为空,请先删除桶内容",
@@ -553,8 +542,7 @@
"Expire": "过期",
"On": "开启",
"Off": "关闭",
"No valid server configuration detected, please check server configuration": "未检测到有效服务器配置,请检查服务器配置",
"Login failed, please check server configuration": "登录失败,请检查服务器配置",
"Click to modify": "点击修改",
"Replicate existing objects": "复制现有对象",
"Sync metadata": "同步元数据",
@@ -564,5 +552,35 @@
"Please enter health check duration": "请输入健康检查时长",
"Please enter bandwidth": "请输入带宽",
"Please enter storage type": "请输入存储类型",
"Add Replication Rule": "添加复制规则"
"Add Replication Rule": "添加复制规则",
"Server Configuration": "服务器配置",
"Please configure your RustFS server address": "请配置您的RustFS服务器地址",
"Server Address": "服务器地址",
"Please enter server address (e.g., http://localhost:9000)": "请输入服务器地址(例如:http://localhost:9000",
"Example: http://localhost:9000 or https://your-domain.com": "示例:http://localhost:9000 或 https://your-domain.com",
"Save Configuration": "保存配置",
"Skip": "跳过",
"Need help?": "需要帮助?",
"View Documentation": "查看文档",
"Need to configure server?": "需要配置服务器?",
"Please enter server address": "请输入服务器地址",
"Server configuration saved successfully": "服务器配置保存成功",
"Invalid server address format": "无效的服务器地址格式",
"Settings": "设置",
"Current Configuration": "当前配置",
"Not configured": "未配置",
"API Base URL": "API基础URL",
"S3 Endpoint": "S3端点",
"S3 Region": "S3区域",
"Reset to Default": "重置为默认",
"Configuration Information": "配置信息",
"Configuration is saved locally in your browser": "配置保存在您的浏览器本地",
"Page will refresh automatically after saving configuration": "保存配置后页面将自动刷新",
"Make sure the server address is accessible from your network": "确保服务器地址在您的网络中可访问",
"Failed to load current configuration": "加载当前配置失败",
"Configuration saved successfully": "配置保存成功",
"Configuration reset successfully": "配置重置成功",
"Reset to default successfully": "重置为默认成功",
"Leave empty to use current host as default": "留空表示使用当前主机作为默认值"
}
+32 -40
View File
@@ -1,16 +1,30 @@
import { joinURL } from "ufo";
import type { AwsClient } from "./aws4fetch";
import { parseApiError } from "~/utils/error-handler";
import { logger } from "~/utils/logger";
interface ApiClientOptions {
baseUrl?: string;
headers?: Record<string, string>;
}
interface RequestOptions {
method?: string;
headers?: Record<string, string>;
body?: any;
params?: Record<string, string>;
}
class ApiClient {
private $api: any;
private config?: { baseUrl?: string; headers?: Record<string, string> };
private $api: AwsClient;
private config?: ApiClientOptions;
constructor(api: AwsClient, options?: any) {
constructor(api: AwsClient, options?: ApiClientOptions) {
this.$api = api;
this.config = options;
}
async request(url: string, options: any = {}, parseJson: boolean = true) {
async request(url: string, options: RequestOptions = {}, parseJson: boolean = true) {
url = this.config?.baseUrl ? joinURL(this.config?.baseUrl, url) : url;
options.headers = { ...this.config?.headers, ...options.headers };
// 处理body的数据格式
@@ -22,37 +36,15 @@ class ApiClient {
delete options.params; // 删除params,以免影响fetch的options
}
console.log("[request] url:", url);
console.log("[request] options:", options);
logger.log("[request] url:", url);
logger.log("[request] options:", options);
const response = await this.$api.fetch(url, options);
console.log("[request] response:", response);
logger.log("[request] response:", response);
if (!response.ok) {
let errorMsg = response.statusText;
try {
// 优先尝试解析为 JSON
const errorData = await response.clone().json();
errorMsg = errorData.message || JSON.stringify(errorData) || errorMsg;
} catch (e) {
try {
// 如果不是 JSON,尝试解析为文本
const text = await response.clone().text();
if (text) {
// 检查是否为 XML
if (text.trim().startsWith("<")) {
// 简单提取 <Message> 或 <Error> 标签内容
const match = text.match(/<Message>(.*?)<\/Message>/i) || text.match(/<Error>(.*?)<\/Error>/i);
errorMsg = match ? match[1] : text;
} else {
errorMsg = text;
}
}
} catch (e2) {
// 保持原有 statusText
}
}
const errorMsg = await parseApiError(response);
throw new Error(errorMsg);
}
@@ -78,7 +70,7 @@ class ApiClient {
}
}
async *streamRequest(url: string, options: any = {}) {
async *streamRequest(url: string, options: RequestOptions = {}) {
const response = await this.request(url, options, false);
if (!response.body) {
@@ -96,39 +88,39 @@ class ApiClient {
const data = JSON.parse(chunk);
yield data; // 使用 yield 返回数据
} catch (error) {
console.error("Failed to parse chunk:", error);
logger.error("Failed to parse chunk:", error);
}
}
}
async get(url: string, options?: any) {
async get(url: string, options?: RequestOptions) {
return this.request(url, { method: "GET", ...options });
}
async post(url: string, body: any, options?: any) {
async post(url: string, body: any, options?: RequestOptions) {
return this.request(url, { method: "POST", body, ...options });
}
async delete(url: string, options?: any) {
async delete(url: string, options?: RequestOptions) {
return this.request(url, { method: "DELETE", ...options });
}
async put(url: string, body: any, options?: any) {
async put(url: string, body: any, options?: RequestOptions) {
return this.request(url, { method: "PUT", body, ...options });
}
async patch(url: string, body: any, options?: any) {
async patch(url: string, body: any, options?: RequestOptions) {
return this.request(url, { method: "PATCH", body, ...options });
}
async head(url: string, options?: any) {
async head(url: string, options?: RequestOptions) {
return this.request(url, { method: "HEAD", ...options });
}
async options(url: string, options?: any) {
async options(url: string, options?: RequestOptions) {
return this.request(url, { method: "OPTIONS", ...options });
}
async trace(url: string, options?: any) {
async trace(url: string, options?: RequestOptions) {
return this.request(url, { method: "TRACE", ...options });
}
}
+15 -12
View File
@@ -1,6 +1,9 @@
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
import type { AwsCredentialIdentity, AwsCredentialIdentityProvider } from "@aws-sdk/types";
import type { SiteConfig } from "~/types/config";
import { AssumeRoleCommand, STSClient } from '@aws-sdk/client-sts'
import type {
AwsCredentialIdentity,
AwsCredentialIdentityProvider
} from '@aws-sdk/types'
import type { SiteConfig } from '~/types/config'
/**
* 获取 STS 临时凭证并返回
@@ -15,30 +18,30 @@ export async function getStsToken(
roleArn: string,
customConfig?: SiteConfig
) {
const siteConfig = customConfig || (useNuxtApp().$siteConfig as SiteConfig);
const siteConfig = customConfig || (useNuxtApp().$siteConfig as SiteConfig)
console.log('S3 siteConfig', siteConfig);
console.log('S3 siteConfig', siteConfig)
// 1. 创建 STS 客户端
const stsClient = new STSClient({
endpoint: siteConfig.s3.endpoint,
region: siteConfig.s3.region || 'us-east-1',
credentials: credentials
});
})
// 2. 构建 AssumeRole 请求
// 你可以根据需要额外添加 DurationSeconds、Policy 等参数
const command = new AssumeRoleCommand({
RoleArn: roleArn,
RoleSessionName: "console", // 自定义角色会话名称
DurationSeconds: siteConfig.session?.durationSeconds || 3600 * 12, // 临时凭证有效期
});
RoleSessionName: 'console', // 自定义角色会话名称
DurationSeconds: siteConfig.session?.durationSeconds || 3600 * 12 // 临时凭证有效期
})
const response = await stsClient.send(command);
const response = await stsClient.send(command)
if (!response.Credentials) {
throw new Error("Failed to retrieve credentials");
throw new Error('Failed to retrieve credentials')
}
return response.Credentials;
return response.Credentials
}
+3 -2
View File
@@ -3,8 +3,9 @@
export default defineNuxtRouteMiddleware((to, from) => {
const isAuthenticated = useAuth().isAuthenticated.value
if (to.path === '/auth/login') {
if (isAuthenticated) {
// 允许访问登录页面和配置页面
if (to.path === '/auth/login' || to.path === '/config') {
if (isAuthenticated && to.path === '/auth/login') {
return navigateTo('/')
}
+10 -10
View File
@@ -1,9 +1,9 @@
import AutoImport from 'unplugin-auto-import/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
import Components from 'unplugin-vue-components/vite'
const isDev = process.env.NODE_ENV === 'development'
const appName = process.env.APP_NAME || 'RustFS'
const baseUrl = (process.env.BASE_URL || '/rustfs/console/').replace(/\/$/, '')
const appDescription = process.env.APP_DESCRIPTION || 'RustFS is a distributed file system written in Rust.'
// https://nuxt.com/docs/api/configuration/nuxt-config
@@ -13,20 +13,18 @@ export default defineNuxtConfig({
compatibilityDate: '2025-04-13',
devtools: { enabled: true },
app: {
baseURL: baseUrl,
head: {
title: appName,
meta: [
{ name: 'description', content: appDescription },
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
// <link rel="icon" type = "image/png" sizes = "32x32" href = "/favicon-32x32.png" >
{ rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32x32.png' },
// <link rel="icon" type = "image/png" sizes = "16x16" href = "/favicon-16x16.png" >
{ rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicon-16x16.png' },
// <link rel="apple-touch-icon" sizes = "180x180" href = "/apple-touch-icon.png" >
{ rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png' },
{ rel: 'manifest', href: '/site.webmanifest' },
{ rel: 'icon', type: 'image/x-icon', href: baseUrl + '/favicon.ico' },
{ rel: 'icon', type: 'image/png', sizes: '32x32', href: baseUrl + '/favicon-32x32.png' },
{ rel: 'icon', type: 'image/png', sizes: '16x16', href: baseUrl + '/favicon-16x16.png' },
{ rel: 'apple-touch-icon', sizes: '180x180', href: baseUrl + '/apple-touch-icon.png' },
{ rel: 'manifest', href: baseUrl + '/site.webmanifest' },
],
charset: 'utf-8',
viewport: 'width=device-width, initial-scale=1',
@@ -46,7 +44,9 @@ export default defineNuxtConfig({
durationSeconds: Number(process.env.SESSION_DURATION_SECONDS) || 3600 * 12
},
// 下面部分修改的时候记得同步修改 public/config.json
// 服务器地址
serverHost: process.env.SERVER_HOST || '',
// admin API 请求基础 URL
api: {
baseURL: process.env.API_BASE_URL || ''
+8 -36
View File
@@ -9,13 +9,13 @@
"dependencies": {
"@aws-sdk/client-s3": "^3.750.0",
"@aws-sdk/client-sts": "^3.777.0",
"@aws-sdk/protocol-http": "^3.374.0",
"@aws-sdk/s3-request-presigner": "^3.715.0",
"@nuxt/icon": "^1.9.1",
"@nuxt/image": "^1.10.0",
"@nuxtjs/i18n": "^9.1.1",
"@nuxtjs/tailwindcss": "^6.12.2",
"@pinia/nuxt": "^0.9.0",
"@smithy/protocol-http": "^5.1.2",
"@tanstack/vue-table": "^8.21.2",
"@types/three": "^0.175.0",
"@vueuse/integrations": "^12.0.0",
@@ -777,38 +777,6 @@
"node": ">=18.0.0"
}
},
"node_modules/@aws-sdk/protocol-http": {
"version": "3.374.0",
"license": "Apache-2.0",
"dependencies": {
"@smithy/protocol-http": "^1.1.0",
"tslib": "^2.5.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@aws-sdk/protocol-http/node_modules/@smithy/protocol-http": {
"version": "1.2.0",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^1.2.0",
"tslib": "^2.5.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@aws-sdk/protocol-http/node_modules/@smithy/types": {
"version": "1.2.0",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.5.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@aws-sdk/region-config-resolver": {
"version": "3.775.0",
"license": "Apache-2.0",
@@ -3694,10 +3662,12 @@
}
},
"node_modules/@smithy/protocol-http": {
"version": "5.1.0",
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.2.tgz",
"integrity": "sha512-rOG5cNLBXovxIrICSBm95dLqzfvxjEmuZx4KK3hWwPFHGdW3lxY0fZNXfv2zebfRO7sJZ5pKJYHScsqopeIWtQ==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.2.0",
"@smithy/types": "^4.3.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -3782,7 +3752,9 @@
}
},
"node_modules/@smithy/types": {
"version": "4.2.0",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.1.tgz",
"integrity": "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
+34 -29
View File
@@ -7,63 +7,68 @@
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
"postinstall": "nuxt prepare",
"lint": "vue-tsc --noEmit && prettier --check .",
"lint:fix": "prettier --write .",
"type-check": "vue-tsc --noEmit"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.750.0",
"@aws-sdk/client-sts": "^3.777.0",
"@aws-sdk/protocol-http": "^3.374.0",
"@aws-sdk/s3-request-presigner": "^3.715.0",
"@nuxt/icon": "^1.9.1",
"@aws-sdk/client-s3": "^3.844.0",
"@aws-sdk/client-sts": "^3.844.0",
"@aws-sdk/s3-request-presigner": "^3.844.0",
"@nuxt/icon": "^1.15.0",
"@nuxt/image": "^1.10.0",
"@nuxtjs/i18n": "^9.1.1",
"@nuxtjs/tailwindcss": "^6.12.2",
"@nuxtjs/i18n": "^9.5.6",
"@nuxtjs/tailwindcss": "^6.14.0",
"@pinia/nuxt": "^0.9.0",
"@tanstack/vue-table": "^8.21.2",
"@smithy/protocol-http": "^5.1.2",
"@tanstack/vue-table": "^8.21.3",
"@types/three": "^0.175.0",
"@vueuse/integrations": "^12.0.0",
"@vueuse/integrations": "^12.8.2",
"aws-sdk": "^2.1692.0",
"axios": "^1.7.9",
"axios": "^1.10.0",
"clipboard": "^2.0.11",
"dayjs": "^1.11.13",
"dotted-map": "^2.2.3",
"echarts": "^5.6.0",
"json-editor-vue": "^0.18.1",
"lodash": "^4.17.21",
"lucide-vue-next": "^0.487.0",
"motion-v": "^1.0.0-beta.2",
"motion-v": "^1.5.0",
"node-forge": "^1.3.1",
"nuxt": "^3.16.2",
"nuxt": "^3.17.6",
"nuxtjs-naive-ui": "^1.0.2",
"pinia": "^2.3.0",
"postprocessing": "^6.37.2",
"strip-json-comments": "^5.0.1",
"pinia": "^2.3.1",
"postprocessing": "^6.37.6",
"strip-json-comments": "^5.0.2",
"tailwindcss": "^3.4.17",
"three": "^0.175.0",
"three-globe": "^2.42.3",
"ufo": "^1.5.4",
"three-globe": "^2.43.0",
"ufo": "^1.6.1",
"universal-cookie": "^7.2.2",
"vue": "^3.5.11",
"vue-router": "^4.0.12"
"vue": "^3.5.17",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@iconify-json/ri": "^1.2.3",
"@aws-sdk/types": "^3.840.0",
"@iconify-json/ri": "^1.2.5",
"@inspira-ui/plugins": "^0.0.1",
"@types/aws4": "^1.11.6",
"@types/node-forge": "^1.3.11",
"@vueuse/core": "^13.1.0",
"@vueuse/nuxt": "^13.1.0",
"@types/node-forge": "^1.3.12",
"@vueuse/core": "^13.5.0",
"@vueuse/nuxt": "^13.5.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"naive-ui": "^2.40.3",
"lodash": "^4.17.21",
"naive-ui": "^2.42.0",
"patch-package": "^8.0.0",
"tailwind-merge": "^3.2.0",
"prettier": "^3.2.5",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.7.2",
"typescript": "^5.8.3",
"unplugin-auto-import": "^0.18.6",
"unplugin-vue-components": "^0.27.5",
"vfonts": "^0.0.3",
"vue-tsc": "^2.1.10"
"vue-tsc": "^2.2.12"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "4.9.5"
+11 -273
View File
@@ -4,7 +4,6 @@ await setPageLayout('plain')
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import { configManager } from '~/utils/config'
const { t } = useI18n()
const router = useRouter()
@@ -22,242 +21,26 @@ const sts = ref({
sessionToken: '',
})
const serverConfig = ref({
protocol: 'http',
host: 'localhost',
port: '9000',
region: 'us-east-1'
})
const message = useMessage()
const auth = useAuth()
// 控制服务器配置折叠面板展开
const expandedNames = ref<string[]>([])
// 警告提示内容
const configAlert = ref('')
// 检查是否需要默认展开服务器配置
onMounted(async () => {
// 使用 configManager 加载配置(按优先级:config.json > localStorage > 默认值)
const config = await configManager.loadConfig()
if (config) {
const serverConfigData = configManager.extractServerConfig(config)
if (serverConfigData) {
// 更新 serverConfig
serverConfig.value = serverConfigData
// 更新 serverUrl
serverUrl.value = `${serverConfigData.protocol}://${serverConfigData.host}:${serverConfigData.port}`
}
}
// 如果没有有效配置,设置默认 serverUrl
if (!serverUrl.value) {
serverUrl.value = `${serverConfig.value.protocol}://${serverConfig.value.host}:${serverConfig.value.port}`
}
if (route.query.showConfig === '1') {
expandedNames.value = ['server-config']
configAlert.value = t('No valid server configuration detected, please check server configuration')
}
})
const handleLogin = async () => {
// 验证并解析URL
validateAndParseUrl()
if (urlValidationStatus.value === 'error') {
expandedNames.value = ['server-config']
return
}
const credentials = method.value === 'accessKeyAndSecretKey' ? accessKeyAndSecretKey.value : sts.value
try {
// 构建当前配置
const nuxtApp = useNuxtApp()
const baseConfig = nuxtApp.$siteConfig
const currentConfig = {
...baseConfig,
api: {
baseURL: `${serverConfig.value.protocol}://${serverConfig.value.host}:${serverConfig.value.port}/rustfs/admin/v3`
},
s3: {
endpoint: `${serverConfig.value.protocol}://${serverConfig.value.host}:${serverConfig.value.port}`,
region: serverConfig.value.region
}
}
// 使用配置管理器获取配置(优先使用localStorage中的配置
const { configManager } = await import('~/utils/config')
const currentConfig = await configManager.loadConfig()
await auth.login(credentials, currentConfig)
// 登录成功后重新初始化插件,确保使用新的配置
await reinitializePlugins()
message.success(t('Login Success'))
// 重新加载页面以确保新的配置生效
window.location.reload()
} catch (error) {
expandedNames.value = ['server-config']
configAlert.value = t('Login failed, please check server configuration')
message.error(t('Login Failed'))
}
}
const serverUrl = ref('')
const urlValidationStatus = ref<'success' | 'warning' | 'error' | undefined>(undefined)
const urlError = ref('')
const validateAndParseUrl = () => {
const url = serverUrl.value.trim()
if (!url) {
urlValidationStatus.value = 'error'
urlError.value = t('Server URL is required')
return
}
try {
const urlObj = new URL(url)
// 验证协议
if (!['http:', 'https:'].includes(urlObj.protocol)) {
urlValidationStatus.value = 'error'
urlError.value = t('Only HTTP and HTTPS protocols are supported')
return
}
// 验证主机
if (!urlObj.hostname) {
urlValidationStatus.value = 'error'
urlError.value = t('Invalid hostname')
return
}
// 解析并更新配置
serverConfig.value.protocol = urlObj.protocol.replace(':', '')
serverConfig.value.host = urlObj.hostname
serverConfig.value.port = urlObj.port || (urlObj.protocol === 'https:' ? '443' : '80')
urlValidationStatus.value = 'success'
urlError.value = ''
// 立即保存配置并更新 $siteConfig
saveConfigAndUpdateSiteConfig()
} catch (error) {
urlValidationStatus.value = 'error'
urlError.value = t('Invalid URL format')
}
}
// 保存配置并更新 $siteConfig 的函数
const saveConfigAndUpdateSiteConfig = async () => {
try {
// 保存配置到 localStorage
const saved = await configManager.saveConfig({
protocol: serverConfig.value.protocol,
host: serverConfig.value.host,
port: serverConfig.value.port,
region: serverConfig.value.region
})
if (saved) {
// 动态更新 $siteConfig
const nuxtApp = useNuxtApp()
const currentConfig = nuxtApp.$siteConfig
const updatedConfig = {
...currentConfig,
api: {
baseURL: `${serverConfig.value.protocol}://${serverConfig.value.host}:${serverConfig.value.port}/rustfs/admin/v3`
},
s3: {
endpoint: `${serverConfig.value.protocol}://${serverConfig.value.host}:${serverConfig.value.port}`,
region: serverConfig.value.region
}
}
// 更新 $siteConfig
nuxtApp.$siteConfig = updatedConfig
// 重新初始化插件以使用新的配置
await reinitializePlugins()
}
} catch (error) {
console.warn('Failed to save config:', error)
}
}
// 重新初始化插件的函数
const reinitializePlugins = async () => {
try {
const nuxtApp = useNuxtApp()
// 清除现有的客户端
if ('$api' in nuxtApp) {
delete (nuxtApp as any).$api
}
if ('$s3Client' in nuxtApp) {
delete (nuxtApp as any).$s3Client
}
// 重新运行插件
// 注意:这里我们需要手动重新创建客户端,因为插件只在应用启动时运行一次
const { credentials, isAuthenticated } = useAuth()
if (isAuthenticated.value && credentials.value) {
const siteConfig = nuxtApp.$siteConfig
// 重新创建 API 客户端
const { AwsClient } = await import('~/lib/aws4fetch')
const ApiClient = (await import('~/lib/api-client')).default
const adminApiClient = new AwsClient({
accessKeyId: credentials.value.AccessKeyId || '',
secretAccessKey: credentials.value.SecretAccessKey || '',
sessionToken: credentials.value.SessionToken || '',
region: siteConfig.s3.region || 'us-east-1',
service: 's3'
})
nuxtApp.$api = new ApiClient(adminApiClient, {
baseUrl: siteConfig.api.baseURL,
headers: {
'Content-Type': 'application/json'
}
})
// 重新创建 S3 客户端
const { S3Client } = await import('@aws-sdk/client-s3')
nuxtApp.$s3Client = new S3Client({
endpoint: siteConfig.s3.endpoint,
region: siteConfig.s3.region || 'us-east-1',
forcePathStyle: true,
requestChecksumCalculation: 'WHEN_REQUIRED',
credentials: {
accessKeyId: credentials.value.AccessKeyId || '',
secretAccessKey: credentials.value.SecretAccessKey || '',
sessionToken: credentials.value.SessionToken || '',
},
})
}
} catch (error) {
console.warn('Failed to reinitialize plugins:', error)
}
}
// 监听 region 变化
const onRegionChange = () => {
saveConfigAndUpdateSiteConfig()
}
const onUrlInput = () => {
// 清除之前的错误状态
if (urlValidationStatus.value === 'error') {
urlValidationStatus.value = undefined
urlError.value = ''
}
}
</script>
<template>
@@ -268,6 +51,13 @@ const onUrlInput = () => {
<auth-heros-static></auth-heros-static>
</div>
<div class="w-full lg:w-1/2 flex flex-col justify-center items-center bg-white dark:bg-neutral-900 dark:border-neutral-700 relative">
<!-- 表单容器右上角的配置按钮 -->
<div class="absolute top-4 right-4 z-10">
<NuxtLink to="/config" class="inline-flex items-center justify-center w-8 h-8 rounded-lg border border-gray-200 bg-white text-gray-600 shadow-sm hover:bg-gray-50 hover:text-gray-800 dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-white transition-colors" :title="t('Server Configuration')">
<Icon name="ri:settings-3-line" class="text-sm" />
</NuxtLink>
</div>
<div class="max-w-sm w-full p-4 sm:p-7">
<img src="https://rustfs.com/rustfs.logo.svg" class="max-w-28" alt="" />
<div class="py-6">
@@ -315,58 +105,6 @@ const onUrlInput = () => {
</div>
</template>
<!-- 服务器配置折叠面板 -->
<div class="mb-2">
<div class="transition-all duration-300 ease-in-out">
<!-- 折叠时的卡片 -->
<div v-if="!expandedNames.includes('server-config')" @click="expandedNames = ['server-config']" class="w-full cursor-pointer">
<div
class="rounded border border-gray-200 dark:border-neutral-700 bg-gray-50 dark:bg-neutral-800 px-4 py-2 flex items-center justify-between hover:bg-gray-100 dark:hover:bg-neutral-700 transition-colors">
<div class="truncate">
<span class="font-mono text-sm">{{ serverConfig.protocol }}://{{ serverConfig.host }}:{{ serverConfig.port }}</span>
<span class="ml-2 text-xs text-gray-500">{{ serverConfig.region }}</span>
</div>
<Icon :size="18" name="ri:arrow-right-s-fill" class="text-gray-600 dark:text-gray-300 hover:text-blue-500 transition-all duration-200" />
</div>
</div>
<!-- 展开时的内容 -->
<div v-else class="border border-gray-200 dark:border-neutral-700 rounded-lg overflow-hidden animate-in fade-in slide-in-from-top-2 duration-300">
<!-- 标题栏 -->
<div @click="expandedNames = []"
class="w-full cursor-pointer flex items-center justify-between px-4 py-3 bg-gray-50 dark:bg-neutral-800 hover:bg-gray-100 dark:hover:bg-neutral-700 transition-colors border-b border-gray-200 dark:border-neutral-700">
<span class="flex items-center text-sm font-medium">
<Icon :size="16" name="ri:settings-3-line" class="mr-2 text-gray-500" />
{{ t('Server Configuration') }}
</span>
<Icon :size="18" name="ri:arrow-down-s-fill" class="text-gray-600 dark:text-gray-300 hover:text-blue-500 transition-all duration-200 transform" />
</div>
<!-- 配置表单 -->
<div class="p-4 bg-white dark:bg-neutral-900">
<div v-if="configAlert"
class="mb-4 px-3 py-2 rounded bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 border border-red-200 dark:border-red-800 text-sm">
{{ configAlert }}
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-2 text-gray-700 dark:text-gray-300">{{ t('Server URL') }}</label>
<n-input v-model:value="serverUrl" size="medium" type="text" :placeholder="'http://localhost:9000'" :status="urlValidationStatus"
@blur="validateAndParseUrl" @input="onUrlInput" />
<div v-if="urlError" class="mt-1 text-xs text-red-500">{{ urlError }}</div>
</div>
<div>
<label class="block text-sm font-medium mb-2 text-gray-700 dark:text-gray-300">{{ t('Region') }}</label>
<n-input v-model:value="serverConfig.region" size="medium" type="text" :placeholder="'us-east-1'" @change="onRegionChange" />
</div>
</div>
</div>
</div>
</div>
</div>
<button type="submit"
class="w-full py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none">
{{ t('Login') }}
@@ -378,7 +116,7 @@ const onUrlInput = () => {
<div class="my-8">
<p class="text-sm text-gray-600 dark:text-neutral-400">
{{ t('Login Problems?') }} <nuxt-link to="https://www.rustfs.com" class="text-blue-600 hover:underline">{{ t('Get Help') }}</nuxt-link>
{{ t('Login Problems?') }} <NuxtLink to="https://www.rustfs.com" class="text-blue-600 hover:underline">{{ t('Get Help') }}</NuxtLink>
</p>
</div>
+167
View File
@@ -0,0 +1,167 @@
<script lang="ts" setup>
await setPageLayout('plain')
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
const { t } = useI18n()
const router = useRouter()
const message = useMessage()
const serverHost = ref('')
const isValid = ref(false)
const validateAndSave = async () => {
try {
if (serverHost.value) {
// 更宽松的URL验证
let urlToValidate = serverHost.value.trim()
// 如果没有协议,自动添加https://
if (!urlToValidate.match(/^https?:\/\//)) {
urlToValidate = 'https://' + urlToValidate
}
// 验证URL格式
const url = new URL(urlToValidate)
console.log('Valid URL:', url.href) // 调试信息
// 保存原始输入(如果用户没输入协议,就保存添加了协议的版本)
const urlToSave = serverHost.value.match(/^https?:\/\//) ? serverHost.value : urlToValidate
localStorage.setItem('rustfs-server-host', urlToSave)
// 如果我们自动添加了协议,更新输入框显示
if (!serverHost.value.match(/^https?:\/\//)) {
serverHost.value = urlToValidate
}
} else {
// 如果为空,清除localStorage,使用默认值
localStorage.removeItem('rustfs-server-host')
}
// 清除配置缓存
const { configManager } = await import('~/utils/config')
configManager.clearCache()
message.success(t('Server configuration saved successfully'))
// 延迟后刷新页面并跳转,确保配置完全生效
setTimeout(() => {
window.location.href = '/auth/login'
}, 200)
} catch (error) {
console.error('URL validation error:', error) // 调试信息
message.error(t('Invalid server address format') + ': ' + (error as Error).message)
}
}
const resetToCurrentHost = async () => {
// 清除localStorage中的配置,让系统回到默认状态(使用当前host)
localStorage.removeItem('rustfs-server-host')
// 清除配置缓存
const { configManager } = await import('~/utils/config')
configManager.clearCache()
// 清空输入框,表示使用默认配置
serverHost.value = ''
message.success(t('Reset to default successfully'))
// 延迟后跳转到登录页面,确保配置完全生效
setTimeout(() => {
window.location.href = '/auth/login'
}, 200)
}
const skipConfig = () => {
router.push('/auth/login')
}
onMounted(() => {
// 检查是否已有配置
const saved = localStorage.getItem('rustfs-server-host')
if (saved) {
serverHost.value = saved
isValid.value = true
}
})
</script>
<template>
<div class="lg:p-20 flex flex-col items-center justify-center min-h-screen bg-gray-100 dark:bg-neutral-800">
<img src="~/assets/backgrounds/scillate.svg" class="absolute inset-0 z-0 opacity-45" alt="" />
<div class="flex-1 flex w-full z-10 max-w-7xl lg:max-h-[85vh] shadow-lg rounded-lg overflow-hidden mx-auto dark:bg-neutral-800 dark:border-neutral-700">
<div class="hidden lg:block w-1/2">
<auth-heros-static></auth-heros-static>
</div>
<div class="w-full lg:w-1/2 flex flex-col justify-center items-center bg-white dark:bg-neutral-900 dark:border-neutral-700 relative">
<div class="max-w-sm w-full p-4 sm:p-7">
<img src="https://rustfs.com/rustfs.logo.svg" class="max-w-28" alt="" />
<div class="py-6">
<h1 class="block text-2xl font-bold text-gray-800 dark:text-white">{{ t('Server Configuration') }}</h1>
<p class="mt-2 text-sm text-gray-600 dark:text-neutral-400">
{{ t('Please configure your RustFS server address') }}
</p>
</div>
<div class="mt-5 space-y-4">
<!-- Form -->
<form @submit.prevent="validateAndSave" autocomplete="off">
<div class="grid gap-y-6">
<div>
<label for="serverHost" class="block text-sm mb-2 dark:text-white">{{ t('Server Address') }}</label>
<div class="text-xs text-gray-500 mb-2">
{{ t('Leave empty to use current host as default') }}
</div>
<n-input
v-model:value="serverHost"
type="text"
:placeholder="t('Please enter server address (e.g., http://localhost:9000)')"
/>
<div class="text-xs text-gray-500 mt-1">
{{ t('Example: http://localhost:9000 or https://your-domain.com') }}
</div>
</div>
<div class="flex gap-3">
<button type="submit"
class="flex-1 py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none">
{{ t('Save Configuration') }}
</button>
<button type="button" @click="resetToCurrentHost"
class="py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-sm hover:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-800">
{{ t('Reset') }}
</button>
<button type="button" @click="skipConfig"
class="py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-sm hover:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-800">
{{ t('Skip') }}
</button>
</div>
</div>
</form>
<!-- End Form -->
</div>
<div class="my-8">
<p class="text-sm text-gray-600 dark:text-neutral-400">
{{ t('Need help?') }} <NuxtLink to="https://docs.rustfs.com" class="text-blue-600 hover:underline">{{ t('View Documentation') }}</NuxtLink>
</p>
</div>
<div class="flex items-center justify-around gap-4 w-1/2 mx-auto">
<div class="inline-flex">
<theme-switcher />
</div>
<div class="inline-flex">
<language-switcher />
</div>
</div>
</div>
</div>
</div>
</div>
</template>
@@ -0,0 +1,64 @@
<template>
<div class="license-article">
<page-header>
<template #title>
<h1 class="text-2xl font-bold">{{ t('License') }}</h1>
</template>
</page-header>
<page-content>
<n-card size="large">
<template #header>
<h1 class="text-2xl font-bold text-center">{{ t('Apache License') }}</h1>
</template>
<article class="max-w-4xl mx-auto">
<div v-if="loading" class="text-center py-8">
<n-spin size="large" />
</div>
<div v-else-if="error" class="text-center py-8">
<n-alert type="error" :title="t('Failed to load license')">
{{ error }}
</n-alert>
</div>
<div v-else class="license-content">
<pre class="whitespace-pre-wrap font-mono text-sm leading-6 bg-gray-50 dark:bg-gray-800 p-6 rounded-lg overflow-auto">{{ licenseContent }}</pre>
</div>
</article>
</n-card>
</page-content>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const loading = ref(true)
const error = ref('')
const licenseContent = ref('')
onMounted(async () => {
try {
// 从public目录读取LICENSE文件
const response = await fetch('/LICENSE')
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
licenseContent.value = await response.text()
} catch (e) {
error.value = e instanceof Error ? e.message : 'Unknown error'
} finally {
loading.value = false
}
})
</script>
<style scoped>
.license-content {
line-height: 1.6;
}
</style>
@@ -9,7 +9,7 @@
<!-- 顶部信息 -->
<n-card>
<n-flex justify="space-between" class="top-info py-4 px-2">
<n-space vertical v-if="new Date().getTime() - siteConfig.license.expired*1000<0">
<n-space vertical v-if="false">
<n-space><n-tag type="success">{{ t('Enterprise License') }}</n-tag> <n-text style="line-height: 28px;" type="success">{{ t('Status') }}{{ t('Normal') }}</n-text></n-space>
<n-space>{{ t('License Valid Until') }}{{ endDate }}</n-space>
</n-space>
@@ -44,7 +44,7 @@
<!-- 许可证详情 -->
<n-card :title="t('License Details')" class="license-details mt-4">
<n-descriptions :columns="2" :bordered="true">
<n-descriptions-item :label="t('Licensed Company')">{{ siteConfig.license?.name }}</n-descriptions-item>
<n-descriptions-item :label="t('Licensed Company')">{{ t('No License') }}</n-descriptions-item>
<n-descriptions-item :label="t('License Key')">{{ licenseKey }}</n-descriptions-item>
<n-descriptions-item :label="t('Licensed Users')">{{ t('Unlimited') }}</n-descriptions-item>
<n-descriptions-item :label="t('Support Level')">{{ t('Enterprise') }} (7x24x365)</n-descriptions-item>
@@ -128,7 +128,7 @@
</page-content >
</div>
<articleL v-else></articleL>
<LicenseArticle v-else></LicenseArticle>
</template>
<script setup lang="ts">
@@ -138,16 +138,17 @@ import { NButton } from 'naive-ui'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import type { SiteConfig } from "~/types/config"
import articleL from './article.vue'
import LicenseArticle from './components/license-article.vue'
const { t } = useI18n()
const siteConfig = useNuxtApp().$siteConfig as SiteConfig;
const hasLicense = siteConfig.license
// license
const hasLicense = false
//
const startDate = new Date('2025-01-01');
//
const endDate = dayjs(siteConfig.license.expired*1000).format('YYYY-MM-DD');
// - 使
const endDate = dayjs().format('YYYY-MM-DD');
//
const licenseKey = ref('RUSTFS-ENTERPRISE-127-183');
-162
View File
@@ -1,162 +0,0 @@
<template>
<div class="license-page eading-7">
<page-header>
<template #title>
<h1 class="text-2xl font-bold">{{ t('License') }}</h1>
</template>
</page-header>
<page-content>
<n-card size="large" >
<template #header>
<h1 class="text-2xl font-bold text-center">{{ t('Apache License') }}</h1>
</template>
<article>
<section style="max-width: 1220px;margin: 0 auto;">
<n-p>
{{ t('Version 2.0, January 2004') }}
</n-p>
<n-p class=" mt-2">
<n-a href="http://www.apache.org/licenses/"><n-text type="info">http://www.apache.org/licenses/</n-text></n-a>
</n-p>
<n-divider />
<n-h3>{{ t('Terms and Conditions for Use, Reproduction, and Distribution') }}</n-h3>
<n-h4>1. {{ t('Definitions') }}</n-h4>
<n-p>
{{ t('License definition paragraph') }}
</n-p>
<n-p>
{{ t('Licensor definition paragraph') }}
</n-p>
<n-p>
{{ t('Legal Entity definition paragraph') }}
</n-p>
<n-p>
{{ t('You definition paragraph') }}
</n-p>
<n-p>
{{ t('Source form definition paragraph') }}
</n-p>
<n-p>
{{ t('Object form definition paragraph') }}
</n-p>
<n-p>
{{ t('Work definition paragraph') }}
</n-p>
<n-p>
{{ t('Derivative Works definition paragraph') }}
</n-p>
<n-p>
{{ t('Contribution definition paragraph') }}
</n-p>
<n-p>
{{ t('Contributor definition paragraph') }}
</n-p>
<n-h4>
2. {{ t('Grant of Copyright License') }}
</n-h4>
<n-p>
{{ t('Copyright license paragraph') }}
</n-p>
<n-h4>
3. {{ t('Grant of Patent License') }}
</n-h4>
<n-p>
{{ t('Patent license paragraph') }}
</n-p>
<n-h4>
4. {{ t('Redistribution') }}
</n-h4>
<n-p>
{{ t('Redistribution paragraph') }}
</n-p>
<n-ol>
<n-li>
1. {{ t('Redistribution condition 1') }}
</n-li>
<n-li>
2. {{ t('Redistribution condition 2') }}
</n-li>
<n-li>
3. {{ t('Redistribution condition 3') }}
</n-li>
<n-li>
4. {{ t('Redistribution condition 4') }}
</n-li>
</n-ol>
<n-p>
{{ t('Additional redistribution paragraph') }}
</n-p>
<n-h4>
5. {{ t('Submission of Contributions') }}
</n-h4>
<n-p>
{{ t('Contributions paragraph') }}
</n-p>
<n-h4>
6. {{ t('Trademarks') }}
</n-h4>
<n-p>
{{ t('Trademarks paragraph') }}
</n-p>
<n-h4>
7. {{ t('Disclaimer of Warranty') }}
</n-h4>
<n-p>
{{ t('Disclaimer paragraph') }}
</n-p>
<n-h4>
8. {{ t('Limitation of Liability') }}
</n-h4>
<n-p>
{{ t('Liability paragraph') }}
</n-p>
<n-h4>
9. {{ t('Accepting Warranty or Additional Liability') }}
</n-h4>
<n-p>
{{ t('Additional liability paragraph') }}
</n-p>
<n-h4>
{{ t('END OF TERMS AND CONDITIONS') }}
</n-h4>
<n-h4>
{{ t('APPENDIX: How to apply the Apache License to your work') }}
</n-h4>
<n-p>
{{ t('Appendix paragraph 1') }}
</n-p>
<n-p>
{{ t('Appendix paragraph 2') }}
</n-p>
<n-p> {{ t('Copyright RustFS') }}</n-p>
<n-p>{{ t('Licensed under Apache 2.0') }}</n-p>
<n-p>{{ t('You may not use this file except in compliance with the License') }}</n-p>
<n-p>{{ t('You may obtain a copy of the License at') }}</n-p>
<n-p>
{{ t('Appendix final paragraph') }}
</n-p>
</section>
</article>
</n-card>
</page-content>
</div>
</template>
<script setup>
import { NCard, NDivider, NOl } from 'naive-ui';
import { useI18n } from 'vue-i18n';
const { t } = useI18n()
</script>
<style scoped>
article h2, article h3 {
margin-top: 20px;
}
article p {
margin-bottom: 15px;
}
</style>
+143 -86
View File
@@ -1,115 +1,172 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { configManager } from '~/utils/config'
const { t } = useI18n()
const message = useMessage()
// 服务端配置
const serverConfig = ref({
protocol: 'http',
host: 'localhost',
port: '9000',
region: 'us-east-1'
// 当前配置信息
const currentConfig = ref({
serverHost: '',
api: {
baseURL: ''
},
s3: {
endpoint: '',
region: ''
}
})
// 表单数据
const formData = ref({
serverHost: ''
})
const loading = ref(false)
// 加载当前配置
const loadCurrentConfig = async () => {
const currentConfig = await configManager.loadConfig()
if (currentConfig) {
const serverConfigData = configManager.extractServerConfig(currentConfig)
if (serverConfigData) {
serverConfig.value = serverConfigData
}
try {
const config = await configManager.loadConfig()
currentConfig.value = config
formData.value.serverHost = config.serverHost
} catch (error) {
message.error(t('Failed to load current configuration'))
}
}
// 保存配置
const saveConfig = async () => {
const saved = await configManager.saveConfig(serverConfig.value)
if (saved) {
message.success(t('Server configuration saved'))
} else {
message.info(t('Using public configuration, cannot save to localStorage'))
if (!formData.value.serverHost) {
message.error(t('Please enter server address'))
return
}
loading.value = true
try {
// 更宽松的URL验证
let urlToValidate = formData.value.serverHost.trim()
// 如果没有协议,自动添加https://
if (!urlToValidate.match(/^https?:\/\//)) {
urlToValidate = 'https://' + urlToValidate
}
// 验证URL格式
const url = new URL(urlToValidate)
// 保存原始输入(如果用户没输入协议,就保存添加了协议的版本)
const urlToSave = formData.value.serverHost.match(/^https?:\/\//) ? formData.value.serverHost : urlToValidate
localStorage.setItem('rustfs-server-host', urlToSave)
// 如果我们自动添加了协议,更新输入框显示
if (!formData.value.serverHost.match(/^https?:\/\//)) {
formData.value.serverHost = urlToValidate
}
// 清除配置缓存
configManager.clearCache()
message.success(t('Configuration saved successfully'))
// 延迟后刷新页面,确保配置完全生效
setTimeout(() => {
window.location.reload()
}, 200)
} catch (error) {
message.error(t('Invalid server address format'))
} finally {
loading.value = false
}
}
// 清除配置
const clearConfig = async () => {
configManager.clearConfig()
message.success('Configuration cleared')
await loadCurrentConfig()
// 重置配置
const resetConfig = async () => {
localStorage.removeItem('rustfs-server-host')
configManager.clearCache()
message.success(t('Configuration reset successfully'))
// 延迟后刷新页面,确保配置完全生效
setTimeout(() => {
window.location.reload()
}, 200)
}
// 页面加载时读取当前配置
onMounted(async () => {
await loadCurrentConfig()
onMounted(() => {
loadCurrentConfig()
})
</script>
<template>
<div class="container mx-auto px-4 py-8">
<div class="max-w-2xl mx-auto">
<div class="bg-white dark:bg-neutral-800 rounded-lg shadow-lg p-6">
<h1 class="text-2xl font-bold text-gray-800 dark:text-white mb-6">
{{ t('Server Configuration') }}
</h1>
<div>
<page-header>
<template #title>
<h1 class="text-2xl font-bold">{{ t('Settings') }}</h1>
</template>
</page-header>
<page-content>
<!-- 当前配置显示 -->
<n-card :title="t('Current Configuration')" class="mb-6">
<n-descriptions :columns="1" :bordered="true">
<n-descriptions-item :label="t('Server Host')">
{{ currentConfig.serverHost || t('Not configured') }}
</n-descriptions-item>
<n-descriptions-item :label="t('API Base URL')">
{{ currentConfig.api.baseURL || t('Not configured') }}
</n-descriptions-item>
<n-descriptions-item :label="t('S3 Endpoint')">
{{ currentConfig.s3.endpoint || t('Not configured') }}
</n-descriptions-item>
<n-descriptions-item :label="t('S3 Region')">
{{ currentConfig.s3.region || t('Not configured') }}
</n-descriptions-item>
</n-descriptions>
</n-card>
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ t('Protocol') }}
</label>
<n-select v-model:value="serverConfig.protocol" :options="[
{ label: 'HTTP', value: 'http' },
{ label: 'HTTPS', value: 'https' }
]" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ t('Port') }}
</label>
<n-input v-model:value="serverConfig.port" type="text" :placeholder="'9000'" />
</div>
</div>
<!-- 配置表单 -->
<n-card :title="t('Server Configuration')">
<n-form @submit.prevent="saveConfig">
<n-form-item :label="t('Server Address')" required>
<n-input
v-model:value="formData.serverHost"
:placeholder="t('Please enter server address (e.g., http://localhost:9000)')"
clearable
/>
<template #feedback>
<div class="text-xs text-gray-500">
{{ t('Example: http://localhost:9000 or https://your-domain.com') }}
</div>
</template>
</n-form-item>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ t('Host') }}
</label>
<n-input v-model:value="serverConfig.host" type="text" :placeholder="'localhost'" />
</div>
<n-form-item>
<n-space>
<n-button
type="primary"
@click="saveConfig"
:loading="loading"
>
{{ t('Save Configuration') }}
</n-button>
<n-button @click="resetConfig">
{{ t('Reset to Default') }}
</n-button>
</n-space>
</n-form-item>
</n-form>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ t('Region') }}
</label>
<n-input v-model:value="serverConfig.region" type="text" :placeholder="'us-east-1'" />
</div>
<div class="bg-gray-50 dark:bg-neutral-700 p-4 rounded-lg">
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ t('Generated Configuration') }}
</h3>
<div class="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<div><strong>API URL:</strong> {{ serverConfig.protocol }}://{{ serverConfig.host }}:{{ serverConfig.port }}/rustfs/admin/v3</div>
<div><strong>S3 Endpoint:</strong> {{ serverConfig.protocol }}://{{ serverConfig.host }}:{{ serverConfig.port }}</div>
<div><strong>Region:</strong> {{ serverConfig.region }}</div>
</div>
</div>
<div class="flex gap-4 pt-4">
<n-button type="primary" @click="saveConfig">
{{ t('Save Configuration') }}
</n-button>
<n-button @click="clearConfig">
{{ t('Clear Configuration') }}
</n-button>
</div>
</div>
</div>
</div>
<n-alert type="info" :title="t('Configuration Information')" class="mt-4">
<ul class="list-disc list-inside space-y-1 text-sm">
<li>{{ t('Configuration is saved locally in your browser') }}</li>
<li>{{ t('Page will refresh automatically after saving configuration') }}</li>
<li>{{ t('Make sure the server address is accessible from your network') }}</li>
</ul>
</n-alert>
</n-card>
</page-content>
</div>
</template>
</template>
+11 -43
View File
@@ -1,4 +1,5 @@
import { configManager } from '~/utils/config'
import { logger } from '~/utils/logger'
export default defineNuxtPlugin({
name: 'load-site-config',
@@ -6,51 +7,18 @@ export default defineNuxtPlugin({
let finalConfig: any
try {
// 使用 configManager 加载配置(内部优先级:config.json > localStorage > runtimeconfig
const userConfig = await configManager.loadConfig()
if (userConfig) {
finalConfig = { ...useRuntimeConfig().public, ...userConfig }
console.log('Configuration loaded from configManager')
// 使用 configManager 加载配置(优先使用当前host
const config = await configManager.loadConfig()
if (config) {
finalConfig = { ...useRuntimeConfig().public, ...config }
logger.log('Configuration loaded from configManager')
} else {
// 如果 configManager 没有配置,使用 runtimeConfig
finalConfig = useRuntimeConfig().public
console.log('Configuration loaded from runtimeConfig')
logger.log('Configuration loaded from runtimeConfig')
}
// 如果有 public/config.json,尝试获取额外的信息(如 license)
if (await configManager.hasPublicConfig()) {
try {
const response = await fetch('/config.json')
const publicConfig = await response.json()
// 保留 license 和其他额外信息
if (publicConfig.license) {
finalConfig.license = publicConfig.license
}
if (publicConfig.release) {
finalConfig.release = publicConfig.release
}
if (publicConfig.doc) {
finalConfig.doc = publicConfig.doc
}
// 获取 license 信息(如果不是开发环境)
if (process.env.NODE_ENV !== 'development') {
try {
const licenseResponse = await fetch('/license')
const licenseConfig = await licenseResponse.json()
finalConfig.license = licenseConfig
} catch (licenseError) {
console.warn('Failed to load license info:', licenseError)
}
}
} catch (error) {
console.warn('Failed to load additional config info:', error)
}
}
} catch (error) {
console.error('Failed to load configuration:', error)
logger.error('Failed to load configuration:', error)
// 如果配置加载失败,重定向到登录页而不是抛出错误
if (process.client) {
@@ -64,12 +32,12 @@ export default defineNuxtPlugin({
finalConfig = useRuntimeConfig().public
}
if (!finalConfig.api.baseURL) {
// 如果仍然没有 baseURL,重定向到登录页
if (!finalConfig.api?.baseURL && !finalConfig.serverHost) {
// 如果仍然没有 baseURL 或 serverHost,重定向到登录页
if (process.client) {
const currentRoute = useRoute()
if (currentRoute.path !== '/auth/login') {
await navigateTo('/auth/login?showConfig=1')
await navigateTo('/auth/login')
}
}
}
+49 -37
View File
@@ -1,87 +1,99 @@
import { S3Client } from "@aws-sdk/client-s3";
import type { SiteConfig } from "~/types/config";
import type { DeserializeHandler, DeserializeHandlerArguments, DeserializeHandlerOutput } from "@aws-sdk/types";
import { S3Client } from '@aws-sdk/client-s3'
import type { SiteConfig } from '~/types/config'
import type {
DeserializeHandler,
DeserializeHandlerArguments,
DeserializeHandlerOutput
} from '@aws-sdk/types'
interface S3Response {
response?: {
body?: string;
};
[key: string]: any;
body?: string
}
[key: string]: any
}
export default defineNuxtPlugin({
name: "s3-client",
name: 's3-client',
setup(nuxtApp) {
const { credentials, isAuthenticated } = useAuth();
const siteConfig = nuxtApp.$siteConfig as SiteConfig;
const { credentials, isAuthenticated } = useAuth()
const siteConfig = nuxtApp.$siteConfig as SiteConfig
if (!isAuthenticated || !credentials.value) {
return;
return
}
const client = new S3Client({
endpoint: siteConfig.s3.endpoint,
region: siteConfig.s3.region || "us-east-1",
region: siteConfig.s3.region || 'us-east-1',
forcePathStyle: true,
// https://github.com/aws/aws-sdk-js-v3/issues/6834#issuecomment-2611346849
requestChecksumCalculation: "WHEN_REQUIRED",
requestChecksumCalculation: 'WHEN_REQUIRED',
credentials: {
accessKeyId: credentials.value?.AccessKeyId || "",
secretAccessKey: credentials.value?.SecretAccessKey || "",
sessionToken: credentials.value?.SessionToken || "",
},
});
accessKeyId: credentials.value?.AccessKeyId || '',
secretAccessKey: credentials.value?.SecretAccessKey || '',
sessionToken: credentials.value?.SessionToken || ''
}
})
// 添加中间件处理XML响应和401错误
client.middlewareStack.add(
(next: DeserializeHandler<any, any>) =>
async (args: DeserializeHandlerArguments<any>): Promise<DeserializeHandlerOutput<any>> => {
async (
args: DeserializeHandlerArguments<any>
): Promise<DeserializeHandlerOutput<any>> => {
try {
const response = (await next(args)) as S3Response;
const response = (await next(args)) as S3Response
// 检查响应是否为XML格式
if (response.response?.body && typeof response.response.body === "string") {
const body = response.response.body.trim();
if (
response.response?.body &&
typeof response.response.body === 'string'
) {
const body = response.response.body.trim()
// 检查是否是空的XML响应
if (body.match(/^<\?xml[^>]*\?><[^>]*><\/[^>]*>$/)) {
// 从XML标签名提取属性名
const tagName = body.match(/<([^>]*)><\/\1>/)?.[1];
const tagName = body.match(/<([^>]*)><\/\1>/)?.[1]
if (tagName) {
// 将XML标签名转换为驼峰命名
const propertyName = tagName.replace(/(?:^|_)([a-z])/g, (_, letter) => letter.toUpperCase());
const propertyName = tagName.replace(
/(?:^|_)([a-z])/g,
(_, letter) => letter.toUpperCase()
)
// 返回null,保持response结构
return {
response: response.response,
[propertyName]: null,
} as unknown as DeserializeHandlerOutput<any>;
[propertyName]: null
} as unknown as DeserializeHandlerOutput<any>
}
}
}
return response as DeserializeHandlerOutput<any>;
return response as DeserializeHandlerOutput<any>
} catch (error: any) {
if (error?.$metadata?.httpStatusCode === 401) {
await useAuth().logoutAndRedirect();
await useAuth().logoutAndRedirect()
}
// 处理 S3 客户端错误,优先抛出 error.message 作为新的 Error
if (error?.Code) {
throw new Error(error.Code);
throw new Error(error.Code)
}
throw error;
throw error
}
},
{
step: "deserialize",
name: "handleXmlResponse",
step: 'deserialize',
name: 'handleXmlResponse'
}
);
)
return {
provide: {
s3Client: client,
},
};
},
});
s3Client: client
}
}
}
})
+13189
View File
File diff suppressed because it is too large Load Diff
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2024 Beijing Henghesha Technology Co., Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-17
View File
@@ -1,17 +0,0 @@
{
"api": {
"baseURL": "<PROTOCOL>://<HOST>:<PORT>/rustfs/admin/v3"
},
"s3": {
"endpoint": "<PROTOCOL>://<HOST>:<PORT>",
"region": "<REGION>"
},
"release": {
"version": "<RELEASE_VERSION>",
"date": "<RELEASE_DATE>"
},
"license": {
"name": "Apache-2.0",
"expired": "1744017568"
}
}
+16
View File
@@ -0,0 +1,16 @@
export interface NavItem {
label: string
to?: string
type?: string
icon?: string
target?: string
key?: string
children?: NavItem[]
}
export interface AppConfig {
navs: NavItem[]
name: string
description: string
[key: string]: any
}
+1 -12
View File
@@ -9,24 +9,13 @@ export type ApiConfig = {
baseURL: string;
}
export type ReleaseConfig = {
version: string;
date: string;
}
export type LicenseConfig = {
name: string;
expired: number;
}
export type SessionConfig = {
durationSeconds: number;
}
export interface SiteConfig {
serverHost: string;
api: ApiConfig;
s3: S3Config;
release: ReleaseConfig;
license: LicenseConfig ;
session?: SessionConfig;
}
+62
View File
@@ -0,0 +1,62 @@
import type { SiteConfig } from '~/types/config'
import { logger } from './logger'
/**
* 创建默认配置
*/
export const createDefaultConfig = (serverHost: string): SiteConfig => {
return {
serverHost,
api: {
baseURL: `${serverHost}/rustfs/admin/v3`
},
s3: {
endpoint: serverHost,
region: 'us-east-1',
accessKeyId: '',
secretAccessKey: ''
}
}
}
/**
* 从localStorage获取保存的主机配置
*/
export const getStoredHostConfig = (): SiteConfig | null => {
if (typeof window === 'undefined') return null
const savedHost = localStorage.getItem('rustfs-server-host')
if (!savedHost) return null
try {
const url = new URL(savedHost)
const serverHost = `${url.protocol}//${url.host}`
return createDefaultConfig(serverHost)
} catch (error) {
logger.warn('Invalid saved host configuration:', error)
return null
}
}
/**
* 获取当前浏览器主机配置
*/
export const getCurrentBrowserConfig = (): SiteConfig | null => {
if (typeof window === 'undefined') return null
const currentHost = window.location.host
const protocol = window.location.protocol.replace(':', '')
const serverHost = `${protocol}://${currentHost}`
return createDefaultConfig(serverHost)
}
/**
* 获取服务端默认配置
*/
export const getServerDefaultConfig = (): SiteConfig => {
// 注意:这里按照用户要求,服务端也应该尽量使用当前host
// 但由于服务端限制,只能使用localhost作为fallback
const defaultServerHost = 'http://localhost:9000'
return createDefaultConfig(defaultServerHost)
}
+74 -106
View File
@@ -1,11 +1,15 @@
export interface ServerConfig {
protocol: string
host: string
port: string
region: string
}
import type { SiteConfig } from '~/types/config'
import { handleConfigError } from './error-handler'
import { logger } from './logger'
import {
createDefaultConfig,
getStoredHostConfig,
getCurrentBrowserConfig,
getServerDefaultConfig
} from './config-helpers'
export interface RustFSConfig {
serverHost: string
api: {
baseURL: string
}
@@ -15,137 +19,101 @@ export interface RustFSConfig {
}
}
const CONFIG_KEY = 'rustfs-config'
// 添加配置缓存
let configCache: SiteConfig | null = null
let configCacheTime = 0
const CACHE_DURATION = 60000 // 1分钟缓存
export const configManager = {
// 从 public/config.json 读取配置
async loadPublicConfig(): Promise<RustFSConfig | null> {
if (process.client) {
try {
const response = await fetch('/config.json')
if (response.ok) {
const publicConfig = await response.json()
if (publicConfig.api?.baseURL) {
return publicConfig
}
}
} catch (error) {
console.log('public/config.json not found')
}
// 获取当前host配置
getCurrentHostConfig(): SiteConfig {
// 优先使用localStorage中保存的配置
const storedConfig = getStoredHostConfig()
if (storedConfig) {
return storedConfig
}
return null
},
// 从 localStorage 读取配置
loadStorageConfig(): RustFSConfig | null {
if (process.client) {
try {
const configStr = localStorage.getItem(CONFIG_KEY)
if (configStr) {
return JSON.parse(configStr)
}
} catch (error) {
console.warn('Failed to parse localStorage config:', error)
}
// 使用当前浏览器地址
const browserConfig = getCurrentBrowserConfig()
if (browserConfig) {
return browserConfig
}
return null
// 服务端fallback
return getServerDefaultConfig()
},
// 从 nuxt runtimeconfig 读取配置
loadRuntimeConfig(): RustFSConfig | null {
loadRuntimeConfig(): SiteConfig | null {
try {
const runtimeConfig = useRuntimeConfig()
if (runtimeConfig.public?.api?.baseURL) {
// 优先使用 serverHost,然后是 API_BASE_URL
const serverHost = runtimeConfig.public?.serverHost ||
runtimeConfig.public?.api?.baseURL?.replace(/\/rustfs\/admin\/v3$/, '')
if (serverHost) {
return {
serverHost,
api: {
baseURL: runtimeConfig.public.api.baseURL
baseURL: runtimeConfig.public.api?.baseURL || `${serverHost}/rustfs/admin/v3`
},
s3: {
endpoint: runtimeConfig.public.s3?.endpoint || runtimeConfig.public.api.baseURL,
region: runtimeConfig.public.s3?.region || 'us-east-1'
}
endpoint: runtimeConfig.public.s3?.endpoint || serverHost,
region: runtimeConfig.public.s3?.region || 'us-east-1',
accessKeyId: '',
secretAccessKey: ''
},
session: runtimeConfig.public.session
}
}
} catch (error) {
console.warn('Failed to load runtime config:', error)
const configError = handleConfigError(error, 'runtime config loading')
logger.warn('Failed to load runtime config:', configError.message)
}
return null
},
// 按优先级加载配置:config.json > localStorage > runtimeconfig
async loadConfig(): Promise<RustFSConfig | null> {
// 首先尝试从 config.json 加载
const publicConfig = await this.loadPublicConfig()
if (publicConfig) {
return publicConfig
// 加载配置:优先使用localStorage,然后是当前host,最后是runtimeconfig
async loadConfig(): Promise<SiteConfig> {
// 检查缓存
const now = Date.now()
if (configCache && (now - configCacheTime) < CACHE_DURATION) {
return configCache
}
// 如果没有 config.json,尝试从 localStorage 加载
const storageConfig = this.loadStorageConfig()
if (storageConfig) {
return storageConfig
}
let config: SiteConfig
// 如果都没有,使用 runtimeconfig 作为默认值
return this.loadRuntimeConfig()
},
// 从 RustFSConfig 提取 ServerConfig
extractServerConfig(config: RustFSConfig): ServerConfig | null {
try {
const urlObj = new URL(config.api.baseURL)
return {
protocol: urlObj.protocol.replace(':', ''),
host: urlObj.hostname,
port: urlObj.port || (urlObj.protocol === 'https:' ? '443' : '80'),
region: config.s3.region
// 优先使用当前host配置(包括localStorage检查)
const currentHostConfig = this.getCurrentHostConfig()
if (currentHostConfig) {
config = currentHostConfig
} else {
// 如果没有当前host配置,使用 runtimeconfig
const runtimeConfig = this.loadRuntimeConfig()
if (runtimeConfig) {
config = runtimeConfig
} else {
// 最后使用浏览器当前地址或服务端默认值
config = getCurrentBrowserConfig() || getServerDefaultConfig()
}
} catch (error) {
console.warn('Failed to extract server config:', error)
return null
}
// 缓存配置
configCache = config
configCacheTime = now
return config
},
// 检查是否有 public/config.json
async hasPublicConfig(): Promise<boolean> {
const config = await this.loadPublicConfig()
return !!config
},
// 保存配置到 localStorage(只有当没有 public/config.json 时)
async saveConfig(serverConfig: ServerConfig): Promise<boolean> {
if (process.client) {
// 如果有 public/config.json,就不保存到 localStorage
if (await this.hasPublicConfig()) {
return false
}
const config: RustFSConfig = {
api: {
baseURL: `${serverConfig.protocol}://${serverConfig.host}:${serverConfig.port}/rustfs/admin/v3`
},
s3: {
endpoint: `${serverConfig.protocol}://${serverConfig.host}:${serverConfig.port}`,
region: serverConfig.region
}
}
localStorage.setItem(CONFIG_KEY, JSON.stringify(config))
return true
}
return false
},
// 清除配置
clearConfig(): void {
if (process.client) {
localStorage.removeItem(CONFIG_KEY)
}
// 清除缓存
clearCache() {
configCache = null
configCacheTime = 0
},
// 检查是否有有效配置
async hasValidConfig(): Promise<boolean> {
const config = await this.loadConfig()
return !!(config?.api?.baseURL)
return !!(config?.serverHost && config?.api?.baseURL)
}
}
}
+84
View File
@@ -0,0 +1,84 @@
export interface ApiError {
message: string
code?: string
statusCode?: number
originalError?: Error
}
export class ConfigLoadError extends Error {
code: 'INVALID_URL' | 'STORAGE_ERROR' | 'NETWORK_ERROR' | 'UNKNOWN_ERROR'
originalError?: Error
constructor(
message: string,
code: ConfigLoadError['code'] = 'UNKNOWN_ERROR',
originalError?: Error
) {
super(message)
this.name = 'ConfigLoadError'
this.code = code
this.originalError = originalError
}
}
export const parseApiError = async (response: Response): Promise<string> => {
try {
// 优先尝试解析为 JSON
const errorData = await response.clone().json()
return errorData.message || JSON.stringify(errorData) || response.statusText
} catch (e) {
try {
// 如果不是 JSON,尝试解析为文本
const text = await response.clone().text()
if (text) {
// 检查是否为 XML
if (text.trim().startsWith('<')) {
// 简单提取 <Message> 或 <Error> 标签内容
const match = text.match(/<Message>(.*?)<\/Message>/i) ||
text.match(/<Error>(.*?)<\/Error>/i)
return match ? match[1] : text
}
return text
}
} catch (e2) {
// 保持原有 statusText
}
}
return response.statusText
}
export const handleConfigError = (error: unknown, context: string): ConfigLoadError => {
if (error instanceof ConfigLoadError) {
return error
}
if (error instanceof Error) {
if (error.message.includes('Invalid URL')) {
return new ConfigLoadError(
`Invalid URL in ${context}: ${error.message}`,
'INVALID_URL',
error
)
}
if (error.message.includes('localStorage') || error.message.includes('storage')) {
return new ConfigLoadError(
`Storage error in ${context}: ${error.message}`,
'STORAGE_ERROR',
error
)
}
if (error.message.includes('fetch') || error.message.includes('network')) {
return new ConfigLoadError(
`Network error in ${context}: ${error.message}`,
'NETWORK_ERROR',
error
)
}
}
return new ConfigLoadError(
`Unknown error in ${context}: ${error}`,
'UNKNOWN_ERROR',
error instanceof Error ? error : new Error(String(error))
)
}
+29
View File
@@ -0,0 +1,29 @@
export const isDevelopment = process.env.NODE_ENV === 'development'
export const logger = {
log: (...args: any[]) => {
if (isDevelopment) {
console.log(...args)
}
},
warn: (...args: any[]) => {
if (isDevelopment) {
console.warn(...args)
}
},
error: (...args: any[]) => {
if (isDevelopment) {
console.error(...args)
}
},
info: (...args: any[]) => {
if (isDevelopment) {
console.info(...args)
}
},
debug: (...args: any[]) => {
if (isDevelopment) {
console.debug(...args)
}
}
}