# 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): `` 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)