Compare commits
10 Commits
e40b453118
...
48be035341
Author | SHA1 | Date |
---|---|---|
Quad | 48be035341 | |
Quad | d7f7f547e4 | |
Quad | 361d3ebbcb | |
Quad | 83d77f8865 | |
Quad | 7c484b18d3 | |
Quad | 88cbeee562 | |
Quad | 44cbd19a8d | |
Quad | a43e45b237 | |
Quad | ac32fa394f | |
Quad | bc2a428432 |
|
@ -3,3 +3,6 @@
|
||||||
|
|
||||||
# Binary name built by nimble
|
# Binary name built by nimble
|
||||||
nimblog
|
nimblog
|
||||||
|
|
||||||
|
# Blog data
|
||||||
|
data/*
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# nimblog
|
# nimblog
|
||||||
|
|
||||||
Placeholder text
|
Basic blog written in nim, mostly for learning purposes.
|
||||||
|
|
||||||
## How to run
|
## How to run
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
CREATE TABLE users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT UNIQUE NOT NULL,
|
||||||
|
displayname TEXT NOT NULL,
|
||||||
|
password TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE posts (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
author_id INTEGER NOT NULL,
|
||||||
|
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
body TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (author_id) REFERENCES users (id)
|
||||||
|
);
|
|
@ -0,0 +1,47 @@
|
||||||
|
# Handles user management. And hopefully stuff like proper password hashing at some point in the future.
|
||||||
|
|
||||||
|
import db
|
||||||
|
import formatter
|
||||||
|
|
||||||
|
proc addUser(username: string, displayname: string, password: string): bool =
|
||||||
|
# Hopefully hashing and stuff will be implemented here at some point
|
||||||
|
return createUser(username=username, displayname=displayname, password=password)
|
||||||
|
|
||||||
|
# Extremely rudimentary, must be fixed later.
|
||||||
|
proc addUserInteractive(): bool =
|
||||||
|
var userConsent: bool = false
|
||||||
|
while userConsent != true:
|
||||||
|
stdout.write "Did not find a user. Create one now? (y/n) "
|
||||||
|
stdout.flushFile()
|
||||||
|
var userInput = readChar(stdin)
|
||||||
|
stdin.flushFile()
|
||||||
|
case userInput
|
||||||
|
of 'y', 'Y':
|
||||||
|
userConsent = true
|
||||||
|
of 'n', 'N':
|
||||||
|
return false
|
||||||
|
else:
|
||||||
|
discard
|
||||||
|
|
||||||
|
stdout.write "Enter a username: "
|
||||||
|
stdout.flushFile()
|
||||||
|
var username = readLine(stdin)
|
||||||
|
stdin.flushFile()
|
||||||
|
|
||||||
|
stdout.write "Enter a display name: "
|
||||||
|
stdout.flushFile()
|
||||||
|
var displayname = readLine(stdin)
|
||||||
|
stdin.flushFile()
|
||||||
|
|
||||||
|
stdout.write "Enter a password: "
|
||||||
|
stdout.flushFile()
|
||||||
|
var password = readLine(stdin)
|
||||||
|
stdin.flushFile()
|
||||||
|
|
||||||
|
if addUser(username=username, displayname=displayname, password=password):
|
||||||
|
logPrint("User " & username & " with displayname " & displayname & " has been created", "Main")
|
||||||
|
return true
|
||||||
|
else:
|
||||||
|
return false
|
||||||
|
|
||||||
|
export addUserInteractive
|
|
@ -1,17 +1,22 @@
|
||||||
|
# This is what ACTUALLY starts the blog
|
||||||
|
|
||||||
import prologue
|
import prologue
|
||||||
import templating
|
import prologue/middlewares/staticfile
|
||||||
|
import views
|
||||||
|
import formatter
|
||||||
|
|
||||||
proc getIndex*(ctx: Context) {.async.} =
|
proc runWebsite(settings: Table[string, bool]) =
|
||||||
resp renderTemplate(templateName="index.nwt")
|
|
||||||
|
|
||||||
proc runWebsite(settings: Table) =
|
|
||||||
let prologueSettings = newSettings(
|
let prologueSettings = newSettings(
|
||||||
debug = settings["debug"]
|
debug = settings["debug"]
|
||||||
)
|
)
|
||||||
let website = newApp(settings = prologueSettings)
|
|
||||||
|
var website = newApp(settings = prologueSettings)
|
||||||
|
|
||||||
website.get("/", getIndex)
|
website.get("/", getIndex)
|
||||||
|
website.get("/about", getAbout)
|
||||||
|
website.use(staticFileMiddleware("static"))
|
||||||
|
|
||||||
|
logPrint("Press Ctrl-C to stop site", "Info")
|
||||||
website.run()
|
website.run()
|
||||||
|
|
||||||
export runWebsite
|
export runWebsite
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
# Everything related to database reads and writes goes in this file
|
||||||
|
|
||||||
|
import tables
|
||||||
|
import std/[os, db_sqlite, strutils, streams]
|
||||||
|
import formatter
|
||||||
|
|
||||||
|
proc connectDb(dbPath: string = "data/blog.db"): DbConn =
|
||||||
|
var db = open(dbPath, "", "", "")
|
||||||
|
return db
|
||||||
|
|
||||||
|
proc createUser(username: string, displayname: string, password: string): bool =
|
||||||
|
try:
|
||||||
|
var db = connectDb()
|
||||||
|
db.exec(sql"INSERT INTO users (username,displayname,password) VALUES (?, ?, ?)", username, displayname, password)
|
||||||
|
db.close()
|
||||||
|
return true
|
||||||
|
except:
|
||||||
|
return false
|
||||||
|
|
||||||
|
proc checkUser(): bool =
|
||||||
|
var db = connectDb()
|
||||||
|
var response = db.getRow(sql"SELECT * FROM users")
|
||||||
|
db.close()
|
||||||
|
if response[0] != "":
|
||||||
|
return true
|
||||||
|
else:
|
||||||
|
return false
|
||||||
|
|
||||||
|
proc createDb(dbSettings: Table): bool =
|
||||||
|
if not fileExists("data/blog.db"):
|
||||||
|
var userConsent: bool = false
|
||||||
|
while userConsent != true:
|
||||||
|
stdout.write "Did not find database. Create one now? (y/n) "
|
||||||
|
stdout.flushFile()
|
||||||
|
var userInput = readChar(stdin)
|
||||||
|
stdin.flushFile()
|
||||||
|
case userInput
|
||||||
|
of 'y', 'Y':
|
||||||
|
userConsent = true
|
||||||
|
of 'n', 'N':
|
||||||
|
return false
|
||||||
|
else:
|
||||||
|
discard
|
||||||
|
|
||||||
|
let
|
||||||
|
schema = readFile(dbSettings["schema_path"])
|
||||||
|
db = connectDb()
|
||||||
|
for command in schema.split(";"):
|
||||||
|
# Skip "command" if it's just a blank line
|
||||||
|
if command == "\c\n" or command == "\n":
|
||||||
|
continue
|
||||||
|
db.exec(sql(command.strip))
|
||||||
|
logPrint("Ran SQL command from schema", "Info")
|
||||||
|
db.close()
|
||||||
|
return true
|
||||||
|
|
||||||
|
export createDb, checkUser, createUser
|
|
@ -0,0 +1,55 @@
|
||||||
|
# Handles formatting of text and outputting of log messages
|
||||||
|
|
||||||
|
import tables
|
||||||
|
import strutils
|
||||||
|
import math
|
||||||
|
|
||||||
|
proc colorEsc(color: string): string {.gcsafe.} =
|
||||||
|
# Stored in proc to be gcsafe
|
||||||
|
let colors = {
|
||||||
|
"black": 30,
|
||||||
|
"red": 31,
|
||||||
|
"green": 32,
|
||||||
|
"orange": 33,
|
||||||
|
"blue": 34,
|
||||||
|
"purple": 35,
|
||||||
|
"cyan": 36,
|
||||||
|
"reset": 0
|
||||||
|
}.toTable()
|
||||||
|
|
||||||
|
return "\e[" & intToStr(colors[color]) & "m"
|
||||||
|
|
||||||
|
proc colorize(text: string, color: string): string {.gcsafe.} =
|
||||||
|
return colorEsc(color) & text & colorEsc("reset")
|
||||||
|
|
||||||
|
proc strCenter(text: string, filler: string = "-", length: int = 8): string {.gcsafe.} =
|
||||||
|
# Simply truncate and return if it fills the entire length
|
||||||
|
if text.len() > length:
|
||||||
|
return text[0..length-1]
|
||||||
|
|
||||||
|
# Find remaining characters and how much to pad each side
|
||||||
|
var remainHalf: float = (length - text.len()) / 2
|
||||||
|
var remainLeft: int = remainHalf.floor().int
|
||||||
|
var remainRight: int = remainHalf.ceil().int
|
||||||
|
|
||||||
|
return filler.repeat(remainLeft) & text & filler.repeat(remainRight)
|
||||||
|
|
||||||
|
proc logFormat(text: string, level: string = "undef"): string {.gcsafe.} =
|
||||||
|
# Stored in proc to be gcsafe
|
||||||
|
let colorMapping = {
|
||||||
|
"Main": "green",
|
||||||
|
"Error": "red",
|
||||||
|
"Info": "purple",
|
||||||
|
"Warning": "orange",
|
||||||
|
"Web": "blue",
|
||||||
|
"undef": "cyan"
|
||||||
|
}.toTable()
|
||||||
|
|
||||||
|
var prefix = "[" & colorize(text=strCenter(level), color=colorMapping[level]) & "]"
|
||||||
|
return prefix & " " & text
|
||||||
|
|
||||||
|
proc logPrint(text: string = "Undefined error message", level: string = "undef") {.gcsafe.} =
|
||||||
|
# In separate function in case I need to for example also write it to a file in the future.
|
||||||
|
echo logFormat(text=text, level=level)
|
||||||
|
|
||||||
|
export logPrint
|
|
@ -1,6 +0,0 @@
|
||||||
import nimja/parser
|
|
||||||
|
|
||||||
proc renderTemplate(templateName: static[string]): string =
|
|
||||||
compileTemplateFile(getScriptDir() & "/templates/" & templateName)
|
|
||||||
|
|
||||||
export renderTemplate
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
# View definition where the fuctions for prologue will all be stashed
|
||||||
|
|
||||||
|
import prologue
|
||||||
|
import nimja/parser
|
||||||
|
import formatter
|
||||||
|
|
||||||
|
proc renderTemplate(templateName: static[string]): string =
|
||||||
|
logPrint("Served template " & templateName, "Web")
|
||||||
|
compileTemplateFile(getScriptDir() & "/templates/" & templateName)
|
||||||
|
|
||||||
|
proc getIndex*(ctx: Context) {.async.} =
|
||||||
|
resp renderTemplate(templateName="index.nwt")
|
||||||
|
|
||||||
|
proc getAbout*(ctx: Context) {.async.} =
|
||||||
|
resp renderTemplate(templateName="about.nwt")
|
||||||
|
|
||||||
|
export getIndex, getAbout
|
|
@ -1,15 +1,41 @@
|
||||||
|
# This module mostly handles startup/config checks and then passes itself onwards
|
||||||
|
|
||||||
when isMainModule:
|
when isMainModule:
|
||||||
import tables
|
import tables
|
||||||
import modules/blog
|
import os
|
||||||
import modules/env
|
import modules/[blog, env, db, formatter, auth]
|
||||||
|
|
||||||
|
let dataDir: string = "data"
|
||||||
|
|
||||||
let settings = {
|
let settings = {
|
||||||
"debug": boolEnvOrDefault("DEBUG", false),
|
"debug": boolEnvOrDefault("DEBUG", false),
|
||||||
"logging": boolEnvOrDefault("NIMBLOG_LOG", true)
|
"logging": boolEnvOrDefault("NIMBLOG_LOG", true)
|
||||||
}.toTable()
|
}.toTable()
|
||||||
|
|
||||||
|
if settings["debug"]:
|
||||||
|
logPrint("Debug mode is enabled", "Warning")
|
||||||
|
|
||||||
|
let dbSettings = {
|
||||||
|
"schema_path": "schema.sql"
|
||||||
|
}.toTable()
|
||||||
|
|
||||||
|
logPrint("Starting website", "Main")
|
||||||
|
|
||||||
|
if not dirExists(dataDir):
|
||||||
|
logPrint("Created data directory", "Info")
|
||||||
|
createDir(dataDir)
|
||||||
|
|
||||||
|
if not createDb(dbSettings):
|
||||||
|
logPrint("A database must be created before use", "Error")
|
||||||
|
quit(QuitFailure)
|
||||||
|
|
||||||
|
if not checkUser():
|
||||||
|
logPrint("No user in database", "Warning")
|
||||||
|
if not addUserInteractive():
|
||||||
|
logPrint("An initial user must be created before use", "Error")
|
||||||
|
quit(QuitFailure)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
echo "Starting website"
|
runWebsite(settings=settings)
|
||||||
runWebsite(settings)
|
|
||||||
except:
|
except:
|
||||||
echo "Could not run website"
|
logPrint("Could not run website", "Error")
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
{% extends templates/components/master.nwt %}
|
||||||
|
|
||||||
|
{% block styles %}<link rel="stylesheet" type="text/css" href="/static/css/article.css" />{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article>
|
||||||
|
<p>
|
||||||
|
Hi there! This is my blog and you're currently reading placeholder text which has been left here so I can test formatting a bit.
|
||||||
|
</p>
|
||||||
|
<h1>
|
||||||
|
Hello world
|
||||||
|
</h1>
|
||||||
|
<p>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
|
@ -1,8 +1,19 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Example</title>
|
<title>Example</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/css/base.css" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/css/master.css" />
|
||||||
|
{% block styles %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{% block content %}{% endblock %}
|
<div id="header-block">
|
||||||
|
<h1> Example </h1>
|
||||||
|
</div>
|
||||||
|
<div id="site-block">
|
||||||
|
<div id="content-block">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
article {
|
||||||
|
font-size: 12pt;
|
||||||
|
line-height: 1.20;
|
||||||
|
}
|
||||||
|
|
||||||
|
article p {
|
||||||
|
margin-top: calc(var(--font-margin) / 5);
|
||||||
|
margin-bottom: var(--font-margin);
|
||||||
|
}
|
||||||
|
|
||||||
|
article h1 {
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 175%;
|
||||||
|
margin-top: calc(var(--font-margin) * 1.75);
|
||||||
|
margin-bottom: calc(var(--font-margin) / 5);
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
div, body {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body, html, div, p, h1, h2, h3, h4, h5 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--site-width: 960px;
|
||||||
|
--site-font-size: 13pt;
|
||||||
|
|
||||||
|
--bgcolor-1: rgb(195,180,180);
|
||||||
|
--bgcolor-2: rgb(225,220,220);
|
||||||
|
--bgcolor-3: rgb(250,250,250);
|
||||||
|
|
||||||
|
--fgcolor-1: rgb(0,0,0);
|
||||||
|
--fgcolor-2: rgb(245,245,245);
|
||||||
|
|
||||||
|
--highlight-1: rgb(195,55,55);
|
||||||
|
--highlight-2: rgb(175,25,5);
|
||||||
|
|
||||||
|
--font-margin: 12px;
|
||||||
|
|
||||||
|
--header-height: 50px;
|
||||||
|
--header-half-height: calc(var(--header-height) / 2);
|
||||||
|
|
||||||
|
--shadow-size: 8px;
|
||||||
|
--shadow-color: rgba(0,0,0,0.35);
|
||||||
|
|
||||||
|
--border-size: 2px;
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
body {
|
||||||
|
background-color: var(--bgcolor-2);
|
||||||
|
color: var(--fgcolor-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#header-block {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
width: 100vw;
|
||||||
|
color: var(--fgcolor-2);
|
||||||
|
height: var(--header-height);
|
||||||
|
padding: 5px;
|
||||||
|
padding-top: var(--header-half-height);
|
||||||
|
background-color: var(--highlight-1);
|
||||||
|
border-bottom: solid var(--border-size) var(--highlight-2);
|
||||||
|
margin-bottom: translateY(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#header-block h1 {
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#site-block {
|
||||||
|
display: block;
|
||||||
|
padding-top: calc(var(--header-height) - var(--border-size));
|
||||||
|
}
|
||||||
|
|
||||||
|
#content-block {
|
||||||
|
display: block;
|
||||||
|
max-width: var(--site-width);
|
||||||
|
width: 100vw;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: var(--bgcolor-3);
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
border: solid var(--border-size) var(--bgcolor-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 960px) {
|
||||||
|
#content-block {
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue