initial commit

This commit is contained in:
itwrx
2025-05-15 08:01:35 -05:00
commit 74e9056c49
68 changed files with 2942 additions and 0 deletions

148
helpers/auth.nim Normal file
View File

@@ -0,0 +1,148 @@
#[Copyright 2025 ITwrx.
This file is part of Simple Site Manager.
Simple Site Manager is released under the GNU Affero General Public License 3.0.
See COPYING or <https://www.gnu.org/licenses/> for details.]#
import std/[strutils, cgi, strtabs, cookies, sysrand, base64]
import guildenstern/[httpserver], sqliteral
import "../models/session", "db", "global"
#[type
Session* = object
sessionId*, csrfToken*: string
id*, userId*: int]#
type
User* = object
email*, password*: string
id*: int
proc getSessionBySessionId*(sessionId: string): Session =
{.gcsafe.}:
var session: Session
for row in db1.rows(SelectSessionBySessionId, sessionId):
session.id = row.getInt(0)
session.sessionId = row.getString(1)
session.userId = row.getInt(2)
session.csrfToken = row.getString(3)
return session
proc createUserSession*(userSession: Session) =
{.gcsafe.}:
db1.transaction:
discard db1.insert(InsertUserSession, userSession.sessionId, userSession.userId, userSession.csrfToken)
proc deleteSession*(sessionId: string) =
{.gcsafe.}:
db1.transaction:
db1.exec(DeleteSessionBySessionId, sessionId)
#Both form and auth have this template. The form.nim copy is exported.
template formInput(input: string): untyped =
readData(getBody()).getOrDefault(input)
proc getSessionIdFromCookies*(): string =
{.gcsafe.}:
let cookieString = http.headers.getOrDefault("cookie")
let allCookies = parseCookies(cookieString)
if allCookies.hasKey(APP_NAME & "_session"):
return allCookies[APP_NAME & "_session"]
else:
return ""
proc setVisitorCsrfToken*(): string =
{.gcsafe.}:
#create csrf token.
var csrfToken = $urandom(32)
csrfToken = base64.encode(csrfToken)
#delete old VisitorSessions, as they are just the one time use CSRF Tokens.
#may need to be redesigned for better multiuser robustness if it's deleting other users' unused tokens.
#deleteVisitorSessions()
#create session in db.
var visitorSession: Session
visitorSession.csrfToken = csrfToken
discard createVisitorSession(visitorSession)
return csrfToken
proc getUserPageCsrfToken*(): string =
{.gcsafe.}:
var sessionId: string
var userSession: Session
sessionId = getSessionIdFromCookies()
userSession = getSessionBySessionId(sessionId)
return userSession.csrfToken
proc newCsrfToken*(): string =
{.gcsafe.}:
#create csrf token.
var csrfToken = $urandom(32)
csrfToken = base64.encode(csrfToken)
return csrfToken
#[proc fCsrfToken*(): string =
{.gcsafe.}:
let sessionId = getSessionIdFromCookies()
let userSession = getUserSessionBySessionId(sessionId)
result = userSession.csrfToken]#
proc isValidVisitorCsrfToken*(csrfToken: string): bool =
#our previoulsy self-generated, valid csrfToken from the DB.
let visitorSessionCsrfToken = getSessionByCsrfToken(csrfToken).csrfToken
if visitorSessionCsrfToken.len > 0 and csrfToken == visitorSessionCsrfToken:
return true
else:
return false
proc isValidUserCsrfToken*(csrfToken: string): bool =
var sessionId: string
var userSession: Session
sessionId = getSessionIdFromCookies()
userSession = getSessionBySessionId(sessionId)
if userSession.csrfToken == csrfToken:
return true
else:
return false
proc isAuthdAdmin*(): bool =
{.gcsafe.}:
#get sessionId from request's cookie and see if it exists in session DB.
var sessionId: string
sessionId = getSessionIdFromCookies()
if sessionId.len() > 0:
var userSession: Session
try:
userSession = getSessionBySessionId(sessionId)
if userSession.id > 0:
return true
else:
return false
except Exception as e:
echo e.msg
return false
else:
return false
#adds CSRF checking for POST requests.
proc isAuthdAdminPost*(): bool =
{.gcsafe.}:
#get sessionId from request's cookie and see if it exists in session DB.
#var sessionId{.threadvar.}: string
var sessionId: string
sessionId = getSessionIdFromCookies()
if sessionId.len() > 0:
try:
let userSession = getSessionBySessionId(sessionId)
if userSession.id > 0:
return true
else:
return false
except Exception as e:
echo e.msg
return false
else:
return false
template ifAuthAdminPost*(procName: untyped): untyped =
if isAuthdAdminPost() == true:
procName
else:
reply(Http302, [location("/login")])

152
helpers/datetime.nim Normal file
View File

@@ -0,0 +1,152 @@
import std/[times, strutils]
proc weekdayFromString(dayStr: string): Weekday =
## Convert a string representation of a weekday to Weekday enum
case dayStr
of "Monday": return dMon
of "Tuesday": return dTue
of "Wednesday": return dWed
of "Thursday": return dThu
of "Friday": return dFri
of "Saturday": return dSat
of "Sunday": return dSun
proc monthFromString(monthStr: string): Month =
## Convert a string representation of a weekday to Weekday enum
case monthStr
of "January": return mJan
of "Febuary": return mFeb
of "March": return mMar
of "April": return mApr
of "May": return mMay
of "June": return mJun
of "July": return mJul
of "August": return mAug
of "September": return mSep
of "October": return mOct
of "November": return mNov
of "December": return mDec
proc nextWeekday*(targetWeekdayString: string): DateTime =
## Calculates the next occurrence of a specific weekday
var nextDate = now()
# We're not including current day, so we move to the next day
nextDate = nextDate + 1.days
#convert passed weekday string to nim Weekday.
let targetWeekday = weekdayFromString(targetWeekdayString)
# Find the next occurrence of the target weekday
while nextDate.weekday != targetWeekday:
nextDate = nextDate + 1.days
return nextDate
proc nthWeekdayInMonth*(year: int, month: Month, weekdayString: string, nth: range[1..3]): DateTime =
# Start from the first day of the month
#var currentDate = dateTime(year, month, 1, 0, 0, 0, 0)
var currentDate = dateTime(year, month, 1)
let weekday = weekdayFromString(weekdayString)
# Find the first occurrence of the specified weekday
while currentDate.weekday != weekday:
currentDate = currentDate + 1.days
# Move to the nth occurrence
currentDate = currentDate + days((nth - 1) * 7)
return currentDate
proc lastWeekdayInMonth(year: int, month: Month, weekdayString: string): DateTime =
# Create a DateTime for the last day of the given month
#var lastDay = dateTime(year, month, getDaysInMonth(month, year), 0, 0, 0, 0)
var lastDay = dateTime(year, month, getDaysInMonth(month, year))
let weekday = weekdayFromString(weekdayString)
# Work backwards until we find the last occurrence of the specified weekday
while lastDay.weekday != weekday:
lastDay = lastDay - 1.days
return lastDay
proc nextMonthlyOnWeekdayOfWeek*(weekdayString: string, ocurrence: string): DateTime =
var nextSendDate: DateTime
let nextMonthsDate = now() + 1.months
let weekNumStrings = @["1", "2", "3"]
if ocurrence in weekNumStrings:
nextSendDate = nthWeekdayInMonth(year(nextMonthsDate), month(nextMonthsDate), weekdayString, parseInt(ocurrence))
#when ocurrence == "last".
else:
nextSendDate = lastWeekdayInMonth(year(nextMonthsDate), month(nextMonthsDate), weekdayString)
#only use next month if that day has already occured this month, otherwise adjust it for this month instead.
if monthDay(nextSendDate - 1.months) > monthDay(now()):
if ocurrence in weekNumStrings:
nextSendDate = nthWeekdayInMonth(year(nextMonthsDate), month(now()), weekdayString, parseInt(ocurrence))
else:
nextSendDate = lastWeekdayInMonth(year(nextMonthsDate), month(now()), weekdayString)
return nextSendDate
proc nextYearlyOnWeekdayOfWeekOfMonth*(weekdayString, ocurrence, monthString: string): DateTime =
var nextSendDate, startingDate: DateTime
let nextYearsDate = now() + 1.years
#let startingDate = dateTime(year(nextYearsDate), monthFromString(monthString), 01, 0, 0, 0, 0)
startingDate = dateTime(year(nextYearsDate), monthFromString(monthString), 01)
let weekNumStrings = @["1", "2", "3"]
if ocurrence in weekNumStrings:
nextSendDate = nthWeekdayInMonth(year(startingDate), month(startingDate), weekdayString, parseInt(ocurrence))
#when ocurrence == "last".
else:
nextSendDate = lastWeekdayInMonth(year(startingDate), month(startingDate), weekdayString)
#only use next year if that month and day has already occured this year, otherwise adjust it for this year instead.
if nextSendDate - 1.years > now():
startingDate = dateTime(year(now()), monthFromString(monthString), 01)
let weekNumStrings = @["1", "2", "3"]
if ocurrence in weekNumStrings:
nextSendDate = nthWeekdayInMonth(year(startingDate), month(startingDate), weekdayString, parseInt(ocurrence))
#when ocurrence == "last".
else:
nextSendDate = lastWeekdayInMonth(year(startingDate), month(startingDate), weekdayString)
return nextSendDate
#[proc nextYearDate*(monthString: string, day: int): DateTime =
## Aalways returns a date in the next year,
## regardless of whether the target date has passed in the current year
#var nextSendDate = dateTime(year(now()) + 1, monthFromString(monthString), day, 0, 0, 0, 0)
#var nextSendDate = dateTime(year(now()) + 1, monthFromString(monthString), day)
#let nextSendDateString =
#echo nextSendDate
#return parse($nextSendDate, "yyyy-MM-dd")
var
let nextYear = year(now()) + 1
let dt = dateTime(nextYear, monthFromString(monthString), day, 00, 00, 00, 00)
#echo dt
let nextSendDateString = format(dt, "yyyy-MM-dd")
#echo nextSendDateString
#let nextSendDateString = $nextYear & "-" & formattedMonthString & "-" & $day
let nextSendDate = parse(nextSendDateString, "yyyy-MM-dd")
#echo nextSendDateString
return nextSendDate]#
proc nextYearlyDate*(monthString: string, targetDay: int): DateTime =
## Calculates a date for the next occurrence of a specific month and day
##
## Parameters:
## - baseDate: The starting date to calculate from
## - targetMonth: The month (Month enum) for the target date
## - targetDay: The day of month for the target date
##
## Returns the next occurrence of the specified month and day, which could be:
## - Later this year if the target date hasn't occurred yet
## - Next year if the target date has already passed this year
let baseDate = now()
let targetMonth = monthFromString(monthString)
# Get the current year
let currentYear = baseDate.year
# Create a DateTime for the target date in the current year
var nextSendDate = dateTime(currentYear, targetMonth, targetDay, 00, 00, 00, 00)
# If the target date has already passed this year, move to next year
if nextSendDate <= baseDate:
nextSendDate = dateTime(currentYear + 1, targetMonth, targetDay, 00, 00, 00, 00)
let nextSendDateString = format(nextSendDate, "yyyy-MM-dd")
#echo nextSendDateString
#let nextSendDateString = $nextYear & "-" & formattedMonthString & "-" & $day
nextSendDate = parse(nextSendDateString, "yyyy-MM-dd")
return nextSendDate

35
helpers/db.nim Normal file
View File

@@ -0,0 +1,35 @@
#[Copyright 2024 ITwrx.
This file is part of Simple Site Manager.
Simple Site Manager is released under the GNU Affero General Public License 3.0.
See COPYING or <https://www.gnu.org/licenses/> for details.]#
import sqliteral
#### Db1 ###
const RemindersSchema* = "CREATE TABLE IF NOT EXISTS Reminders(id INTEGER PRIMARY KEY, title TEXT, message TEXT, notify_via TEXT, repeats INTEGER, repeat_freq TEXT, weekly_on TEXT, monthly_on_day INTEGER, monthly_on_weekday TEXT, monthly_on_week TEXT, yearly_on_month TEXT, yearly_on_day INTEGER, yearly_on_week TEXT, yearly_on_weekday TEXT, yearly_on_month2 TEXT, send_date TEXT, send_time_hr INTEGER, send_time_min INTEGER, send_time_am_pm TEXT)"
const UsersSchema* = "CREATE TABLE IF NOT EXISTS Users(id INTEGER PRIMARY KEY, email TEXT NOT NULL, password TEXT NOT NULL)"
const SessionsSchema* = "CREATE TABLE IF NOT EXISTS Sessions(id INTEGER PRIMARY KEY, session_id TEXT, user_id INTEGER, csrf_token TEXT NOT NULL)"
type
Db1Sql* = enum
#Reminders
SelectAllReminders = "SELECT * FROM Reminders"
InsertReminder = """INSERT INTO Reminders (title, message, notify_via, repeats, repeat_freq, weekly_on, monthly_on_day, monthly_on_weekday, monthly_on_week, yearly_on_month, yearly_on_day, yearly_on_week, yearly_on_weekday, yearly_on_month2, send_date, send_time_hr, send_time_min, send_time_am_pm) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"""
UpdateReminder = """UPDATE Reminders SET title = ?, message = ?, notify_via = ?, repeats = ?, repeat_freq = ?, weekly_on = ?, monthly_on_day = ?, monthly_on_weekday = ?, monthly_on_week = ?, yearly_on_month = ?, yearly_on_day = ?, yearly_on_week = ?, yearly_on_weekday = ?, yearly_on_month2 = ?, send_date = ?, send_time_hr = ?, send_time_min = ?, send_time_am_pm = ? WHERE id = ?"""
UpdateReminderSendDate = """UPDATE Reminders SET send_date = ? WHERE id = ?"""
DeleteReminder = """DELETE FROM Reminders WHERE id = ?"""
#Users
SelectUsersByEmail = """SELECT * FROM Users WHERE email = ?"""
SelectUserById = """SELECT * FROM Users WHERE id = ? LIMIT 1"""
InsertUser = """INSERT INTO Users (email, password) VALUES (?,?)"""
#Sessions
SelectSessions = "SELECT * FROM Sessions"
SelectSessionBySessionId = "SELECT * FROM Sessions WHERE session_id = ?"
InsertUserSession = """INSERT INTO Sessions (session_id, user_id, csrf_token) VALUES (?,?,?)"""
DeleteSessionBySessionId = """DELETE FROM Sessions WHERE session_id = ?"""
DeleteSessions = """DELETE FROM Sessions"""
SelectSessionByCsrfToken = """SELECT * FROM Sessions WHERE csrf_token = ?"""
InsertVisitorSession = """INSERT INTO Sessions (csrf_token) VALUES (?)"""
var
db1*: SQLiteral

