wiki-plugin-farmmanager 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +10 -0
- package/LICENSE +21 -0
- package/README.md +94 -0
- package/package.json +39 -0
- package/pages/about-farmmanager-plugin +20 -0
- package/server/server.js +292 -0
package/.editorconfig
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) <YEAR> <COPYRIGHT HOLDER>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Federated Wiki - FarmManager Plugin
|
|
2
|
+
|
|
3
|
+
Plugin for managing a wiki instance as a farm of multiple wiki sites.
|
|
4
|
+
|
|
5
|
+
The focus of this plugin is to allow for programmatic, centralized management of a wiki farm by a farm administrator. It is intended to supersede `wiki-plugin-register` by providing administrative oversight rather than self-service registration.
|
|
6
|
+
|
|
7
|
+
## Design Philosophy
|
|
8
|
+
|
|
9
|
+
- **Admin-Focused**: All features are designed to be used by a farm administrator. Access will be restricted to admin users.
|
|
10
|
+
- **JSON API**: The server-side component will expose a well-defined API for managing sites. This ensures the API is predictable and can be used by other tools, not just our web interface.
|
|
11
|
+
- **Clear Separation**: The backend (API) and frontend (web interface) will be clearly separated. The web interface will be a client of the API.
|
|
12
|
+
- **Safety**: Destructive operations (like deleting a site) are non-destructive by default and require confirmation.
|
|
13
|
+
|
|
14
|
+
## API Design
|
|
15
|
+
|
|
16
|
+
Authentication for all endpoints will be handled by a middleware that checks `app.securityhandler.isAdmin(req)`.
|
|
17
|
+
|
|
18
|
+
### Endpoints
|
|
19
|
+
|
|
20
|
+
- **`GET /plugin/farmmanager/sites`**
|
|
21
|
+
- **Action**: List all wiki sites in the farm.
|
|
22
|
+
- **Response Body**: An array of site objects.
|
|
23
|
+
- `[{ "name": "site1.myfarm.com", "owner": "Alice", "pages": 12, "status": "active" }, ...]`
|
|
24
|
+
|
|
25
|
+
- **`POST /plugin/farmmanager/sites`**
|
|
26
|
+
- **Action**: Create a new wiki site.
|
|
27
|
+
- **Request Body**: `{ "domain": "newsite", "owner": "Bob" }`
|
|
28
|
+
- **Response Body**: The new site object.
|
|
29
|
+
|
|
30
|
+
- **`GET /plugin/farmmanager/sites/:domain`**
|
|
31
|
+
- **Action**: Get detailed information for a single site.
|
|
32
|
+
- **URL Parameter**: `:domain` is the full domain of the site (e.g., `site1.myfarm.com`).
|
|
33
|
+
- **Response Body**: A single site object.
|
|
34
|
+
|
|
35
|
+
- **`PATCH /plugin/farmmanager/sites/:domain`**
|
|
36
|
+
- **Action**: Partially update a site's properties (preserves unspecified fields).
|
|
37
|
+
- **Request Body**: `{ "owner": {"name": "Carol", ...} }` or `{ "status": "active" }` or both
|
|
38
|
+
- **Response Body**: The updated site object.
|
|
39
|
+
|
|
40
|
+
- **`DELETE /plugin/farmmanager/sites/:domain`**
|
|
41
|
+
- **Action**: Deactivate a wiki site (soft delete). Adds a `status.json` file marking the site as inactive.
|
|
42
|
+
- **Query Parameters**:
|
|
43
|
+
- `hard=true` - Permanently delete the site directory and all its contents (irreversible).
|
|
44
|
+
- **Response Body**: `{ "status": "ok", "message": "Site site1.myfarm.com deactivated." }`
|
|
45
|
+
|
|
46
|
+
## Web Interface Design
|
|
47
|
+
|
|
48
|
+
The web interface will be developed after the backend API is complete. It will follow the established FedWiki pattern of providing a client-side component that can be embedded and used within a wiki page.
|
|
49
|
+
|
|
50
|
+
## Development Plan
|
|
51
|
+
|
|
52
|
+
We can build this in phases.
|
|
53
|
+
|
|
54
|
+
- **Phase 1: Backend API**
|
|
55
|
+
1. **Setup**: Create the basic `server/server.js` file and `admin` security middleware.
|
|
56
|
+
2. **List Sites (Read)**: Implement the `GET /sites` endpoint. This will involve scanning the data directory for site folders and reading their `owner.json` and `status.json`.
|
|
57
|
+
3. **Create Site**: Implement the `POST /sites` endpoint. This can borrow logic from `wiki-plugin-register` but will be wrapped in admin security.
|
|
58
|
+
4. **Deactivate Site**: Implement the `DELETE /sites/:domain` endpoint. This will involve creating or updating a `status.json` file in the site's directory.
|
|
59
|
+
5. **Update Site**: Implement the `PUT /sites/:domain` endpoint to change the `owner.json`.
|
|
60
|
+
|
|
61
|
+
- **Phase 2: Frontend UI**
|
|
62
|
+
- TBD after the backend API is stable.
|
|
63
|
+
|
|
64
|
+
## Roadmap
|
|
65
|
+
|
|
66
|
+
- **Search/Filter**: Add a search or filter feature to the UI for farms with many sites.
|
|
67
|
+
- **UI Pattern Documentation**: Create documentation for the pattern of building plugins that render as components within a wiki page.
|
|
68
|
+
|
|
69
|
+
## Architecture Notes
|
|
70
|
+
|
|
71
|
+
### Site-Level Plugin with Farm-Level Scope
|
|
72
|
+
|
|
73
|
+
This plugin follows the standard FedWiki plugin architecture, which means it is loaded **per-site** when you navigate to a specific site in the farm. However, its purpose is to manage **farm-level** resources (all sites in the farm).
|
|
74
|
+
|
|
75
|
+
**What this means in practice:**
|
|
76
|
+
|
|
77
|
+
1. **Access Pattern**: You must first navigate to a specific site (e.g., `http://localhost`) to load the plugin. Once loaded, the API endpoints are available and can manage all sites in the farm.
|
|
78
|
+
|
|
79
|
+
2. **Admin Site Approach**: In practice, designate one site as your "admin site" (e.g., `localhost` or `admin.example.com`) and always access the farm management API through that site.
|
|
80
|
+
|
|
81
|
+
3. **Data Directory Resolution**: The plugin receives `argv.data` pointing to the current site's directory (e.g., `/data-farm/localhost`). It uses `path.resolve(argv.data, '..')` to access the parent farm directory and enumerate all sites.
|
|
82
|
+
|
|
83
|
+
4. **Security Context**: Admin authentication and authorization are tied to the site you're accessing the API from, which aligns with FedWiki's per-site security model.
|
|
84
|
+
|
|
85
|
+
This is consistent with how other farm-management plugins (like `wiki-plugin-register`) operate in the FedWiki ecosystem.
|
|
86
|
+
|
|
87
|
+
## Build
|
|
88
|
+
|
|
89
|
+
`npm install`
|
|
90
|
+
`npm run build`
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wiki-plugin-farmmanager",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Federated Wiki - Farm Manager Plugin",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://git.coopcloud.tech/wiki-cafe/wiki-plugin-farmmanager.git"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"wiki",
|
|
11
|
+
"federated wiki",
|
|
12
|
+
"plugin",
|
|
13
|
+
"farm",
|
|
14
|
+
"manager",
|
|
15
|
+
"admin",
|
|
16
|
+
"fedwiki"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"prettier:format": "prettier --write './**/*.js'",
|
|
20
|
+
"prettier:check": "prettier --check ./**/*.js"
|
|
21
|
+
},
|
|
22
|
+
"author": {
|
|
23
|
+
"name": "Christian Galo",
|
|
24
|
+
"email": "christian.galo@wiki.cafe",
|
|
25
|
+
"url": "http://galo.wiki.cafe"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"type": "module",
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20.x"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@eslint/js": "^9.28.0",
|
|
34
|
+
"esbuild": "^0.25.5",
|
|
35
|
+
"eslint": "^9.28.0",
|
|
36
|
+
"globals": "^16.2.0",
|
|
37
|
+
"prettier": "^3.5.3"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "About FarmManager Plugin",
|
|
3
|
+
"story": [
|
|
4
|
+
{ "type": "paragraph", "id": "9c1d7c35ad98ba82", "text": "Describe the plugin here." },
|
|
5
|
+
{ "type": "farmmanager", "id": "c5ab6c3b5cadc5f9", "text": "SAMPLE Markup" }
|
|
6
|
+
],
|
|
7
|
+
"journal": [
|
|
8
|
+
{
|
|
9
|
+
"type": "create",
|
|
10
|
+
"item": {
|
|
11
|
+
"title": "About FarmManager Plugin",
|
|
12
|
+
"story": [
|
|
13
|
+
{ "type": "paragraph", "id": "9c1d7c35ad98ba82", "text": "Describe the plugin here." },
|
|
14
|
+
{ "type": "farmmanager", "id": "c5ab6c3b5cadc5f9" , "text":"SAMPLE Markup" }
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"date": 1749415263346
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
package/server/server.js
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import fsp from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
// Helper function to read owner information from a site directory
|
|
5
|
+
const readSiteOwner = async function (siteDir) {
|
|
6
|
+
try {
|
|
7
|
+
const ownerPath = path.join(siteDir, 'status', 'owner.json')
|
|
8
|
+
const ownerData = await fsp.readFile(ownerPath, 'utf8')
|
|
9
|
+
const owner = JSON.parse(ownerData)
|
|
10
|
+
return owner.name || 'Unknown'
|
|
11
|
+
} catch (err) {
|
|
12
|
+
return 'Unknown'
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Helper function to read site status
|
|
17
|
+
const readSiteStatus = async function (siteDir) {
|
|
18
|
+
try {
|
|
19
|
+
const statusPath = path.join(siteDir, 'status.json')
|
|
20
|
+
const statusData = await fsp.readFile(statusPath, 'utf8')
|
|
21
|
+
const status = JSON.parse(statusData)
|
|
22
|
+
return status.status || 'active'
|
|
23
|
+
} catch (err) {
|
|
24
|
+
// If no status.json exists, assume the site is active
|
|
25
|
+
return 'active'
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Helper function to count pages in a site
|
|
30
|
+
const countSitePages = async function (siteDir) {
|
|
31
|
+
try {
|
|
32
|
+
const pagesDir = path.join(siteDir, 'pages')
|
|
33
|
+
const files = await fsp.readdir(pagesDir)
|
|
34
|
+
return files.length
|
|
35
|
+
} catch (err) {
|
|
36
|
+
return 0
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Helper function to get site information
|
|
41
|
+
const getSiteInfo = async function (siteName, dataDir) {
|
|
42
|
+
const siteDir = path.join(dataDir, siteName)
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const stats = await fsp.stat(siteDir)
|
|
46
|
+
if (!stats.isDirectory()) {
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check if this is actually a site by looking for status directory
|
|
51
|
+
try {
|
|
52
|
+
const statusStat = await fsp.stat(path.join(siteDir, 'status'))
|
|
53
|
+
if (!statusStat.isDirectory()) {
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
} catch (err) {
|
|
57
|
+
// Missing status directory means this isn't a valid site
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const [owner, status, pageCount] = await Promise.all([
|
|
62
|
+
readSiteOwner(siteDir),
|
|
63
|
+
readSiteStatus(siteDir),
|
|
64
|
+
countSitePages(siteDir),
|
|
65
|
+
])
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
name: siteName,
|
|
69
|
+
owner: owner,
|
|
70
|
+
pages: pageCount,
|
|
71
|
+
status: status,
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Main plugin export
|
|
79
|
+
const startServer = async function (params) {
|
|
80
|
+
const { app, argv } = params
|
|
81
|
+
|
|
82
|
+
// Calculate data directory once
|
|
83
|
+
// In farm mode, argv.data points to the current site's directory
|
|
84
|
+
// We need to go up one level to get to the farm root
|
|
85
|
+
const dataDir = path.resolve(argv.data || './data', '..')
|
|
86
|
+
|
|
87
|
+
// Middleware to ensure only admin users can access these endpoints
|
|
88
|
+
const admin = function (req, res, next) {
|
|
89
|
+
if (req.app.securityhandler && req.app.securityhandler.isAdmin(req)) {
|
|
90
|
+
next()
|
|
91
|
+
} else {
|
|
92
|
+
res.status(403).json({ error: 'Admin access required' })
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Middleware to ensure server is running in farm mode
|
|
97
|
+
const farm = function (req, res, next) {
|
|
98
|
+
if (argv.farm) {
|
|
99
|
+
next()
|
|
100
|
+
} else {
|
|
101
|
+
res.status(400).json({ error: 'Server must be running in farm mode' })
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// GET /plugin/farmmanager/sites - List all sites
|
|
106
|
+
app.get('/plugin/farmmanager/sites', farm, admin, async function (req, res) {
|
|
107
|
+
try {
|
|
108
|
+
const entries = await fsp.readdir(dataDir)
|
|
109
|
+
|
|
110
|
+
const sitePromises = entries.map(async entry => {
|
|
111
|
+
return await getSiteInfo(entry, dataDir)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const sites = await Promise.all(sitePromises)
|
|
115
|
+
const validSites = sites.filter(site => site !== null)
|
|
116
|
+
|
|
117
|
+
res.json(validSites)
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error('Error listing sites:', err)
|
|
120
|
+
res.status(500).json({ error: 'Failed to list sites' })
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// GET /plugin/farmmanager/sites/:domain - Get single site info
|
|
125
|
+
app.get('/plugin/farmmanager/sites/:domain', farm, admin, async function (req, res) {
|
|
126
|
+
try {
|
|
127
|
+
const domain = req.params.domain
|
|
128
|
+
|
|
129
|
+
const siteInfo = await getSiteInfo(domain, dataDir)
|
|
130
|
+
|
|
131
|
+
if (!siteInfo) {
|
|
132
|
+
return res.status(404).json({ error: 'Site not found' })
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
res.json(siteInfo)
|
|
136
|
+
} catch (err) {
|
|
137
|
+
console.error('Error getting site info:', err)
|
|
138
|
+
res.status(500).json({ error: 'Failed to get site information' })
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// POST /plugin/farmmanager/sites - Create a new site
|
|
143
|
+
app.post('/plugin/farmmanager/sites', farm, admin, async function (req, res) {
|
|
144
|
+
try {
|
|
145
|
+
const { domain, owner } = req.body
|
|
146
|
+
|
|
147
|
+
if (!domain || !owner) {
|
|
148
|
+
return res.status(400).json({ error: 'Domain and owner are required' })
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const siteDir = path.join(dataDir, domain)
|
|
152
|
+
|
|
153
|
+
// Check if site already exists
|
|
154
|
+
try {
|
|
155
|
+
await fsp.stat(siteDir)
|
|
156
|
+
return res.status(409).json({ error: 'Site already exists' })
|
|
157
|
+
} catch (err) {
|
|
158
|
+
// Site doesn't exist, which is what we want
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Create site directory structure (minimal - let wiki-server create others on-demand)
|
|
162
|
+
await fsp.mkdir(siteDir, { recursive: true })
|
|
163
|
+
await fsp.mkdir(path.join(siteDir, 'status'), { recursive: true })
|
|
164
|
+
|
|
165
|
+
// Create owner.json
|
|
166
|
+
const ownerData = req.body.owner
|
|
167
|
+
await fsp.writeFile(path.join(siteDir, 'status', 'owner.json'), JSON.stringify(ownerData, null, 2))
|
|
168
|
+
|
|
169
|
+
// Get the new site info and return it
|
|
170
|
+
const siteInfo = await getSiteInfo(domain, dataDir)
|
|
171
|
+
res.status(201).json(siteInfo)
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.error('Error creating site:', err)
|
|
174
|
+
res.status(500).json({ error: 'Failed to create site' })
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
// PATCH /plugin/farmmanager/sites/:domain - Partial update of site properties
|
|
179
|
+
app.patch('/plugin/farmmanager/sites/:domain', farm, admin, async function (req, res) {
|
|
180
|
+
try {
|
|
181
|
+
const domain = req.params.domain
|
|
182
|
+
const { owner, status } = req.body
|
|
183
|
+
|
|
184
|
+
if (!owner && !status) {
|
|
185
|
+
return res.status(400).json({ error: 'At least one field (owner or status) must be specified' })
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const siteDir = path.join(dataDir, domain)
|
|
189
|
+
const ownerPath = path.join(siteDir, 'status', 'owner.json')
|
|
190
|
+
const statusPath = path.join(siteDir, 'status.json')
|
|
191
|
+
|
|
192
|
+
// Check if site exists
|
|
193
|
+
try {
|
|
194
|
+
await fsp.stat(siteDir)
|
|
195
|
+
} catch (err) {
|
|
196
|
+
return res.status(404).json({ error: 'Site not found' })
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Update owner if specified (preserve existing data)
|
|
200
|
+
if (owner && typeof owner.name === 'string') {
|
|
201
|
+
let ownerData
|
|
202
|
+
try {
|
|
203
|
+
const existingData = await fsp.readFile(ownerPath, 'utf8')
|
|
204
|
+
ownerData = JSON.parse(existingData)
|
|
205
|
+
} catch (err) {
|
|
206
|
+
ownerData = {} // Start with an empty object if file doesn't exist
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
ownerData.name = owner.name
|
|
210
|
+
await fsp.writeFile(ownerPath, JSON.stringify(ownerData, null, 2))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Update status if specified
|
|
214
|
+
if (status) {
|
|
215
|
+
if (status !== 'active' && status !== 'inactive') {
|
|
216
|
+
return res.status(400).json({ error: 'Status must be either "active" or "inactive"' })
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let statusData = {}
|
|
220
|
+
try {
|
|
221
|
+
const existingData = await fsp.readFile(statusPath, 'utf8')
|
|
222
|
+
statusData = JSON.parse(existingData)
|
|
223
|
+
} catch (err) {
|
|
224
|
+
// No existing status file, which is fine
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
statusData.status = status
|
|
228
|
+
if (status === 'inactive') {
|
|
229
|
+
statusData.deactivatedAt = new Date().toISOString()
|
|
230
|
+
} else {
|
|
231
|
+
// If re-activating, remove the deactivation timestamp
|
|
232
|
+
delete statusData.deactivatedAt
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
await fsp.writeFile(statusPath, JSON.stringify(statusData, null, 2))
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Return updated site info
|
|
239
|
+
const siteInfo = await getSiteInfo(domain, dataDir)
|
|
240
|
+
res.json(siteInfo)
|
|
241
|
+
} catch (err) {
|
|
242
|
+
console.error('Error updating site properties:', err)
|
|
243
|
+
res.status(500).json({ error: 'Failed to update site properties' })
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
// DELETE /plugin/farmmanager/sites/:domain - Deactivate or permanently delete site
|
|
248
|
+
app.delete('/plugin/farmmanager/sites/:domain', farm, admin, async function (req, res) {
|
|
249
|
+
try {
|
|
250
|
+
const domain = req.params.domain
|
|
251
|
+
const hardDelete = req.query.hard === 'true'
|
|
252
|
+
const siteDir = path.join(dataDir, domain)
|
|
253
|
+
|
|
254
|
+
// Check if site exists
|
|
255
|
+
try {
|
|
256
|
+
await fsp.stat(siteDir)
|
|
257
|
+
} catch (err) {
|
|
258
|
+
return res.status(404).json({ error: 'Site not found' })
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (hardDelete) {
|
|
262
|
+
// Hard delete - permanently remove the entire site directory
|
|
263
|
+
await fsp.rm(siteDir, { recursive: true, force: true })
|
|
264
|
+
|
|
265
|
+
res.json({
|
|
266
|
+
status: 'ok',
|
|
267
|
+
message: `Site ${domain} permanently deleted.`,
|
|
268
|
+
})
|
|
269
|
+
} else {
|
|
270
|
+
// Soft delete - set status to inactive and add timestamp
|
|
271
|
+
const statusPath = path.join(siteDir, 'status.json')
|
|
272
|
+
const statusData = {
|
|
273
|
+
status: 'inactive',
|
|
274
|
+
deactivatedAt: new Date().toISOString(),
|
|
275
|
+
}
|
|
276
|
+
await fsp.writeFile(statusPath, JSON.stringify(statusData, null, 2))
|
|
277
|
+
|
|
278
|
+
res.json({
|
|
279
|
+
status: 'ok',
|
|
280
|
+
message: `Site ${domain} deactivated.`,
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
} catch (err) {
|
|
284
|
+
console.error('Error deleting site:', err)
|
|
285
|
+
res.status(500).json({ error: 'Failed to delete site' })
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
console.log('FarmManager plugin server started')
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export { startServer }
|