From 382b315e9660723571394cbc71746d9bb31820ed Mon Sep 17 00:00:00 2001 From: bruno Date: Sun, 31 Jul 2022 16:45:47 -0400 Subject: [PATCH] first commit --- .gitignore | 2 + README.md | 0 code/database_utils.nim | 98 ++++++++++++ code/password_utils.nim | 34 ++++ config/config.cfg | 12 ++ main.nim | 227 ++++++++++++++++++++++++++ public/index.html | 316 ++++++++++++++++++++++++++++++++++++ public/logo.html | 72 +++++++++ public/logo.png | Bin 0 -> 32873 bytes public/script.js | 6 + public/styles.css | 253 +++++++++++++++++++++++++++++ tmpl/user.tmpl | 43 +++++ tmpl/website.tmpl | 344 ++++++++++++++++++++++++++++++++++++++++ 13 files changed, 1407 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 code/database_utils.nim create mode 100644 code/password_utils.nim create mode 100644 config/config.cfg create mode 100644 main.nim create mode 100755 public/index.html create mode 100755 public/logo.html create mode 100755 public/logo.png create mode 100755 public/script.js create mode 100755 public/styles.css create mode 100644 tmpl/user.tmpl create mode 100644 tmpl/website.tmpl diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8f748a6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +main +data/website.db \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/code/database_utils.nim b/code/database_utils.nim new file mode 100644 index 0000000..cb55a59 --- /dev/null +++ b/code/database_utils.nim @@ -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.") \ No newline at end of file diff --git a/code/password_utils.nim b/code/password_utils.nim new file mode 100644 index 0000000..1ce6e5b --- /dev/null +++ b/code/password_utils.nim @@ -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) \ No newline at end of file diff --git a/config/config.cfg b/config/config.cfg new file mode 100644 index 0000000..760179e --- /dev/null +++ b/config/config.cfg @@ -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" \ No newline at end of file diff --git a/main.nim b/main.nim new file mode 100644 index 0000000..3ac8ff2 --- /dev/null +++ b/main.nim @@ -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("/") diff --git a/public/index.html b/public/index.html new file mode 100755 index 0000000..aa2dcd9 --- /dev/null +++ b/public/index.html @@ -0,0 +1,316 @@ + + + + + + + Document + + + + +
+ +
+
+ +
Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex rem ipsum porro delectus ipsa placeat modi aut non blanditiis, consequatur earum provident atque quaerat. Sint veniam facere minima nostrum exercitationem sit iste placeat iure non officia adipisci porro similique dolorum vero temporibus corrupti, incidunt, fugit recusandae. Eos voluptas ipsam reprehenderit fugit ad obcaecati ipsum ratione officiis ipsa. Voluptas nam modi, commodi rerum repellendus quae amet, illum expedita esse quis officiis impedit cumque deserunt ipsum asperiores corrupti nulla repudiandae atque mollitia soluta sit ea? A ratione voluptatem sapiente excepturi possimus, ad, atque repellat iusto, architecto iste commodi ut? Cupiditate aliquid quae ad quaerat maxime, ut recusandae perferendis tempore itaque reiciendis fugiat, odio ea vero non illo voluptatibus laborum! Dolore molestias ab hic? Dolorem, eveniet dolores non at voluptatum laudantium recusandae sapiente reprehenderit soluta maxime provident vero vel maiores aperiam ullam eum necessitatibus quibusdam? Corrupti necessitatibus corporis et, dolore reiciendis dolorum dolor earum recusandae incidunt optio nostrum accusantium? Dignissimos et modi adipisci illo labore rerum! Sed libero doloremque repellendus quaerat iste veniam, error adipisci sint porro exercitationem obcaecati eum, perspiciatis illo aliquam enim debitis assumenda reiciendis architecto repudiandae sequi velit! Quos ad enim sapiente autem incidunt debitis. Quaerat nam praesentium, beatae iste vel consectetur doloremque laboriosam asperiores hic nobis assumenda laudantium amet illum, repellat facere enim sequi reprehenderit at et! Expedita blanditiis nulla vitae corporis pariatur eius tenetur fuga, natus cumque autem similique soluta voluptate excepturi esse dolores deleniti ducimus illo exercitationem! Suscipit perferendis debitis ad reiciendis nesciunt, maxime magni labore fugiat soluta perspiciatis commodi, veniam illo nihil magnam exercitationem at, itaque consectetur unde modi accusantium similique. Cum fugit maiores enim, tempora culpa aliquam accusamus debitis consequuntur libero, sed voluptate distinctio voluptatum impedit praesentium incidunt? Placeat laboriosam sunt tenetur quod beatae eum unde error eos repellat! Eaque eius rerum porro dolore, cupiditate quisquam vel. Ipsam, quos accusantium incidunt fugit explicabo minus aliquid sed ullam sapiente, quo facilis dolorem excepturi sunt nam corporis, amet perferendis repellat eum reprehenderit! Autem cum alias veritatis consequuntur harum aspernatur culpa, aliquid voluptas natus in repellendus at rem tenetur praesentium dolores, et minima. Doloribus reprehenderit fugiat dolores tempora repudiandae id officiis aperiam, a quaerat alias corrupti praesentium totam in obcaecati aspernatur aliquid odio velit quo necessitatibus, maiores veritatis quidem nisi recusandae! Accusantium non dolorem natus eaque dicta sed impedit esse sit obcaecati laudantium dolore magni, excepturi cum amet perspiciatis! Dolor cumque excepturi voluptas tenetur! Accusamus, officia amet unde eos sit est sapiente neque nobis quos rerum accusantium, debitis quisquam obcaecati? Sit provident sint dolore ipsum voluptas modi aut eligendi quibusdam natus est ex quasi culpa, iste iure enim quisquam vero dicta aspernatur. Placeat harum dolores sapiente porro ut. Iusto, dolores earum sed perferendis, illum aliquam est necessitatibus, nesciunt laborum sunt exercitationem nostrum blanditiis neque suscipit ipsam? Quas ea dicta nihil, natus necessitatibus blanditiis praesentium iure consequuntur voluptatem perferendis, neque iusto commodi officia minima ipsa eligendi tempora laborum tenetur, quaerat atque fugit accusantium minus impedit. Quaerat impedit officiis odit aliquam animi, magni delectus eius sunt fuga. Ipsa doloremque doloribus eveniet iusto velit quod repellat tenetur consequatur aliquid sapiente beatae voluptates quaerat fugit ratione odio sint, adipisci earum obcaecati quisquam. Ratione similique tempora cumque eos ducimus sunt veritatis eius distinctio maxime rerum aut voluptatibus temporibus, eum cum ex neque sint non facere, eligendi officia. Consequuntur reprehenderit, ipsa ex quod necessitatibus ratione nemo illo doloremque saepe perferendis cupiditate praesentium consectetur provident totam inventore exercitationem, dolorem accusantium debitis voluptates amet sunt odit. At reprehenderit illum, sed fuga a cupiditate amet dolorem, non eos nobis eaque recusandae facere saepe esse. Magnam incidunt, eveniet ipsa et, ea assumenda in repudiandae omnis itaque voluptatum quibusdam atque, reprehenderit totam quis saepe quod obcaecati cumque ratione perferendis eaque corporis doloremque! Sed maiores iure aliquam unde similique amet eveniet sapiente cupiditate, deserunt quia repudiandae, voluptates numquam ipsa cumque perspiciatis facilis tempora! Esse consectetur modi libero incidunt tenetur, nisi voluptas quasi ratione eligendi nostrum itaque, excepturi veritatis delectus. Vero dolorum optio accusantium, quaerat aut iure beatae explicabo quas amet dolores cupiditate eveniet magni facere, vel consectetur repudiandae nesciunt voluptates consequatur ad facilis distinctio neque blanditiis illum unde! Est sit consectetur delectus quos labore! Eius expedita hic repellendus. Reprehenderit suscipit fugit cum id vero, consequatur expedita sint quae sit, pariatur aspernatur harum cupiditate debitis tempora magnam quod praesentium autem asperiores. Modi, architecto. Non nostrum sint porro minus itaque neque quas. Minus nihil enim eveniet. Obcaecati error provident id, temporibus molestiae consequuntur cumque odit, consectetur ducimus dolor illum vel illo numquam nam minima quisquam minus debitis eius fuga. Illo culpa id assumenda ipsam? Nemo accusantium quibusdam fugiat adipisci expedita nostrum iste eum quas, animi sequi quaerat sint aspernatur tenetur maiores saepe quidem? Ab reprehenderit cum quae quidem ullam repellendus vel aliquid ipsam veritatis quaerat. Repellendus, recusandae adipisci aperiam est dolorum quos, obcaecati repudiandae optio esse nemo eveniet id accusantium maxime architecto. Praesentium corporis architecto, pariatur fugit quam ducimus exercitationem a sit et cupiditate facere est eum ipsa ut illo. Corrupti eum id quo quisquam. Sequi eaque veniam quam fuga error explicabo, distinctio beatae quasi doloribus consequuntur maxime. Libero inventore labore in obcaecati doloribus voluptas facilis cumque atque voluptatem magnam ex tempore, necessitatibus corporis, velit ut modi eligendi, qui explicabo eaque sit cum illum. Ipsum delectus numquam quisquam adipisci velit ad laboriosam cum ab. Saepe at perspiciatis ratione nostrum ipsam voluptas incidunt inventore libero dolores blanditiis nemo, eos esse earum neque recusandae? Ab quidem temporibus similique commodi asperiores tempore reprehenderit accusamus. Reiciendis suscipit ullam alias sint impedit labore laudantium vel. Ratione, sequi quam? Adipisci quidem doloribus dignissimos omnis ad optio ex, numquam aperiam aut rerum, sequi dolorum natus tempora quis? Assumenda corrupti reprehenderit rerum aut eos numquam optio unde necessitatibus omnis perferendis iusto, perspiciatis quis eaque possimus asperiores veniam placeat temporibus eveniet ratione minima pariatur sequi! Neque doloremque sunt dolor repudiandae, similique asperiores. Totam, ipsum cupiditate! Facere quidem aliquid illum magni deleniti at et necessitatibus tempora. Suscipit iusto, odit alias distinctio aspernatur voluptates, eveniet, ipsam ducimus nam harum ad iste? Molestias odit distinctio aspernatur odio obcaecati perferendis maxime, excepturi et explicabo sed?
+
+ + \ No newline at end of file diff --git a/public/logo.html b/public/logo.html new file mode 100755 index 0000000..025e724 --- /dev/null +++ b/public/logo.html @@ -0,0 +1,72 @@ + + + + + + + + Document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/logo.png b/public/logo.png new file mode 100755 index 0000000000000000000000000000000000000000..14953674013ae954aaf9a377ba97e397953ca9a3 GIT binary patch literal 32873 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 literal 0 HcmV?d00001 diff --git a/public/script.js b/public/script.js new file mode 100755 index 0000000..ec95081 --- /dev/null +++ b/public/script.js @@ -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") +}) diff --git a/public/styles.css b/public/styles.css new file mode 100755 index 0000000..532c197 --- /dev/null +++ b/public/styles.css @@ -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; +} */ diff --git a/tmpl/user.tmpl b/tmpl/user.tmpl new file mode 100644 index 0000000..03d2418 --- /dev/null +++ b/tmpl/user.tmpl @@ -0,0 +1,43 @@ +#? stdtmpl | standard +# +#proc genLogin(c: var TData, errorMsg = ""): string = +# result = "" +# if not c.loggedIn: + +
+ +
+ + #else: +
+ + +
+# end if +#end proc \ No newline at end of file diff --git a/tmpl/website.tmpl b/tmpl/website.tmpl new file mode 100644 index 0000000..d165362 --- /dev/null +++ b/tmpl/website.tmpl @@ -0,0 +1,344 @@ +#? stdtmpl | standard +# +#proc genMain(c: var TData): string = +# result = "" + + + + + + + +#end proc +# +#proc genSecret(c: var TData): string = +# result = "" + + + + + + + Document + + + + +
+ +
+
+ +
Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex rem ipsum porro delectus ipsa placeat modi aut non blanditiis, consequatur earum provident atque quaerat. Sint veniam facere minima nostrum exercitationem sit iste placeat iure non officia adipisci porro similique dolorum vero temporibus corrupti, incidunt, fugit recusandae. Eos voluptas ipsam reprehenderit fugit ad obcaecati ipsum ratione officiis ipsa. Voluptas nam modi, commodi rerum repellendus quae amet, illum expedita esse quis officiis impedit cumque deserunt ipsum asperiores corrupti nulla repudiandae atque mollitia soluta sit ea? A ratione voluptatem sapiente excepturi possimus, ad, atque repellat iusto, architecto iste commodi ut? Cupiditate aliquid quae ad quaerat maxime, ut recusandae perferendis tempore itaque reiciendis fugiat, odio ea vero non illo voluptatibus laborum! Dolore molestias ab hic? Dolorem, eveniet dolores non at voluptatum laudantium recusandae sapiente reprehenderit soluta maxime provident vero vel maiores aperiam ullam eum necessitatibus quibusdam? Corrupti necessitatibus corporis et, dolore reiciendis dolorum dolor earum recusandae incidunt optio nostrum accusantium? Dignissimos et modi adipisci illo labore rerum! Sed libero doloremque repellendus quaerat iste veniam, error adipisci sint porro exercitationem obcaecati eum, perspiciatis illo aliquam enim debitis assumenda reiciendis architecto repudiandae sequi velit! Quos ad enim sapiente autem incidunt debitis. Quaerat nam praesentium, beatae iste vel consectetur doloremque laboriosam asperiores hic nobis assumenda laudantium amet illum, repellat facere enim sequi reprehenderit at et! Expedita blanditiis nulla vitae corporis pariatur eius tenetur fuga, natus cumque autem similique soluta voluptate excepturi esse dolores deleniti ducimus illo exercitationem! Suscipit perferendis debitis ad reiciendis nesciunt, maxime magni labore fugiat soluta perspiciatis commodi, veniam illo nihil magnam exercitationem at, itaque consectetur unde modi accusantium similique. Cum fugit maiores enim, tempora culpa aliquam accusamus debitis consequuntur libero, sed voluptate distinctio voluptatum impedit praesentium incidunt? Placeat laboriosam sunt tenetur quod beatae eum unde error eos repellat! Eaque eius rerum porro dolore, cupiditate quisquam vel. Ipsam, quos accusantium incidunt fugit explicabo minus aliquid sed ullam sapiente, quo facilis dolorem excepturi sunt nam corporis, amet perferendis repellat eum reprehenderit! Autem cum alias veritatis consequuntur harum aspernatur culpa, aliquid voluptas natus in repellendus at rem tenetur praesentium dolores, et minima. Doloribus reprehenderit fugiat dolores tempora repudiandae id officiis aperiam, a quaerat alias corrupti praesentium totam in obcaecati aspernatur aliquid odio velit quo necessitatibus, maiores veritatis quidem nisi recusandae! Accusantium non dolorem natus eaque dicta sed impedit esse sit obcaecati laudantium dolore magni, excepturi cum amet perspiciatis! Dolor cumque excepturi voluptas tenetur! Accusamus, officia amet unde eos sit est sapiente neque nobis quos rerum accusantium, debitis quisquam obcaecati? Sit provident sint dolore ipsum voluptas modi aut eligendi quibusdam natus est ex quasi culpa, iste iure enim quisquam vero dicta aspernatur. Placeat harum dolores sapiente porro ut. Iusto, dolores earum sed perferendis, illum aliquam est necessitatibus, nesciunt laborum sunt exercitationem nostrum blanditiis neque suscipit ipsam? Quas ea dicta nihil, natus necessitatibus blanditiis praesentium iure consequuntur voluptatem perferendis, neque iusto commodi officia minima ipsa eligendi tempora laborum tenetur, quaerat atque fugit accusantium minus impedit. Quaerat impedit officiis odit aliquam animi, magni delectus eius sunt fuga. Ipsa doloremque doloribus eveniet iusto velit quod repellat tenetur consequatur aliquid sapiente beatae voluptates quaerat fugit ratione odio sint, adipisci earum obcaecati quisquam. Ratione similique tempora cumque eos ducimus sunt veritatis eius distinctio maxime rerum aut voluptatibus temporibus, eum cum ex neque sint non facere, eligendi officia. Consequuntur reprehenderit, ipsa ex quod necessitatibus ratione nemo illo doloremque saepe perferendis cupiditate praesentium consectetur provident totam inventore exercitationem, dolorem accusantium debitis voluptates amet sunt odit. At reprehenderit illum, sed fuga a cupiditate amet dolorem, non eos nobis eaque recusandae facere saepe esse. Magnam incidunt, eveniet ipsa et, ea assumenda in repudiandae omnis itaque voluptatum quibusdam atque, reprehenderit totam quis saepe quod obcaecati cumque ratione perferendis eaque corporis doloremque! Sed maiores iure aliquam unde similique amet eveniet sapiente cupiditate, deserunt quia repudiandae, voluptates numquam ipsa cumque perspiciatis facilis tempora! Esse consectetur modi libero incidunt tenetur, nisi voluptas quasi ratione eligendi nostrum itaque, excepturi veritatis delectus. Vero dolorum optio accusantium, quaerat aut iure beatae explicabo quas amet dolores cupiditate eveniet magni facere, vel consectetur repudiandae nesciunt voluptates consequatur ad facilis distinctio neque blanditiis illum unde! Est sit consectetur delectus quos labore! Eius expedita hic repellendus. Reprehenderit suscipit fugit cum id vero, consequatur expedita sint quae sit, pariatur aspernatur harum cupiditate debitis tempora magnam quod praesentium autem asperiores. Modi, architecto. Non nostrum sint porro minus itaque neque quas. Minus nihil enim eveniet. Obcaecati error provident id, temporibus molestiae consequuntur cumque odit, consectetur ducimus dolor illum vel illo numquam nam minima quisquam minus debitis eius fuga. Illo culpa id assumenda ipsam? Nemo accusantium quibusdam fugiat adipisci expedita nostrum iste eum quas, animi sequi quaerat sint aspernatur tenetur maiores saepe quidem? Ab reprehenderit cum quae quidem ullam repellendus vel aliquid ipsam veritatis quaerat. Repellendus, recusandae adipisci aperiam est dolorum quos, obcaecati repudiandae optio esse nemo eveniet id accusantium maxime architecto. Praesentium corporis architecto, pariatur fugit quam ducimus exercitationem a sit et cupiditate facere est eum ipsa ut illo. Corrupti eum id quo quisquam. Sequi eaque veniam quam fuga error explicabo, distinctio beatae quasi doloribus consequuntur maxime. Libero inventore labore in obcaecati doloribus voluptas facilis cumque atque voluptatem magnam ex tempore, necessitatibus corporis, velit ut modi eligendi, qui explicabo eaque sit cum illum. Ipsum delectus numquam quisquam adipisci velit ad laboriosam cum ab. Saepe at perspiciatis ratione nostrum ipsam voluptas incidunt inventore libero dolores blanditiis nemo, eos esse earum neque recusandae? Ab quidem temporibus similique commodi asperiores tempore reprehenderit accusamus. Reiciendis suscipit ullam alias sint impedit labore laudantium vel. Ratione, sequi quam? Adipisci quidem doloribus dignissimos omnis ad optio ex, numquam aperiam aut rerum, sequi dolorum natus tempora quis? Assumenda corrupti reprehenderit rerum aut eos numquam optio unde necessitatibus omnis perferendis iusto, perspiciatis quis eaque possimus asperiores veniam placeat temporibus eveniet ratione minima pariatur sequi! Neque doloremque sunt dolor repudiandae, similique asperiores. Totam, ipsum cupiditate! Facere quidem aliquid illum magni deleniti at et necessitatibus tempora. Suscipit iusto, odit alias distinctio aspernatur voluptates, eveniet, ipsam ducimus nam harum ad iste? Molestias odit distinctio aspernatur odio obcaecati perferendis maxime, excepturi et explicabo sed?
+
+ + +#end proc \ No newline at end of file