211
helpers/form.nim Normal file
View File

@@ -0,0 +1,211 @@
#[Copyright 2025 ITwrx.
This file is part of Simple Site Manager.
Simple Site Manager is released under the GNU Affero General Public License 3.0.
See COPYING or <https://www.gnu.org/licenses/> for details.]#
import std/[cgi, strtabs, strutils, cookies, uri, tables]
import jsony, guildenstern/httpserver, sqliteral
import "auth", "global"
type
FormError* = object
fieldName*, fieldMessage*: string
type
FormOldInput* = object
fieldName*, fieldOldInput*: string
type
FormResult* = object
id*: int
message*, messageClass*, errors*, oldInputs*: string
var formError*: FormError
var formErrors*: seq[FormError]
var formOldInput*: FormOldInput
var formOldInputs*: seq[FormOldInput]
var formResult*: FormResult
#might still be in use by cookie FR type. too lazy to investigate now.
proc clearFormResult*() =
formResult = FormResult(message : "",
messageClass : "",
errors : "",
oldInputs : "")
proc assignErrorFR*(formErrors: seq[FormError], formOldInputs: seq[FormOldInput]): FormResult =
{.gcsafe.}:
formResult.message = "Error(s) encountered while validating form input(s)."
formResult.messageClass = "form-error"
formResult.errors = toJson(formErrors)
formResult.oldInputs = toJson(formOldInputs)
proc assignGeneralErrorFR*(errorMsg: string): FormResult =
{.gcsafe.}:
formResult.message = errorMsg
formResult.messageClass = "form-error"
proc assignGeneralSuccessFR*(successMsg: string): FormResult =
{.gcsafe.}:
formResult.message = successMsg
formResult.messageClass = "form-success"
proc assignLoginSuccessFR*(): FormResult =
{.gcsafe.}:
formResult.message = "You have logged in successfully."
formResult.messageClass = "form-success"
proc assignCECreateSuccessFR*(): FormResult =
{.gcsafe.}:
formResult.message = "Content Entity successfully created."
formResult.messageClass = "form-success"
proc assignCEEditSuccessFR*(): FormResult =
{.gcsafe.}:
formResult.message = "Content Entity successfully edited."
formResult.messageClass = "form-success"
proc assignCEDeleteSuccessFR*(): FormResult =
{.gcsafe.}:
formResult.message = "Content Entity successfully deleted."
formResult.messageClass = "form-success"
#using cookie to store login page formResult, because there's no session to use as formResult id yet.
proc getCookieFormResult*(): FormResult =
let cookieString = http.headers.getOrDefault("cookie")
let cookiesTable = parseCookies(cookieString)
var frJsonString: string
if cookiesTable.hasKey("form_result"):
frJsonString = cookiesTable["form_result"]
var formResult: FormResult
if frJsonString.len > 0:
formResult = frJsonString.fromJson(FormResult)
formErrors = @[]
clearFormResult()
if not formResult.message.len > 0:
formResult.id = 0
formResult.message = ""
formResult.messageClass = ""
formResult.errors = ""
formResult.oldInputs = ""
return formResult
#using strtabs for storing all other formResults.
proc getFormResult*(): FormResult =
let sessionId = getSessionIdFromCookies()
var frJsonString, strTabSessionId: string
if frStrTab.hasKey("sessionId"):
strTabSessionId = frStrTab["sessionId"]
if strTabSessionId == sessionId:
if frStrTab.hasKey("frJson"):
frJsonString = frStrTab["frJson"]
var newFormResult: FormResult
if frJsonString.len > 0:
newFormResult = frJsonString.fromJson(FormResult)
#reset formErrors seq variable used in form handlers.
formErrors = @[]
#clear the existing formResult so it won't show on next page's GET (without new POST).
clear(frStrTab, modeCaseSensitive)
if not newFormResult.message.len > 0:
newFormResult.id = 0
newFormResult.message = ""
newFormResult.messageClass = ""
newFormResult.errors = ""
newFormResult.oldInputs = ""
return newFormResult
proc setFR*() =
let sessionId = getSessionIdFromCookies()
let frJson = formResult.toJson()
frStrTab = {"sessionId": sessionId, "frJson": frJson}.newStringTable
proc formInput*(input: string): string =
{.gcsafe.}:
if server.contenttype == Compact:
#readData:cgi, getBody:Guildenstern, getOrDefault:strtabs.
return readData(getBody()).getOrDefault(input)
else:
#getMPStringInput(input: string)
echo "not url-encoded"
proc formInputAll*(): Table[string, string] =
{.gcsafe.}:
if server.contenttype == Compact:
#getBody:Guildenstern
let formDataStr = getBody()
var formData = initTable[string, string]()
# Use decodeQuery from the uri module
for (key, value) in decodeQuery(formDataStr):
formData[key] = value
return formData
else:
echo "not url-encoded"
proc formInputInt*(input: string): int =
{.gcsafe.}:
let readInput = readData(getBody()).getOrDefault(input)
return parseIntIf(readInput)
template formInputSeq*(input: string): seq[string] =
{.gcsafe.}:
readData(getBody()).getOrDefault(input)
proc addFormError*(inputName: string, msgString: string) =
{.gcsafe.}:
formError.fieldName = inputName
formError.fieldMessage = msgString
formErrors.add(formError)
proc addFormOldInput*(inputName: string, inputData: string) =
{.gcsafe.}:
formOldInput.fieldName = inputName
formOldInput.fieldOldInput = inputData
formOldInputs.add(formOldInput)
#takes a fieldName and returns the fieldMessage.
proc fFieldMsg*(fr: FormResult, fieldName: string): string =
{.gcsafe.}:
if fr.errors.len() > 0:
let errors = fromJson(fr.errors, seq[FormError])
for error in errors:
if error.fieldName == fieldName:
return error.fieldMessage
#this may need to handle more than one message per form field at some point, but not for this app (yet).
proc fErrorMsg*(fr: FormResult, fieldName:string): string =
{.gcsafe.}:
if fFieldMsg(fr, fieldName).len > 0:
return """<span class="text-red-500">""" & fFieldMsg(fr, fieldName) & "</span><br>"
#takes a fieldName and returns the fieldOldInput.
proc fOldInput*(fr: FormResult, fieldName: string, defaultValue = ""): string =
{.gcsafe.}:
var fieldOldInput: string
if fr.oldInputs.len() > 0:
let oldInputs = fromJson(fr.oldInputs, seq[FormOldInput])
for oldInput in oldInputs:
if oldInput.fieldName == fieldName:
fieldOldInput = oldInput.fieldOldInput
return fieldOldInput
else:
return defaultValue
proc getOldInputJson*(): string =
{.gcsafe.}:
let postData = readData(getBody())
#pairs is a strtabs iterator.
for key,value in pairs(postData):
if key.len() > 0:
if key != "password" and key != "csrf_token":
addFormOldInput(key, value)
return toJson(formOldInputs)
#the empty string defaultValue makes it possible to optionally supply a value from the DB: as needed in edit forms.
#checked proc works on radios and checboxes.
proc checked*(fr: FormResult, groupName: string, targetValue: string, defaultValue = ""): string =
{.gcsafe.}:
if fOldInput(fr, groupName, defaultValue) == targetValue:
return "checked"
proc selected*(fr: FormResult, selectName: string, targetValue: string, defaultValue = ""): string =
{.gcsafe.}:
if fOldInput(fr, selectName, defaultValue) == targetValue:
return "selected='selected'"

90
helpers/global.nim Normal file
View File

@@ -0,0 +1,90 @@
#import std/[times, logging]
import std/[times, strutils, re, uri, paths, random, strtabs]
#universal
const APP_PATH* = "/var/www/forget-me-not-gs"
const ASSETS_PATH* = "/var/www/forget-me-not-gs/app/assets"
const APP_NAME* = "Forget-Me-Not"
const APP_MODE* = "dev"
#dev
const APP_URL* = "http://fmn-gs"
#const SITE_URL* = "http://"
const ASSETS_URL* = "http://assets.fmn-gs"
#prod
#const APP_URL* = "https://ssm.itwrx.org"
#const SITE_URL* = "https://itwrx.org"
#const ASSETS_URL* = "https://assets.itwrx.org"
var frStrTab* = newStringTable()
#Guildensterns logger is conflicting with my, evidently incorrect, usage of the std lib logger so i'll just write some lines to a file for now.
#var logger* = newFileLogger("errors.log")
let dt = now()
let nowDT* = dt.format("M-d-YYYY h:mm:ss tt")
proc writeLogLine*(errorMsg: string) =
{.gcsafe.}:
let logFile = open("errors.log", fmAppend)
defer: logFile.close()
logFile.writeLine(errorMsg)
#template location*(slug: string, csrfToken: string, fr: FormResult): untyped =
template location*(slug: string): untyped =
"location: " & APP_URL & slug
template locationBack*(): string =
"location: " & http.headers.getOrDefault("referer")
template locationOrigin*(origin: string): untyped =
"location: " & origin
proc filenameToSentence*(filename: string): string =
#remove file extension.
let filePath = Path filename
let filePathEnum = splitFile(filePath)
var name = filePathEnum[1].string
#replace dashes and underscores with spaces.
name = name.replace(re"_", " ")
name = name.replace(re"-", " ")
#strip numbers.
name = name.replace(re"[0-9]", "")
return name
proc titleToSlug*(title: string): string =
#replace one or more spaces with dash
var dataString = title.replace(re" +", "-")
#replace anything that is not a letter, number or underscore with nothing.
dataString = dataString.replace(re"[^a-zA-Z0-9-]", "")
#convert to all lowercase.
dataString = dataString.toLowerAscii()
return dataString
proc getIdFromURI*(uri: string): int =
#let parsedUri = parseUri(uri)
let pathSeq = parseUri(uri).path.split('/')
result = strutils.parseInt(pathSeq[2])
proc parseIntIf*(input: string): int =
if input.len > 0:
return parseInt(input)
else:
return 0
proc parseFloatIf*(input: string): float =
if input.len > 0:
return parseFloat(input)
else:
return 0.0
#not cryptographically secure.
proc rndStr20*(): string =
for _ in 0..20:
add(result, char(rand(int('A') .. int('z'))))
proc boolToInt*(myBool: bool): int =
if myBool == true:
return 1
else:
return 0

