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.
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
| 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 |
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 |
Routes registered in public/Default.asp with :param segments.
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:
Public Sub Show(slug) : End Sub
Public Sub Move(id) : End Sub
URL generation:
Routes.UrlTo "Boards", "Index", Empty
Routes.UrlToWithParams "Boards", "Show", Array("my-board"), Empty
Routes.UrlTo "Boards", "Index", Array("page", 2)
cscript //nologo scripts\generateController.vbs MyController "Index;Show(slug);Store"app/controllers/RegisterController "mycontroller"<!--#include file="MyController.asp"-->app/views/MyController/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.
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()).
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".
Every table must include created_at, created_by, updated_at, updated_by.
' 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
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
Never use .First, .Last, .m_first, .m_next, or .m_value — they are private internals.
Dim iter : Set iter = myList.Iterator()
Do While iter.HasNext()
Set item = iter.GetNext()
Loop
Or convert: Dim arr : arr = myList.TO_Array()
Classic ASP Request.Form only parses application/x-www-form-urlencoded. new FormData() sends multipart/form-data which Classic ASP silently ignores.
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 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.
Always bracket every identifier. Use migration.ExecuteSQL for all DDL. Never use migration.CreateTable or migration.CreateIndex.
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.
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().AddError "Something went wrong"
flash.Success = "Saved successfully"
Never use Flash().SetError or Flash().SetSuccess — those methods do not exist (Error 438).
core/ — framework internals.ControllerRegistry.KeycloakClientSecret values.public/.H() when rendering user-supplied data (XSS prevention).GenerateSlug().Private Const or Public Const inside a VBScript class — use a Private Function returning the value instead.GetRawJsonFromRequest() with Request.Form in the same action..First/.Last/.m_first/.m_next/.m_value on a LinkedList_Class.Powered by TurnKey Linux.