A Deno-powered backend service for Plants vs. Zombies: MODDED. [Read-only GitHub mirror] docs.pvzm.net
express typescript expressjs plant deno jspvz pvzm game online backend plants-vs-zombies zombie javascript plants modded vs plantsvszombies openapi pvz noads
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix readme

Clay dfcd130c 8e9f291f

+138 -136
+1
.markdownlint.jsonc
··· 1 1 { 2 2 "MD013": false, // line length 3 3 "MD010": false, // hard tabs 4 + "MD033": false // inline HTML 4 5 }
+137 -136
README.md
··· 1 1 # PVZM Backend 2 2 3 - > v0.2.0 3 + <sup><sub>v0.2.0</sup></sub> 4 + 4 5 > A Deno-powered backend service for [Plants vs. Zombies: MODDED](https://github.com/roblnet13/pvz). This service provides APIs for uploading, downloading, listing, favoriting, and reporting user-created _I, Zombie_ levels. 5 6 6 7 ## Features ··· 20 21 21 22 - **Access**: Navigate to `/admin.html` or click the "Admin Dashboard" link on the main page 22 23 - **Features**: 23 - - View all levels with pagination 24 - - Search levels by name, author, or ID 25 - - Edit level properties (name, author, sun, water status, difficulty, statistics) 26 - - Delete levels (including related files and database entries) > Authentication: The admin UI supports optional GitHub OAuth. If `USE_GITHUB_AUTH=true`, users must sign in with GitHub and be included in `GITHUB_ALLOWED_USERS` to access admin endpoints. If `USE_GITHUB_AUTH=false`, the admin endpoints are not protected (not recommended in production). > The admin dashboard also supports one-time-token links for a single edit/delete action: 27 - - Edit: `/admin.html?token=...&action=edit&level=123` 28 - - Delete: `/admin.html?token=...&action=delete&level=123` 24 + - View all levels with pagination 25 + - Search levels by name, author, or ID 26 + - Edit level properties (name, author, sun, water status, difficulty, statistics) 27 + - Delete levels (including related files and database entries) > Authentication: The admin UI supports optional GitHub OAuth. If `USE_GITHUB_AUTH=true`, users must sign in with GitHub and be included in `GITHUB_ALLOWED_USERS` to access admin endpoints. If `USE_GITHUB_AUTH=false`, the admin endpoints are not protected (not recommended in production). > The admin dashboard also supports one-time-token links for a single edit/delete action: 28 + - Edit: `/admin.html?token=...&action=edit&level=123` 29 + - Delete: `/admin.html?token=...&action=delete&level=123` 29 30 After a successful token-based edit/delete, the page attempts to call `window.close()` (some browsers only allow this for windows opened by script). 30 31 31 32 ## Getting Started ··· 81 82 - **URL:** `/api/levels` 82 83 - **Method:** `POST` 83 84 - **Content Types:** 84 - - `application/octet-stream` 85 + - `application/octet-stream` 85 86 - **URL Params:** None 86 87 - **Query Params:** (for octet-stream) 87 - - `author`: Author name 88 - - `turnstileResponse`: Captcha verification token (if enabled) 88 + - `author`: Author name 89 + - `turnstileResponse`: Captcha verification token (if enabled) 89 90 - **Notes:** Only IZL3 is supported (v2 is deprecated). 90 91 - **Request Body:** Raw binary level data (`.izl3`), sent as the request body. 91 92 - **Success Response:** 92 - - **Code:** 201 93 - - **Content:** 93 + - **Code:** 201 94 + - **Content:** 94 95 95 - ```json 96 - { 97 - "id": 123, 98 - "name": "Level Name", 99 - "author": "Author Name", 100 - "created_at": 1714680000, 101 - "sun": 100, 102 - "is_water": true, 103 - "version": 3 104 - } 105 - ``` 96 + ```json 97 + { 98 + "id": 123, 99 + "name": "Level Name", 100 + "author": "Author Name", 101 + "created_at": 1714680000, 102 + "sun": 100, 103 + "is_water": true, 104 + "version": 3 105 + } 106 + ``` 106 107 107 - Note: `is_water` is stored as `0/1` in the database and is returned as `0/1` in list/detail endpoints. 108 + Note: `is_water` is stored as `0/1` in the database and is returned as `0/1` in list/detail endpoints. 108 109 109 110 - **Error Responses:** 110 - - **Code:** 400 111 - - **Content:** `{ "error": "Missing required fields" }` 112 - - **Code:** 400 113 - - **Content:** `{ "error": "Content contains inappropriate language or content" }` 114 - - **Code:** 400 115 - - **Content:** `{ "error": "Captcha verification required" }` 116 - - **Code:** 400 117 - - **Content:** `{ "error": "Invalid captcha" }` 118 - - **Code:** 500 119 - - **Content:** `{ "error": "Failed to upload level" }` 111 + - **Code:** 400 112 + - **Content:** `{ "error": "Missing required fields" }` 113 + - **Code:** 400 114 + - **Content:** `{ "error": "Content contains inappropriate language or content" }` 115 + - **Code:** 400 116 + - **Content:** `{ "error": "Captcha verification required" }` 117 + - **Code:** 400 118 + - **Content:** `{ "error": "Invalid captcha" }` 119 + - **Code:** 500 120 + - **Content:** `{ "error": "Failed to upload level" }` 120 121 121 122 ##### List Levels 122 123 ··· 124 125 - **Method:** `GET` 125 126 - **URL Params:** None 126 127 - **Query Params:** 127 - - `page`: Page number (default: 1) 128 - - `limit`: Results per page (default: 10) 129 - - `author`: Filter by author name (partial match) 130 - - `is_water`: Filter by water levels ("true"/"false") 131 - - `version`: Filter by level version (currently always `3`; reserved for future versions) 132 - - `sort`: Sorting mode. Default is by play count (`plays`). Use `recent` to sort by creation time (`created_at`) and `favorites` to sort by favorite count. 133 - - `reversed_order`: Reverse the sort order (`true` or `1`). By default, sorting is descending. 134 - - `token`: One-time token. If provided and valid, the response is filtered to the single level associated with that token (and pagination becomes `page=1`, `limit=1`). If the token is invalid, the endpoint returns `401`. 128 + - `page`: Page number (default: 1) 129 + - `limit`: Results per page (default: 10) 130 + - `author`: Filter by author name (partial match) 131 + - `is_water`: Filter by water levels ("true"/"false") 132 + - `version`: Filter by level version (currently always `3`; reserved for future versions) 133 + - `sort`: Sorting mode. Default is by play count (`plays`). Use `recent` to sort by creation time (`created_at`) and `favorites` to sort by favorite count. 134 + - `reversed_order`: Reverse the sort order (`true` or `1`). By default, sorting is descending. 135 + - `token`: One-time token. If provided and valid, the response is filtered to the single level associated with that token (and pagination becomes `page=1`, `limit=1`). If the token is invalid, the endpoint returns `401`. 135 136 - **Success Response:** 136 - - **Code:** 200 137 - - **Content:** 137 + - **Code:** 200 138 + - **Content:** 138 139 139 - ```json 140 - { 141 - "levels": [ 142 - { 143 - "id": 123, 144 - "name": "Level Name", 145 - "author": "Author Name", 146 - "created_at": 1714680000, 147 - "sun": 100, 148 - "is_water": 1, 149 - "favorites": 5, 150 - "plays": 10, 151 - "difficulty": 7, 152 - "thumbnail": [[0, 10, 10, 40, 40, 1]], 153 - "version": 3 154 - } 155 - ], 156 - "pagination": { 157 - "total": 50, 158 - "page": 1, 159 - "limit": 10, 160 - "pages": 5 161 - } 162 - } 163 - ``` 140 + ```json 141 + { 142 + "levels": [ 143 + { 144 + "id": 123, 145 + "name": "Level Name", 146 + "author": "Author Name", 147 + "created_at": 1714680000, 148 + "sun": 100, 149 + "is_water": 1, 150 + "favorites": 5, 151 + "plays": 10, 152 + "difficulty": 7, 153 + "thumbnail": [[0, 10, 10, 40, 40, 1]], 154 + "version": 3 155 + } 156 + ], 157 + "pagination": { 158 + "total": 50, 159 + "page": 1, 160 + "limit": 10, 161 + "pages": 5 162 + } 163 + } 164 + ``` 164 165 165 166 - **Error Response:** 166 - - **Code:** 401 167 - - **Content:** `{ "error": "Invalid token" }` 168 - - **Code:** 500 169 - - **Content:** `{ "error": "Failed to list levels" }` 167 + - **Code:** 401 168 + - **Content:** `{ "error": "Invalid token" }` 169 + - **Code:** 500 170 + - **Content:** `{ "error": "Failed to list levels" }` 170 171 171 172 ##### Get Level Details 172 173 173 174 - **URL:** `/api/levels/:id` 174 175 - **Method:** `GET` 175 176 - **URL Params:** 176 - - `id`: Level ID 177 + - `id`: Level ID 177 178 - **Success Response:** 178 - - **Code:** 200 179 - - **Content:** 179 + - **Code:** 200 180 + - **Content:** 180 181 181 - ```json 182 - { 183 - "id": 123, 184 - "name": "Level Name", 185 - "author": "Author Name", 186 - "created_at": 1714680000, 187 - "sun": 100, 188 - "is_water": 1, 189 - "favorites": 5, 190 - "plays": 10, 191 - "difficulty": 7, 192 - "thumbnail": null, 193 - "version": 3 194 - } 195 - ``` 182 + ```json 183 + { 184 + "id": 123, 185 + "name": "Level Name", 186 + "author": "Author Name", 187 + "created_at": 1714680000, 188 + "sun": 100, 189 + "is_water": 1, 190 + "favorites": 5, 191 + "plays": 10, 192 + "difficulty": 7, 193 + "thumbnail": null, 194 + "version": 3 195 + } 196 + ``` 196 197 197 198 - **Error Responses:** 198 - - **Code:** 400 199 - - **Content:** `{ "error": "Invalid level ID" }` 200 - - **Code:** 404 201 - - **Content:** `{ "error": "Level not found" }` 202 - - **Code:** 500 203 - - **Content:** `{ "error": "Failed to get level" }` 199 + - **Code:** 400 200 + - **Content:** `{ "error": "Invalid level ID" }` 201 + - **Code:** 404 202 + - **Content:** `{ "error": "Level not found" }` 203 + - **Code:** 500 204 + - **Content:** `{ "error": "Failed to get level" }` 204 205 205 206 ##### Download Level 206 207 207 208 - **URL:** `/api/levels/:id/download` 208 209 - **Method:** `GET` 209 210 - **URL Params:** 210 - - `id`: Level ID 211 + - `id`: Level ID 211 212 - **Success Response:** 212 - - **Code:** 200 213 - - **Content:** Binary file download with `.izl3` extension 213 + - **Code:** 200 214 + - **Content:** Binary file download with `.izl3` extension 214 215 - **Error Responses:** 215 - - **Code:** 400 216 - - **Content:** `{ "error": "Invalid level ID" }` 217 - - **Code:** 404 218 - - **Content:** `{ "error": "Level not found" }` or `{ "error": "Level file not found" }` 219 - - **Code:** 500 220 - - **Content:** `{ "error": "Failed to download level" }` 216 + - **Code:** 400 217 + - **Content:** `{ "error": "Invalid level ID" }` 218 + - **Code:** 404 219 + - **Content:** `{ "error": "Level not found" }` or `{ "error": "Level file not found" }` 220 + - **Code:** 500 221 + - **Content:** `{ "error": "Failed to download level" }` 221 222 222 223 #### Favorites 223 224 ··· 226 227 - **URL:** `/api/levels/:id/favorite` 227 228 - **Method:** `POST` 228 229 - **URL Params:** 229 - - `id`: Level ID 230 + - `id`: Level ID 230 231 - **Request Body:** None (this endpoint always toggles favorite on/off) 231 232 - **Success Response:** 232 - - **Code:** 200 233 - - **Content:** `{ "success": true, "level": { "id": 123, "favorites": 5, ... } }` 233 + - **Code:** 200 234 + - **Content:** `{ "success": true, "level": { "id": 123, "favorites": 5, ... } }` 234 235 - **Error Responses:** 235 - - **Code:** 400 236 - - **Content:** `{ "error": "Invalid level ID" }` 237 - - **Code:** 404 238 - - **Content:** `{ "error": "Level not found" }` 239 - - **Code:** 500 240 - - **Content:** `{ "error": "Failed to favorite level" }` 236 + - **Code:** 400 237 + - **Content:** `{ "error": "Invalid level ID" }` 238 + - **Code:** 404 239 + - **Content:** `{ "error": "Level not found" }` 240 + - **Code:** 500 241 + - **Content:** `{ "error": "Failed to favorite level" }` 241 242 Note: Captcha verification is not required for favoriting. 242 243 243 244 #### Reporting ··· 247 248 - **URL:** `/api/levels/:id/report` 248 249 - **Method:** `POST` 249 250 - **URL Params:** 250 - - `id`: Level ID 251 + - `id`: Level ID 251 252 - **Request Body:** 252 253 253 254 ```json ··· 257 258 ``` 258 259 259 260 - **Behavior:** 260 - - If `USE_REPORTING=false`, this endpoint returns 404. 261 - - If `DISCORD_REPORT_WEBHOOK_URL` is configured, the server sends the report to the Discord webhook (and attaches the level file if available). 262 - - If no webhook is configured, the server still accepts the report and returns success. 261 + - If `USE_REPORTING=false`, this endpoint returns 404. 262 + - If `DISCORD_REPORT_WEBHOOK_URL` is configured, the server sends the report to the Discord webhook (and attaches the level file if available). 263 + - If no webhook is configured, the server still accepts the report and returns success. 263 264 - **Success Response:** 264 - - **Code:** 200 265 - - **Content:** `{ "success": true }` 265 + - **Code:** 200 266 + - **Content:** `{ "success": true }` 266 267 - **Error Responses:** 267 - - **Code:** 400 268 - - **Content:** `{ "error": "Invalid input" }` 269 - - **Code:** 404 270 - - **Content:** `{ "error": "Level not found" }` 271 - - **Code:** 500 272 - - **Content:** `{ "error": "Failed to report level" }` 268 + - **Code:** 400 269 + - **Content:** `{ "error": "Invalid input" }` 270 + - **Code:** 404 271 + - **Content:** `{ "error": "Level not found" }` 272 + - **Code:** 500 273 + - **Content:** `{ "error": "Failed to report level" }` 273 274 274 275 #### Configuration 275 276 ··· 278 279 - **URL:** `/api/config` 279 280 - **Method:** `GET` 280 281 - **Success Response:** 281 - - **Code:** 200 282 - - **Content:** 282 + - **Code:** 200 283 + - **Content:** 283 284 284 - ```json 285 - { 286 - "turnstileEnabled": true, 287 - "turnstileSiteKey": "0x0000000000000000000000", 288 - "moderationEnabled": true 289 - } 290 - ``` 285 + ```json 286 + { 287 + "turnstileEnabled": true, 288 + "turnstileSiteKey": "0x0000000000000000000000", 289 + "moderationEnabled": true 290 + } 291 + ``` 291 292 292 293 ## Environment Variables 293 294