90
helpers/reminder.nim Normal file
View File

@@ -0,0 +1,90 @@
import std/[times, osproc, strutils], smtp
import ../models/reminder, ../models/user, datetime
proc setFutureSendDate(reminderId: int) =
var reminder: Reminder
reminder = getReminderById(reminderId)
var newSendDate: DateTime
case reminder.repeatFreq:
of "day":
newSendDate = now() + 1.days
reminder.sendDate = $format(newSendDate, "yyyy-MM-dd")
updateReminderSendDate(reminder)
of "week":
newSendDate = nextWeekday(reminder.weeklyOn)
reminder.sendDate = $format(newSendDate, "yyyy-MM-dd")
updateReminderSendDate(reminder)
of "month":
if reminder.monthlyOnDay > 0:
#create DateTime with current month and year and reminder.monthlyOnDay
newSendDate = dateTime(year(now()), month(now()), reminder.monthlyOnDay)
#add 1 month only if monthlyOnDay hasn't occured in current month yet.
if reminder.monthlyOnDay < monthDay(now()):
newSendDate = newSendDate + 1.months
reminder.sendDate = $format(newSendDate, "yyyy-MM-dd")
updateReminderSendDate(reminder)
else:
#monthly on week number and weekday. e.g. "third thursday of every month".
newSendDate = nextMonthlyOnWeekdayOfWeek($reminder.monthlyOnWeekday, $reminder.monthlyOnWeek)
reminder.sendDate = $format(newSendDate, "yyyy-MM-dd")
updateReminderSendDate(reminder)
of "year":
#yearly on month and day.
#string zeros (db artifacts) have a length of 1...
if reminder.yearlyOnMonth.len() > 1:
newSendDate = nextYearlyDate(reminder.yearlyOnMonth, reminder.yearlyOnDay)
reminder.sendDate = $format(newSendDate, "yyyy-MM-dd")
updateReminderSendDate(reminder)
#yearly on weekday of week of month. e.g. third thursday of november of each year.
else:
newSendDate = nextYearlyOnWeekdayOfWeekOfMonth($reminder.yearlyOnWeekday, $reminder.yearlyOnWeek, $reminder.yearlyOnMonth2)
reminder.sendDate = $format(newSendDate, "yyyy-MM-dd")
updateReminderSendDate(reminder)
else:
echo "Invalid value for reminder.repeatFreq in setFutureSendDate()"
clearAllReminders()
proc sendEmail(reminderMsg: string) =
let userEmailAddress = getEmailByUserId("1")
let headers = @[("From", "mailer@itwrx.org")]
let msg = createMessage("Reminder from Forget-Me-Not", reminderMsg, @[userEmailAddress], mCc = @[""], otherHeaders = headers)
{.cast(raises: []).}:
let smtpConn = newSmtp(debug=false)
smtpConn.connect("email.itwrx.org", Port 587)
smtpConn.startTls()
smtpConn.auth("mailer@itwrx.org", ".AQ8u((xB(AgZh^a`jEJ~W~{0Eq?fd$")
#loop through email messages and send to reuse connection?
smtpConn.sendmail("mailer@itwrx.org", @[userEmailAddress], $msg)
#manually closing not necessary?
smtpConn.close()
proc sendXMPP(reminderMsg: string) =
var output: string
var status: int
(output, status) = execCmdEx("xmppc -m message chat itwrx@sec-chat.itwrx.org \"" & reminderMsg & "\"")
if status != 0:
#log this later instead.
echo output
echo "error sending XMPP msg"
#send reminders that haven't been sent yet and set new sendDate (if repeating).
proc sendReminders*() =
let nowDT = now()
let reminders = getAllReminders()
for reminder in reminders:
let sendDateDTString = $reminder.sendDate & " " & $reminder.sendTimeHr & ":" & $reminder.sendTimeMin & ":" & $reminder.sendTimeAmPm
#single digits for minutes, as db send_time_min is integer and won't use "00", which results in runtime parse error.
let sendDateDT = parse(sendDateDTString, "yyyy-M-d h:m:tt")
#if sendDate is not in the future, it hasn't been sent yet (we are changing sendDate to future date during each send) and needs to be sent.
if sendDateDT <= nowDT:
#send reminder notification
case reminder.notifyVia:
of "email":
sendEmail(reminder.message)
of "xmpp":
sendXMPP(reminder.message)
else:
sendXMPP(reminder.message)
sendEmail(reminder.message)
if reminder.repeats == 1:
setFutureSendDate(reminder.id)

226
helpers/validation.nim Normal file
View File

