radix-cms 0.1.0 → 1.0.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.
Files changed (59) hide show
  1. package/.env.example +17 -0
  2. package/CHANGELOG.md +10 -0
  3. package/CONTRIBUTING.md +40 -0
  4. package/LICENSE +197 -0
  5. package/NOTICE +3 -0
  6. package/README.md +126 -56
  7. package/SECURITY.md +22 -0
  8. package/dist/app.js +80 -0
  9. package/dist/config/db.js +66 -0
  10. package/dist/init/dbInit.js +132 -0
  11. package/dist/init/dbInit2.js +234 -0
  12. package/dist/middleware/auth.js +48 -0
  13. package/dist/plugins/my-seo-plugin/index.js +29 -0
  14. package/dist/routes/admin.js +808 -0
  15. package/dist/routes/index.js +218 -0
  16. package/dist/sql/radix.sql +118 -0
  17. package/dist/types/plugin.js +10 -0
  18. package/dist/utils/PluginManager.js +112 -0
  19. package/dist/utils/settings.js +80 -0
  20. package/dist/utils/themeScanner.js +33 -0
  21. package/dist/utils/versionCheck.js +74 -0
  22. package/docs/CMS_CODE_GUIDE.md +372 -0
  23. package/docs/CMS_USER_GUIDE.md +344 -0
  24. package/docs/Publishing Guidelines.txt +14 -0
  25. package/package.json +62 -6
  26. package/public/css/admin.css +178 -0
  27. package/public/css/fallback.css +53 -0
  28. package/public/hello.html +1 -0
  29. package/views/admin/create-page.ejs +82 -0
  30. package/views/admin/dashboard.ejs +44 -0
  31. package/views/admin/edit-page.ejs +87 -0
  32. package/views/admin/edit-user.ejs +45 -0
  33. package/views/admin/login.ejs +39 -0
  34. package/views/admin/media.ejs +79 -0
  35. package/views/admin/pages.ejs +82 -0
  36. package/views/admin/partials/footer.ejs +4 -0
  37. package/views/admin/partials/header.ejs +57 -0
  38. package/views/admin/settings.ejs +91 -0
  39. package/views/admin/setup-admin.ejs +69 -0
  40. package/views/admin/setup-db.ejs +112 -0
  41. package/views/admin/themes.ejs +24 -0
  42. package/views/admin/updates.ejs +79 -0
  43. package/views/admin/users.ejs +107 -0
  44. package/views/defaults/404.ejs +20 -0
  45. package/views/defaults/500.ejs +20 -0
  46. package/views/layouts/admin-layout.ejs +18 -0
  47. package/views/pages/gallery.ejs +5 -0
  48. package/views/pages/hello.ejs +4 -0
  49. package/views/pages/services.ejs +3 -0
  50. package/views/themes/default/assets/css/style.css +199 -0
  51. package/views/themes/default/assets/js/main.js +8 -0
  52. package/views/themes/default/index.ejs +55 -0
  53. package/views/themes/landing/assets/css/style.css +6 -0
  54. package/views/themes/landing/assets/js/main.js +8 -0
  55. package/views/themes/landing/index.ejs +26 -0
  56. package/views/themes/test/default.ejs +0 -0
  57. package/views/themes/test/full-width.ejs +0 -0
  58. package/views/themes/test/landing-page.ejs +0 -0
  59. package/index.js +0 -15
