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.
- package/.env.example +17 -0
- package/CHANGELOG.md +10 -0
- package/CONTRIBUTING.md +40 -0
- package/LICENSE +197 -0
- package/NOTICE +3 -0
- package/README.md +126 -56
- package/SECURITY.md +22 -0
- package/dist/app.js +80 -0
- package/dist/config/db.js +66 -0
- package/dist/init/dbInit.js +132 -0
- package/dist/init/dbInit2.js +234 -0
- package/dist/middleware/auth.js +48 -0
- package/dist/plugins/my-seo-plugin/index.js +29 -0
- package/dist/routes/admin.js +808 -0
- package/dist/routes/index.js +218 -0
- package/dist/sql/radix.sql +118 -0
- package/dist/types/plugin.js +10 -0
- package/dist/utils/PluginManager.js +112 -0
- package/dist/utils/settings.js +80 -0
- package/dist/utils/themeScanner.js +33 -0
- package/dist/utils/versionCheck.js +74 -0
- package/docs/CMS_CODE_GUIDE.md +372 -0
- package/docs/CMS_USER_GUIDE.md +344 -0
- package/docs/Publishing Guidelines.txt +14 -0
- package/package.json +62 -6
- package/public/css/admin.css +178 -0
- package/public/css/fallback.css +53 -0
- package/public/hello.html +1 -0
- package/views/admin/create-page.ejs +82 -0
- package/views/admin/dashboard.ejs +44 -0
- package/views/admin/edit-page.ejs +87 -0
- package/views/admin/edit-user.ejs +45 -0
- package/views/admin/login.ejs +39 -0
- package/views/admin/media.ejs +79 -0
- package/views/admin/pages.ejs +82 -0
- package/views/admin/partials/footer.ejs +4 -0
- package/views/admin/partials/header.ejs +57 -0
- package/views/admin/settings.ejs +91 -0
- package/views/admin/setup-admin.ejs +69 -0
- package/views/admin/setup-db.ejs +112 -0
- package/views/admin/themes.ejs +24 -0
- package/views/admin/updates.ejs +79 -0
- package/views/admin/users.ejs +107 -0
- package/views/defaults/404.ejs +20 -0
- package/views/defaults/500.ejs +20 -0
- package/views/layouts/admin-layout.ejs +18 -0
- package/views/pages/gallery.ejs +5 -0
- package/views/pages/hello.ejs +4 -0
- package/views/pages/services.ejs +3 -0
- package/views/themes/default/assets/css/style.css +199 -0
- package/views/themes/default/assets/js/main.js +8 -0
- package/views/themes/default/index.ejs +55 -0
- package/views/themes/landing/assets/css/style.css +6 -0
- package/views/themes/landing/assets/js/main.js +8 -0
- package/views/themes/landing/index.ejs +26 -0
- package/views/themes/test/default.ejs +0 -0
- package/views/themes/test/full-width.ejs +0 -0
- package/views/themes/test/landing-page.ejs +0 -0
- 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.
|