@@ -0,0 +1,226 @@
#[Copyright 2024 ITwrx.
This file is part of ITwrxorg-SiteUpdata.
ITwrxorg-SiteUpdata is released under the GNU Affero General Public License 3.0.
See COPYING or <https://www.gnu.org/licenses/> for details.]#
import std/[strutils, re, typetraits, times, tables]
import valido/[email, password]
import "form", "global"
var msgString{.threadvar.}: string
proc vSize(sizeName: string, sizeValue: int, inputName: string, inputData: string, inputType: string) =
case sizeName:
of "min":
#how we determine size depends on input type.
case inputType:
of "string":
if not (inputData.len >= sizeValue):
msgString = inputName & " must have a character count of at least " & $sizeValue & " ."
addFormError(inputName, msgString)
of "integer":
if not (parseIntIf(inputData) >= sizeValue):
msgString = inputName & " must have a value of at least " & $sizeValue & " ."
addFormError(inputName, msgString)
of "float":
if not (parseFloat(inputData) >= sizeValue.float):
msgString = inputName & " must have a value of at least " & $sizeValue & " ."
addFormError(inputName, msgString)
of "max":
#how we determine size depends on input type.
case inputType:
of "string":
if inputData.len > sizeValue:
msgString = inputName & " must have a character count that is no greater than " & $sizeValue & " ."
addFormError(inputName, msgString)
of "integer":
if parseIntIf(inputData) > sizeValue:
msgString = inputName & " must have a value that is no greater than " & $sizeValue & " ."
addFormError(inputName, msgString)
of "float":
if parseFloat(inputData) > sizeValue.float:
msgString = inputName & " must have a value that is no greater than " & $sizeValue & " ."
addFormError(inputName, msgString)
proc vType(inputName: string, inputData: string, validators: seq[string]): string =
var inputType: string
if "integer" in validators:
try:
#don't try to parse empty string as int.
if inputData.len > 0:
discard parseIntIf(inputData)
inputType = "integer"
except:
msgString = inputName & " must be a whole number."
addFormError(inputName, msgString)
elif "float" in validators:
try:
discard parseFloat(inputData)
inputType = "float"
except:
msgString = inputName & " must be a floating point number; a number with a decimal point. i.e. '1.5'"
addFormError(inputName, msgString)
elif "boolean" in validators:
if (inputData == "true") or not (inputData == "false"):
msgString = inputName & " must be interpretable as a boolean. i.e. true, or false."
addFormError(inputName, msgString)
inputType = "boolean"
else:
inputType = "string"
return inputType
proc vRegex(inputName: string, inputData: string, inputType: string, validators: seq[string]) =
#check for regex validators.
for v in validators:
if match(v, re"^(min):([0-9]+)$"):
#get size's value from the string.
let vNameSeq = v.split(':')
let sizeName = vNameSeq[0]
let sizeValue = parseIntIf(vNameSeq[1])
vSize(sizeName, sizeValue, inputName, inputData, inputType)
if match(v, re"^(max):([0-9]+)$"):
let vNameSeq = v.split(':')
let sizeName = vNameSeq[0]
let sizeValue = parseIntIf(vNameSeq[1])
vSize(sizeName, sizeValue, inputName, inputData, inputType)
#ex. matches: "required_without:parent_id"
if match(v, re"^(required_without):([a-z_]+)$"):
let vNameSeq = v.split(':')
#skip 0 index, as that's just the name of the validator.
let conditionField = vNameSeq[1]
var conditionFieldInputData{.threadvar.}: string
conditionFieldInputData = formInput(conditionField)
if conditionFieldInputData.len == 0:
if inputData.len == 0:
msgString = inputName & " is required when " & conditionField & " isn't set."
addFormError(inputName, msgString)
if match(v, re"^(must_unset_with):([a-z_]+)$"):
let vNameSeq = v.split(':')
#skip 0 index, as that's just the name of the validator.
let conditionField = vNameSeq[1]
var conditionFieldInputData: string
conditionFieldInputData = formInput(conditionField)
if conditionFieldInputData.len > 0:
if inputData.len > 0:
msgString = inputName & " must be empty if " & conditionField & " is not."
addFormError(inputName, msgString)
if match(v, re"^(required_with):([a-z_]+)$"):
let vNameSeq = v.split(':')
#skip 0 index, as that's just the name of the validator.
let conditionField = vNameSeq[1]
var conditionFieldInputData: string
conditionFieldInputData = formInput(conditionField)
if conditionFieldInputData.len > 0:
if inputData.len == 0:
msgString = inputName & " is also required when " & conditionField & " is set."
addFormError(inputName, msgString)
if match(v, re"^(required_with):([a-z_]+):(without):([a-z_]+)$"):
let vNameSeq = v.split(':')
#skip 0 index, as that's just the name of the validator.
let conditionField1 = vNameSeq[1]
var conditionField1InputData: string
conditionField1InputData = formInput(conditionField1)
let conditionField2 = vNameSeq[3]
var conditionField2InputData: string
conditionField2InputData = formInput(conditionField2)
if conditionField1InputData.len > 0 and not conditionField2InputData.len > 0:
if inputData.len == 0:
msgString = inputName & " is required when " & conditionField1 & " is set and " & conditionField2 & " isn't set."
addFormError(inputName, msgString)
if match(v, re"^(required_with):([a-z_]+):(without):([a-z_]+):(and_without):([a-z_]+)$"):
let vNameSeq = v.split(':')
#skip 0 index, as that's just the name of the validator.
let conditionField1 = vNameSeq[1]
var conditionField1InputData: string
conditionField1InputData = formInput(conditionField1)
let conditionField2 = vNameSeq[3]
var conditionField2InputData: string
conditionField2InputData = formInput(conditionField2)
let conditionField3 = vNameSeq[5]
var conditionField3InputData: string
conditionField3InputData = formInput(conditionField3)
if conditionField1InputData.len > 0 and not conditionField2InputData.len > 0 and not conditionField3InputData.len > 0:
if inputData.len == 0:
msgString = inputName & " is required when " & conditionField1 & " is set and " & conditionField2 & " and " & conditionField3 & " aren't set."
addFormError(inputName, msgString)
if match(v, re"^(required_when):([a-z_]+):(equals):([a-z_]+)$"):
let vNameSeq = v.split(':')
#skip 0 index, as that's just the name of the validator.
let conditionField1 = vNameSeq[1]
var conditionField1InputData: string
conditionField1InputData = formInput(conditionField1)
let conditionField2 = vNameSeq[3]
if conditionField1InputData.len > 0 and conditionField1InputData == conditionField2:
if inputData.len == 0:
msgString = inputName & " is required when " & conditionField1 & " equals " & conditionField2
addFormError(inputName, msgString)
if match(v, re"^(required_when):([a-z_]+):(equals):([a-z_]+):(without):([a-z_]+)$"):
let vNameSeq = v.split(':')
#skip 0 index, as that's just the name of the validator.
let conditionField1 = vNameSeq[1]
var conditionField1InputData: string
conditionField1InputData = formInput(conditionField1)
let conditionField2 = vNameSeq[3]
let conditionField3 = vNameSeq[5]
var conditionField3InputData: string
conditionField3InputData = formInput(conditionField3)
if conditionField1InputData.len > 0 and conditionField1InputData == conditionField2 and conditionField3InputData.len == 0:
if inputData.len == 0:
msgString = inputName & " is required when " & conditionField1 & " equals " & conditionField2 & " and " & conditionField3 & " is not set."
addFormError(inputName, msgString)
proc vStandard(inputName: string, inputData: string, inputType: string, validators: seq[string]) =
if "email" in validators:
if not isEmail(inputData):
msgString = inputName & " is not recognized as a valid email address."
addFormError(inputName, msgString)
if "min_complexity" in validators:
if not isStrongPassword(inputData):
msgString = inputName & " is not random/complex enough. Try to make your " & inputName & " more unpredictable by adding random numbers, random letter casing, unrelated words, special characters, etc."
addFormError(inputName, msgString)
proc vHardcoded*(inputDataAll: Table[string, string], validators: seq[string]) =
##Warning: this validator requires specific (hardcoded) input names to exist in the posted form data.
if "future_datetime" in validators:
let nowDT = now()
var inputDT: DateTime
let inputDateString = inputDataAll.getOrDefault("send_date", "")
let inputTimeHrString = inputDataAll.getOrDefault("send_time_hr", "")
let inputTimeMinString = inputDataAll.getOrDefault("send_time_min", "")
let inputTimeAmPmString = inputDataAll.getOrDefault("send_time_am_pm", "")
if inputDateString.len > 0 and inputTimeHrString.len > 0 and inputTimeMinString.len > 0 and inputTimeAmPmString.len > 0:
let inputDatetimeString = inputDateString & " " & inputTimeHrString & ":" & inputTimeMinString & ":" & inputTimeAmPmString
inputDT = parse(inputDatetimeString, "yyyy-M-d h:m:tt")
else:
inputDT = now()
if inputDT <= nowDT:
msgString = "send_date and send_time combined must be a DateTime in the future (compared to the DateTime at form submit)."
addFormError("send_date", msgString)
proc vInput*(inputName: string, validators: seq[string]) =
var inputType: string
var inputData: string
var inputDataAll: Table[string, string]
inputData = formInput(inputName)
inputDataAll = formInputAll()
#check if 'required' is set and validate input if it exists. otherwise return error.
if "required" in validators:
if (inputData.len == 0):
msgString = inputName & " is required."
addFormError(inputName, msgString)
else:
#proceed with validation.
#get inputType.
inputType = vType(inputName, inputData, validators)
#check for other validators and run if they exist.
vStandard(inputName, inputData, inputType, validators)
vHardcoded(inputDataAll, validators)
vRegex(inputName, inputData, inputType, validators)
#required is not set, but input could still exist. validate it.
else:
#get inputType.
inputType = vType(inputName, inputData, validators)
#check for other validators and run if they exist.
vStandard(inputName, inputData, inputType, validators)
vHardcoded(inputDataAll, validators)
vRegex(inputName, inputData, inputType, validators)