Setup a website with Nim

This small tutorial will help you create a website with Nim. We'll use libraries from the standard library and some externals from Nim-users.

The tutorial will not explain how procs, templates, etc. works - there are many other good Nim resources for that.

We'll create a small website with user management having a public page and page only visible for users logged in.

Difficulty

The tutorial is meant for programmers with little experience with Nim.

The first chapters are easy, but thereafter the difficulty will increase with stuff such as macros.

Goal

A website created in Nim. The result will be simplified replica of NimWC. You can find the NimWC at the NimWC Github repo.

Requirements

Nim with a version higher or equal to 19.2.

The final code

The final code is hosted here: Tutorial code


Setup you battlestation

External libraries

nimble install jester
nimble install bcrypt

Not used libraries

There's a lot of helpful libraries, which we are not going to use. But you can enhance your code afterward with them.

Checkout Nimble Directory for libraries.

  • sqlbuilder (format your queries)
  • recaptcha (Google's reCAPTCHA verification)

Code editor

Visual Studio Code has good integration of the Nim. Download VSC and install the Nim extension.


Folder structure

Create the following folder structure. We will fill the folders with files as we go through the tutorial.

nimweb/
nimweb/config   <-- Used to store our configuration
nimweb/data     <-- For our SQLite database
nimweb/code     <-- The code
nimweb/public   <-- Containing CSS and JS files
nimweb/tmpl     <-- The HTML-Nim files

Config files

The config files contains our secret information, such as database password.

Create the config file nimweb/config/config.cfg

Insert the following data:

[Database]
folder = "data"
host = "data/website.db"
name = "website"
user = "user"
pass = ""

[Server]
website = "https://myurl.org"
title = "Nim Website"
url = "127.0.0.1"
port = "7000"

The main file

This file is our main file. It is within this we'll import and include our files. Create a file nimweb/main.nim

Import required libraries

Insert the following at the top of your new file.

import db_sqlite # SQLite
import jester    # Our webserver
import logging   # Logging utils
import os        # Used to get arguments
import parsecfg  # Parse CFG (config) files
import strutils  # Basic functions
import times     # Time and date
import uri       # We need to encode urls: encodeUrl()
import code/database_utils # Utils used in the database
import code/password_utils # Our file with password utils

Parse config.cfg

We'll start by reading and parsing the config.cfg to get the database information.

# First we'll load config files
let dict = loadConfig("config/config.cfg")

# Now we get the values and assign them.
# We do not need to change them later, therefore
# we'll use `let`
let db_user   = dict.getSectionValue("Database", "user")
let db_pass   = dict.getSectionValue("Database", "pass")
let db_name   = dict.getSectionValue("Database", "name")
let db_host   = dict.getSectionValue("Database", "host")

let mainURL   = dict.getSectionValue("Server", "url")
let mainPort  = parseInt dict.getSectionValue("Server", "port")
let mainWebsite = dict.getSectionValue("Server", "website")

Database variable

We are assigning db to our database connection. Each time we need to access the database, we can use db

# Database var
var db: DbConn

Setup our webserver

We'll assign jester (our webserver) with the right values.

# Jester setting server settings
settings:
  port = Port(mainPort)
  bindAddr = mainURL

User management

In this tutorial we'll only add 1 user - the Admin

User details

When the admin user logs in, we'll assign values to user. These values "follows" the user around; that allows us to access them, e.g. the users email etc.

The user details are assigned to a c. This is passed around as c: var TData

Setup a type containing the user data

# Setup user data
type
  TData* = ref object of RootObj
    loggedIn*: bool
    userid, username*, userpass*, email*: string
    req*: Request

The init() proc reset these values to ensure, that the user is logged in and that the values always correspond to values saved in the database.

proc init(c: var TData) =
  ## Empty out user session data
  c.userpass = ""
  c.username = ""
  c.userid   = ""
  c.loggedIn = false

Check is user is logged in

Each time the user is requesting a new page, we have to check if the user is logged in. We do that by checking, if the user has a username assigned (we'll assign the username later).

func loggedIn(c: TData): bool =
  ## Check if user is logged in by verifying that c.username exists
  c.username.len > 0

Is user logged in check full

We create a proc to check our database table session to see, if their cookie and IP matches.

proc checkLoggedIn(c: var TData) =
  ## Check if user is logged in

  # Get the users cookie named `sid`. If it does not exist, return
  if not c.req.cookies.hasKey("sid"): return

  # Assign cookie to `let sid`
  let sid = c.req.cookies["sid"]

  # Update the value lastModified for the user in the
  # table session where the sid and IP match. If there's
  # any results (above 0) assign values
  if execAffectedRows(db, sql("UPDATE session SET lastModified = " & $toInt(epochTime()) & " " & "WHERE ip = ? AND key = ?"), c.req.ip, sid) > 0:

    # Get user data based on userID from session table
    # Assign values to user details - `c`
    c.userid = getValue(db, sql"SELECT userid FROM session WHERE ip = ? AND key = ?", c.req.ip, sid)

    # Get user data based on userID from person table
    let row = getRow(db, sql"SELECT name, email, status FROM person WHERE id = ?", c.userid)

    # Assign user data
    c.username  = row[0]
    c.email     = toLowerAscii(row[1])

    # Update our session table with info about activity
    discard tryExec(db, sql"UPDATE person SET lastOnline = ? WHERE id = ?", toInt(epochTime()), c.userid)

  else:
    # If the user is not found in the session table
    c.loggedIn = false

Login proc

Next we create a proc used for login. This proc is only used when the user logs in.

proc login(c: var TData, email, pass: string): tuple[b: bool, s: string] =
  ## User login

  # We have predefined query
  const query = sql"SELECT id, name, password, email, salt, status FROM person WHERE email = ?"

  # If the email or pass passed in the proc's parameters is empty, fail
  if email.len == 0 or pass.len == 0:
    return (false, "Missing password or username")

  # We'll use fastRows for a quick query.
  # Notice that the email is set to lower ascii
  # to avoid problems if the user has any
  # capitalized letters.
  for row in fastRows(db, query, toLowerAscii(email)):

    # Now our password library is going to work. It'll
    # check the password against the hashed password
    # and salt.
    if row[2] == makePassword(pass, row[4], row[2]):
      # Assign the values
      c.userid   = row[0]
      c.username = row[1]
      c.userpass = row[2]
      c.email    = toLowerAscii(row[3])

      # Generate session key and save it
      let key = makeSessionKey()
      exec(db, sql"INSERT INTO session (ip, key, userid) VALUES (?, ?, ?)", c.req.ip, key, row[0])

      info("Login successful")
      return (true, key)

  info("Login failed")
  return (false, "Login failed")

A logout proc

The logout proc deletes the users instance in the session table. This results in, that the proc checkLoggedIn() cannot find any matching values to users sid and IP.

proc logout(c: var TData) =
  ## Logout

  c.username = ""
  c.userpass = ""
  const query = sql"DELETE FROM session WHERE ip = ? AND key = ?"
  exec(db, query, c.req.ip, c.req.cookies["sid"])

Do the check inside our routes

Template used in our routes. The template is "injected" into all of our URL routes, where it uses the proc's above to check if the user is logged in.

You'll see this template used in the routes-files.

template createTFD() =
  ## Check if logged in and assign data to user

  # Assign the c to TDATA
  var c {.inject.}: TData
  # New instance of c
  new(c)
  # Set standard values
  init(c)
  # Get users request
  c.req = request
  # Check for cookies (we need the cookie named sid)
  if request.cookies.len > 0:
    # Check if user is logged in
    checkLoggedIn(c)
  # Use the func()
  c.loggedIn = loggedIn(c)

isMainModule

when isMainModule: is running when the file is the main module, which this is!

Insert the following:

when isMainModule:
  echo "Nim Web is now running: " & $now()

  # Generate DB if newdb is in the arguments
  # or if the database does not exists
  if "newdb" in commandLineParams() or not fileExists(db_host):
    generateDB()
    quit()

  # Connect to DB
  try:
    # We are using the values which we assigned earlier
    db = open(connection=db_host, user=db_user, password=db_pass, database=db_name)
    info("Connection to DB is established.")
  except:
    fatal("Connection to DB could not be established.")
    sleep(5_000)
    quit()

  # Add an admin user if newuser is in the args
  if "newuser" in commandLineParams():
    createAdminUser(db, commandLineParams())
    quit()

Include template files

The only thing we are missing now is our URL routes and HTML templates. We are going to include the HTML-Nim files which means, that they are inserted 1-1 into our code instead of imported.

include "tmpl/main.tmpl"
include "tmpl/user.tmpl"

Setup routes (URL's)

Routes is the name for our URL's.

routes:
  get "/":
    createTFD()
    resp genMain(c)

  get "/secret":
    createTFD()
    if c.loggedIn:
      resp genSecret(c)

  get "/login":
    createTFD()
    resp genLogin(c, @"msg")

  post "/dologin":
    createTFD()

    let (loginB, loginS) = login(c, replace(toLowerAscii(@"email"), " ", ""), replace(@"password", " ", ""))
    if loginB:
      jester.setCookie("sid", loginS, daysForward(7))
      redirect("/secret")
    else:
      redirect("/login?msg=" & encodeUrl(loginS))

  get "/logout":
    createTFD()
    logout(c)
    redirect("/")

Password utils

Create the password file here nimweb/code/password_utils.nim and insert:

import md5, bcrypt, math, random, os
randomize()

var urandom: File
let useUrandom = urandom.open("/dev/urandom")

proc makeSalt*(): string =
  ## Generate random salt. Uses cryptographically secure /dev/urandom
  ## on platforms where it is available, and Nim's random module in other cases.
  result = ""
  if useUrandom:
    var randomBytes: array[0..127, char]
    discard urandom.readBuffer(addr(randomBytes), 128)
    for ch in randomBytes:
      if ord(ch) in {32..126}:
        result.add(ch)
  else:
    for i in 0..127:
      result.add(chr(rand(94) + 32)) # Generate numbers from 32 to 94 + 32 = 126

proc makeSessionKey*(): string =
  ## Creates a random key to be used to authorize a session.
  let random = makeSalt()
  return bcrypt.hash(random, genSalt(8))


proc makePassword*(password, salt: string, comparingTo = ""): string =
  ## Creates an MD5 hash by combining password and salt
  let bcryptSalt = if comparingTo != "": comparingTo else: genSalt(8)
  result = hash(getMD5(salt & getMD5(password)), bcryptSalt)

Database utils

Create the file nimweb/code/database_utils

This file will contain 2 procs: 1 for generating the database and 1 for creating the admin user. Insert both of the proc's into the file.

Generating database

import db_sqlite, os, parsecfg, strutils, logging
import ../code/password_utils

proc generateDB*() =
  echo "Generating database"

  # Load the connection details
  let
    dict = loadConfig("config/config.cfg")
    db_user = dict.getSectionValue("Database","user")
    db_pass = dict.getSectionValue("Database","pass")
    db_name = dict.getSectionValue("Database","name")
    db_host = dict.getSectionValue("Database","host")
    db_folder = dict.getSectionValue("Database","folder")
    dbexists = if fileExists(db_host): true else: false

  if dbexists:
    echo " - Database already exists. Inserting tables if they do not exist."

  # Creating database folder if it doesn't exist
  discard existsOrCreateDir(db_folder)

  # Open DB
  echo " - Opening database"
  var db = open(connection=db_host, user=db_user, password=db_pass, database=db_name)

  # Person table contains information about the
  # registrered users
  if not db.tryExec(sql("""
  create table if not exists person(
    id integer primary key,
    name varchar(60) not null,
    password varchar(300) not null,
    email varchar(254) not null,
    creation timestamp not null default (STRFTIME('%s', 'now')),
    modified timestamp not null default (STRFTIME('%s', 'now')),
    salt varbin(128) not null,
    status varchar(30) not null,
    timezone VARCHAR(100),
    secretUrl VARCHAR(250),
    lastOnline timestamp not null default (STRFTIME('%s', 'now'))
  );""")):
    echo " - Database: person table already exists"

  # Session table contains information about the users
  # cookie ID, IP and last visit
  if not db.tryExec(sql("""
  create table if not exists session(
    id integer primary key,
    ip inet not null,
    key varchar(300) not null,
    userid integer not null,
    lastModified timestamp not null default (STRFTIME('%s', 'now')),
    foreign key (userid) references person(id)
  );""")):
    echo " - Database: session table already exists"

Generating admin user

This proc generates the admin user using arguments passed when running the program. To generate the user, you have to provide the following arguments:

./main newuser u:UserName p:Password e:email@email.com

proc createAdminUser*(db: DbConn, args: seq[string]) =
  ## Create new admin user

  var iName = ""
  var iEmail = ""
  var iPwd = ""

  # Loop through all the arguments and get the args
  # containing the user information
  for arg in args:
    if arg.substr(0, 1) == "u:":
      iName = arg.substr(2, arg.len())
    elif arg.substr(0, 1) == "p:":
      iPwd = arg.substr(2, arg.len())
    elif arg.substr(0, 1) == "e:":
      iEmail = arg.substr(2, arg.len())

  # If the name, password or emails does not exists
  # return error
  if iName == "" or iPwd == "" or iEmail == "":
    error("Missing either name, password or email to create the Admin user.")

  # Generate the password using a salt and hashing.
  # Read more about hashing and salting here:
  #   - https://crackstation.net/hashing-security.htm
  #   - https://en.wikipedia.org/wiki/Salt_(cryptography)
  let salt = makeSalt()
  let password = makePassword(iPwd, salt)

  # Insert user into database
  if insertID(db, sql"INSERT INTO person (name, email, password, salt, status) VALUES (?, ?, ?, ?, ?)", $iName, $iEmail, password, salt, "Admin") > 0:
    echo "Admin user added"
  else:
    error("Something went wrong")

  info("Admin added.")

Template files

The templates is a mix of HTML and Nim. The Nim code is identified with a hashtag in the files: #

main.tmpl

Create the file nimweb/tmpl/main.tmpl

#? stdtmpl | standard
#
#proc genMain(c: var TData): string =
# result = ""

Hello World

#end proc # #proc genSecret(c: var TData): string = # result = ""

Welcome to the secret World

#end proc

user.tmpl

Create the file nimweb/tmpl/user.tmpl

#? stdtmpl | standard
#
#proc genLogin(c: var TData, errorMsg = ""): string =
# result = ""
# if not c.loggedIn:
  <div id="login">
    <form name="login" action="/dologin" method="POST" class="box">
      <h3 style="line-height: 1.9rem;">Login</h3>

      # if errorMsg.len() != 0:
      <div class="notification is-danger" style="text-align: center;font-size: 1.2rem; line-height: 1.8rem;"><b>${errorMsg}</b></div>
      # end if

      <div class="field form-group">
        <label class="label">Email</label>
        <div class="control has-icons-left has-icons-right">
          <input type="email" class="form-control input is-rounded" name="email" placeholder="Email" minlength="5" dir="auto" required autofocus>
        </div>
      </div>
      <div class="field form-group">
        <label class="label">Password</label>
        <div class="control has-icons-left has-icons-right">
          <input type="password" class="form-control input is-rounded" name="password" autocomplete="current-password" minlength="4" placeholder="Password" dir="auto" required>
        </div>
      </div>

      <input href="#" type="submit" class="btn btn-custom btn-blue-secondary button is-primary is-fullwidth is-rounded" value="Login" />

    </form>
  </div>

  #else:
  <div class="notification is-danger" style="text-align: center">
    <h1>You are already logged in!</h1>
  </div>
# end if
#end proc

CSS and JS files

Stylesheets (CSS) and Javascript files can be loaded from the public folder. Let's create CSS file.

Create the file nimweb/public/style.css

h1 { color: red; }

Run it!

We are no ready to startup our little website. First we'll compile it, then we generate the database and then we add the admin user.

# Compile
nim c main.nim

# Generate database
./main newdb

# Add admin user
./main newuser u:Admin p:Pass e:email@email.com

# Run
./main

# Now access your website on 127.0.0.1:7000
# Login on 127.0.0.1:7000/login


Next step

Webserver

If you want to use your new program in production - other people can access it, you should setup a webserver infront, e.g. Nginx.

SSL

To activate SSL due to a SSL certificate or if you are loading HTTPS resources, you need to compile with -d:ssl