first commit

This commit is contained in:
bruno 2022-07-31 16:45:47 -04:00
commit 382b315e96
13 changed files with 1407 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
main
data/website.db

0
README.md Normal file
View File

98
code/database_utils.nim Normal file
View File

@ -0,0 +1,98 @@
# Copyright 2019 - Thomas T. Jarløv
# import db_sqlite, os, parsecfg, strutils, logging
import db_sqlite, os, parsecfg, 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"
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.")

34
code/password_utils.nim Normal file
View File

@ -0,0 +1,34 @@
# Copyright 2019 - Thomas T. Jarløv
# Credit Nimforum - https://github.com/nim-lang/nimforum
# import md5, bcrypt, math, random, os
import md5, bcrypt, random
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)

12
config/config.cfg Normal file
View File

@ -0,0 +1,12 @@
[Database]
folder = "data"
host = "data/website.db"
name = "website"
user = "user"
pass = ""
[Server]
website = "https://myurl.org"
title = "Joplin The New Web"
url = "127.0.0.1"
port = "7000"

227
main.nim Normal file
View File

@ -0,0 +1,227 @@
# Copyright 2019 - Thomas T. Jarløv
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
# 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 var
var db: DbConn
# Jester setting server settings
settings:
port = Port(mainPort)
bindAddr = mainURL
# Setup user data
type
TData* = ref object of RootObj
loggedIn*: bool
userid, username*, userpass*, email*: string
req*: Request
proc init(c: var TData) =
## Empty out user session data
c.userpass = ""
c.username = ""
c.userid = ""
c.loggedIn = false
func loggedIn(c: TData): bool =
## Check if user is logged in by verifying that c.username exists
c.username.len > 0
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
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")
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 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 cookies(request).len > 0:
# Check if user is logged in
checkLoggedIn(c)
# Use the func()
c.loggedIn = loggedIn(c)
# isMainModule
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
#include "tmpl/main.tmpl"
include "tmpl/user.tmpl"
include "tmpl/website.tmpl"
# Setup routes (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:
when defined(dev):
jester.setCookie("sid", loginS, daysForward(7))
else:
jester.setCookie("sid", loginS, daysForward(7), samesite = Lax, secure = true, httpOnly = true)
redirect("/secret")
else:
redirect("/login?msg=" & encodeUrl(loginS))
get "/logout":
createTFD()
logout(c)
redirect("/")

316
public/index.html Executable file

File diff suppressed because one or more lines are too long

72
public/logo.html Executable file
View File

@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="styles.css">
<title>Document</title>
</head>
<body>
<svg class="svg-icon" viewBox="0 0 20 20">
<path
d="M17.237,3.056H2.93c-0.694,0-1.263,0.568-1.263,1.263v8.837c0,0.694,0.568,1.263,1.263,1.263h4.629v0.879c-0.015,0.086-0.183,0.306-0.273,0.423c-0.223,0.293-0.455,0.592-0.293,0.92c0.07,0.139,0.226,0.303,0.577,0.303h4.819c0.208,0,0.696,0,0.862-0.379c0.162-0.37-0.124-0.682-0.374-0.955c-0.089-0.097-0.231-0.252-0.268-0.328v-0.862h4.629c0.694,0,1.263-0.568,1.263-1.263V4.319C18.5,3.625,17.932,3.056,17.237,3.056 M8.053,16.102C8.232,15.862,8.4,15.597,8.4,15.309v-0.89h3.366v0.89c0,0.303,0.211,0.562,0.419,0.793H8.053z M17.658,13.156c0,0.228-0.193,0.421-0.421,0.421H2.93c-0.228,0-0.421-0.193-0.421-0.421v-1.263h15.149V13.156z M17.658,11.052H2.509V4.319c0-0.228,0.193-0.421,0.421-0.421h14.308c0.228,0,0.421,0.193,0.421,0.421V11.052z">
</path>
</svg>
<svg viewBox="0 0 24 24" preserveAspectRatio="xMidYMid meet" focusable="false" class="menu-icon">
<g>
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"></path>
</g>
</svg>
<svg class="sidebar-icon" viewBox="0 0 24 24" preserveAspectRatio="xMidYMid meet" focusable="false">
<g>
<path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"></path>
</g>
</svg>
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" width="489px" height="489.001px" viewBox="0 0 489 489.001" style="enable-background:new 0 0 489 489.001;"
xml:space="preserve">
<g>
<path d="M355.768,0H86.218C53.33,0,26.577,26.753,26.577,59.636v369.729c0,32.883,26.752,59.636,59.641,59.636h316.566
c32.889-0.001,59.641-26.754,59.641-59.637V109.16L355.768,0z M402.784,446.479H86.218c-9.437,0-17.119-7.678-17.119-17.113V59.636
c0-9.437,7.683-17.114,17.119-17.114H334.86v47.604c0,21.043,17.109,38.162,38.152,38.375l46.891,0.477v300.388
C419.903,438.801,412.219,446.479,402.784,446.479z" />
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>
</body>
</html>

BIN
public/logo.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

6
public/script.js Executable file
View File

@ -0,0 +1,6 @@
const menuIconButton = document.querySelector("[data-menu-icon-btn]")
const sidebar = document.querySelector("[data-sidebar]")
menuIconButton.addEventListener("click", () => {
sidebar.classList.toggle("open")
})

