init frontend
This commit is contained in:
1
frontend/user-management-frontend/.env.development
Normal file
1
frontend/user-management-frontend/.env.development
Normal file
@@ -0,0 +1 @@
|
||||
VITE_API_HOST=http://localhost:8080
|
||||
1
frontend/user-management-frontend/.env.production
Normal file
1
frontend/user-management-frontend/.env.production
Normal file
@@ -0,0 +1 @@
|
||||
VITE_API_HOST=
|
||||
27
frontend/user-management-frontend/.gitignore
vendored
Normal file
27
frontend/user-management-frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
*.tar.gz
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
src/views
|
||||
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/
|
||||
.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
3
frontend/user-management-frontend/.vscode/extensions.json
vendored
Normal file
3
frontend/user-management-frontend/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
5
frontend/user-management-frontend/README.md
Normal file
5
frontend/user-management-frontend/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Vue 3 + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
||||
13
frontend/user-management-frontend/index.html
Normal file
13
frontend/user-management-frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>User Manager System</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
2923
frontend/user-management-frontend/package-lock.json
generated
Normal file
2923
frontend/user-management-frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
frontend/user-management-frontend/package.json
Normal file
27
frontend/user-management-frontend/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "user-management-frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^12.4.0",
|
||||
"axios": "^1.7.9",
|
||||
"marked": "^15.0.6",
|
||||
"pinia": "^2.3.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0",
|
||||
"vue3-markdown-it": "^1.0.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.5.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"vite": "^6.0.5"
|
||||
}
|
||||
}
|
||||
6
frontend/user-management-frontend/postcss.config.js
Normal file
6
frontend/user-management-frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
1
frontend/user-management-frontend/public/vite.svg
Normal file
1
frontend/user-management-frontend/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
440
frontend/user-management-frontend/src/App.vue
Normal file
440
frontend/user-management-frontend/src/App.vue
Normal file
@@ -0,0 +1,440 @@
|
||||
<template>
|
||||
<div v-if="route.name === 'login'">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
<div v-else class="min-h-screen bg-gray-900">
|
||||
<!-- Top Navigation Bar -->
|
||||
<nav class="bg-gray-800 border-b border-gray-700 px-4 py-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center mr-2">
|
||||
<span class="text-white font-bold">L</span>
|
||||
</div>
|
||||
<span class="text-xl font-semibold text-white">System Management Web-GUI</span>
|
||||
<button
|
||||
@click="toggleSidebar"
|
||||
class="ml-4 text-gray-300 hover:text-white focus:outline-none"
|
||||
>
|
||||
<svg
|
||||
class="w-5 h-5 transition-transform duration-200"
|
||||
:class="{ 'rotate-180': !isSidebarOpen }"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<button class="text-gray-300 hover:text-white">
|
||||
<span class="text-sm">🌐</span>
|
||||
</button>
|
||||
<button
|
||||
@click="navigateTo('/markdown/todo.md')"
|
||||
class="text-gray-300 hover:text-white">
|
||||
<span class="text-sm">❓</span>
|
||||
</button>
|
||||
<div class="relative">
|
||||
<div class="flex items-center text-gray-300">
|
||||
<span class="text-sm mr-2">你好, {{ authStore.user?.username || '管理员' }}</span>
|
||||
<button
|
||||
@click="toggleUserMenu"
|
||||
class="hover:text-white focus:outline-none"
|
||||
ref="userMenuButton"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 transition-transform duration-200"
|
||||
:class="{ 'rotate-180': isUserMenuOpen }"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- User Dropdown Menu -->
|
||||
<div
|
||||
v-if="isUserMenuOpen"
|
||||
class="absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-gray-700 ring-1 ring-black ring-opacity-5"
|
||||
>
|
||||
<div class="py-1">
|
||||
<button
|
||||
@click="showModifyPasswordDialog = true"
|
||||
class="w-full text-left px-4 py-2 text-sm text-gray-300 hover:bg-gray-600"
|
||||
>
|
||||
修改密码
|
||||
</button>
|
||||
<button
|
||||
@click="logout"
|
||||
class="w-full text-left px-4 py-2 text-sm text-gray-300 hover:bg-gray-600"
|
||||
>
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="flex">
|
||||
<!-- Side Navigation -->
|
||||
<aside
|
||||
class="bg-gray-800 min-h-screen transition-all duration-300"
|
||||
:class="[isSidebarOpen ? 'w-52' : 'w-16']"
|
||||
>
|
||||
<nav class="px-2 py-4">
|
||||
<div
|
||||
v-for="item in menuItems"
|
||||
:key="item.name"
|
||||
class="mb-1"
|
||||
>
|
||||
<div v-if="!item.children">
|
||||
<!-- Menu Item as Route -->
|
||||
<div
|
||||
v-if="item.type === 'route'"
|
||||
@click="navigateTo(item.path)"
|
||||
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-700 rounded-lg cursor-pointer"
|
||||
:class="{ 'bg-gray-700': route.path === item.path }"
|
||||
:title="!isSidebarOpen ? item.name : ''"
|
||||
>
|
||||
<div class="flex items-center justify-center" :class="{ 'w-full': !isSidebarOpen }">
|
||||
<span class="text-xl" v-html="item.icon"></span>
|
||||
<span
|
||||
class="ml-2 text-sm"
|
||||
:class="{ 'hidden': !isSidebarOpen, 'block': isSidebarOpen }"
|
||||
>
|
||||
{{ item.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Menu Item as Button -->
|
||||
<div
|
||||
v-else-if="item.type === 'button'"
|
||||
@click="item.action"
|
||||
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-700 rounded-lg cursor-pointer"
|
||||
:title="!isSidebarOpen ? item.name : ''"
|
||||
>
|
||||
<div class="flex items-center justify-center" :class="{ 'w-full': !isSidebarOpen }">
|
||||
<span class="text-xl" v-html="item.icon"></span>
|
||||
<span
|
||||
class="ml-2 text-sm"
|
||||
:class="{ 'hidden': !isSidebarOpen, 'block': isSidebarOpen }"
|
||||
>
|
||||
{{ item.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submenu -->
|
||||
<div v-else>
|
||||
<div
|
||||
@click="toggleSubmenu(item)"
|
||||
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-700 rounded-lg cursor-pointer"
|
||||
:class="{ 'bg-gray-700': isSubmenuOpen(item) }"
|
||||
:title="!isSidebarOpen ? item.name : ''"
|
||||
>
|
||||
<div class="flex items-center justify-center w-full">
|
||||
<span class="text-xl" v-html="item.icon"></span>
|
||||
<span
|
||||
class="ml-2 flex-grow text-sm"
|
||||
:class="{ 'hidden': !isSidebarOpen, 'block': isSidebarOpen }"
|
||||
>
|
||||
{{ item.name }}
|
||||
</span>
|
||||
<svg
|
||||
v-if="isSidebarOpen"
|
||||
class="w-4 h-4 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': isSubmenuOpen(item) }"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submenu Items -->
|
||||
<div
|
||||
v-show="isSubmenuOpen(item) && isSidebarOpen"
|
||||
class="ml-4 mt-1"
|
||||
>
|
||||
<div
|
||||
v-for="child in item.children"
|
||||
:key="child.name"
|
||||
@click="child.type === 'route' ? navigateTo(child.path) : child.action()"
|
||||
class="flex items-center px-4 py-2 text-gray-300 hover:bg-gray-700 rounded-lg cursor-pointer"
|
||||
:class="{ 'bg-gray-700': child.type === 'route' && route.path === child.path }"
|
||||
>
|
||||
<span class="text-xl" v-html="child.icon"></span>
|
||||
<span class="ml-2 text-sm">{{ child.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="flex-1 p-6">
|
||||
<router-view></router-view>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div v-if="showModifyPasswordDialog" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div class="bg-gray-800 p-6 rounded-lg w-full max-w-md text-white">
|
||||
<h3 class="text-xl font-semibold mb-4">Modify Password</h3>
|
||||
<form @submit.prevent="modifyCurrentUserPassword">
|
||||
<!-- Add admin form inputs -->
|
||||
<div class="mb-4">
|
||||
<label class="block mb-1">Password</label>
|
||||
<input
|
||||
v-model="newPasswoed.password"
|
||||
type="password"
|
||||
class="w-full border rounded px-3 py-2 bg-gray-700 border-gray-600 text-white"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
@click="showModifyPasswordDialog = false"
|
||||
class="bg-gray-500 hover:bg-gray-600 px-4 py-2 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded"
|
||||
>
|
||||
Modify
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Restart Success Dialog -->
|
||||
<div v-if="showRestartSuccessDialog" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div class="bg-gray-800 p-6 rounded-lg shadow-xl max-w-md w-full mx-4">
|
||||
<h3 class="text-xl font-semibold mb-4">Service Restarted</h3>
|
||||
<p class="mb-4">Samba service has been successfully restarted.</p>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
@click="closeDialog"
|
||||
class="px-4 py-2 bg-blue-500 hover:bg-blue-600 rounded text-white"
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Restart Dialog -->
|
||||
<div v-if="showConfirmRestartDialog" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div class="bg-gray-800 p-6 rounded-lg shadow-xl max-w-md w-full mx-4">
|
||||
<h3 class="text-xl font-semibold mb-4">Settings Saved Successfully</h3>
|
||||
<p class="mb-4">Would you like to restart the Samba service to apply the changes?</p>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button
|
||||
@click="closeDialog"
|
||||
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 rounded text-white"
|
||||
:disabled="isRestarting"
|
||||
>
|
||||
Later
|
||||
</button>
|
||||
<button
|
||||
@click="restartSambaService"
|
||||
class="px-4 py-2 bg-blue-500 hover:bg-blue-600 rounded text-white"
|
||||
:disabled="isRestarting"
|
||||
>
|
||||
{{ isRestarting ? 'Restarting...' : 'Restart Now' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAuthStore } from './stores/auth'
|
||||
import axios from './utils/axios'
|
||||
|
||||
const API_BASE = `${import.meta.env.VITE_API_HOST}/api/admin`
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const isSidebarOpen = ref(true)
|
||||
const openSubmenus = ref(new Set())
|
||||
const showModifyPasswordDialog = ref(false)
|
||||
const newPasswoed = ref({ password: '' })
|
||||
const isUserMenuOpen = ref(false)
|
||||
const userMenuButton = ref(null)
|
||||
|
||||
const showConfirmRestartDialog = ref(false);
|
||||
const showRestartSuccessDialog = ref(false);
|
||||
const isRestarting = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
const handleClickOutside = (event) => {
|
||||
if (userMenuButton.value && !userMenuButton.value.contains(event.target)) {
|
||||
isUserMenuOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const toggleUserMenu = () => {
|
||||
isUserMenuOpen.value = !isUserMenuOpen.value
|
||||
}
|
||||
|
||||
const menuItems = ref([
|
||||
{
|
||||
name: 'DashBoard',
|
||||
path: '/',
|
||||
icon: '📊',
|
||||
type: 'route'
|
||||
},
|
||||
{
|
||||
name: 'User Management',
|
||||
path: '/user_manage',
|
||||
icon: '👥',
|
||||
type: 'route'
|
||||
},
|
||||
{
|
||||
name: 'Admin Management',
|
||||
path: '/admin',
|
||||
icon: '👤',
|
||||
type: 'route'
|
||||
},
|
||||
{
|
||||
name: 'Samba Settings',
|
||||
icon: '⚙️',
|
||||
type: 'submenu',
|
||||
children: [
|
||||
{
|
||||
name: 'Global Settings',
|
||||
path: '/system/samba/global',
|
||||
icon: '🌐',
|
||||
type: 'route'
|
||||
},
|
||||
{
|
||||
name: 'Sections Settings',
|
||||
path: '/system/samba/sections',
|
||||
icon: '🔧',
|
||||
type: 'route'
|
||||
},
|
||||
{
|
||||
name: 'Restart Service',
|
||||
icon: '🔄',
|
||||
type: 'button',
|
||||
action: () => openSuccessDialog()
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Refresh Data',
|
||||
icon: '🔄',
|
||||
type: 'button',
|
||||
action: () => alert('Data refreshed!')
|
||||
}
|
||||
])
|
||||
|
||||
const toggleSidebar = () => {
|
||||
isSidebarOpen.value = !isSidebarOpen.value
|
||||
if (!isSidebarOpen.value) {
|
||||
openSubmenus.value.clear()
|
||||
}
|
||||
}
|
||||
|
||||
const toggleSubmenu = (item) => {
|
||||
if (openSubmenus.value.has(item.name)) {
|
||||
openSubmenus.value.delete(item.name)
|
||||
} else {
|
||||
openSubmenus.value.add(item.name)
|
||||
}
|
||||
}
|
||||
|
||||
const isSubmenuOpen = (item) => {
|
||||
return openSubmenus.value.has(item.name)
|
||||
}
|
||||
|
||||
const navigateTo = (path) => {
|
||||
if (path) {
|
||||
router.push(path)
|
||||
}
|
||||
}
|
||||
|
||||
const modifyCurrentUserPassword = async () => {
|
||||
try {
|
||||
await axios.put(`${API_BASE}/admins/${authStore.user.id}/password`, { password: newPasswoed.value.password })
|
||||
alert('Password changed successfully!')
|
||||
showModifyPasswordDialog.value = false
|
||||
newPasswoed.value.password = ""
|
||||
} catch (error) {
|
||||
alert('Error changing password: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
const openSuccessDialog = () => {
|
||||
showConfirmRestartDialog.value = true;
|
||||
}
|
||||
|
||||
const closeDialog = () => {
|
||||
showConfirmRestartDialog.value = false;
|
||||
showRestartSuccessDialog.value = false;
|
||||
};
|
||||
|
||||
const restartSambaService = async () => {
|
||||
try {
|
||||
isRestarting.value = true;
|
||||
await axios.post(`${import.meta.env.VITE_API_HOST}/api/samba/config/restart_samba`);
|
||||
showConfirmRestartDialog.value = false;
|
||||
showRestartSuccessDialog.value = true;
|
||||
} catch (error) {
|
||||
alert('Error restarting Samba service: ' + error.message);
|
||||
} finally {
|
||||
isRestarting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
isUserMenuOpen.value = false
|
||||
await authStore.logout()
|
||||
router.push('/login')
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.rotate-180 {
|
||||
transform: rotate(180deg);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.rotate-90 {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
</style>
|
||||
BIN
frontend/user-management-frontend/src/assets/background.png
Normal file
BIN
frontend/user-management-frontend/src/assets/background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 191 KiB |
1
frontend/user-management-frontend/src/assets/vue.svg
Normal file
1
frontend/user-management-frontend/src/assets/vue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
@@ -0,0 +1,43 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps({
|
||||
msg: String,
|
||||
})
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<button type="button" @click="count++">count is {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test HMR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out
|
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||
>create-vue</a
|
||||
>, the official Vue + Vite starter
|
||||
</p>
|
||||
<p>
|
||||
Learn more about IDE Support for Vue in the
|
||||
<a
|
||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||
target="_blank"
|
||||
>Vue Docs Scaling up Guide</a
|
||||
>.
|
||||
</p>
|
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
3
frontend/user-management-frontend/src/main.css
Normal file
3
frontend/user-management-frontend/src/main.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
14
frontend/user-management-frontend/src/main.js
Normal file
14
frontend/user-management-frontend/src/main.js
Normal file
@@ -0,0 +1,14 @@
|
||||
// src/main.js
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
// import './style.css' // if you have any global CSS
|
||||
import './main.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
46
frontend/user-management-frontend/src/stores/auth.js
Normal file
46
frontend/user-management-frontend/src/stores/auth.js
Normal file
@@ -0,0 +1,46 @@
|
||||
// src/stores/auth.js
|
||||
import { defineStore } from 'pinia'
|
||||
import axios from 'axios'
|
||||
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
state: () => ({
|
||||
token: localStorage.getItem('token') || null,
|
||||
user: JSON.parse(localStorage.getItem('user')) || null
|
||||
}),
|
||||
|
||||
getters: {
|
||||
isAuthenticated: (state) => !!state.token,
|
||||
},
|
||||
|
||||
actions: {
|
||||
async login(username, password) {
|
||||
try {
|
||||
const response = await axios.post(`${import.meta.env.VITE_API_HOST}/api/admin/login`, {
|
||||
username,
|
||||
password
|
||||
})
|
||||
|
||||
this.token = response.data.token
|
||||
this.user = response.data.admin
|
||||
|
||||
localStorage.setItem('token', this.token)
|
||||
localStorage.setItem('user', JSON.stringify(this.user))
|
||||
|
||||
// Set default Authorization header for all future requests
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${this.token}`
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
throw error.response?.data?.error || error.message
|
||||
}
|
||||
},
|
||||
|
||||
logout() {
|
||||
this.token = null
|
||||
this.user = null
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
delete axios.defaults.headers.common['Authorization']
|
||||
}
|
||||
}
|
||||
})
|
||||
79
frontend/user-management-frontend/src/style.css
Normal file
79
frontend/user-management-frontend/src/style.css
Normal file
@@ -0,0 +1,79 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
/* min-width: 320px;
|
||||
min-height: 100vh; */
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
33
frontend/user-management-frontend/src/utils/axios.js
Normal file
33
frontend/user-management-frontend/src/utils/axios.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import axios from 'axios';
|
||||
|
||||
// Request Interceptor
|
||||
axios.interceptors.request.use(config => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
} else {
|
||||
console.warn('No token found in localStorage.');
|
||||
}
|
||||
return config;
|
||||
}, error => {
|
||||
console.error('Request error:', error);
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
// Response Interceptor
|
||||
axios.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
console.error('Response error:', error); // Debug log
|
||||
if (error.response?.status === 401) {
|
||||
// Token expired or invalid
|
||||
console.warn('401 Unauthorized - redirecting to login.');
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default axios;
|
||||
262
frontend/user-management-frontend/src/views/AdminManageView.vue
Normal file
262
frontend/user-management-frontend/src/views/AdminManageView.vue
Normal file
@@ -0,0 +1,262 @@
|
||||
<template>
|
||||
<div class="p-4 bg-gray-900 text-white min-h-screen">
|
||||
<h1 class="text-2xl font-bold mb-4">Website User Management</h1>
|
||||
|
||||
<!-- Admin List -->
|
||||
<div class="mb-4">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-semibold">Admin List</h2>
|
||||
<div class="flex space-x-2">
|
||||
<button
|
||||
@click="showAddAdminDialog = true"
|
||||
class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded"
|
||||
>
|
||||
Add Admin
|
||||
</button>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
@input="searchAdmin"
|
||||
type="text"
|
||||
placeholder="Search username..."
|
||||
class="bg-gray-700 border border-gray-600 rounded px-4 py-2 text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Admins Table -->
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full bg-gray-800 border border-gray-700">
|
||||
<thead>
|
||||
<tr class="bg-gray-700">
|
||||
<th class="text-left p-3 border border-gray-600">Username</th>
|
||||
<th class="text-center p-3 border border-gray-600">Active</th>
|
||||
<th class="text-center p-3 border border-gray-600">Created Date</th>
|
||||
<th class="text-center p-3 border border-gray-600">Updated Date</th>
|
||||
<th class="text-center p-3 border border-gray-600">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="admin in paginatedAdmins" :key="admin.id" class="border-b border-gray-700 hover:bg-gray-750">
|
||||
<td class="p-3 border border-gray-700">{{ admin.username }}</td>
|
||||
<td class="p-3 border border-gray-700 text-center">
|
||||
<span v-if="admin.is_active" class="text-green-500">✔</span>
|
||||
<span v-else class="text-red-500">✘</span>
|
||||
</td>
|
||||
<td class="p-3 border border-gray-700 text-center">{{ formatDate(admin.created_at) }}</td>
|
||||
<td class="p-3 border border-gray-700 text-center">{{ formatDate(admin.updated_at) }}</td>
|
||||
<td class="p-3 border border-gray-700 text-center">
|
||||
<button
|
||||
@click="changePassword(admin)"
|
||||
class="bg-blue-500 hover:bg-blue-600 text-white px-3 py-1 rounded mr-2"
|
||||
>
|
||||
Change Password
|
||||
</button>
|
||||
<button
|
||||
:class="{
|
||||
'bg-green-500 hover:bg-green-600': !admin.is_active,
|
||||
'bg-red-500 hover:bg-red-600': admin.is_active
|
||||
}"
|
||||
class="text-white px-3 py-1 rounded mr-2"
|
||||
@click="toggleActiveStatus(admin)"
|
||||
>
|
||||
{{ admin.is_active ? 'Deactivate' : 'Activate' }}
|
||||
</button>
|
||||
<button
|
||||
@click="confirmDeleteAdmin(admin)"
|
||||
class="bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
<span>Total {{ filteredAdmins.length }} admins</span>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button
|
||||
@click="goToPage(currentPage - 1)"
|
||||
:disabled="currentPage === 1"
|
||||
class="px-3 py-1 bg-gray-700 hover:bg-gray-600 text-white rounded"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<span>Page {{ currentPage }} of {{ totalPages }}</span>
|
||||
<button
|
||||
@click="goToPage(currentPage + 1)"
|
||||
:disabled="currentPage === totalPages"
|
||||
class="px-3 py-1 bg-gray-700 hover:bg-gray-600 text-white rounded"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Admin Dialog -->
|
||||
<div v-if="showAddAdminDialog" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div class="bg-gray-800 p-6 rounded-lg w-full max-w-md text-white">
|
||||
<h3 class="text-xl font-semibold mb-4">Add New Admin</h3>
|
||||
<form @submit.prevent="createAdmin">
|
||||
<!-- Add admin form inputs -->
|
||||
<div class="mb-4">
|
||||
<label class="block mb-1">Username</label>
|
||||
<input
|
||||
v-model="newAdmin.username"
|
||||
class="w-full border rounded px-3 py-2 bg-gray-700 border-gray-600 text-white"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block mb-1">Password</label>
|
||||
<input
|
||||
v-model="newAdmin.password"
|
||||
type="password"
|
||||
class="w-full border rounded px-3 py-2 bg-gray-700 border-gray-600 text-white"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
@click="showAddAdminDialog = false"
|
||||
class="bg-gray-500 hover:bg-gray-600 px-4 py-2 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded"
|
||||
>
|
||||
Add Admin
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import axios from '../utils/axios'
|
||||
|
||||
const API_BASE = `${import.meta.env.VITE_API_HOST}/api/admin`
|
||||
|
||||
const admins = ref([])
|
||||
const showAddAdminDialog = ref(false)
|
||||
const newAdmin = ref({ username: '', password: '' })
|
||||
const currentPage = ref(1)
|
||||
const itemsPerPage = 10
|
||||
const searchQuery = ref('')
|
||||
const filteredAdmins = ref([])
|
||||
|
||||
const paginatedAdmins = computed(() => {
|
||||
const start = (currentPage.value - 1) * itemsPerPage
|
||||
const end = start + itemsPerPage
|
||||
return filteredAdmins.value.slice(start, end)
|
||||
})
|
||||
|
||||
const totalPages = computed(() => Math.ceil(filteredAdmins.value.length / itemsPerPage))
|
||||
|
||||
const goToPage = (page) => {
|
||||
if (page >= 1 && page <= totalPages.value) {
|
||||
currentPage.value = page
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchAdmins()
|
||||
})
|
||||
|
||||
const fetchAdmins = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE}/admins`)
|
||||
admins.value = response.data
|
||||
filteredAdmins.value = admins.value
|
||||
} catch (error) {
|
||||
alert('Error fetching admins: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
const createAdmin = async () => {
|
||||
try {
|
||||
await axios.post(`${API_BASE}/admins`, newAdmin.value)
|
||||
showAddAdminDialog.value = false
|
||||
alert('Admin successfully added!')
|
||||
newAdmin.value = { username: '', password: '' }
|
||||
await fetchAdmins()
|
||||
} catch (error) {
|
||||
alert('Error creating admin: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDeleteAdmin = async (admin) => {
|
||||
const confirmed = confirm(`Are you sure you want to delete admin: ${admin.username}?`)
|
||||
if (confirmed) {
|
||||
await deleteAdmin(admin)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteAdmin = async (admin) => {
|
||||
try {
|
||||
await axios.delete(`${API_BASE}/admins/${admin.id}`)
|
||||
await fetchAdmins()
|
||||
} catch (error) {
|
||||
alert('Error deleting admin: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
const searchAdmin = () => {
|
||||
if (searchQuery.value.trim() === '') {
|
||||
filteredAdmins.value = admins.value
|
||||
} else {
|
||||
filteredAdmins.value = admins.value.filter(admin =>
|
||||
admin.username.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
)
|
||||
}
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
const changePassword = async (admin) => {
|
||||
const newPassword = prompt(`Enter new password for ${admin.username}:`)
|
||||
if (newPassword) {
|
||||
try {
|
||||
await axios.put(`${API_BASE}/admins/${admin.id}/password`, { password: newPassword })
|
||||
alert('Password changed successfully!')
|
||||
await fetchAdmins() // Refresh the table
|
||||
} catch (error) {
|
||||
alert('Error changing password: ' + error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toggleActiveStatus = async (admin) => {
|
||||
const newStatus = !admin.is_active
|
||||
try {
|
||||
await axios.put(`${API_BASE}/admins/${admin.id}/active`, { is_active: newStatus })
|
||||
await fetchAdmins() // Refresh the table
|
||||
} catch (error) {
|
||||
alert('Error toggling active status: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
const date = new Date(dateString)
|
||||
date.setHours(date.getHours() + 8) // Adjust for UTC+8
|
||||
return date.toISOString().replace('T', ' ').split('.')[0]
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
background-color: #1e1e2f;
|
||||
color: #fff;
|
||||
font-family: 'Arial', sans-serif;
|
||||
}
|
||||
</style>
|
||||
142
frontend/user-management-frontend/src/views/MarkDownView.vue
Normal file
142
frontend/user-management-frontend/src/views/MarkDownView.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div class="p-4 bg-gray-900 text-white min-h-screen">
|
||||
<h1 class="text-2xl font-bold mb-4">Help Documentation</h1>
|
||||
|
||||
<div v-if="loading" class="text-center text-gray-400">
|
||||
Loading content...
|
||||
</div>
|
||||
<div v-else-if="error" class="text-center text-red-400">
|
||||
Error loading content: {{ error }}
|
||||
</div>
|
||||
<div v-else class="prose prose-invert max-w-none markdown-body">
|
||||
<VueMarkdown
|
||||
:source="markdownContent"
|
||||
:options="{
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
breaks: true
|
||||
}"
|
||||
class="markdown-content"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import axios from '../utils/axios'
|
||||
import VueMarkdown from 'vue3-markdown-it'
|
||||
|
||||
const API_BASE = `${import.meta.env.VITE_API_HOST}/api/markdowns`
|
||||
|
||||
const props = defineProps({
|
||||
filePath: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const loading = ref(true)
|
||||
const error = ref(null)
|
||||
const markdownContent = ref('')
|
||||
|
||||
const fetchMarkdown = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE}/${props.filePath}`)
|
||||
markdownContent.value = response.data
|
||||
} catch (err) {
|
||||
error.value = err.message || 'Failed to load content'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchMarkdown)
|
||||
watch(() => props.filePath, fetchMarkdown)
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.markdown-body {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.markdown-content {
|
||||
@apply text-white;
|
||||
}
|
||||
|
||||
/* Lists styling */
|
||||
.markdown-content ul {
|
||||
list-style-type: disc;
|
||||
padding-left: 1.5em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.markdown-content ol {
|
||||
list-style-type: decimal;
|
||||
padding-left: 1.5em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.markdown-content li {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.markdown-content li > ul,
|
||||
.markdown-content li > ol {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
.markdown-content pre {
|
||||
@apply bg-gray-800 p-4 rounded-lg overflow-x-auto my-4;
|
||||
}
|
||||
|
||||
.markdown-content code {
|
||||
@apply bg-gray-800 px-1 py-0.5 rounded text-sm;
|
||||
}
|
||||
|
||||
/* Headers */
|
||||
.markdown-content h1 {
|
||||
@apply text-2xl font-bold mb-4 mt-6;
|
||||
}
|
||||
|
||||
.markdown-content h2 {
|
||||
@apply text-xl font-bold mb-3 mt-5;
|
||||
}
|
||||
|
||||
.markdown-content h3 {
|
||||
@apply text-lg font-bold mb-2 mt-4;
|
||||
}
|
||||
|
||||
/* Links */
|
||||
.markdown-content a {
|
||||
@apply text-blue-400 hover:text-blue-300 underline;
|
||||
}
|
||||
|
||||
/* Blockquotes */
|
||||
.markdown-content blockquote {
|
||||
@apply border-l-4 border-gray-600 pl-4 italic my-4;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.markdown-content table {
|
||||
@apply w-full border-collapse my-4;
|
||||
}
|
||||
|
||||
.markdown-content th,
|
||||
.markdown-content td {
|
||||
@apply border border-gray-700 p-2;
|
||||
}
|
||||
|
||||
.markdown-content thead {
|
||||
@apply bg-gray-800;
|
||||
}
|
||||
|
||||
/* Horizontal rule */
|
||||
.markdown-content hr {
|
||||
@apply border-gray-600 my-6;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,307 @@
|
||||
<template>
|
||||
<div class="p-4 bg-gray-900 text-white min-h-screen">
|
||||
<h1 class="text-2xl font-bold mb-4">Samba Global Settings</h1>
|
||||
|
||||
<!-- Settings List -->
|
||||
<div class="mb-4">
|
||||
<h2 class="text-xl font-semibold mb-4">Global Settings</h2>
|
||||
|
||||
<!-- Table for displaying settings -->
|
||||
<div class="overflow-x-auto mb-4">
|
||||
<table class="min-w-full bg-gray-800 border border-gray-700">
|
||||
<thead>
|
||||
<tr class="bg-gray-700">
|
||||
<th class="text-left p-3 border border-gray-600">Key</th>
|
||||
<th class="text-left p-3 border border-gray-600">Value</th>
|
||||
<th class="text-center p-3 border border-gray-600">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(value, key) in displaySettings"
|
||||
:key="key"
|
||||
class="border-b border-gray-700 hover:bg-gray-750"
|
||||
:class="{
|
||||
'line-through opacity-50': deletedSettings[key],
|
||||
'bg-green-900': newSettings[key]
|
||||
}"
|
||||
>
|
||||
<td class="p-3 border border-gray-700">
|
||||
{{ key }}
|
||||
<span v-if="newSettings[key]" class="text-green-500 ml-2">(New)</span>
|
||||
</td>
|
||||
<td class="p-3 border border-gray-700">
|
||||
<input
|
||||
v-model="editableSettings[key]"
|
||||
class="bg-gray-700 border border-gray-600 rounded px-2 py-1 text-white w-full"
|
||||
:disabled="deletedSettings[key]"
|
||||
/>
|
||||
</td>
|
||||
<td class="p-3 border border-gray-700 text-center">
|
||||
<div class="flex justify-center space-x-2">
|
||||
<!-- Show Remove for existing settings -->
|
||||
<button
|
||||
v-if="!deletedSettings[key] && !newSettings[key]"
|
||||
@click="markAsDeleted(key)"
|
||||
class="bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
<!-- Show Undo for deleted settings -->
|
||||
<button
|
||||
v-if="deletedSettings[key]"
|
||||
@click="undoDelete(key)"
|
||||
class="bg-blue-500 hover:bg-blue-600 text-white px-3 py-1 rounded"
|
||||
>
|
||||
Undo
|
||||
</button>
|
||||
<!-- Show Remove for new settings -->
|
||||
<button
|
||||
v-if="newSettings[key]"
|
||||
@click="removeNewSetting(key)"
|
||||
class="bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Add New Setting -->
|
||||
<div class="mb-4">
|
||||
<h3 class="text-lg font-semibold mb-2">Add New Setting</h3>
|
||||
<div class="space-y-4">
|
||||
<div class="flex space-x-2">
|
||||
<div class="w-1/2">
|
||||
<label class="block text-sm font-medium mb-1">
|
||||
Key <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="newSetting.key"
|
||||
type="text"
|
||||
placeholder="Enter setting key"
|
||||
class="bg-gray-700 border border-gray-600 rounded px-4 py-2 text-white w-full"
|
||||
:class="{'border-red-500': showValidation && !newSetting.key}"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-1/2">
|
||||
<label class="block text-sm font-medium mb-1">
|
||||
Value <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="newSetting.value"
|
||||
type="text"
|
||||
placeholder="Enter setting value"
|
||||
class="bg-gray-700 border border-gray-600 rounded px-4 py-2 text-white w-full"
|
||||
:class="{'border-red-500': showValidation && !newSetting.value}"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<button
|
||||
@click="addSetting"
|
||||
class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modified Save Button -->
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
@click="saveSettings"
|
||||
class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded"
|
||||
:disabled="isSaving"
|
||||
>
|
||||
{{ isSaving ? 'Saving...' : 'Save Changes' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Success Dialog -->
|
||||
<div v-if="showSuccessDialog" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div class="bg-gray-800 p-6 rounded-lg shadow-xl max-w-md w-full mx-4">
|
||||
<h3 class="text-xl font-semibold mb-4">Settings Saved Successfully</h3>
|
||||
<p class="mb-4">Would you like to restart the Samba service to apply the changes?</p>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button
|
||||
@click="closeSuccessDialog"
|
||||
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 rounded text-white"
|
||||
:disabled="isRestarting"
|
||||
>
|
||||
Later
|
||||
</button>
|
||||
<button
|
||||
@click="restartSambaService"
|
||||
class="px-4 py-2 bg-blue-500 hover:bg-blue-600 rounded text-white"
|
||||
:disabled="isRestarting"
|
||||
>
|
||||
{{ isRestarting ? 'Restarting...' : 'Restart Now' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Restart Success Dialog -->
|
||||
<div v-if="showRestartSuccessDialog" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div class="bg-gray-800 p-6 rounded-lg shadow-xl max-w-md w-full mx-4">
|
||||
<h3 class="text-xl font-semibold mb-4">Service Restarted</h3>
|
||||
<p class="mb-4">Samba service has been successfully restarted.</p>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
@click="closeRestartSuccessDialog"
|
||||
class="px-4 py-2 bg-blue-500 hover:bg-blue-600 rounded text-white"
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import axios from '../utils/axios';
|
||||
|
||||
const API_BASE = `${import.meta.env.VITE_API_HOST}/api/samba/config`;
|
||||
|
||||
const globalSettings = ref({ name: 'global', settings: {} });
|
||||
const editableSettings = ref({});
|
||||
const deletedSettings = ref({});
|
||||
const newSettings = ref({});
|
||||
const newSetting = ref({ key: '', value: '' });
|
||||
const showValidation = ref(false);
|
||||
|
||||
const showSuccessDialog = ref(false);
|
||||
const showRestartSuccessDialog = ref(false);
|
||||
const isSaving = ref(false);
|
||||
const isRestarting = ref(false);
|
||||
|
||||
// Computed property to combine all settings for display
|
||||
const displaySettings = computed(() => {
|
||||
return {
|
||||
...globalSettings.value.settings,
|
||||
...newSettings.value
|
||||
};
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchGlobalSettings();
|
||||
});
|
||||
|
||||
const fetchGlobalSettings = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE}/global_setting`);
|
||||
globalSettings.value = response.data;
|
||||
editableSettings.value = { ...response.data.settings };
|
||||
// Reset all tracking states
|
||||
deletedSettings.value = {};
|
||||
newSettings.value = {};
|
||||
showValidation.value = false;
|
||||
} catch (error) {
|
||||
alert('Error fetching global settings: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const addSetting = () => {
|
||||
showValidation.value = true;
|
||||
|
||||
if (!newSetting.value.key || !newSetting.value.value) {
|
||||
return alert('Please provide both key and value for the new setting.');
|
||||
}
|
||||
|
||||
if (displaySettings.value[newSetting.value.key]) {
|
||||
return alert('This key already exists. Please use a different key.');
|
||||
}
|
||||
|
||||
newSettings.value[newSetting.value.key] = newSetting.value.value;
|
||||
editableSettings.value[newSetting.value.key] = newSetting.value.value;
|
||||
|
||||
// Reset new setting form
|
||||
newSetting.value = { key: '', value: '' };
|
||||
showValidation.value = false;
|
||||
};
|
||||
|
||||
const markAsDeleted = (key) => {
|
||||
const confirmed = confirm(`Are you sure you want to remove the setting: ${key}?`);
|
||||
if (confirmed) {
|
||||
deletedSettings.value[key] = true;
|
||||
}
|
||||
};
|
||||
|
||||
const undoDelete = (key) => {
|
||||
deletedSettings.value[key] = false;
|
||||
};
|
||||
|
||||
const removeNewSetting = (key) => {
|
||||
const confirmed = confirm(`Are you sure you want to remove the new setting: ${key}?`);
|
||||
if (confirmed) {
|
||||
delete newSettings.value[key];
|
||||
delete editableSettings.value[key];
|
||||
}
|
||||
};
|
||||
|
||||
// Modified saveSettings function
|
||||
const saveSettings = async () => {
|
||||
try {
|
||||
isSaving.value = true;
|
||||
const updatedSettings = {
|
||||
name: globalSettings.value.name,
|
||||
settings: Object.entries(editableSettings.value).reduce((acc, [key, value]) => {
|
||||
if (!deletedSettings.value[key]) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {})
|
||||
};
|
||||
|
||||
await axios.post(`${API_BASE}/update_global_setting`, {
|
||||
global_settings: JSON.stringify(updatedSettings)
|
||||
});
|
||||
|
||||
await fetchGlobalSettings();
|
||||
|
||||
// Show success dialog instead of alert
|
||||
showSuccessDialog.value = true;
|
||||
} catch (error) {
|
||||
alert('Error saving settings: ' + error.message);
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Add new functions for dialog control
|
||||
const closeSuccessDialog = () => {
|
||||
showSuccessDialog.value = false;
|
||||
};
|
||||
|
||||
const closeRestartSuccessDialog = () => {
|
||||
showRestartSuccessDialog.value = false;
|
||||
};
|
||||
|
||||
const restartSambaService = async () => {
|
||||
try {
|
||||
isRestarting.value = true;
|
||||
await axios.post(`${API_BASE}/restart_samba`);
|
||||
showSuccessDialog.value = false;
|
||||
showRestartSuccessDialog.value = true;
|
||||
} catch (error) {
|
||||
alert('Error restarting Samba service: ' + error.message);
|
||||
} finally {
|
||||
isRestarting.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
background-color: #1e1e2f;
|
||||
color: #fff;
|
||||
font-family: 'Arial', sans-serif;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,559 @@
|
||||
<template>
|
||||
<div class="p-4 bg-gray-900 text-white min-h-screen">
|
||||
<h1 class="text-2xl font-bold mb-4">Samba Section Settings</h1>
|
||||
|
||||
<!-- Modified Section Selector -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium mb-2">Select Section</label>
|
||||
<div class="flex space-x-2">
|
||||
<select
|
||||
v-model="selectedSection"
|
||||
class="bg-gray-700 border border-gray-600 rounded px-4 py-2 text-white w-64"
|
||||
>
|
||||
<option value="">Select a section...</option>
|
||||
<optgroup label="Active Sections">
|
||||
<option
|
||||
v-for="(section, name) in activeSections"
|
||||
:key="name"
|
||||
:value="name"
|
||||
:class="{'font-bold': newSections[name]}"
|
||||
>
|
||||
{{ name }} {{ newSections[name] ? '(New)' : '' }}
|
||||
</option>
|
||||
</optgroup>
|
||||
<optgroup v-if="hasDeletedSections" label="Deleted Sections">
|
||||
<option
|
||||
v-for="(section, name) in deletedSectionsGroup"
|
||||
:key="name"
|
||||
:value="name"
|
||||
>
|
||||
{{ name }} (Deleted)
|
||||
</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<button
|
||||
@click="showNewSectionForm = true"
|
||||
class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded"
|
||||
>
|
||||
Add New Section
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modified New Section Form -->
|
||||
<div v-if="showNewSectionForm" class="mb-6 p-4 bg-gray-800 rounded">
|
||||
<h3 class="text-lg font-semibold mb-2">Add New Section</h3>
|
||||
<div class="space-y-4">
|
||||
<div class="flex space-x-2">
|
||||
<input
|
||||
v-model="newSectionName"
|
||||
type="text"
|
||||
placeholder="Section Name (e.g., shared_folder)"
|
||||
class="bg-gray-700 border border-gray-600 rounded px-4 py-2 text-white w-64"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div v-for="field in defaultFields" :key="field.key">
|
||||
<label class="block text-sm font-medium mb-1">
|
||||
{{ field.label }}
|
||||
<span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="newSectionDefaults[field.key]"
|
||||
type="text"
|
||||
:placeholder="field.placeholder"
|
||||
class="bg-gray-700 border border-gray-600 rounded px-4 py-2 text-white w-full"
|
||||
:class="{'border-red-500': showValidation && !newSectionDefaults[field.key]}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-2 mt-4">
|
||||
<button
|
||||
@click="addNewSection"
|
||||
class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded"
|
||||
>
|
||||
Add Section
|
||||
</button>
|
||||
<button
|
||||
@click="cancelNewSection"
|
||||
class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section Settings -->
|
||||
<div v-if="selectedSection" class="mb-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-semibold">
|
||||
Settings for: {{ selectedSection }}
|
||||
<span v-if="newSections[selectedSection]" class="text-green-500 ml-2">(New)</span>
|
||||
<span v-if="deletedSections[selectedSection]" class="text-red-500 ml-2">(Deleted)</span>
|
||||
</h2>
|
||||
<div class="space-x-2">
|
||||
<button
|
||||
v-if="!deletedSections[selectedSection]"
|
||||
@click="markSectionAsDeleted(selectedSection)"
|
||||
class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded"
|
||||
>
|
||||
Remove Section
|
||||
</button>
|
||||
<button
|
||||
v-if="deletedSections[selectedSection]"
|
||||
@click="undoSectionDelete(selectedSection)"
|
||||
class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded"
|
||||
>
|
||||
Undo Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Table -->
|
||||
<div class="overflow-x-auto mb-4">
|
||||
<table class="min-w-full bg-gray-800 border border-gray-700">
|
||||
<thead>
|
||||
<tr class="bg-gray-700">
|
||||
<th class="text-left p-3 border border-gray-600">Key</th>
|
||||
<th class="text-left p-3 border border-gray-600">Value</th>
|
||||
<th class="text-center p-3 border border-gray-600">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(value, key) in currentSectionSettings"
|
||||
:key="key"
|
||||
class="border-b border-gray-700 hover:bg-gray-750"
|
||||
:class="{
|
||||
'line-through opacity-50': deletedSettings[selectedSection]?.[key],
|
||||
'bg-green-900': newSettings[selectedSection]?.[key]
|
||||
}"
|
||||
>
|
||||
<td class="p-3 border border-gray-700">{{ key }}</td>
|
||||
<td class="p-3 border border-gray-700">
|
||||
<input
|
||||
v-model="editableSettings[selectedSection][key]"
|
||||
class="bg-gray-700 border border-gray-600 rounded px-2 py-1 text-white w-full"
|
||||
:disabled="deletedSettings[selectedSection]?.[key]"
|
||||
/>
|
||||
</td>
|
||||
<td class="p-3 border border-gray-700 text-center">
|
||||
<button
|
||||
v-if="!deletedSettings[selectedSection]?.[key] && !newSettings[selectedSection]?.[key]"
|
||||
@click="markSettingAsDeleted(selectedSection, key)"
|
||||
class="bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
<button
|
||||
v-if="deletedSettings[selectedSection]?.[key]"
|
||||
@click="undoSettingDelete(selectedSection, key)"
|
||||
class="bg-blue-500 hover:bg-blue-600 text-white px-3 py-1 rounded"
|
||||
>
|
||||
Undo
|
||||
</button>
|
||||
<button
|
||||
v-if="newSettings[selectedSection]?.[key]"
|
||||
@click="removeNewSetting(selectedSection, key)"
|
||||
class="bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Add New Setting -->
|
||||
<div class="mb-4">
|
||||
<h3 class="text-lg font-semibold mb-2">Add New Setting</h3>
|
||||
<div class="flex space-x-2">
|
||||
<input
|
||||
v-model="newSetting.key"
|
||||
type="text"
|
||||
placeholder="Key"
|
||||
class="bg-gray-700 border border-gray-600 rounded px-4 py-2 text-white w-1/2"
|
||||
/>
|
||||
<input
|
||||
v-model="newSetting.value"
|
||||
type="text"
|
||||
placeholder="Value"
|
||||
class="bg-gray-700 border border-gray-600 rounded px-4 py-2 text-white w-1/2"
|
||||
/>
|
||||
<button
|
||||
@click="addSetting"
|
||||
class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modified Save Button -->
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
@click="saveSettings"
|
||||
class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded"
|
||||
:disabled="isSaving"
|
||||
>
|
||||
{{ isSaving ? 'Saving...' : 'Save All Changes' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Success Dialog -->
|
||||
<div v-if="showSuccessDialog" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div class="bg-gray-800 p-6 rounded-lg shadow-xl max-w-md w-full mx-4">
|
||||
<h3 class="text-xl font-semibold mb-4">Settings Saved Successfully</h3>
|
||||
<p class="mb-4">Would you like to restart the Samba service to apply the changes?</p>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button
|
||||
@click="closeSuccessDialog"
|
||||
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 rounded text-white"
|
||||
:disabled="isRestarting"
|
||||
>
|
||||
Later
|
||||
</button>
|
||||
<button
|
||||
@click="restartSambaService"
|
||||
class="px-4 py-2 bg-blue-500 hover:bg-blue-600 rounded text-white"
|
||||
:disabled="isRestarting"
|
||||
>
|
||||
{{ isRestarting ? 'Restarting...' : 'Restart Now' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Restart Success Dialog -->
|
||||
<div v-if="showRestartSuccessDialog" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div class="bg-gray-800 p-6 rounded-lg shadow-xl max-w-md w-full mx-4">
|
||||
<h3 class="text-xl font-semibold mb-4">Service Restarted</h3>
|
||||
<p class="mb-4">Samba service has been successfully restarted.</p>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
@click="closeRestartSuccessDialog"
|
||||
class="px-4 py-2 bg-blue-500 hover:bg-blue-600 rounded text-white"
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import axios from '../utils/axios';
|
||||
|
||||
const API_BASE = `${import.meta.env.VITE_API_HOST}/api/samba/config`;
|
||||
|
||||
const sections = ref({});
|
||||
const selectedSection = ref('');
|
||||
const showNewSectionForm = ref(false);
|
||||
const newSectionName = ref('');
|
||||
const newSetting = ref({ key: '', value: '' });
|
||||
|
||||
const editableSettings = ref({});
|
||||
const deletedSettings = ref({});
|
||||
const newSettings = ref({});
|
||||
const deletedSections = ref({});
|
||||
const newSections = ref({});
|
||||
|
||||
const showSuccessDialog = ref(false);
|
||||
const showRestartSuccessDialog = ref(false);
|
||||
const isSaving = ref(false);
|
||||
const isRestarting = ref(false);
|
||||
|
||||
const newSectionDefaults = ref({});
|
||||
const showValidation = ref(false);
|
||||
const defaultFields = [
|
||||
{
|
||||
key: 'path',
|
||||
label: 'Path',
|
||||
placeholder: '/path/to/share (e.g., /mnt/shared)'
|
||||
},
|
||||
{
|
||||
key: 'comment',
|
||||
label: 'Comment',
|
||||
placeholder: 'Description of the share (e.g., Shared Documents)'
|
||||
},
|
||||
{
|
||||
key: 'browseable',
|
||||
label: 'Browseable',
|
||||
placeholder: 'yes/no (e.g., yes)'
|
||||
},
|
||||
{
|
||||
key: 'writeable',
|
||||
label: 'Writeable',
|
||||
placeholder: 'yes/no (e.g., yes)'
|
||||
},
|
||||
{
|
||||
key: 'create mask',
|
||||
label: 'Create Mask',
|
||||
placeholder: 'File permissions (e.g., 0777)'
|
||||
},
|
||||
{
|
||||
key: 'directory mask',
|
||||
label: 'Directory Mask',
|
||||
placeholder: 'Folder permissions (e.g., 0777)'
|
||||
},
|
||||
{
|
||||
key: 'read only',
|
||||
label: 'Read Only',
|
||||
placeholder: 'yes/no (e.g., no)'
|
||||
},
|
||||
{
|
||||
key: 'guest ok',
|
||||
label: 'Guest OK',
|
||||
placeholder: 'yes/no (e.g., no)'
|
||||
},
|
||||
{
|
||||
key: 'valid users',
|
||||
label: 'Valid Users',
|
||||
placeholder: 'List of users (e.g., @users, john, mary)'
|
||||
}
|
||||
];
|
||||
|
||||
// Modified computed properties
|
||||
const activeSections = computed(() => {
|
||||
const allSections = { ...sections.value, ...newSections.value };
|
||||
return Object.keys(allSections).reduce((acc, key) => {
|
||||
if (!deletedSections.value[key]) {
|
||||
acc[key] = allSections[key];
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
});
|
||||
|
||||
const deletedSectionsGroup = computed(() => {
|
||||
const allSections = { ...sections.value, ...newSections.value };
|
||||
return Object.keys(allSections).reduce((acc, key) => {
|
||||
if (deletedSections.value[key]) {
|
||||
acc[key] = allSections[key];
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
});
|
||||
|
||||
const hasDeletedSections = computed(() => {
|
||||
return Object.keys(deletedSectionsGroup.value).length > 0;
|
||||
});
|
||||
|
||||
// We can keep displaySections for other parts of the component that need all sections
|
||||
const displaySections = computed(() => {
|
||||
return { ...sections.value, ...newSections.value };
|
||||
});
|
||||
|
||||
// Computed property for current section settings
|
||||
const currentSectionSettings = computed(() => {
|
||||
if (!selectedSection.value) return {};
|
||||
const baseSettings = sections.value[selectedSection.value]?.settings || {};
|
||||
const newSectionSettings = newSettings.value[selectedSection.value] || {};
|
||||
return { ...baseSettings, ...newSectionSettings };
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchSections();
|
||||
});
|
||||
|
||||
const fetchSections = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE}/section_setting`);
|
||||
sections.value = response.data;
|
||||
initializeEditableSettings();
|
||||
} catch (error) {
|
||||
alert('Error fetching sections: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const initializeEditableSettings = () => {
|
||||
editableSettings.value = Object.keys(sections.value).reduce((acc, section) => {
|
||||
acc[section] = { ...sections.value[section].settings };
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const resetNewSectionForm = () => {
|
||||
newSectionName.value = '';
|
||||
newSectionDefaults.value = {};
|
||||
showValidation.value = false;
|
||||
};
|
||||
|
||||
const cancelNewSection = () => {
|
||||
resetNewSectionForm();
|
||||
showNewSectionForm.value = false;
|
||||
};
|
||||
|
||||
// Modified addNewSection function
|
||||
const addNewSection = () => {
|
||||
showValidation.value = true;
|
||||
|
||||
if (!newSectionName.value) {
|
||||
alert('Please provide a section name.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (sections.value[newSectionName.value] || newSections.value[newSectionName.value]) {
|
||||
alert('This section already exists.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if all required fields are filled
|
||||
const missingFields = defaultFields
|
||||
.filter(field => !newSectionDefaults.value[field.key])
|
||||
.map(field => field.label);
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
alert(`Please fill in all required fields:\n${missingFields.join('\n')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new section with default fields
|
||||
newSections.value[newSectionName.value] = {
|
||||
name: newSectionName.value,
|
||||
settings: { ...newSectionDefaults.value }
|
||||
};
|
||||
|
||||
editableSettings.value[newSectionName.value] = { ...newSectionDefaults.value };
|
||||
newSettings.value[newSectionName.value] = { ...newSectionDefaults.value };
|
||||
|
||||
selectedSection.value = newSectionName.value;
|
||||
resetNewSectionForm();
|
||||
showNewSectionForm.value = false;
|
||||
};
|
||||
|
||||
const markSectionAsDeleted = (section) => {
|
||||
const confirmed = confirm(`Are you sure you want to remove the section: ${section}?`);
|
||||
if (confirmed) {
|
||||
deletedSections.value[section] = true;
|
||||
}
|
||||
};
|
||||
|
||||
const undoSectionDelete = (section) => {
|
||||
deletedSections.value[section] = false;
|
||||
};
|
||||
|
||||
const addSetting = () => {
|
||||
if (!newSetting.value.key || !newSetting.value.value) {
|
||||
return alert('Please provide both key and value for the new setting.');
|
||||
}
|
||||
|
||||
if (!newSettings.value[selectedSection.value]) {
|
||||
newSettings.value[selectedSection.value] = {};
|
||||
}
|
||||
|
||||
if (!editableSettings.value[selectedSection.value]) {
|
||||
editableSettings.value[selectedSection.value] = {};
|
||||
}
|
||||
|
||||
if (currentSectionSettings.value[newSetting.value.key]) {
|
||||
return alert('This key already exists in the section.');
|
||||
}
|
||||
|
||||
newSettings.value[selectedSection.value][newSetting.value.key] = newSetting.value.value;
|
||||
editableSettings.value[selectedSection.value][newSetting.value.key] = newSetting.value.value;
|
||||
newSetting.value = { key: '', value: '' };
|
||||
};
|
||||
|
||||
const markSettingAsDeleted = (section, key) => {
|
||||
if (!deletedSettings.value[section]) {
|
||||
deletedSettings.value[section] = {};
|
||||
}
|
||||
deletedSettings.value[section][key] = true;
|
||||
};
|
||||
|
||||
const undoSettingDelete = (section, key) => {
|
||||
if (deletedSettings.value[section]) {
|
||||
deletedSettings.value[section][key] = false;
|
||||
}
|
||||
};
|
||||
|
||||
const removeNewSetting = (section, key) => {
|
||||
if (newSettings.value[section]) {
|
||||
delete newSettings.value[section][key];
|
||||
delete editableSettings.value[section][key];
|
||||
}
|
||||
};
|
||||
|
||||
// Modified saveSettings function
|
||||
const saveSettings = async () => {
|
||||
try {
|
||||
isSaving.value = true;
|
||||
const updatedSections = {};
|
||||
|
||||
// Process existing and new sections
|
||||
Object.keys({ ...sections.value, ...newSections.value }).forEach(sectionName => {
|
||||
if (!deletedSections.value[sectionName]) {
|
||||
const sectionSettings = {};
|
||||
const currentSettings = editableSettings.value[sectionName] || {};
|
||||
|
||||
Object.keys(currentSettings).forEach(key => {
|
||||
if (!deletedSettings.value[sectionName]?.[key]) {
|
||||
sectionSettings[key] = currentSettings[key];
|
||||
}
|
||||
});
|
||||
|
||||
updatedSections[sectionName] = {
|
||||
name: sectionName,
|
||||
settings: sectionSettings
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
await axios.post(`${API_BASE}/update_section_setting`, {
|
||||
section_settings: JSON.stringify(updatedSections)
|
||||
});
|
||||
|
||||
await fetchSections();
|
||||
|
||||
// Reset tracking states
|
||||
deletedSettings.value = {};
|
||||
newSettings.value = {};
|
||||
deletedSections.value = {};
|
||||
newSections.value = {};
|
||||
|
||||
// Show success dialog instead of alert
|
||||
showSuccessDialog.value = true;
|
||||
} catch (error) {
|
||||
alert('Error saving settings: ' + error.message);
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Add new functions for dialog control
|
||||
const closeSuccessDialog = () => {
|
||||
showSuccessDialog.value = false;
|
||||
};
|
||||
|
||||
const closeRestartSuccessDialog = () => {
|
||||
showRestartSuccessDialog.value = false;
|
||||
};
|
||||
|
||||
const restartSambaService = async () => {
|
||||
try {
|
||||
isRestarting.value = true;
|
||||
await axios.post(`${API_BASE}/restart_samba`);
|
||||
showSuccessDialog.value = false;
|
||||
showRestartSuccessDialog.value = true;
|
||||
} catch (error) {
|
||||
alert('Error restarting Samba service: ' + error.message);
|
||||
} finally {
|
||||
isRestarting.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
background-color: #1e1e2f;
|
||||
color: #fff;
|
||||
font-family: 'Arial', sans-serif;
|
||||
}
|
||||
</style>
|
||||
421
frontend/user-management-frontend/src/views/UserManageView.vue
Normal file
421
frontend/user-management-frontend/src/views/UserManageView.vue
Normal file
@@ -0,0 +1,421 @@
|
||||
<template>
|
||||
<div class="p-4 bg-gray-900 text-white min-h-screen">
|
||||
<h1 class="text-2xl font-bold mb-4">Linux User Management</h1>
|
||||
|
||||
<!-- User List -->
|
||||
<div class="mb-4">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-semibold">User List</h2>
|
||||
<div class="flex space-x-2">
|
||||
<button
|
||||
@click="showAddUserDialog = true"
|
||||
class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded"
|
||||
>
|
||||
Add User
|
||||
</button>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
@input="searchUser"
|
||||
type="text"
|
||||
placeholder="Search username..."
|
||||
class="bg-gray-700 border border-gray-600 rounded px-4 py-2 text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Users Table -->
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full bg-gray-800 border border-gray-700">
|
||||
<thead>
|
||||
<tr class="bg-gray-700">
|
||||
<th class="text-left p-3 border border-gray-600">Username</th>
|
||||
<th class="text-left p-3 border border-gray-600">UID</th>
|
||||
<th class="text-left p-3 border border-gray-600">GID</th>
|
||||
<th class="text-left p-3 border border-gray-600">Home Directory</th>
|
||||
<th class="text-left p-3 border border-gray-600">Shell</th>
|
||||
<th class="text-center p-3 border border-gray-600">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="user in paginatedUsers" :key="user.username" class="border-b border-gray-700 hover:bg-gray-750">
|
||||
<td class="p-3 border border-gray-700">{{ user.username }}</td>
|
||||
<td class="p-3 border border-gray-700">{{ user.uid }}</td>
|
||||
<td class="p-3 border border-gray-700">{{ user.gid }}</td>
|
||||
<td class="p-3 border border-gray-700">{{ user.home_dir }}</td>
|
||||
<td class="p-3 border border-gray-700">{{ user.shell }}</td>
|
||||
<td class="p-3 border border-gray-700 text-center">
|
||||
<button
|
||||
@click="changePassword(user)"
|
||||
class="bg-blue-500 hover:bg-blue-600 text-white px-3 py-1 rounded mr-2"
|
||||
>
|
||||
Change Password
|
||||
</button>
|
||||
<button
|
||||
@click="openEditGroupsDialog(user)"
|
||||
class="bg-yellow-500 hover:bg-yellow-600 text-white px-3 py-1 rounded mr-2"
|
||||
>
|
||||
Edit Groups
|
||||
</button>
|
||||
<button
|
||||
@click="confirmDeleteUser(user)"
|
||||
class="bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
<span>Total {{ filteredUsers.length }} users</span>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button
|
||||
@click="goToPage(currentPage - 1)"
|
||||
:disabled="currentPage === 1"
|
||||
class="px-3 py-1 bg-gray-700 hover:bg-gray-600 text-white rounded"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<span>Page {{ currentPage }} of {{ totalPages }}</span>
|
||||
<button
|
||||
@click="goToPage(currentPage + 1)"
|
||||
:disabled="currentPage === totalPages"
|
||||
class="px-3 py-1 bg-gray-700 hover:bg-gray-600 text-white rounded"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dialogs remain unchanged but styled for dark theme -->
|
||||
<div v-if="showAddUserDialog" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div class="bg-gray-800 p-6 rounded-lg w-full max-w-md text-white">
|
||||
<h3 class="text-xl font-semibold mb-4">Add New User</h3>
|
||||
<form @submit.prevent="createUser">
|
||||
<!-- Add user form inputs -->
|
||||
<div class="mb-4">
|
||||
<label class="block mb-1">Username</label>
|
||||
<input
|
||||
v-model="newUser.username"
|
||||
class="w-full border rounded px-3 py-2 bg-gray-700 border-gray-600 text-white"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block mb-1">Password</label>
|
||||
<input
|
||||
v-model="newUser.password"
|
||||
type="password"
|
||||
class="w-full border rounded px-3 py-2 bg-gray-700 border-gray-600 text-white"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block mb-1">Group</label>
|
||||
<input
|
||||
v-model="newUser.group"
|
||||
class="w-full border rounded px-3 py-2 bg-gray-700 border-gray-600 text-white"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block mb-1">Shell</label>
|
||||
<select
|
||||
v-model="newUser.shell"
|
||||
class="w-full border rounded px-3 py-2 bg-gray-700 border-gray-600 text-white"
|
||||
>
|
||||
<option value="/bin/bash">/bin/bash</option>
|
||||
<option value="/sbin/nologin">/sbin/nologin</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="inline-flex items-center">
|
||||
<input
|
||||
v-model="newUser.isAdmin"
|
||||
type="checkbox"
|
||||
class="form-checkbox bg-gray-700 border-gray-600 text-green-500"
|
||||
/>
|
||||
<span class="ml-2">Administrator</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
@click="showAddUserDialog = false"
|
||||
class="bg-gray-500 hover:bg-gray-600 px-4 py-2 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded"
|
||||
>
|
||||
Add User
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Groups Dialog -->
|
||||
<div v-if="showEditGroupsDialog" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div class="bg-gray-800 p-6 rounded-lg w-full max-w-md text-white">
|
||||
<h3 class="text-xl font-semibold mb-4">Edit Groups for {{ selectedUser.username }}</h3>
|
||||
|
||||
<!-- Display existing groups -->
|
||||
<div class="mb-4">
|
||||
<label class="block mb-1">Current Groups</label>
|
||||
<ul class="list-none space-y-2">
|
||||
<li v-for="group in userGroups" :key="group" class="flex justify-between items-center">
|
||||
<span>{{ group }}</span>
|
||||
<button
|
||||
@click="removeGroup(group)"
|
||||
class="bg-red-500 hover:bg-red-600 text-white px-2 py-1 rounded"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Select Groups to Add -->
|
||||
<div class="mb-4">
|
||||
<label class="block mb-1">Select Groups to Add</label>
|
||||
<select
|
||||
v-model="selectedGroupsToAdd"
|
||||
multiple
|
||||
class="w-full border rounded px-3 py-2 bg-gray-700 border-gray-600 text-white"
|
||||
>
|
||||
<option v-for="group in allGroups" :key="group" :value="group">{{ group }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex justify-between gap-4">
|
||||
<button
|
||||
@click="addGroups"
|
||||
class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded"
|
||||
>
|
||||
Add Groups
|
||||
</button>
|
||||
<button
|
||||
@click="closeEditGroupsDialog"
|
||||
class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import axios from '../utils/axios'
|
||||
|
||||
const API_BASE = `${import.meta.env.VITE_API_HOST}/api`
|
||||
|
||||
const users = ref([])
|
||||
const showAddUserDialog = ref(false)
|
||||
const newUser = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
group: '',
|
||||
shell: '/bin/bash',
|
||||
isAdmin: false
|
||||
})
|
||||
|
||||
const showEditGroupsDialog = ref(false)
|
||||
const selectedUser = ref({})
|
||||
const userGroups = ref([]) // Groups the user currently belongs to
|
||||
const allGroups = ref([]) // All available groups to select from
|
||||
const selectedGroupsToAdd = ref([]) // Groups to be added
|
||||
|
||||
const currentPage = ref(1)
|
||||
const itemsPerPage = 10
|
||||
const searchQuery = ref('')
|
||||
const filteredUsers = ref([])
|
||||
|
||||
const paginatedUsers = computed(() => {
|
||||
const start = (currentPage.value - 1) * itemsPerPage
|
||||
const end = start + itemsPerPage
|
||||
return filteredUsers.value.slice(start, end)
|
||||
})
|
||||
|
||||
const totalPages = computed(() => Math.ceil(filteredUsers.value.length / itemsPerPage))
|
||||
|
||||
const goToPage = (page) => {
|
||||
if (page >= 1 && page <= totalPages.value) {
|
||||
currentPage.value = page
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchUsers()
|
||||
})
|
||||
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE}/users`)
|
||||
users.value = response.data
|
||||
filteredUsers.value = users.value
|
||||
} catch (error) {
|
||||
alert('Error fetching users: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const createUser = async () => {
|
||||
try {
|
||||
if (newUser.value.isAdmin) {
|
||||
// Create admin user
|
||||
await axios.post(`${API_BASE}/admin/admins`, {
|
||||
username: newUser.value.username,
|
||||
password: newUser.value.password
|
||||
}, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// Also create regular user if needed
|
||||
await axios.post(`${API_BASE}/users`, newUser.value)
|
||||
} else {
|
||||
// Create regular user only
|
||||
await axios.post(`${API_BASE}/users`, newUser.value)
|
||||
}
|
||||
|
||||
showAddUserDialog.value = false
|
||||
alert('User successfully added!')
|
||||
newUser.value = { username: '', password: '', group: '', shell: '/bin/bash', isAdmin: false }
|
||||
await fetchUsers()
|
||||
} catch (error) {
|
||||
alert('Error creating user: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDeleteUser = async (user) => {
|
||||
const confirmed = confirm(`Are you sure you want to delete user: ${user.username}?`)
|
||||
if (confirmed) {
|
||||
await deleteUser(user)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteUser = async (user) => {
|
||||
try {
|
||||
await axios.delete(`${API_BASE}/users/${user.username}`)
|
||||
await fetchUsers()
|
||||
} catch (error) {
|
||||
alert('Error deleting user: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
const searchUser = () => {
|
||||
if (searchQuery.value.trim() === '') {
|
||||
filteredUsers.value = users.value
|
||||
} else {
|
||||
filteredUsers.value = users.value.filter(user =>
|
||||
user.username.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
)
|
||||
}
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
// Open the Edit Groups dialog and fetch user groups
|
||||
const openEditGroupsDialog = async (user) => {
|
||||
selectedUser.value = user
|
||||
showEditGroupsDialog.value = true
|
||||
try {
|
||||
// Fetch the groups the user is already a member of
|
||||
const response = await axios.get(`${API_BASE}/groups/${user.username}`)
|
||||
userGroups.value = response.data.map(group => group.groupName)
|
||||
|
||||
// Fetch all groups
|
||||
const allGroupsResponse = await axios.get(`${API_BASE}/groups`)
|
||||
allGroups.value = allGroupsResponse.data.map(group => group.groupName)
|
||||
|
||||
// Filter out the groups the user is already a member of
|
||||
allGroups.value = allGroups.value.filter(group => !userGroups.value.includes(group))
|
||||
|
||||
} catch (error) {
|
||||
alert('Error fetching groups: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add multiple groups to the user
|
||||
const addGroups = async () => {
|
||||
if (selectedGroupsToAdd.value.length === 0) {
|
||||
return alert('Please select at least one group to add!')
|
||||
}
|
||||
|
||||
try {
|
||||
// Send an array of groups to the API
|
||||
await axios.post(`${API_BASE}/groups/${selectedUser.value.username}`, {
|
||||
groups: selectedGroupsToAdd.value
|
||||
})
|
||||
// Add selected groups to userGroups locally
|
||||
userGroups.value.push(...selectedGroupsToAdd.value)
|
||||
alert('Groups added successfully!')
|
||||
selectedGroupsToAdd.value = [] // Reset the selected groups
|
||||
await openEditGroupsDialog(selectedUser.value) // Refresh available groups and update the list
|
||||
} catch (error) {
|
||||
alert('Error adding groups: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove a group from the user
|
||||
const removeGroup = async (group) => {
|
||||
const confirmed = confirm(`Are you sure you want to remove ${selectedUser.value.username} from ${group}?`)
|
||||
if (!confirmed) return
|
||||
|
||||
try {
|
||||
await axios.delete(`${API_BASE}/groups/${selectedUser.value.username}`, {
|
||||
data: { group }
|
||||
})
|
||||
// Remove the group locally
|
||||
userGroups.value = userGroups.value.filter((g) => g !== group)
|
||||
alert('Group removed successfully!')
|
||||
|
||||
// After removing, refresh the available groups
|
||||
await openEditGroupsDialog(selectedUser.value) // Refresh available groups and update the list
|
||||
|
||||
} catch (error) {
|
||||
alert('Error removing group: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// Close the Edit Groups dialog
|
||||
const closeEditGroupsDialog = () => {
|
||||
showEditGroupsDialog.value = false
|
||||
selectedUser.value = {}
|
||||
userGroups.value = []
|
||||
allGroups.value = []
|
||||
selectedGroupsToAdd.value = [] // Reset the selection
|
||||
}
|
||||
|
||||
const changePassword = async (user) => {
|
||||
const newPassword = prompt(`Enter new password for ${user.username}:`)
|
||||
if (newPassword) {
|
||||
try {
|
||||
await axios.put(`${API_BASE}/users/${user.username}`, { password: newPassword })
|
||||
alert('Password changed successfully!')
|
||||
} catch (error) {
|
||||
alert('Error changing password: ' + error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
background-color: #1e1e2f;
|
||||
color: #fff;
|
||||
font-family: 'Arial', sans-serif;
|
||||
}
|
||||
</style>
|
||||
12
frontend/user-management-frontend/tailwind.config.js
Normal file
12
frontend/user-management-frontend/tailwind.config.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
6
frontend/user-management-frontend/vite.config.js
Normal file
6
frontend/user-management-frontend/vite.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
})
|
||||
Reference in New Issue
Block a user