@@ -0,0 +1,372 @@
1
+ # Radix CMS: Beginner's Code Guide
2
+
3
+ This guide explains how the current Radix CMS works by following the code from
4
+ startup to a browser request. It describes what the code does today; it is not
5
+ an architectural proposal for a future plugin system.
6
+
7
+ ## 1. The short version
8
+
9
+ Radix is a Node.js application written in TypeScript.
10
+
11
+ - **Node.js** runs JavaScript/TypeScript code on the server.
12
+ - **Express** receives HTTP requests and chooses what code handles each URL.
13
+ - **MySQL/MariaDB** stores pages, users, and settings.
14
+ - **EJS** creates HTML from templates.
15
+ - **TypeScript** is compiled from `src/` into JavaScript in `dist/`.
16
+ - **The browser** receives HTML, CSS, JavaScript, and uploaded images.
17
+
18
+ The application has two related parts:
19
+
20
+ 1. The **public site**, where a URL such as `/about` displays a page.
21
+ 2. The **admin site**, where authorized users manage pages, users, media, themes,
22
+ settings, and updates under `/admin`.
23
+
24
+ ## 2. Important folders
25
+
26
+ ```text
27
+ src/
28
+ app.ts Application entry point and Express setup
29
+ routes/
30
+ index.ts Public page routes
31
+ admin.ts Admin, login, setup, and management routes
32
+ config/db.ts MySQL connection pool wrapper
33
+ init/ Database installation and initialization
34
+ middleware/auth.ts Reusable role-checking middleware
35
+ utils/ Settings, theme, update, and plugin helpers
36
+ types/ TypeScript type declarations
37
+ plugins/ Example plugin code (not currently auto-loaded)
38
+ sql/ Database schema SQL
39
+
40
+ views/
41
+ themes/ Public EJS page templates
42
+ pages/ Static EJS page overrides
43
+ admin/ Admin EJS screens
44
+ defaults/ Error pages
45
+ layouts/ Layout templates (not all routes currently use this)
46
+
47
+ public/
48
+ css/ Browser CSS
49
+ uploads/ Uploaded media files
50
+
51
+ dist/ Compiled output generated by the build
52
+ package.json Dependencies and run commands
53
+ .env Local configuration and secrets
54
+ ```
55
+
56
+ Do not edit `dist/` directly. It is generated from `src/`. Do not commit `.env`
57
+ or put passwords/API keys in source code.
58
+
59
+ ## 3. How the application starts
60
+
61
+ The scripts in `package.json` are:
62
+
63
+ - `npm run dev`: runs `src/app.ts` through `tsx` and watches for changes.
64
+ - `npm run build`: compiles TypeScript into `dist/` and copies the SQL file.
65
+ - `npm start`: compiles, then runs `dist/app.js`.
66
+ - `npm run app`: runs the already-compiled `dist/app.js`.
67
+
68
+ The startup path is:
69
+
70
+ 1. `src/app.ts` imports Express, sessions, routers, and database initialization.
71
+ 2. Express is created with `const app = express()`.
72
+ 3. EJS is selected as the view engine.
73
+ 4. JSON and HTML form-body parsing middleware is enabled.
74
+ 5. A session middleware is enabled. The logged-in user is stored in the session.
75
+ 6. Static folders are exposed:
76
+ - `/...` can serve files in `public/`.
77
+ - `/themes/...` serves theme assets.
78
+ - `/uploads/...` serves uploaded media.
79
+ 7. The admin router is mounted at `/admin`.
80
+ 8. The public router is mounted at `/`.
81
+ 9. `startServer()` initializes the database and starts listening on `SITE_PORT`
82
+ (normally port 3000).
83
+
84
+ In Express, `app.use('/admin', adminRoutes)` means that a route written as
85
+ `router.get('/pages', ...)` inside `admin.ts` is reached at `/admin/pages`.
86
+
87
+ ## 4. What happens when someone visits a public page
88
+
89
+ Public routes are in [`src/routes/index.ts`](../src/routes/index.ts).
90
+
91
+ ### Request flow
92
+
93
+ 1. The request reaches the router mounted at `/`.
94
+ 2. `/` is treated as the slug `home`.
95
+ 3. `/:slug` handles URLs such as `/about` or `/hello`.
96
+ 4. `renderPageBySlug()` first checks `views/pages/<slug>.ejs`.
97
+ 5. If that file exists, it is rendered immediately. This is a **static override**.
98
+ 6. Otherwise, the code queries the `pages` database table by slug.
99
+ 7. If no row exists, the 404 template is rendered.
100
+ 8. The page's status, publish date, and minimum role are checked.
101
+ 9. The selected theme/template is resolved.
102
+ 10. EJS renders the theme with the database page and current user as variables.
103
+
104
+ The database page contains fields such as:
105
+
106
+ - `title`: heading and browser title
107
+ - `slug`: URL part, such as `about`
108
+ - `content`: HTML content, usually entered through TinyMCE
109
+ - `template_name`: theme/template name
110
+ - `status`: Draft, Published, Archived, and so on
111
+ - `min_role`: minimum role allowed to view the page
112
+ - `publish_at`: scheduled publication date
113
+
114
+ ### Static page override
115
+
116
+ If `views/pages/about.ejs` exists, `/about` uses that file instead of the
117
+ database row. This lets a developer create a fully custom page, but it also
118
+ means changing the database page may appear to have no effect while the
119
+ override file exists.
120
+
121
+ ### Theme rendering
122
+
123
+ The default template is
124
+ [`views/themes/default/index.ejs`](../views/themes/default/index.ejs).
125
+
126
+ The route passes values into EJS, including:
127
+
128
+ ```text
129
+ title
130
+ content
131
+ page
132
+ user
133
+ currentTheme
134
+ siteName
135
+ appVersion
136
+ ```
137
+
138
+ EJS uses:
139
+
140
+ - `<%= value %>` for escaped output
141
+ - `<%- value %>` for unescaped output
142
+ - `<% code %>` for JavaScript logic
143
+
144
+ The default theme uses `<%- content %>` so saved rich-text HTML is rendered as
145
+ HTML. This is useful for CMS content, but it makes content sanitization and
146
+ trusted editor access important security concerns.
147
+
148
+ ## 5. What happens in the admin area
149
+
150
+ Admin routes are in [`src/routes/admin.ts`](../src/routes/admin.ts), and the
151
+ router is mounted at `/admin`.
152
+
153
+ ### Login and sessions
154
+
155
+ 1. `GET /admin/login` displays the login form.
156
+ 2. `POST /admin/login` looks up a user by username or email.
157
+ 3. The submitted password is compared with the stored bcrypt hash.
158
+ 4. On success, the user identity and role are placed in `req.session.user`.
159
+ 5. The browser receives a session cookie.
160
+ 6. `GET /admin/logout` destroys the session.
161
+
162
+ The session is server-side state associated with a cookie. It is not the same
163
+ thing as a user record in the database.
164
+
165
+ ### Role checks
166
+
167
+ The roles are ordered from least to most privileged:
168
+
169
+ ```text
170
+ Unregistered < Registered < Editor < Manager < Administrator
171
+ ```
172
+
173
+ The admin router has its own `requireRole()` function. The reusable version is
174
+ also in [`src/middleware/auth.ts`](../src/middleware/auth.ts).
175
+
176
+ Current access pattern:
177
+
178
+ - **Editor and above**: dashboard, page management, media upload, themes,
179
+ updates screen
180
+ - **Administrator only**: permanent deletion, user management, settings,
181
+ media deletion, applying updates
182
+
183
+ The role check runs before the route handler. If there is no logged-in user, it
184
+ redirects to login. If the role is too low, it returns a 403 response.
185
+
186
+ ### Page management
187
+
188
+ The main page operations are:
189
+
190
+ | URL | Purpose |
191
+ | --- | --- |
192
+ | `GET /admin/pages` | List database pages and optionally filter status |
193
+ | `GET /admin/create` | Show the create form |
194
+ | `POST /admin/create` | Insert a new row into `pages` |
195
+ | `GET /admin/edit/:id` | Load one page into the edit form |
196
+ | `POST /admin/edit/:id` | Update a page row |
197
+ | `POST /admin/delete/:id` | Mark a page as `Deleted` |
198
+ | `POST /admin/restore/:id` | Restore a deleted page |
199
+ | `POST /admin/permanent-delete/:id` | Permanently remove a page |
200
+
201
+ The forms are EJS files under `views/admin/`. SQL parameters use `?`
202
+ placeholders, which is the correct pattern for avoiding SQL injection from form
203
+ values.
204
+
205
+ ### Media
206
+
207
+ Multer handles uploads in `admin.ts`.
208
+
209
+ - Files are stored in `public/uploads/`.
210
+ - The filename is generated from the field name, timestamp, and random suffix.
211
+ - The upload limit is 10 MB.
212
+ - The configured extensions and MIME types are image types.
213
+ - Files are served publicly at `/uploads/<filename>`.
214
+
215
+ The media library is file-system based; media metadata is not stored in the
216
+ database.
217
+
218
+ ### Settings
219
+
220
+ Settings are rows in the `settings` table using a key/value design:
221
+
222
+ ```text
223
+ setting_key | setting_value
224
+ ```
225
+
226
+ [`src/utils/settings.ts`](../src/utils/settings.ts) loads all settings into an
227
+ in-memory cache. `getSetting()` reads one value, and
228
+ `clearSettingsCache()` must be called after settings are changed so a later
229
+ request sees the new values.
230
+
231
+ Some configuration is instead read from environment variables, especially
232
+ database connection details, application version, port, and session secret.
233
+
234
+ ## 6. Database behavior
235
+
236
+ The schema is defined in [`src/sql/radix.sql`](../src/sql/radix.sql). The main
237
+ tables are:
238
+
239
+ - `pages`: published and draft content
240
+ - `users`: accounts, password hashes, email addresses, and roles
241
+ - `settings`: application settings
242
+ - `plugins`: a table reserved for plugin activation state
243
+ - `schema_meta`: initialization and schema version markers
244
+
245
+ [`src/config/db.ts`](../src/config/db.ts) creates a lazy MySQL connection pool.
246
+ “Lazy” means the pool is created when the first query is made, not necessarily
247
+ when the module is imported. If environment database settings change, the
248
+ wrapper closes the old pool and creates a new one.
249
+
250
+ [`src/init/dbInit2.ts`](../src/init/dbInit2.ts) is used by the current startup
251
+ and installer flow. It:
252
+
253
+ 1. Reads the bundled SQL file.
254
+ 2. Removes seed `INSERT` statements before creating a new database.
255
+ 3. Checks whether the configured database exists.
256
+ 4. Creates the schema if needed.
257
+ 5. Ensures the `schema_meta` marker table exists.
258
+
259
+ The admin installer writes database values to `.env`, reloads them, initializes
260
+ the schema, and then creates the first administrator account.
261
+
262
+ ## 7. Installation and configuration
263
+
264
+ The admin router has an installation guard. For most admin requests it checks
265
+ whether database environment variables exist and whether `SELECT 1` succeeds.
266
+ If not, it redirects to `/admin/install`.
267
+
268
+ The installer flow is:
269
+
270
+ 1. `GET /admin/install` displays database settings.
271
+ 2. `POST /admin/setup/db/test` tests a connection without saving it.
272
+ 3. `POST /admin/setup/db` writes database configuration and initializes the schema.
273
+ 4. `GET /admin/setup/admin` displays the first-admin form.
274
+ 5. `POST /admin/setup/admin` creates the administrator and saves initial settings.
275
+ 6. The user is redirected to `/admin/login`.
276
+
277
+ The `.env` file is loaded by `dotenv`. It is intended for per-installation
278
+ configuration and must be protected like a password file.
279
+
280
+ ## 8. Themes and templates
281
+
282
+ Themes live under `views/themes/`. The theme scanner in
283
+ [`src/utils/themeScanner.ts`](../src/utils/themeScanner.ts) lists theme
284
+ directories/files so the admin page editor can offer them in a selector.
285
+
286
+ Theme CSS is served from `/themes`, while general public CSS is served from
287
+ `public/`.
288
+
289
+ A theme is an EJS template. It is not a separate Node package. A theme can:
290
+
291
+ - change the page layout and navigation
292
+ - include its own CSS/assets
293
+ - display variables passed by the public route
294
+ - render the page's HTML content
295
+
296
+ ## 9. Update checking and email
297
+
298
+ `src/utils/versionCheck.ts` fetches a JSON version document from an update URL
299
+ stored in settings (or a default remote URL). It compares the remote version
300
+ with `APP_VERSION` and reports whether an update exists.
301
+
302
+ The admin settings route uses Nodemailer for test email. SMTP values are
303
+ stored as settings and are used to build a mail transport when a test message
304
+ is requested.
305
+
306
+ ## 10. Current extension-related code
307
+
308
+ There is an example under `src/plugins/my-seo-plugin/` and a type contract in
309
+ `src/types/plugin.ts`. The proposed plugin context exposes:
310
+
311
+ - the Express router
312
+ - hook registration
313
+ - filter registration
314
+ - the database wrapper
315
+
316
+ `src/utils/PluginManager.ts` contains hook/filter storage and a plugin loader.
317
+ However, the current application entry point does not call `loadPlugins()`, and
318
+ the loader's path assumptions do not line up cleanly with the compiled
319
+ directory layout. Hooks and filters are also not currently triggered by the
320
+ public/admin routes.
321
+
322
+ Therefore, plugins are currently an unfinished extension point, not a complete
323
+ runtime feature. For now, a custom application should use normal Express
324
+ routes/services under `src/` and explicitly import them from `app.ts` or a
325
+ router.
326
+
327
+ ## 11. A beginner's mental model for adding a feature
328
+
329
+ For a feature such as a newsletter:
330
+
331
+ 1. **URL:** add an Express route in a router file.
332
+ 2. **Form:** add an EJS form under `views/`.
333
+ 3. **Database:** add a table or columns in SQL/migration code.
334
+ 4. **Business logic:** put validation and database operations in a service
335
+ module rather than making the route handler do everything.
336
+ 5. **Security:** add the appropriate role middleware and validate all input.
337
+ 6. **Assets:** put browser CSS/JavaScript in `public/` or feature-specific
338
+ static assets.
339
+ 7. **Startup wiring:** import and mount the router from `app.ts`.
340
+ 8. **Build:** run `npm run build`; test using the compiled application or
341
+ `npm run dev`.
342
+
343
+ A request normally travels through this chain:
344
+
345
+ ```text
346
+ Browser
347
+ -> Express app
348
+ -> middleware (body parsing, session, static files)
349
+ -> router
350
+ -> role/auth check (if needed)
351
+ -> route handler
352
+ -> database/service work
353
+ -> EJS template
354
+ -> HTML response
355
+ ```
356
+
357
+ ## 12. Current caveats to know before extending the CMS
358
+
359
+ - `dist/` is build output and can become stale if the project is run without
360
+ compiling after a source change.
361
+ - Public page rendering has a static-file override that takes priority over
362
+ database content.
363
+ - The default theme renders stored content unescaped.
364
+ - There are two role-checking implementations; future changes should consolidate
365
+ them to avoid behavior drifting.
366
+ - The plugin manager is present but not fully wired into startup or request
367
+ events.
368
+ - `views/layouts/admin-layout.ejs` exists, but the admin pages currently use
369
+ header/footer includes directly.
370
+ - Database initialization deliberately strips seed data when creating a new
371
+ database, so the installed database may not contain the sample rows in the
372
+ checked-in SQL dump.
@@ -0,0 +1,344 @@
1
+ # Radix CMS User Guide
2
+
3
+ This guide explains how to use the current Radix CMS through the web
4
+ interface, and how to create templates and static pages as a developer.
5
+
6
+ ## 1. Getting started
7
+
8
+ ### Start the application
9
+
10
+ From the project folder:
11
+
12
+ ```powershell
13
+ npm run dev
14
+ ```
15
+
16
+ Open:
17
+
18
+ - Public site: `http://localhost:3000/`
19
+ - Administration: `http://localhost:3000/admin`
20
+
21
+ The port comes from `SITE_PORT` in `.env`.
22
+
23
+ ### First installation
24
+
25
+ If the database is not configured, open `/admin/install`. The installer:
26
+
27
+ 1. Tests the MySQL/MariaDB connection.
28
+ 2. Saves the database settings to `.env`.
29
+ 3. Creates the database schema.
30
+ 4. Creates the first administrator account.
31
+ 5. Redirects to the login page.
32
+
33
+ The `.env` file contains passwords and API keys. Keep it private and do not
34
+ commit it to source control.
35
+
36
+ ## 2. Log in and understand permissions
37
+
38
+ Go to `/admin/login` and sign in with a user account.
39
+
40
+ Roles are ordered from least to most powerful:
41
+
42
+ ```text
43
+ Unregistered < Registered < Editor < Manager < Administrator
44
+ ```
45
+
46
+ Current permissions are:
47
+
48
+ | Role | Typical access |
49
+ | --- | --- |
50
+ | Unregistered | Public pages only |
51
+ | Registered | Pages requiring registered-user access |
52
+ | Editor | Dashboard, create/edit pages, upload media, view themes and updates |
53
+ | Manager | Same route access as Editor in the current implementation |
54
+ | Administrator | All Editor features plus users, settings, media deletion, and applying updates |
55
+
56
+ A page's **Minimum Access Role** controls who may view a database-backed dynamic
57
+ page. The visitor must be logged in with that role or a higher role.
58
+
59
+ ## 3. Create a dynamic page
60
+
61
+ A dynamic page is stored in the `pages` database table. Its content, status,
62
+ access level, and selected template can be changed through the admin interface.
63
+
64
+ 1. Log in as an Editor or Administrator.
65
+ 2. Open **Pages**.
66
+ 3. Click **Create New Page**.
67
+ 4. Enter a **Title**, such as `Members Area`.
68
+ 5. Enter a unique **Slug**, such as `members`.
69
+ - The page URL will be `/members`.
70
+ - Use letters, numbers, and hyphens.
71
+ - Do not reuse a slug already assigned to another page.
72
+ 6. Select a **Status**:
73
+ - `Draft`: not normally visible to public visitors.
74
+ - `Pending Review`: not normally visible to public visitors.
75
+ - `Published`: visible when its publish date has arrived.
76
+ - `Archived`: not normally visible to public visitors.
77
+ 7. Select the **Minimum Access Role**.
78
+ 8. Select a **Theme / Template**.
79
+ 9. Enter the page content in the editor.
80
+ 10. Click **Save Page**.
81
+
82
+ ### Give a dynamic page access permissions
83
+
84
+ Use the **Minimum Access Role** field:
85
+
86
+ - `Unregistered`: anyone can view it.
87
+ - `Registered`: only logged-in registered users and higher roles.
88
+ - `Editor`: Editors, Managers, and Administrators.
89
+ - `Manager`: Managers and Administrators.
90
+ - `Administrator`: Administrators only.
91
+
92
+ The page must also be `Published`, and its `publish_at` date must not be in
93
+ the future. Editors and higher roles can see unpublished pages because they are
94
+ treated as staff by the current public route.
95
+
96
+ ### Edit, archive, or delete a dynamic page
97
+
98
+ From **Pages**:
99
+
100
+ - Click **Edit** to change the title, slug, content, template, status, or role.
101
+ - Deleting a page first moves it to the trash by setting its status to
102
+ `Deleted`.
103
+ - Use **Restore** to return a deleted page to `Draft`.
104
+ - Permanent deletion is available only to Administrators and cannot be undone.
105
+
106
+ ## 4. Create a static page
107
+
108
+ A static page is an EJS file in `views/pages/`. It is useful when the page
109
+ needs custom HTML or custom server-rendered logic rather than ordinary CMS
110
+ content.
111
+
112
+ Create a file such as:
113
+
114
+ ```text
115
+ views/pages/contact.ejs
116
+ ```
117
+
118
+ Example:
119
+
120
+ ```ejs
121
+ <%# theme: landing %>
122
+ <%# minRole: Registered %>
123
+ <h1>Contact Us</h1>
124
+ <p>This page is rendered from views/pages/contact.ejs.</p>
125
+ <p>Email: support@example.com</p>
126
+ ```
127
+
128
+ The optional first-line EJS comment selects the theme for this static page:
129
+
130
+ ```ejs
131
+ <%# theme: landing %>
132
+ ```
133
+
134
+ Use the theme directory name under `views/themes/`. The `theme` directive
135
+ overrides the database page's template, if a page with the same slug exists.
136
+ `layout` can be used as an equivalent directive for compatibility with
137
+ layout-oriented terminology. If the selected theme is not found, the default
138
+ theme is used.
139
+
140
+ Static pages can also require a minimum user role:
141
+
142
+ ```ejs
143
+ <%# minRole: Registered %>
144
+ ```
145
+
146
+ Supported roles are `Unregistered`, `Registered`, `Editor`, `Manager`, and
147
+ `Administrator`. If `minRole` is omitted, the page defaults to
148
+ `Unregistered`. Unauthenticated visitors are sent to the login page, while
149
+ authenticated users without sufficient privileges receive a forbidden response.
150
+
151
+ After restarting the application, visit:
152
+
153
+ ```text
154
+ http://localhost:3000/contact
155
+ ```
156
+
157
+ The public router checks `views/pages/<slug>.ejs` before checking the database.
158
+ Therefore, `views/pages/contact.ejs` takes priority over a database page whose
159
+ slug is `contact`.
160
+
161
+ ### Important: static-page permissions
162
+
163
+ Static page access is controlled by the optional `minRole` directive in the
164
+ file. Checking `user` in the EJS file only hides HTML; it does not reliably
165
+ protect the URL or data.
166
+
167
+ For a protected page, use a dynamic page and set its minimum role, or add a
168
+ dedicated Express route with role middleware in the application source. The
169
+ reusable role middleware is in `src/middleware/auth.ts`.
170
+
171
+ ## 5. Create a theme/template
172
+
173
+ In the current CMS, a selectable template is an EJS file named `index.ejs`
174
+ inside a directory under `views/themes/`.
175
+
176
+ Create this structure:
177
+
178
+ ```text
179
+ views/
180
+ themes/
181
+ marketing/
182
+ index.ejs
183
+ assets/
184
+ css/
185
+ style.css
186
+ ```
187
+
188
+ Example `views/themes/marketing/index.ejs`:
189
+
190
+ ```ejs
191
+ <!DOCTYPE html>
192
+ <html lang="en">
193
+ <head>
194
+ <meta charset="UTF-8">
195
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
196
+ <title><%= title %></title>
197
+ <link rel="stylesheet" href="/themes/<%= currentTheme %>/assets/css/style.css">
198
+ </head>
199
+ <body>
200
+ <header>
201
+ <a href="/"><%= siteName || 'Radix' %></a>
202
+ </header>
203
+
204
+ <main>
205
+ <h1><%= title %></h1>
206
+ <%- content %>
207
+ </main>
208
+ </body>
209
+ </html>
210
+ ```
211
+
212
+ Example `views/themes/marketing/assets/css/style.css`:
213
+
214
+ ```css
215
+ body {
216
+ max-width: 900px;
217
+ margin: 0 auto;
218
+ font-family: sans-serif;
219
+ }
220
+ ```
221
+
222
+ ### Apply the template to a page
223
+
224
+ 1. Restart the application if necessary.
225
+ 2. Open **Admin > Themes** to confirm that `marketing` was discovered.
226
+ 3. Create or edit a dynamic page.
227
+ 4. Select `Marketing` in **Theme / Template**.
228
+ 5. Save the page.
229
+ 6. Visit the page URL.
230
+
231
+ The theme scanner discovers directories under `views/themes/`. The public
232
+ renderer then loads `views/themes/<template_name>/index.ejs`.
233
+
234
+ ### Template variables
235
+
236
+ Dynamic themes currently receive values including:
237
+
238
+ ```text
239
+ title Page title
240
+ content Saved HTML content
241
+ page Complete database page object
242
+ user Logged-in user, or undefined
243
+ currentTheme Selected template name
244
+ siteName APP_NAME from environment
245
+ appVersion Current application version
246
+ ```
247
+
248
+ Use `<%= value %>` for escaped text. Use `<%- content %>` only for trusted HTML
249
+ that should be rendered as markup. The current CMS stores rich-text HTML and
250
+ the default theme renders it unescaped.
251
+
252
+ ## 6. Create and manage users
253
+
254
+ Only an Administrator can manage users.
255
+
256
+ 1. Open **Admin > Users & Roles**.
257
+ 2. Complete **Create New User**:
258
+ - **Username**: required and unique.
259
+ - **Full Name**: optional.
260
+ - **Email**: optional, but must be unique when supplied.
261
+ - **Password**: required.
262
+ - **Role**: choose the user's highest allowed role.
263
+ 3. Click **Add User**.
264
+
265
+ Passwords are stored as bcrypt hashes, not as plain text.
266
+
267
+ ### Edit a user
268
+
269
+ Click **Edit** beside a user to change:
270
+
271
+ - Full name
272
+ - Username
273
+ - Email
274
+ - Role
275
+ - Password
276
+
277
+ Leave **New Password** blank to keep the current password. Changing your own
278
+ role updates the current session's role as well.
279
+
280
+ ### Delete a user
281
+
282
+ Click **Delete** beside the account. The currently logged-in administrator
283
+ cannot delete their own active account.
284
+
285
+ ## 7. Media uploads
286
+
287
+ Editors and Administrators can open **Admin > Media Library**.
288
+
289
+ 1. Choose an image file.
290
+ 2. Upload it.
291
+ 3. Use its `/uploads/<filename>` URL in page content or settings.
292
+
293
+ The current upload limit is 10 MB. Supported image types are JPG, PNG, GIF,
294
+ WEBP, and SVG. Administrators can delete media files.
295
+
296
+ ## 8. Site settings
297
+
298
+ Administrators can open **Admin > Settings** to configure:
299
+
300
+ - Site logo path or URL
301
+ - SMTP host, port, encryption, username, and password
302
+ - Sender email address
303
+ - Test email delivery
304
+
305
+ Settings are stored in the database. Database connection details and secrets
306
+ remain in `.env`.
307
+
308
+ ## 9. Troubleshooting
309
+
310
+ ### My page shows the wrong content
311
+
312
+ Check whether `views/pages/<slug>.ejs` exists. A static file overrides the
313
+ database page with the same slug.
314
+
315
+ ### My new template is not listed
316
+
317
+ Confirm the file is exactly:
318
+
319
+ ```text
320
+ views/themes/<name>/index.ejs
321
+ ```
322
+
323
+ Then restart the application and refresh the admin page.
324
+
325
+ ### My page is not visible publicly
326
+
327
+ For a dynamic page, check:
328
+
329
+ 1. Status is `Published`.
330
+ 2. `publish_at` is not in the future.
331
+ 3. The visitor's role meets **Minimum Access Role**.
332
+ 4. No static override is interfering.
333
+
334
+ ### Source changes do not appear
335
+
336
+ Use `npm run dev` during development. For production-style execution, rebuild
337
+ before starting:
338
+
339
+ ```powershell
340
+ npm run build
341
+ npm run app
342
+ ```
343
+
344
+ The `dist/` folder is generated output and should not be edited directly.