|
|
|
@@ -0,0 +1,257 @@ |
|
|
|
# DP Jobs Kanban Board |
|
|
|
|
|
|
|
A Kanban Board web application running on IIS (Windows). Uses the **RouteKit** framework (front-controller pattern) with Keycloak OpenID Connect for authentication. Database is Microsoft Access (`.accdb`). No Node, npm, or build step — everything is server-side VBScript served by IIS. |
|
|
|
|
|
|
|
## Project layout |
|
|
|
|
|
|
|
``` |
|
|
|
public/ IIS site root — point IIS here |
|
|
|
Default.asp Front controller and route table |
|
|
|
web.config App settings, URL rewrite rules |
|
|
|
|
|
|
|
core/ Framework internals — do not modify |
|
|
|
autoload_core.asp |
|
|
|
mvc.asp |
|
|
|
router.wsc |
|
|
|
lib.*.asp Core libraries |
|
|
|
helpers.asp Global utility functions (always available) |
|
|
|
|
|
|
|
app/ |
|
|
|
controllers/ Controller classes (one per feature area) |
|
|
|
views/ View partials (folders match controller name) |
|
|
|
shared/ Header, footer, layout partials |
|
|
|
models/ POBOs (plain-old business objects) |
|
|
|
repositories/ Data access classes |
|
|
|
|
|
|
|
db/ |
|
|
|
migrations/ Sequential migration scripts |
|
|
|
webdata.accdb Access database |
|
|
|
|
|
|
|
tests/ Dev-only aspunit test harness (separate IIS app) |
|
|
|
scripts/ VBScript code generators |
|
|
|
``` |
|
|
|
|
|
|
|
## Core libraries (core/lib.*.asp) |
|
|
|
|
|
|
|
| File | Purpose | |
|
|
|
|---|---| |
|
|
|
| `lib.Keycloak.asp` | OpenID Connect / Keycloak auth helper | |
|
|
|
| `lib.Routes.asp` | URL generation helpers (`Routes()` singleton) | |
|
|
|
| `lib.ControllerRegistry.asp` | Controller whitelist (security) | |
|
|
|
| `lib.DAL.asp` | Database access layer (`DAL` singleton) | |
|
|
|
| `lib.Collections.asp` | `LinkedList_Class` and other collection types | |
|
|
|
| `lib.Enumerable.asp` | Collection iteration helpers | |
|
|
|
| `lib.Validations.asp` | Input validation | |
|
|
|
| `lib.Flash.asp` | Flash message helpers | |
|
|
|
| `lib.FormCache.asp` | Form value re-population | |
|
|
|
| `lib.HTML.asp` | HTML rendering helpers | |
|
|
|
| `lib.HTML.Security.asp` | XSS escaping — `H()` function | |
|
|
|
| `lib.Strings.asp` | String utilities | |
|
|
|
| `lib.Automapper.asp` | `Automapper.AutoMap(rs, "POBO_TableName")` | |
|
|
|
| `lib.json.asp` | JSON parsing/serialization | |
|
|
|
|
|
|
|
## Global helper functions (core/helpers.asp) |
|
|
|
|
|
|
|
Always available — never re-implement them. |
|
|
|
|
|
|
|
| Function | Signature | Purpose | |
|
|
|
|---|---|---| |
|
|
|
| `H` | `H(s)` | XSS-safe HTML encode — use on all user data rendered to HTML | |
|
|
|
| `GenerateSlug` | `GenerateSlug(title)` | Converts title to safe URL slug | |
|
|
|
| `GetRawJsonFromRequest` | `GetRawJsonFromRequest()` | Reads raw JSON body from AJAX POST | |
|
|
|
| `GetAppSetting` | `GetAppSetting(key)` | Reads value from `web.config` appSettings | |
|
|
|
| `IIf` | `IIf(condition, trueVal, falseVal)` | Inline conditional | |
|
|
|
| `Destroy` | `Destroy(obj)` | Safely closes and sets object to Nothing | |
|
|
|
| `FormatDateForSql` | `FormatDateForSql(date)` | Formats VBScript date as SQL datetime string | |
|
|
|
| `Active` | `Active(controllerName)` | Returns `"active"` if current request maps to that controller | |
|
|
|
|
|
|
|
## Router — URL parameters |
|
|
|
|
|
|
|
Routes registered in [public/Default.asp](public/Default.asp) with `:param` segments. |
|
|
|
|
|
|
|
```vbscript |
|
|
|
router.AddRoute "GET", "/boards", "BoardsController", "Index" |
|
|
|
router.AddRoute "POST", "/boards", "BoardsController", "Store" |
|
|
|
router.AddRoute "GET", "/board/:slug", "BoardsController", "Show" |
|
|
|
router.AddRoute "POST", "/cards/:id/move", "CardsController", "Move" |
|
|
|
``` |
|
|
|
|
|
|
|
Controller action receives URL params as arguments in order: |
|
|
|
|
|
|
|
```vbscript |
|
|
|
Public Sub Show(slug) : End Sub |
|
|
|
Public Sub Move(id) : End Sub |
|
|
|
``` |
|
|
|
|
|
|
|
URL generation: |
|
|
|
|
|
|
|
```vbscript |
|
|
|
Routes.UrlTo "Boards", "Index", Empty |
|
|
|
Routes.UrlToWithParams "Boards", "Show", Array("my-board"), Empty |
|
|
|
Routes.UrlTo "Boards", "Index", Array("page", 2) |
|
|
|
``` |
|
|
|
|
|
|
|
## Wiring up a new controller — checklist |
|
|
|
|
|
|
|
1. Generate: `cscript //nologo scripts\generateController.vbs MyController "Index;Show(slug);Store"` |
|
|
|
2. Move file to `app/controllers/` |
|
|
|
3. Register in [core/lib.ControllerRegistry.asp](core/lib.ControllerRegistry.asp): `RegisterController "mycontroller"` |
|
|
|
4. Include in [app/controllers/autoload_controllers.asp](app/controllers/autoload_controllers.asp): `<!--#include file="MyController.asp"-->` |
|
|
|
5. Add routes in [public/Default.asp](public/Default.asp) |
|
|
|
6. Create views in `app/views/MyController/` |
|
|
|
|
|
|
|
## POBO pattern (app/models/) |
|
|
|
|
|
|
|
```vbscript |
|
|
|
Class POBO_boards |
|
|
|
Public Properties ' array of all column names — required by Automapper |
|
|
|
Private p_id, p_name, p_slug, p_created_at, p_created_by, p_updated_at, p_updated_by |
|
|
|
|
|
|
|
Private Sub Class_Initialize() |
|
|
|
Properties = Array("id","name","slug","created_at","created_by","updated_at","updated_by") |
|
|
|
End Sub |
|
|
|
|
|
|
|
Public Property Get PrimaryKey() : PrimaryKey = "id" : End Property |
|
|
|
Public Property Get TableName() : TableName = "boards" : End Property |
|
|
|
|
|
|
|
Public Property Get id() : id = p_id : End Property |
|
|
|
Public Property Let id(v) : p_id = CDbl(v) : End Property |
|
|
|
' ... remaining Get/Let pairs follow same pattern |
|
|
|
End Class |
|
|
|
``` |
|
|
|
|
|
|
|
Private backing fields use `p_` prefix. `Properties`, `PrimaryKey`, and `TableName` are required. |
|
|
|
|
|
|
|
## Repository pattern (app/repositories/) |
|
|
|
|
|
|
|
Uses `DAL` singleton and `Automapper`. Key methods: `FindByID`, `GetAll`, `Find`, `AddNew`, `Update`, `Delete`. After insert, read identity with `SELECT @@IDENTITY AS NewID`. Expose as a singleton function (e.g. `boards_Repository()`). |
|
|
|
|
|
|
|
## Controller pattern (app/controllers/) |
|
|
|
|
|
|
|
```vbscript |
|
|
|
Class BoardsController_Class |
|
|
|
Private m_useLayout, m_title |
|
|
|
Private Sub Class_Initialize() : m_useLayout = True : m_title = "Boards" : End Sub |
|
|
|
|
|
|
|
Public Sub Index() |
|
|
|
If Not KeycloakRequireLogin("") Then Exit Sub |
|
|
|
End Sub |
|
|
|
|
|
|
|
Public Sub Move(id) ' JSON/AJAX action |
|
|
|
m_useLayout = False |
|
|
|
Response.ContentType = "application/json" |
|
|
|
If Not KeycloakIsLoggedIn() Then |
|
|
|
Response.Write "{""ok"":false,""error"":""Unauthorized""}" : Exit Sub |
|
|
|
End If |
|
|
|
End Sub |
|
|
|
End Class |
|
|
|
``` |
|
|
|
|
|
|
|
JSON/AJAX actions must set `m_useLayout = False` and `Response.ContentType = "application/json"`. |
|
|
|
|
|
|
|
## Audit columns |
|
|
|
|
|
|
|
Every table must include `created_at`, `created_by`, `updated_at`, `updated_by`. |
|
|
|
|
|
|
|
```vbscript |
|
|
|
' Insert |
|
|
|
Dim currentUser : Set currentUser = KeycloakCurrentUser() |
|
|
|
Dim currentUsername : currentUsername = "" |
|
|
|
If Not currentUser Is Nothing Then currentUsername = currentUser.Item("preferred_username") |
|
|
|
model.created_at = Now() : model.created_by = currentUsername |
|
|
|
model.updated_at = Now() : model.updated_by = currentUsername |
|
|
|
repo.AddNew model |
|
|
|
|
|
|
|
' Update — never touch created_at / created_by |
|
|
|
model.updated_at = Now() : model.updated_by = currentUsername |
|
|
|
repo.Update model |
|
|
|
``` |
|
|
|
|
|
|
|
## Authentication (Keycloak) |
|
|
|
|
|
|
|
```vbscript |
|
|
|
KeycloakRequireLogin("") ' Gate full-page actions |
|
|
|
KeycloakIsLoggedIn() ' Use for JSON/AJAX actions |
|
|
|
KeycloakCurrentUser() ' Returns userinfo Dictionary (preferred_username, email, name, sub) |
|
|
|
KeycloakHasRealmRole("admin") ' Role check |
|
|
|
KeycloakLogout("") ' Clear session and redirect |
|
|
|
``` |
|
|
|
|
|
|
|
## LinkedList_Class — correct traversal |
|
|
|
|
|
|
|
Never use `.First`, `.Last`, `.m_first`, `.m_next`, or `.m_value` — they are private internals. |
|
|
|
|
|
|
|
```vbscript |
|
|
|
Dim iter : Set iter = myList.Iterator() |
|
|
|
Do While iter.HasNext() |
|
|
|
Set item = iter.GetNext() |
|
|
|
Loop |
|
|
|
``` |
|
|
|
|
|
|
|
Or convert: `Dim arr : arr = myList.TO_Array()` |
|
|
|
|
|
|
|
## AJAX form data — always URLSearchParams, never FormData |
|
|
|
|
|
|
|
Classic ASP `Request.Form` only parses `application/x-www-form-urlencoded`. `new FormData()` sends `multipart/form-data` which Classic ASP silently ignores. |
|
|
|
|
|
|
|
```javascript |
|
|
|
var params = new URLSearchParams(); |
|
|
|
Object.keys(data).forEach(function(k) { params.append(k, data[k]); }); |
|
|
|
fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: params.toString() }) |
|
|
|
``` |
|
|
|
|
|
|
|
`Request.BinaryRead` / `GetRawJsonFromRequest()` and `Request.Form` are mutually exclusive in the same action — never mix them. |
|
|
|
|
|
|
|
## View files — Classic ASP scoping rule |
|
|
|
|
|
|
|
View files are SSI-included inside the controller action's Sub scope. Never define `Function` or `Sub` inside a view file — VBScript forbids nested procedure definitions (`Syntax error 800a03ea`). Keep views to pure HTML rendering: `<%= %>`, `H()`, simple `If`/`For`/`Do` blocks. |
|
|
|
|
|
|
|
## Database DDL — Access/Jet reserved words |
|
|
|
|
|
|
|
Always bracket every identifier. Use `migration.ExecuteSQL` for all DDL. Never use `migration.CreateTable` or `migration.CreateIndex`. |
|
|
|
|
|
|
|
```vbscript |
|
|
|
migration.ExecuteSQL "CREATE TABLE [board_columns] ([id] AUTOINCREMENT PRIMARY KEY, [name] VARCHAR(255), [position] INTEGER)" |
|
|
|
``` |
|
|
|
|
|
|
|
Common reserved words: `COLUMNS`, `NAME`, `POSITION`, `VALUE`, `DATE`, `KEY`, `LEVEL`, `BY`. |
|
|
|
|
|
|
|
## Running migrations (32-bit only) |
|
|
|
|
|
|
|
``` |
|
|
|
C:\Windows\SysWOW64\cscript.exe //nologo scripts\runMigrations.vbs up |
|
|
|
``` |
|
|
|
|
|
|
|
IIS app pool must have `enable32BitAppOnWin64="true"`. Use 32-bit IIS Express: `%ProgramFiles(x86)%\IIS Express\iisexpress.exe`. |
|
|
|
|
|
|
|
If `migration.DB.Execute/Query` fails in standalone runner, provide a `migration.Connection` + `ADODB.Command` fallback. |
|
|
|
|
|
|
|
## Flash messages |
|
|
|
|
|
|
|
```vbscript |
|
|
|
Flash().AddError "Something went wrong" |
|
|
|
flash.Success = "Saved successfully" |
|
|
|
``` |
|
|
|
|
|
|
|
Never use `Flash().SetError` or `Flash().SetSuccess` — those methods do not exist (Error 438). |
|
|
|
|
|
|
|
## Things to avoid |
|
|
|
|
|
|
|
- Do not modify files under `core/` — framework internals. |
|
|
|
- Do not add controllers without registering in `ControllerRegistry`. |
|
|
|
- Do not commit real `KeycloakClientSecret` values. |
|
|
|
- Do not add test routes/pages under `public/`. |
|
|
|
- Always use `H()` when rendering user-supplied data (XSS prevention). |
|
|
|
- Never write your own slug generator — use `GenerateSlug()`. |
|
|
|
- Never use `Private Const` or `Public Const` inside a VBScript class — use a `Private Function` returning the value instead. |
|
|
|
- Never run migrations with 64-bit cscript on this machine. |
|
|
|
- Never mix `GetRawJsonFromRequest()` with `Request.Form` in the same action. |
|
|
|
- Never call `.First`/`.Last`/`.m_first`/`.m_next`/`.m_value` on a `LinkedList_Class`. |
|
|
|
- Always set all four audit columns on every insert and update. |
|
|
|
|
|
|
|
## Requirements |
|
|
|
|
|
|
|
- Windows Server / Windows with IIS, Classic ASP enabled |
|
|
|
- IIS URL Rewrite module |
|
|
|
- Microsoft Access Database Engine (ACE OLEDB 12.0) — 32-bit |
|
|
|
- Keycloak server (for auth flows) |