253
public/styles.css Executable file
View File

@ -0,0 +1,253 @@
body {
margin: 0;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
:root {
--accent-color: #0053b8;
--lightest-gray: rgb(244, 244, 244);
--light-gray: rgb(144, 144, 144);
--medium-gray: rgb(96, 96, 96);
--dark-gray: rgb(13, 13, 13);
--header-height: 40px;
--animation-duration: 200ms;
--animation-timing-curve: ease-in-out;
--blue-joplin-color: #0053b8;
--light-blue-joplin-color: rgb(237, 241, 243);
}
.header {
display: flex;
align-items: center;
position: sticky;
top: 0;
background-color: white;
box-shadow: 0 1px 10px 0 rgba(0, 0, 0, 0.4);
padding: 0 0.5rem;
height: var(--header-height);
}
.login {
font-size: large;
fill: var(--blue-joplin-color);
max-width: fit-content;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.menu-icon-btn {
background: none;
border: none;
padding: 0;
}
.menu-icon {
width: 25px;
height: 25px;
fill: var(--medium-gray);
cursor: pointer;
}
.menu-icon:hover {
fill: var(--dark-gray);
}
.sidebar {
flex-shrink: 0;
overflow: hidden;
width: 75px;
border-right: 1px solid var(--light-gray);
display: flex;
flex-direction: column;
height: calc(100vh - var(--header-height));
padding-top: 1rem;
align-items: center;
justify-content: stretch;
transition: width var(--animation-duration) var(--animation-timing-curve);
position: sticky;
left: 0;
top: var(--header-height);
}
.sidebar .hidden-sidebar {
opacity: 0;
width: 0;
transition: opacity var(--animation-duration) var(--animation-timing-curve);
}
.sidebar.open .hidden-sidebar {
width: 100%;
height: auto;
opacity: 1;
}
.sidebar .top-sidebar {
display: flex;
flex-direction: column;
align-items: center;
}
.sidebar .channel-logo {
display: block;
width: 30px;
height: 30px;
transition: var(--animation-duration) var(--animation-timing-curve);
}
.sidebar.open .channel-logo {
width: 90px;
height: 90px;
}
.sidebar .channel-logo > img {
width: 100%;
height: 100%;
}
.middle-sidebar {
overflow-y: auto;
overflow-x: hidden;
flex-grow: 1;
margin: 1rem 0;
}
.middle-sidebar,
.bottom-sidebar {
width: 100%;
}
.container {
display: flex;
}
.vertical-center {
margin: 0;
position: absolute;
top: 50%;
-ms-transform: translateY(-50%);
transform: translateY(-50%);
/* border: 5px solid #FFFF00; */
text-align: center;
}
.content {
margin: 1rem;
}
.sidebar-list {
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
list-style: none;
}
.sidebar.open .sidebar-link {
justify-content: flex-start;
}
.sidebar-icon {
width: 25px;
height: 25px;
flex-shrink: 0;
fill: var(--blue-joplin-color);
}
.sidebar-list .hidden-sidebar {
margin-left: 1.5rem;
white-space: nowrap;
}
.sidebar-link {
display: flex;
width: 100%;
padding: 0.7rem 0;
color: var(--light-gray);
text-decoration: none;
align-items: center;
padding-left: 25px;
}
.sidebar-list-item {
position: relative;
width: 100%;
fill: var(--light-gray);
}
.sidebar-list-item.active {
fill: var(--accent-color);
background-color: var(--light-blue-joplin-color);
}
.sidebar-list-item:hover {
background-color: var(--light-blue-joplin-color);
}
.sidebar-list-item.active::before {
content: "";
background-color: var(--accent-color);
height: 100%;
left: 0;
width: 3px;
position: absolute;
}
.sidebar.open {
width: 200px;
}
.your-channel {
color: var(--dark-gray);
font-size: 0.75rem;
font-weight: bold;
margin-bottom: 0.15rem;
margin-top: 0.5rem;
}
.channel-name {
color: var(--medium-gray);
font-size: 0.75rem;
}
.sidebar .top-sidebar {
height: 30px;
transition: height var(--animation-duration) var(--animation-timing-curve);
}
.sidebar.open .top-sidebar {
height: 125px;
}
.sidebar .top-sidebar .hidden-sidebar {
text-align: center;
width: 100%;
}
/* .svg-icon {
width: 2em;
height: 2em;
}
.svg-icon:hover {
fill: var(--animation-duration);
}
.svg-icon path,
.svg-icon polygon,
.svg-icon rect {
fill: #4691f6;
}
.svg-icon circle {
stroke: #4691f6;
stroke-width: 1;
} */

43
tmpl/user.tmpl Normal file
View File

@ -0,0 +1,43 @@
#? stdtmpl | standard
#
#proc genLogin(c: var TData, errorMsg = ""): string =
# result = ""
# if not c.loggedIn:
<link rel="stylesheet" href="/styles.css">
<center>
<div class="login">
<div class="vertical-centerr">
<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>
</div>
</center>
#else:
<div class="notification is-danger" style="text-align: center">
<meta http-equiv="refresh" content="0; URL=./secret" />
</div>
# end if
#end proc

344
tmpl/website.tmpl Normal file

File diff suppressed because one or more lines are too long