first commit
This commit is contained in:
commit
382b315e96
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
main
|
||||
data/website.db
|
98
code/database_utils.nim
Normal file
98
code/database_utils.nim
Normal 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
34
code/password_utils.nim
Normal 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
12
config/config.cfg
Normal 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
227
main.nim
Normal 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
316
public/index.html
Executable file
File diff suppressed because one or more lines are too long
72
public/logo.html
Executable file
72
public/logo.html
Executable 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
BIN
public/logo.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
6
public/script.js
Executable file
6
public/script.js
Executable 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
253
public/styles.css
Executable 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
43
tmpl/user.tmpl
Normal 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
344
tmpl/website.tmpl
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user