Compare commits

...

3 Commits

Author SHA1 Message Date
3f43e9c911 init frontend 2025-02-19 15:55:54 +08:00
fc46d48490 init frontend 2025-02-19 15:51:48 +08:00
de2d71301e init host_service 2025-02-19 15:42:33 +08:00
31 changed files with 6030 additions and 0 deletions

View File

@@ -0,0 +1 @@
VITE_API_HOST=http://localhost:8080

View File

@@ -0,0 +1 @@
VITE_API_HOST=

View File

@@ -0,0 +1,28 @@
# 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
frontend/user-management-frontend/src/views/*.tmp
dist-ssr
*.local
# Editor directories and files
.vscode/
.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

View 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).

View 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>

File diff suppressed because it is too large Load Diff

View 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"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View 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

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

View 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

View File

@@ -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>

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View 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')

View 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']
}
}
})

View 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;
}
}

View 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;

View 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>

View 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>

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

View File

@@ -0,0 +1,6 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
})

5
host_service/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.idea
utils/smb*
host_service
host_service_x86

3
host_service/go.mod Normal file
View File

@@ -0,0 +1,3 @@
module host_service
go 1.22.2

347
host_service/main.go Normal file
View File

@@ -0,0 +1,347 @@
package main
import (
"encoding/json"
"fmt"
"host_service/utils"
"io"
"log"
"net"
"os"
"os/exec"
"strings"
)
const SOCKET_PATH = "/var/run/usermgmt.sock"
type Command struct {
Action string `json:"action"`
Params map[string]interface{} `json:"params"`
}
type Response struct {
Success bool `json:"success"`
Data interface{} `json:"data"`
Error string `json:"error,omitempty"`
}
func main() {
// Ensure socket file does not exist
if err := exec.Command("rm", "-f", SOCKET_PATH).Run(); err != nil {
log.Fatal(err)
}
listener, err := net.Listen("unix", SOCKET_PATH)
if err != nil {
log.Fatal(err)
}
defer listener.Close()
// Change socket permissions to allow Docker container access
if err := exec.Command("chmod", "777", SOCKET_PATH).Run(); err != nil {
log.Fatal(err)
}
log.Printf("Listening on %s", SOCKET_PATH)
for {
conn, err := listener.Accept()
if err != nil {
log.Printf("Accept error: %v", err)
continue
}
go handleConnection(conn)
}
}
func execCommand(name string, args ...string) error {
cmd := exec.Command(name, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func checkGroupExists(group string) bool {
err := exec.Command("getent", "group", group).Run()
return err == nil
}
func handleConnection(conn net.Conn) {
defer conn.Close()
decoder := json.NewDecoder(conn)
encoder := json.NewEncoder(conn)
var cmd Command
if err := decoder.Decode(&cmd); err != nil {
encoder.Encode(Response{Success: false, Error: err.Error()})
return
}
var result Response
switch cmd.Action {
case "get_users":
output, err := exec.Command("bash", "-c", "getent passwd | awk -F: '$3 >= 1000 && $7 !~ /(false)/ {print $0}'").CombinedOutput()
if err != nil {
result = Response{Success: false, Error: err.Error()}
} else {
result = Response{Success: true, Data: string(output)}
}
case "create_user":
username := cmd.Params["username"].(string)
password := cmd.Params["password"].(string)
group := cmd.Params["group"].(string)
shell := cmd.Params["shell"].(string)
isAdmin := cmd.Params["is_admin"].(bool)
err := createUser(username, password, group, shell, isAdmin)
if err != nil {
result = Response{Success: false, Error: err.Error()}
} else {
result = Response{Success: true}
}
case "modify_user_passwd":
username := cmd.Params["username"].(string)
password := cmd.Params["password"].(string)
result = modifyUserPassword(username, password)
case "delete_user":
username := cmd.Params["username"].(string)
output, err := exec.Command("sudo", "deluser", username).CombinedOutput()
if err != nil {
result = Response{Success: false, Data: fmt.Sprintf("error delete user: %s", err)}
} else {
result = Response{Success: true, Data: string(output)}
}
case "get_samba_status":
output, err := exec.Command("smbstatus", "-f").CombinedOutput()
if err != nil {
result = Response{Success: false, Error: err.Error()}
} else {
result = Response{Success: true, Data: string(output)}
}
case "get_groups":
output, err := exec.Command("bash", "-c", "getent group | awk -F: '$3 >= 1000 {print $1}'").CombinedOutput()
if err != nil {
result = Response{Success: false, Error: err.Error()}
} else {
result = Response{Success: true, Data: strings.TrimSpace(string(output))}
}
case "get_user_groups":
username := cmd.Params["username"].(string)
// Get all groups for the user
groupOutput, err := exec.Command("groups", username).CombinedOutput()
if err != nil {
result = Response{Success: false, Error: err.Error()}
break
}
// Get group details with GIDs
allGroupsOutput, err := exec.Command("bash", "-c", "getent group | awk -F: '$3 >= 1000 {print $1}'").CombinedOutput()
if err != nil {
result = Response{Success: false, Error: err.Error()}
break
}
// Extract user groups and filter against user-created groups
userGroups := strings.Fields(string(groupOutput))[2:]
userCreatedGroups := strings.Split(strings.TrimSpace(string(allGroupsOutput)), "\n")
// Filter user groups to include only those in user-created groups
var filteredGroups []string
for _, group := range userGroups {
for _, userGroup := range userCreatedGroups {
if group == userGroup {
filteredGroups = append(filteredGroups, group)
break
}
}
}
result = Response{Success: true, Data: filteredGroups}
case "add_user_to_group":
username := cmd.Params["username"].(string)
group := cmd.Params["group"].(string)
err := addUserToGroup(username, group)
if err != nil {
result = Response{Success: false, Error: err.Error()}
} else {
result = Response{Success: true}
}
case "remove_user_from_group":
username := cmd.Params["username"].(string)
group := cmd.Params["group"].(string)
err := removeUserFromGroup(username, group)
if err != nil {
result = Response{Success: false, Error: err.Error()}
} else {
result = Response{Success: true}
}
case "get_samba_settings":
result = getSambaSettings()
case "update_section_setting":
jsonConfig, ok := cmd.Params["config"].(string)
if !ok {
result = Response{Success: false, Error: "Invalid or missing config parameter"}
break
}
result = updateSectionSetting(jsonConfig)
case "get_samba_service_info":
result = getSambaServiceInfo()
case "restart_samba_service":
result = restartSambaService()
}
encoder.Encode(result)
}
func modifyUserPassword(username string, password string) Response {
cmd := exec.Command("sudo", "chpasswd")
cmd.Stdin = strings.NewReader(fmt.Sprintf("%s:%s", username, password))
cmd = exec.Command("sudo", "smbpasswd", "-a", username)
// Create a pipe to handle multiple password inputs
r, w := io.Pipe()
cmd.Stdin = r
// Write passwords to pipe
go func() {
defer w.Close()
w.Write([]byte(fmt.Sprintf("%s\n%s\n", password, password)))
}()
if err := cmd.Run(); err != nil {
return Response{Success: false, Data: fmt.Sprintf("error changing password: %s", err)}
}
if err := cmd.Run(); err != nil {
return Response{Success: false, Data: fmt.Sprintf("error changing password: %s", err)}
}
return Response{Success: true, Data: "Change password success"}
}
func createUser(username, password, group string, shell string, isAdmin bool) error {
if group == "" {
group = "sambashare"
}
if err := execCommand("sudo", "useradd", "-M", "-s", shell, "-g", group, username); err != nil {
return fmt.Errorf("error creating user: %w", err)
}
cmd := exec.Command("sudo", "chpasswd")
cmd.Stdin = strings.NewReader(fmt.Sprintf("%s:%s", username, password))
if err := cmd.Run(); err != nil {
return fmt.Errorf("error setting user password: %w", err)
}
if checkGroupExists(group) {
if err := execCommand("sudo", "usermod", "-aG", group, username); err != nil {
return fmt.Errorf("error adding user to group: %w", err)
}
} else {
fmt.Printf("Warning: Group '%s' does not exist.\n", group)
}
if err := execCommand("sudo", "usermod", "-L", username); err != nil {
return fmt.Errorf("error locking user account: %w", err)
}
if err := execCommand("sudo", "usermod", "-s", shell, username); err != nil {
return fmt.Errorf("error setting shell: %w", err)
}
// Add user to Samba
smbCmd := exec.Command("sudo", "smbpasswd", "-a", username)
smbCmd.Stdin = strings.NewReader(fmt.Sprintf("%s\n%s\n", password, password))
if err := smbCmd.Run(); err != nil {
return fmt.Errorf("error adding user to Samba: %w", err)
}
fmt.Printf("User '%s' created successfully and configured for Samba access.\n", username)
return nil
}
func addUserToGroup(username, group string) error {
// Use gpasswd to add the user to the group
cmd := exec.Command("sudo", "gpasswd", "-a", username, group)
if err := cmd.Run(); err != nil {
return fmt.Errorf("error adding user to group: %w", err)
}
fmt.Printf("User '%s' added to group '%s'.\n", username, group)
return nil
}
func removeUserFromGroup(username, group string) error {
// Use gpasswd to remove the user from the group
cmd := exec.Command("sudo", "gpasswd", "-d", username, group)
if err := cmd.Run(); err != nil {
return fmt.Errorf("error removing user from group: %w", err)
}
fmt.Printf("User '%s' removed from group '%s'.\n", username, group)
return nil
}
func getSambaSettings() Response {
config, err := utils.NewSambaConfig("/etc/samba/smb.conf")
if err != nil {
return Response{Success: false, Error: fmt.Sprintf("Error creating config: %v", err)}
}
if err := config.LoadConfig(); err != nil {
return Response{Success: false, Error: fmt.Sprintf("Error loading config: %v", err)}
}
jsonData, err := json.Marshal(config)
if err != nil {
return Response{Success: false, Error: fmt.Sprintf("Error converting to JSON: %v", err)}
}
return Response{Success: true, Data: string(jsonData)}
}
func updateSectionSetting(jsonConfig string) Response {
config, err := utils.NewSambaConfig("/etc/samba/smb.conf")
if err != nil {
return Response{Success: false, Error: fmt.Sprintf("Error creating config: %v", err)}
}
if err := config.SaveConfig(jsonConfig); err != nil {
return Response{Success: false, Error: fmt.Sprintf("Error saving config: %v", err)}
}
return Response{Success: true, Data: "Configuration updated successfully"}
}
func getSambaServiceInfo() Response {
// service smbd restart
output, err := exec.Command("sudo", "smbstatus", "-j").CombinedOutput()
if err != nil {
return Response{Success: false, Data: fmt.Errorf("error to get samba status infomation: %w", err)}
}
fmt.Println("Success getting smb status")
return Response{Success: true, Data: string(output)}
}
func restartSambaService() Response {
// service smbd restart
cmd := exec.Command("sudo", "service", "smbd", "restart")
if err := cmd.Run(); err != nil {
return Response{Success: false, Data: fmt.Errorf("error to restart smbd service: %w", err)}
}
fmt.Println("Success restart samba service")
return Response{Success: true, Data: "Success restart samba service"}
}

Binary file not shown.

View File

@@ -0,0 +1,299 @@
package utils
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
// Custom errors
type ConfigError struct {
Op string
Err error
}
func (e *ConfigError) Error() string {
return fmt.Sprintf("%s: %v", e.Op, e.Err)
}
// Section represents a configuration section
type Section struct {
Name string `json:"name"`
Settings map[string]string `json:"settings"`
}
// SambaConfig represents the main configuration manager class
type SambaConfig struct {
filepath string
Global Section `json:"global"`
Sections map[string]Section `json:"sections"`
}
// validateFilePath checks if the file path is valid and accessible
func validateFilePath(path string) error {
if path == "" {
return fmt.Errorf("file path cannot be empty")
}
absPath, err := filepath.Abs(path)
if err != nil {
return fmt.Errorf("invalid path: %v", err)
}
dir := filepath.Dir(absPath)
if _, err := os.Stat(dir); os.IsNotExist(err) {
return fmt.Errorf("directory does not exist: %s", dir)
}
return nil
}
// NewSambaConfig creates a new instance of SambaConfig
func NewSambaConfig(filepath string) (*SambaConfig, error) {
if err := validateFilePath(filepath); err != nil {
return nil, &ConfigError{Op: "new_config", Err: err}
}
return &SambaConfig{
filepath: filepath,
Global: Section{
Name: "global",
Settings: make(map[string]string),
},
Sections: make(map[string]Section),
}, nil
}
// createBackup creates a timestamped backup of the existing config file
func (sc *SambaConfig) createBackup() error {
// Check if original file exists
if _, err := os.Stat(sc.filepath); os.IsNotExist(err) {
return nil // No need to backup if file doesn't exist
}
// Create timestamp for backup file
timestamp := time.Now().Format("20060102_150405")
backupPath := fmt.Sprintf("%s.%s.bak", sc.filepath, timestamp)
source, err := os.Open(sc.filepath)
if err != nil {
return &ConfigError{Op: "backup_open", Err: err}
}
defer source.Close()
destination, err := os.Create(backupPath)
if err != nil {
return &ConfigError{Op: "backup_create", Err: err}
}
defer destination.Close()
if _, err := destination.ReadFrom(source); err != nil {
return &ConfigError{Op: "backup_copy", Err: err}
}
return nil
}
// LoadConfig reads and parses the configuration file
func (sc *SambaConfig) LoadConfig() error {
if _, err := os.Stat(sc.filepath); os.IsNotExist(err) {
return &ConfigError{Op: "load_config", Err: fmt.Errorf("file does not exist: %s", sc.filepath)}
}
file, err := os.OpenFile(sc.filepath, os.O_RDONLY, 0644)
if err != nil {
return &ConfigError{Op: "load_config", Err: err}
}
defer file.Close()
scanner := bufio.NewScanner(file)
var currentSection Section
currentSection = sc.Global
const maxCapacity = 512 * 1024
buf := make([]byte, maxCapacity)
scanner.Buffer(buf, maxCapacity)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
continue
}
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
sectionName := line[1 : len(line)-1]
if sectionName == "global" {
currentSection = sc.Global
} else {
currentSection = Section{
Name: sectionName,
Settings: make(map[string]string),
}
}
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
currentSection.Settings[key] = value
if currentSection.Name == "global" {
sc.Global = currentSection
} else {
sc.Sections[currentSection.Name] = currentSection
}
}
}
if err := scanner.Err(); err != nil {
return &ConfigError{Op: "load_config_scan", Err: err}
}
return nil
}
// SaveConfig updates the configuration from JSON string and saves to file
// SaveConfig saves the Samba configuration after validating it
func (sc *SambaConfig) SaveConfig(jsonConfig string) error {
// Parse JSON config
var newConfig SambaConfig
if err := json.Unmarshal([]byte(jsonConfig), &newConfig); err != nil {
return &ConfigError{Op: "parse_json", Err: err}
}
// Create backup with timestamp
if err := sc.createBackup(); err != nil {
return err
}
// Update current config with new values
sc.Global = newConfig.Global
sc.Sections = newConfig.Sections
// Create temporary file
tmpFile, err := os.CreateTemp(filepath.Dir(sc.filepath), "smb.conf.tmp")
if err != nil {
return &ConfigError{Op: "save_config_temp", Err: err}
}
tmpPath := tmpFile.Name()
defer os.Remove(tmpPath)
// Write to temporary file
if err := sc.writeConfig(tmpFile); err != nil {
tmpFile.Close()
return err
}
tmpFile.Close()
// Validate the configuration file using `testparm`
cmd := exec.Command("testparm", "-s", tmpPath)
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return &ConfigError{
Op: "validate_config",
Err: fmt.Errorf("validation failed: %s", stderr.String()),
}
}
// Rename temporary file to target file
if err := os.Rename(tmpPath, sc.filepath); err != nil {
return &ConfigError{Op: "save_config_rename", Err: err}
}
// Set permissions
if err := os.Chmod(sc.filepath, 0644); err != nil {
return &ConfigError{Op: "save_config_chmod", Err: err}
}
return nil
}
// writeConfig writes the configuration to the provided file
func (sc *SambaConfig) writeConfig(file *os.File) error {
writer := bufio.NewWriter(file)
// Write global section
if _, err := writer.WriteString("[global]\n"); err != nil {
return &ConfigError{Op: "write_config", Err: err}
}
for key, value := range sc.Global.Settings {
if _, err := writer.WriteString(fmt.Sprintf(" %s = %s\n", key, value)); err != nil {
return &ConfigError{Op: "write_config", Err: err}
}
}
if _, err := writer.WriteString("\n"); err != nil {
return &ConfigError{Op: "write_config", Err: err}
}
// Write other sections
for _, section := range sc.Sections {
if _, err := writer.WriteString(fmt.Sprintf("[%s]\n", section.Name)); err != nil {
return &ConfigError{Op: "write_config", Err: err}
}
for key, value := range section.Settings {
if _, err := writer.WriteString(fmt.Sprintf(" %s = %s\n", key, value)); err != nil {
return &ConfigError{Op: "write_config", Err: err}
}
}
if _, err := writer.WriteString("\n"); err != nil {
return &ConfigError{Op: "write_config", Err: err}
}
}
return writer.Flush()
}
func main() {
// Example usage with JSON
config, err := NewSambaConfig("utils/smb.conf")
if err != nil {
fmt.Printf("Error creating config: %v\n", err)
return
}
// Load existing config
if err := config.LoadConfig(); err != nil {
fmt.Printf("Error loading config: %v\n", err)
return
}
// Example JSON configuration
jsonConfig := `{
"global": {
"name": "global",
"settings": {
"workgroup": "WORKGROUP",
"server string": "Samba Server"
}
},
"sections": {
"share1": {
"name": "share1",
"settings": {
"path": "/path/to/share1",
"valid users": "@users",
"writeable": "yes"
}
}
}
}`
// Update config with JSON
if err := config.SaveConfig(jsonConfig); err != nil {
fmt.Printf("Error saving config: %v\n", err)
return
}
}