# --==--==--==--==--==--==--==--==--==--==--==--==--==--==--==-- # # Bruno Charest # 2022-08-09 # # __ DESCRIPTIONS __ # main : stating point of the program # # Project that create a web interface for Joplin base on # Joplin Terminal running in background # # Inspiration of : https://ttj.dk/blog/2019/01/20/setup-a-website-with-nim # Copyright 2019 - Thomas T. Jarløv # [LINK] debug url : http://127.0.0.1:7000/ # --==--==--==--==--==--==--==--==--==--==--==--==--==--==--==-- # # --==--==--==--==--==--==--==--==--==--==-- # # Import section # --==--==--==--==--==--==--==--==--==--==-- # import os # Used to get arguments import uri # We need to encode urls: encodeUrl() import times # Time and date import jester # Our webserver import logging # Logging utils import strutils # Basic functions import parsecfg # Parse CFG (config) files import std/json # json manipulation import db_sqlite # SQLite import nim-lib/outils/db/database_utils # Utils used in the database import nim-lib/outils/commun/password_utils # Our file with password utils import nim-lib/outils/communication/joplin/joplin_utils # Joplin utils procedures and types import nim-lib/outils/web/web_utils # Web utils procedures and types # --==--==--==--==--==--==--==--==--==--==-- # # First we'll load config files # --==--==--==--==--==--==--==--==--==--==-- # let dict = loadConfig("config/config.cfg") # --==--==--==--==--==--==--==--==--==--==-- # # Get parameters from config.cfg # --==--==--==--==--==--==--==--==--==--==-- # # Database 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") # Website let mainURL = dict.getSectionValue("Server", "url") let mainPort = parseInt dict.getSectionValue("Server", "port") #let mainWebsite = dict.getSectionValue("Server", "website") # Joplin let joplin_token = dict.getSectionValue("Joplin", "token") # --==--==--==--==--==--==--==--==--==--==-- # # 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 notification*: int req*: Request # --==--==--==--==--==--==--==--==--==--==-- # # proc init : initialisation variables # --==--==--==--==--==--==--==--==--==--==-- # proc init(c: var TData, cld: var ColomnLeftData, crd: var ColomnRightData) = ## Empty out user session data c.userpass = "" c.username = "" c.userid = "" c.loggedIn = false c.notification = 0 # --==--==--==--==--==--==--==--==--==--==-- # # function : loggedIn # --==--==--==--==--==--==--==--==--==--==-- # func loggedIn(c: TData): bool = ## Check if user is logged in by verifying that c.username exists c.username.len > 0 # --==--==--==--==--==--==--==--==--==--==-- # # proc checkLoggedIn # --==--==--==--==--==--==--==--==--==--==-- # 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 user # --==--==--==--==--==--==--==--==--==--==-- # 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 user # --==--==--==--==--==--==--==--==--==--==-- # 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 # Assign the cld et crd to ColomnData var cld {.inject.}: ColomnLeftData var crd {.inject.}: ColomnRightData # Assign the dashData to DashbordData # var dashData {.inject.}: DashbordData # New instance of c new(c) new(cld) new(crd) # new(dashData) # Set standard values init(c, cld, crd) # Get users request c.req = request cld.req = request crd.req = request # dashData.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) # Read Dashbord file # dashData = getDashbordData() # 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.nim" include "tmpl/dashboard.nim" include "tmpl/website.nim" # Tests pages include # ---------------------------- # include "tmpl/tests/test_homepage.nim" include "tmpl/tests/test_ping.nim" include "tmpl/tests/test_notebooks.nim" include "tmpl/tests/test_notes.nim" include "tmpl/tests/test_tags.nim" include "tmpl/tests/test_viewtree.nim" include "tmpl/tests/test_bouton.nim" # --==--==--==--==--==--==--==--==--==--==-- # # Setup routes (URL's) # --==--==--==--==--==--==--==--==--==--==-- # routes: # default route # --==--==--==--==--==--==--==--==--==--==-- # get "/": createTFD() resp genMain(c) # master site once login # --==--==--==--==--==--==--==--==--==--==-- # get "/secret": createTFD() echo c.loggedIn var isStart: int = 0 # if joplin_cli_status() == false: # isStart = joplin_cli_start() echo "MESSAGE :", @"msg" echo "NOTE_ID :", @"noteid" let selectedNoteId = @"noteid" var url_note = "/secret?msg=notes" # URL msg and note id if exist noteid if selectedNoteId != "": url_note = "/secret?msg=notes¬eid=" & selectedNoteId if c.loggedIn: # Start joplin terminal cli if stropped if @"msg" == "startStopJoplin": var isStart = joplin_cli_start_stop() redirect(url_note) # if Joplin application work var checkJoplin = waitFor ping_joplin(joplin_token) if checkJoplin.ping_status: cld.j_status = true else: cld.j_status = false # determine the section to uptade if @"msg" == "newNote": cld.option = newNote echo "=> Section newNote" elif @"msg" == "search": cld.option = search echo "=> Section search" elif @"msg" == "shortcuts": cld.option = shortcuts echo "=> Section shortcuts" elif @"msg" == "notebooks": echo "=> Section notebooks" cld.option = notebooks cld.j_notebooks = waitFor get_joplin_notebooks(joplin_token) elif @"msg" == "notes": echo "=> Section notes" cld.option = notes cld.j_notes = waitFor get_joplin_notes(joplin_token) cld.j_notes_nb = cld.j_notes.id.len() if selectedNoteId != "": crd.j_SelectedNote = waitFor get_joplin_note(joplin_token, selectedNoteId) elif @"msg" == "tags": echo "=> Section tags" cld.option = tags cld.j_tags = waitFor get_joplin_tags(joplin_token) elif @"msg" == "dashbord": echo "=> Section dashbord" #url_note = "/secret?msg=dashbord" cld.option = dashbord cld.dashbord = waitFor getDashbordData() # resp test_bouton(cld) #resp dashbord(cld) elif @"msg" == "sendFeedBack": echo "Todo" resp Http200, {"Access-Control-Allow-Origin": "http://127.0.0.1:7000"}, genSecret(c, cld, crd) # Login route # --==--==--==--==--==--==--==--==--==--==-- # get "/login": createTFD() resp Http200, {"Access-Control-Allow-Origin": "http://127.0.0.1:7000"}, genLogin(c, @"msg") # action route during login # --==--==--==--==--==--==--==--==--==--==-- # 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) #jester.setCookie("sid", loginS, daysForward(7), samesite = Lax, secure = true, httpOnly = true) redirect("/secret") else: redirect("/login?msg=" & encodeUrl(loginS)) # Logout route # --==--==--==--==--==--==--==--==--==--==-- # get "/logout": createTFD() logout(c) redirect("/") # start_joplin route # --==--==--==--==--==--==--==--==--==--==-- # # post "/start_joplin": # if joplin_cli_status() == false: # joplin_cli_start() # --==--==--==--==--==--==--==--==--==--==-- # # # ROUTES TESTS SECTION ## # --==--==--==--==--==--==--==--==--==--==-- # # master tests page # --==--==--==--==--==--==--==--==--==--==-- # get "/test": createTFD() resp test_homepage(c) # Test ping joplin - query api validation # --==--==--==--==--==--==--==--==--==--==-- # get "/test_pingjoplin": createTFD() var pingCheck: joplin_ping pingCheck = waitFor ping_joplin(joplin_token) echo pingCheck.ping_status resp test_ping(c, pingCheck) # Test geting list of all notebooks # --==--==--==--==--==--==--==--==--==--==-- # get "/test_notebooks": createTFD() cld.j_notebooks = waitFor get_joplin_notebooks(joplin_token) resp test_notebooks(c, cld) # Test geting list of all notes # --==--==--==--==--==--==--==--==--==--==-- # get "/test_notes": createTFD() cld.j_notes = waitFor get_joplin_notes(joplin_token) resp test_notes(c, cld) # Test geting list of all tags # --==--==--==--==--==--==--==--==--==--==-- # get "/test_tags": createTFD() cld.j_tags = waitFor get_joplin_tags(joplin_token) resp test_tags(c, cld) # Test a viewtree # --==--==--==--==--==--==--==--==--==--==-- # get "/test_viewtree": createTFD() resp test_viewtree(c) # Test a viewtree # --==--==--==--==--==--==--==--==--==--==-- # get "/test_bouton": createTFD() cld.dashbord = waitFor getDashbordData() resp test_bouton(cld) # Test geting all tags as JSON output # --==--==--==--==--==--==--==--==--==--==-- # get "/test_tags_json": createTFD() var tags: JsonNodeObj # http://localhost:41184/notes/77ec1bfbaccd4708a9f649f42896f437&token=e5f6644fbf6a97ddc55648dae72b11caecda6c6642d8ce0d3b20129b89b196385737eb908923542c3343649ebbf865b55bda031ab4c3a16edc7723ef2ad77d8f # http://localhost:41184/notes/77ec1bfbaccd4708a9f649f42896f437?fields=id,parent_id,title,body,created_time,updated_time,is_conflict,latitude,longitude,altitude,author,source_url,is_todo,todo_due,todo_completed,source,source_application,application_data,order,user_created_time,user_updated_time,encryption_cipher_text,encryption_applied,markup_language,is_shared,share_id,conflict_original_id,master_key_id&token=e5f6644fbf6a97ddc55648dae72b11caecda6c6642d8ce0d3b20129b89b196385737eb908923542c3343649ebbf865b55bda031ab4c3a16edc7723ef2ad77d8f # # ## # # END TESTS SECTION ## # # ##