summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--README.md45
-rw-r--r--ci/.gitignore2
-rw-r--r--ci/Dockerfile13
-rw-r--r--ci/data/bogenherr-starter.lisp7
-rw-r--r--ci/data/etc/bogenherr/figwheel-main.edn34
-rw-r--r--ci/data/etc/bogenherr/options.lisp15
-rw-r--r--ci/data/etc/sbclrc6
-rwxr-xr-xci/data/install-root.sh30
-rwxr-xr-xci/data/install-user-sbcl.sh10
-rwxr-xr-xci/data/start_app.sh49
-rwxr-xr-xci/docker-build.sh28
-rw-r--r--ci/docker-compose.yaml.example47
-rwxr-xr-xci/docker-run.sh7
-rw-r--r--ci/env.sh4
-rw-r--r--doc/sql/resumes.dot81
-rw-r--r--doc/sql/resumes.pngbin0 -> 53121 bytes
-rw-r--r--lisp/core/core.lisp7
-rw-r--r--lisp/resume.asd73
-rw-r--r--lisp/service/about-us-service.lisp66
-rw-r--r--lisp/service/auth-service.lisp48
-rw-r--r--lisp/service/base-service.lisp21
-rw-r--r--lisp/service/contact-us-service.lisp84
-rw-r--r--lisp/service/generic-form.lisp87
-rw-r--r--lisp/service/generics.lisp15
-rw-r--r--lisp/service/home-service.lisp18
-rw-r--r--lisp/service/login-service.lisp64
-rw-r--r--lisp/service/logout-service.lisp14
-rw-r--r--lisp/service/menu-service.lisp96
-rw-r--r--lisp/service/messages-service.lisp57
-rw-r--r--lisp/service/password-service.lisp50
-rw-r--r--lisp/service/profile-service.lisp61
-rw-r--r--lisp/service/rest-service.lisp41
-rw-r--r--lisp/service/users-service.lisp282
-rw-r--r--lisp/sql/about-us.lisp18
-rw-r--r--lisp/sql/auth-pkg.lisp205
-rw-r--r--lisp/sql/contact-pkg.lisp32
-rw-r--r--lisp/sql/contact-us.lisp50
-rw-r--r--lisp/sql/general-pkg.lisp11
-rw-r--r--lisp/sql/generics.lisp143
-rw-r--r--lisp/sql/registration.lisp42
-rw-r--r--lisp/sql/role-group.lisp24
-rw-r--r--lisp/sql/user-role.lisp36
-rw-r--r--lisp/sql/user-session-pkg.lisp116
-rw-r--r--lisp/sql/user-session.lisp35
-rw-r--r--lisp/sql/user.lisp50
-rw-r--r--lisp/webapps/generics.lisp12
l---------lisp/webapps/resume/cljs-out1
-rw-r--r--lisp/webapps/resume/clojurescript/resume/.gitignore16
-rw-r--r--lisp/webapps/resume/clojurescript/resume/dev.cljs.edn2
-rw-r--r--lisp/webapps/resume/clojurescript/resume/project.clj24
-rw-r--r--lisp/webapps/resume/clojurescript/resume/src/core.cljs1073
-rw-r--r--lisp/webapps/resume/conf/.gitignore1
-rw-r--r--lisp/webapps/resume/conf/options.lisp.example13
-rw-r--r--lisp/webapps/resume/site.lisp206
-rw-r--r--lisp/webapps/resume/static/css/stylesheet.css117
-rw-r--r--lisp/webapps/resume/static/images/add.pngbin0 -> 1832 bytes
-rw-r--r--lisp/webapps/resume/static/images/classical-bow.jpgbin0 -> 158141 bytes
-rw-r--r--lisp/webapps/resume/static/images/delete.pngbin0 -> 1427 bytes
-rw-r--r--lisp/webapps/resume/static/images/document-open.pngbin0 -> 608 bytes
-rw-r--r--lisp/webapps/resume/static/images/down.pngbin0 -> 1297 bytes
-rw-r--r--lisp/webapps/resume/static/images/download.pngbin0 -> 577 bytes
-rw-r--r--lisp/webapps/resume/static/images/edit-redo.pngbin0 -> 1584 bytes
-rw-r--r--lisp/webapps/resume/static/images/edit-undo.pngbin0 -> 1585 bytes
-rw-r--r--lisp/webapps/resume/static/images/edit.pngbin0 -> 1034 bytes
-rw-r--r--lisp/webapps/resume/static/images/emblem-favorite.pngbin0 -> 1523 bytes
-rw-r--r--lisp/webapps/resume/static/images/emblem-nowrite.pngbin0 -> 1032 bytes
-rw-r--r--lisp/webapps/resume/static/images/instrument-cabinet.jpgbin0 -> 7636700 bytes
-rw-r--r--lisp/webapps/resume/static/images/no.pngbin0 -> 1089 bytes
-rw-r--r--lisp/webapps/resume/static/images/sort-alpha.pngbin0 -> 625 bytes
-rw-r--r--lisp/webapps/resume/static/images/up.pngbin0 -> 1315 bytes
-rw-r--r--lisp/webapps/resume/static/images/violin-strip.jpgbin0 -> 198050 bytes
-rw-r--r--lisp/webapps/resume/static/images/yes.pngbin0 -> 1013 bytes
-rw-r--r--lisp/webapps/webapp-loader.lisp177
-rw-r--r--sql/functions/auth.delete_registration.sql14
-rw-r--r--sql/functions/auth.delete_role_groups_for_user.sql16
-rw-r--r--sql/functions/auth.get_active_role_groups_for_user.sql25
-rw-r--r--sql/functions/auth.get_active_user_by_id.sql27
-rw-r--r--sql/functions/auth.get_active_user_by_username_pwd.sql30
-rw-r--r--sql/functions/auth.get_all_active_users.sql25
-rw-r--r--sql/functions/auth.get_all_inactive_users.sql24
-rw-r--r--sql/functions/auth.get_all_role_groups_for_user.sql36
-rw-r--r--sql/functions/auth.get_all_roles_for_user.sql163
-rw-r--r--sql/functions/auth.get_registration_by_hash.sql25
-rw-r--r--sql/functions/auth.get_registration_by_id.sql25
-rw-r--r--sql/functions/auth.get_user_by_id.sql26
-rw-r--r--sql/functions/auth.has_role.sql28
-rw-r--r--sql/functions/auth.insert_registration.sql42
-rw-r--r--sql/functions/auth.insert_user.sql59
-rw-r--r--sql/functions/auth.insert_user_role_group.sql44
-rw-r--r--sql/functions/auth.registrations_gc.sql11
-rw-r--r--sql/functions/auth.superuser_p.sql34
-rw-r--r--sql/functions/auth.update_password.sql19
-rw-r--r--sql/functions/auth.update_profile.sql32
-rw-r--r--sql/functions/auth.update_user.sql45
-rw-r--r--sql/functions/auth.upsert_user.sql66
-rw-r--r--sql/functions/auth.user_delete.sql50
-rw-r--r--sql/functions/auth.user_toggle_active.sql31
-rw-r--r--sql/functions/contact.get_contact_us_posts_by_id_and_read.sql30
-rw-r--r--sql/functions/contact.insert_contact_us_post.sql45
-rw-r--r--sql/functions/contact.mark_contact_us_post.sql38
-rw-r--r--sql/schemas/schemas.sql6
-rw-r--r--sql/tables/auth.registrations.sql11
-rw-r--r--sql/tables/auth.role_groups.sql7
-rw-r--r--sql/tables/auth.role_groups_roles.sql7
-rw-r--r--sql/tables/auth.roles.sql7
-rw-r--r--sql/tables/auth.user_session_objects.sql8
-rw-r--r--sql/tables/auth.user_sessions.sql7
-rw-r--r--sql/tables/auth.users.sql13
-rw-r--r--sql/tables/auth.users_role_groups.sql7
-rw-r--r--sql/tables/auth.users_roles_append.sql7
-rw-r--r--sql/tables/auth.users_roles_exclude.sql7
-rw-r--r--sql/tables/contact.contact_us.sql6
-rw-r--r--sql/tables/contact.contact_us_posts.sql11
-rw-r--r--sql/tables/contact.contact_us_posts_read.sql7
-rw-r--r--sql/tables/general.about_us.sql7
-rw-r--r--sql/tables/resume.contactinfo.sql11
-rw-r--r--sql/tables/resume.education.sql12
-rw-r--r--sql/tables/resume.job.sql10
-rw-r--r--sql/tables/resume.job_skill.sql7
-rw-r--r--sql/tables/resume.language.sql11
-rw-r--r--sql/tables/resume.resume.sql7
-rw-r--r--sql/tables/resume.resume_contactinfo.sql7
-rw-r--r--sql/tables/resume.resume_education.sql7
-rw-r--r--sql/tables/resume.resume_job.sql7
-rw-r--r--sql/tables/resume.resume_language.sql7
-rw-r--r--sql/tables/resume.skill.sql6
-rw-r--r--sql/tables/resume.states.sql7
-rw-r--r--sql/tables/resume.visastatus.sql6
-rw-r--r--sql/types/auth.registrations_t.sql12
-rw-r--r--sql/types/auth.role_groups_t.sql9
-rw-r--r--sql/types/auth.user_roles_t.sql13
132 files changed, 5136 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..696753b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+*~
+*swp
+*.fasl
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..822123a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,45 @@
+# bogenherr
+
+`bogenherr` is the public-facing business storefront website for
+Carlos Konstanski's Violin and Viola Studio in Pocatello ID.
+
+Visit the website: https://www.bogenherr.org/
+
+`bogenherr` is written in Lisp. The server-side is written in SBCL
+while the client-side is written in ClojureScript. Additionally the
+client-side uses React.js via the Reagent library.
+
+Eight of the Common Lisp libraries are written by me:
+
+https://github.com/ckonstanski/org-ckons-condition
+
+https://github.com/ckonstanski/org-ckons-core
+
+https://github.com/ckonstanski/org-ckons-file
+
+https://github.com/ckonstanski/org-ckons-http
+
+https://github.com/ckonstanski/org-ckons-json
+
+https://github.com/ckonstanski/org-ckons-serializable
+
+https://github.com/ckonstanski/org-ckons-session
+
+https://github.com/ckonstanski/org-ckons-sql
+
+In addition two of the ClojureScript library dependencies are written
+by me:
+
+https://github.com/ckonstanski/org-ckons-cljs.form
+
+https://github.com/ckonstanski/org-ckons-cljs.notifications
+
+`bogenherr` is deployed to production via docker-compose. For local
+development I use the standard Emacs, SLIME and CIDER. The
+ClojureScript project is managed with leiningen-bin.
+
+This repo was made public primarily as a small sample of my
+programming work for potential IT hiring managers. I don't expect that
+anyone will ever want to run this app locally. But if you do then
+message me and I will help with the non-obvious bits, of which there
+are a few, mostly having to do with file paths.
diff --git a/ci/.gitignore b/ci/.gitignore
new file mode 100644
index 0000000..416ae59
--- /dev/null
+++ b/ci/.gitignore
@@ -0,0 +1,2 @@
+tmp
+docker-compose.yaml
diff --git a/ci/Dockerfile b/ci/Dockerfile
new file mode 100644
index 0000000..18a0dab
--- /dev/null
+++ b/ci/Dockerfile
@@ -0,0 +1,13 @@
+FROM ubuntu:noble
+MAINTAINER Carlos Konstanski
+ADD tmp/docker-start.tar.xz /
+ARG OLO_USERNAME=root
+ARG OLO_GROUPNAME=root
+ARG OLO_UID=0
+ARG OLO_GID=0
+RUN data/install-root.sh
+USER ${OLO_UID}:${OLO_GID}
+RUN data/install-user-sbcl.sh
+ENTRYPOINT [ "data/start_app.sh" ]
+#ENTRYPOINT [ "/bin/bash", "-l", "-c" ]
+CMD []
diff --git a/ci/data/bogenherr-starter.lisp b/ci/data/bogenherr-starter.lisp
new file mode 100644
index 0000000..1b95947
--- /dev/null
+++ b/ci/data/bogenherr-starter.lisp
@@ -0,0 +1,7 @@
+(load "/etc/sbclrc")
+(asdf:operate 'asdf:load-op 'swank)
+(setf swank::*loopback-interface* "0.0.0.0")
+(swank:create-server :port 4110 :dont-close t)
+(asdf:operate 'asdf:load-op 'bogenherr)
+(in-package :bogenherr)
+(bogenherr)
diff --git a/ci/data/etc/bogenherr/figwheel-main.edn b/ci/data/etc/bogenherr/figwheel-main.edn
new file mode 100644
index 0000000..864266c
--- /dev/null
+++ b/ci/data/etc/bogenherr/figwheel-main.edn
@@ -0,0 +1,34 @@
+;; Figwheel-main configuration options see: https://figwheel.org/config-options
+;; these will be overriden by the metadata config options in dev.cljs.edn build file
+{
+ ;; Set the server port https://figwheel.org/config-options#ring-server-options
+ ;; :ring-server-options {:port 9500}
+
+ ;; Change the target directory from the "target" to "resources"
+ ;; https://figwheel.org/config-options#target-dir
+ ;; :target-dir "resources"
+
+ ;; Server Ring Handler (optional) https://figwheel.org/docs/ring-handler.html
+ ;; If you want to embed a ring handler into the figwheel server, this
+ ;; is for simple ring servers
+ ;; :ring-handler hello_world.server/handler
+
+ ;; To be able to open files in your editor from the heads up display
+ ;; you will need to put a script on your path. This script will have
+ ;; to take a file path and a line number ie.
+ ;; in ~/bin/myfile-opener:
+ ;;
+ ;; #! /bin/sh
+ ;; emacsclient -n +$2:$3 $1
+ ;;
+ ;; :open-file-command "myfile-opener"
+
+ ;; if you are using emacsclient you can just use
+ ;; :open-file-command "emacsclient"
+
+ ;; Logging output gets printed to the REPL, if you want to redirect it to a file:
+ ;; :log-file "figwheel-main.log"
+
+ :target-dir "resources"
+ :open-url false
+}
diff --git a/ci/data/etc/bogenherr/options.lisp b/ci/data/etc/bogenherr/options.lisp
new file mode 100644
index 0000000..e4d8934
--- /dev/null
+++ b/ci/data/etc/bogenherr/options.lisp
@@ -0,0 +1,15 @@
+(:name "bogenherr"
+ :scheme "http"
+ :url "www.bogenherr.org"
+ :document-root "bogenherr"
+ :title "Bogenherr Violin and Viola Studio"
+ :meta-description "Bogenherr Violin and Viola Studio written in Common Lisp/Hunchentoot and ClojureScript."
+ :databases (:db-bogenherr ("bogenherr" "bogenherr" "neweboy" "127.0.0.1"))
+ :mail-mx "mail.bogenherr.org"
+ :mail-from "postmaster@bogenherr.org"
+ :mail-postmaster "postmaster@bogenherr.org"
+ :mail-webmaster "webmaster@bogenherr.org"
+ :mail-info "postmaster@bogenherr.org"
+ :mail-login-notify "postmaster@bogenherr.org"
+ :mail-authentication (:login "me@ckons.org" "ayluvmiym")
+ :mail-ssl :starttls)
diff --git a/ci/data/etc/sbclrc b/ci/data/etc/sbclrc
new file mode 100644
index 0000000..3a931d0
--- /dev/null
+++ b/ci/data/etc/sbclrc
@@ -0,0 +1,6 @@
+;;; The following lines added by ql:add-to-init-file:
+#-quicklisp
+(let ((quicklisp-init (merge-pathnames "quicklisp/setup.lisp"
+ (user-homedir-pathname))))
+ (when (probe-file quicklisp-init)
+ (load quicklisp-init)))
diff --git a/ci/data/install-root.sh b/ci/data/install-root.sh
new file mode 100755
index 0000000..be226f0
--- /dev/null
+++ b/ci/data/install-root.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+echo 'APT::Acquire::Retries "3";' > /etc/apt/apt.conf.d/80-retries
+export DEBIAN_FRONTEND=noninteractive
+apt-get update
+apt-get -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" dist-upgrade
+apt-get -y install -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" aptitude
+aptitude -y install -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" apt-transport-https ca-certificates software-properties-common language-pack-de
+update-ca-certificates
+aptitude update
+aptitude -y install -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" sbcl sbcl-source leiningen dnsutils bind9-host vim curl wget rlwrap pass rsync sudo jq lsof zip unzip
+
+rsync -av /data/ /
+
+if (( OLO_UID != 0 )); then
+ userdel ubuntu || true
+ groupdel ubuntu || true
+ rm -rf /home/ubuntu || true
+ export HOMEDIR="/home/${OLO_USERNAME}"
+ groupadd -g "${OLO_GID}" "${OLO_GROUPNAME}" || OLO_GROUPNAME=$(grep -F "x:${OLO_GID}:" /etc/group | awk -F: '{print $1}')
+ useradd -s /bin/bash -m -u "${OLO_UID}" -g "${OLO_GROUPNAME}" "${OLO_USERNAME}" || OLO_USERNAME=$(grep -F "x:${OLO_UID}:" /etc/passwd | awk -F: '{print $1}')
+ SUDOERS_FILENAME="${OLO_USERNAME//\./_}"
+ echo "${OLO_USERNAME} ALL=(ALL) NOPASSWD: ALL" > "/etc/sudoers.d/${SUDOERS_FILENAME}"
+ chmod 0440 "/etc/sudoers.d/${SUDOERS_FILENAME}"
+ mkdir -p "${HOMEDIR}/.gnupg"
+ chown "${OLO_USERNAME}":"${OLO_GROUPNAME}" "${HOMEDIR}/.gnupg"
+ chmod 0700 "${HOMEDIR}/.gnupg"
+fi
+
+exit 0
diff --git a/ci/data/install-user-sbcl.sh b/ci/data/install-user-sbcl.sh
new file mode 100755
index 0000000..b473fd4
--- /dev/null
+++ b/ci/data/install-user-sbcl.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+curl -o ~/quicklisp.lisp https://beta.quicklisp.org/quicklisp.lisp
+sbcl --load ~/quicklisp.lisp --eval "(quicklisp-quickstart:install)" --eval "(sb-ext:exit)"
+sbcl --eval "(loop for pkg in '(cl-ppcre cl-smtp hunchentoot easy-routes cl-log ironclad cl-markdown tmpdir net-telent-date uffi drakma cl-json postmodern fiveam local-time trivial-octet-streams swank) do (ql:quickload (symbol-name pkg)))" --eval "(sb-ext:exit)"
+
+mkdir -p ~/.config/common-lisp
+echo "(:source-registry (:tree (:home \"common-lisp/systems\")) :inherit-configuration)" | tee ~/.config/common-lisp/source-registry.conf
+
+exit 0
diff --git a/ci/data/start_app.sh b/ci/data/start_app.sh
new file mode 100755
index 0000000..6f83bf7
--- /dev/null
+++ b/ci/data/start_app.sh
@@ -0,0 +1,49 @@
+#!/bin/bash
+
+mkdir -p ~/common-lisp/systems ~/opt
+cd ~/opt
+git clone -b master git@ns:repos/bogenherr.git
+git clone -b master git@github.com:ckonstanski/org-ckons-cljs.form.git
+git clone -b master git@github.com:ckonstanski/org-ckons-cljs.notifications
+git clone -b master git@github.com:ckonstanski/org-ckons-condition
+git clone -b master git@github.com:ckonstanski/org-ckons-core
+git clone -b master git@github.com:ckonstanski/org-ckons-file
+git clone -b master git@github.com:ckonstanski/org-ckons-http
+git clone -b master git@github.com:ckonstanski/org-ckons-json
+git clone -b master git@github.com:ckonstanski/org-ckons-serializable
+git clone -b master git@github.com:ckonstanski/org-ckons-session
+git clone -b master git@github.com:ckonstanski/org-ckons-sql
+
+logdir="/var/log/lisp"
+mv -f ${logdir}/bogenherr.log.1 ${logdir}/bogenherr.log.2
+mv -f ${logdir}/bogenherr.log ${logdir}/bogenherr.log.1
+
+pushd ~/common-lisp/systems
+ln -s ~/opt/bogenherr/lisp/bogenherr.asd bogenherr.asd
+ln -s ~/opt/org-ckons-condition/org-ckons-condition.asd org-ckons-condition.asd
+ln -s ~/opt/org-ckons-core/org-ckons-core.asd org-ckons-core.asd
+ln -s ~/opt/org-ckons-file/org-ckons-file.asd org-ckons-file.asd
+ln -s ~/opt/org-ckons-http/org-ckons-http.asd org-ckons-http.asd
+ln -s ~/opt/org-ckons-json/org-ckons-json.asd org-ckons-json.asd
+ln -s ~/opt/org-ckons-serializable/org-ckons-serializable.asd org-ckons-serializable.asd
+ln -s ~/opt/org-ckons-session/org-ckons-session.asd org-ckons-session.asd
+ln -s ~/opt/org-ckons-sql/org-ckons-sql.asd org-ckons-sql.asd
+popd
+
+pushd ~/opt/bogenherr/lisp/webapps/bogenherr/conf
+ln -s /etc/bogenherr/options.lisp options.lisp
+popd
+
+pushd ~/opt/bogenherr/lisp/webapps/bogenherr/clojurescript/bogenherr
+ln -s /etc/bogenherr/figwheel-main.edn figwheel-main.edn
+mkdir checkouts
+pushd checkouts
+ln -s ~/opt/org-ckons-cljs.form org-ckons-cljs.form
+ln -s ~/opt/org-ckons-cljs.notifications org-ckons-cljs.notifications
+popd
+
+lein clean
+lein fig:build | tee /tmp/bogenherr/lein.log &
+popd
+
+SBCL_HOME=/usr/lib/sbcl SBCL_SOURCE_ROOT=/usr/lib/sbcl/src exec sbcl --dynamic-space-size 2048 --userinit /data/bogenherr-starter.lisp | tee /tmp/bogenherr/sbcl.log
diff --git a/ci/docker-build.sh b/ci/docker-build.sh
new file mode 100755
index 0000000..b927920
--- /dev/null
+++ b/ci/docker-build.sh
@@ -0,0 +1,28 @@
+#!/bin/bash -e
+
+. env.sh
+
+builddir='tmp'
+container='bogenherr'
+
+rm -rf ${builddir}
+mkdir ${builddir}
+rsync -aq data ${builddir}
+pushd ${builddir} 2>/dev/null
+
+tar Jcvf docker-start.tar.xz data/
+docker stop ${container} || true
+docker container prune -f
+docker rmi --force ${container} || true
+
+DOCKER_BUILDKIT=0 docker compose \
+ --progress "plain" \
+ --ansi "never" \
+ build \
+ --no-cache \
+ --pull
+
+popd 2>/dev/null
+rm -rf tmp
+
+exit 0
diff --git a/ci/docker-compose.yaml.example b/ci/docker-compose.yaml.example
new file mode 100644
index 0000000..ecb6836
--- /dev/null
+++ b/ci/docker-compose.yaml.example
@@ -0,0 +1,47 @@
+services:
+ bogenherr:
+ image: bogenherr
+ tty: true
+ build:
+ context: .
+ args:
+ OLO_USERNAME: ${OLO_USERNAME}
+ OLO_GROUPNAME: ${OLO_GROUPNAME}
+ OLO_UID: ${OLO_UID}
+ OLO_GID: ${OLO_GID}
+ network_mode: "host"
+ container_name: bogenherr
+ volumes:
+ - type: bind
+ source: ~/.m2
+ target: /home/${OLO_USERNAME}/.m2
+ - type: bind
+ source: ~/.bashrc
+ target: /home/${OLO_USERNAME}/.bashrc
+ - type: bind
+ source: ~/.bash_history
+ target: /home/${OLO_USERNAME}/.bash_history
+ - type: bind
+ source: ~/.vimrc
+ target: /home/${OLO_USERNAME}/.vimrc
+ - type: bind
+ source: ~/.gitconfig
+ target: /home/${OLO_USERNAME}/.gitconfig
+ - type: bind
+ source: ~/.ssh
+ target: /home/${OLO_USERNAME}/.ssh
+ - type: bind
+ source: /etc/hosts
+ target: /etc/hosts
+ - type: bind
+ source: /etc/ssh
+ target: /etc/ssh
+ - type: bind
+ source: /var/log/lisp
+ target: /var/log/lisp
+ environment:
+ - TERM=xterm-256color
+ - https_proxy=${https_proxy}
+ - http_proxy=${http_proxy}
+ - no_proxy=${no_proxy}
+ - GITHUB_TOKEN=${GITHUB_TOKEN}
diff --git a/ci/docker-run.sh b/ci/docker-run.sh
new file mode 100755
index 0000000..c368224
--- /dev/null
+++ b/ci/docker-run.sh
@@ -0,0 +1,7 @@
+#!/bin/bash -e
+
+. env.sh
+
+docker compose --ansi "never" up -d
+
+exit 0
diff --git a/ci/env.sh b/ci/env.sh
new file mode 100644
index 0000000..a49fb54
--- /dev/null
+++ b/ci/env.sh
@@ -0,0 +1,4 @@
+export OLO_USERNAME=$(id -un)
+export OLO_GROUPNAME=$(id -gn)
+export OLO_UID=$(id -u)
+export OLO_GID=$(id -g)
diff --git a/doc/sql/resumes.dot b/doc/sql/resumes.dot
new file mode 100644
index 0000000..210840c
--- /dev/null
+++ b/doc/sql/resumes.dot
@@ -0,0 +1,81 @@
+digraph G {
+ fontname = "LispM"
+ fontsize = 40
+
+ node [
+ fontname = "LispM"
+ fontsize = 40
+ shape = "record"
+ ]
+
+ edge [
+ fontname = "LispM"
+ fontsize = 40
+ arrowhead = "empty"
+ ]
+
+ Resume_States [
+ label = "{Table: resume.states|id : serial8 primary key\lstate character varying(255)\labbr character varying(2)\l}"
+ ]
+
+ Resume_Visastatus [
+ label = "{Table: resume.visastatus|id : serial8 primary key\lstatus : character varying(255)\l}"
+ ]
+
+ Resume_Resume [
+ label = "{Table: resume.resume|id : serial8 primary key\luser_id : int not null\lname : character varying(255)\l}"
+ ]
+
+ Resume_Contactinfo [
+ label = "{Table: resume.contactinfo|id : serial8 primary key\laddress : character varying(255)\lcity : character varying(255)\lstate_id : int\lphone : character varying(255)\lemail : character varying(255)\lvisastatus_id : int\l}"
+ ]
+
+ Resume_Resume_Contactinfo [
+ label = "{Table: resume.resume_contactinfo|id : serial8 primary key\lresume_id: int\lcontactinfo_id : int\l}"
+ ]
+
+ Resume_Job [
+ label = "{Table: resume.job|id : serial8 primary key\lcompany : character varying(255)\llocation : character varying(255)\lstart_date : timestamp\lend_date : timestamp\loverview : text\l}"
+ ]
+
+ Resume_Resume_Job [
+ label = "{Table: resume.resume_job|id : serial8 primary key\lresume_id : int\ljob_id : int\l}"
+ ]
+
+ Resume_Skill [
+ label = "{Table: resume.skill|id : serial8 primary key\lskill : text\l}"
+ ]
+
+ Resume_Job_Skill [
+ label = "{Table: resume.job_skill|id : serial8 primary key\ljob_id : int\lskill_id : int\l}"
+ ]
+
+ Resume_Education [
+ label = "{Table: resume.education|id : serial8 primary key\lschool : character varying(255)\lstart_date : timestamp\lend_date : timestamp\lmajor : character varying(255)\lminor : character varying(255)\ldegree : character varying(255)\lprogress : character varying(255)\l}"
+ ]
+
+ Resume_Resume_Education [
+ label = "{Table: resume.resume_education|id : serial8 primary key\lresume_id : int\leducation_id : int\l}"
+ ]
+
+ Resume_Language [
+ label = "{Table: resume.language|id : serial8 primary key\lname : character varying(255)\lunderstanding_listening : character varying(2)\lunderstanding_reading : character varying(2)\lspeaking_interaction : character varying(2)\lspeaking_production : character varying(2)\lwriting : character varying(2)\l}"
+ ]
+
+ Resume_Resume_Language [
+ label = "{Table: resume.resume_language|id : serial8 primary key\lresume_id : int\llanguage_id : int\l}"
+ ]
+
+ Resume_Resume_Contactinfo -> Resume_Resume
+ Resume_Resume_Contactinfo -> Resume_Contactinfo
+ Resume_Contactinfo -> Resume_States
+ Resume_Contactinfo -> Resume_Visastatus
+ Resume_Resume_Job -> Resume_Resume
+ Resume_Resume_Job -> Resume_Job
+ Resume_Job_Skill -> Resume_Job
+ Resume_Job_Skill -> Resume_Skill
+ Resume_Resume_Education -> Resume_Resume
+ Resume_Resume_Education -> Resume_Education
+ Resume_Resume_Language -> Resume_Resume
+ Resume_Resume_Language -> Resume_Language
+}
diff --git a/doc/sql/resumes.png b/doc/sql/resumes.png
new file mode 100644
index 0000000..8947579
--- /dev/null
+++ b/doc/sql/resumes.png
Binary files differ
diff --git a/lisp/core/core.lisp b/lisp/core/core.lisp
new file mode 100644
index 0000000..873b371
--- /dev/null
+++ b/lisp/core/core.lisp
@@ -0,0 +1,7 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(defpackage :resume
+ (:use :cl :cl-log :hunchentoot :org-ckons-sql))
+
+(in-package :resume)
diff --git a/lisp/resume.asd b/lisp/resume.asd
new file mode 100644
index 0000000..a63bc46
--- /dev/null
+++ b/lisp/resume.asd
@@ -0,0 +1,73 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :cl)
+
+(defpackage :resume-system (:use :cl :asdf))
+(in-package :resume-system)
+
+(defmacro do-defsystem (&key name version maintainer author description long-description depends-on components)
+ `(defsystem ,name
+ :name ,name
+ :version ,version
+ :maintainer ,maintainer
+ :author ,author
+ :description ,description
+ :long-description ,long-description
+ :depends-on ,(eval depends-on)
+ :components ,components))
+
+(defparameter *quicklisp-packages* '(cl-ppcre cl-smtp hunchentoot easy-routes cl-log ironclad cl-markdown tmpdir net-telent-date uffi drakma cl-json postmodern fiveam local-time trivial-octet-streams))
+(defparameter *asdf-packages* '(org-ckons-core org-ckons-http org-ckons-json org-ckons-file org-ckons-serializable org-ckons-session org-ckons-condition org-ckons-sql))
+(defparameter *all-packages* (append *quicklisp-packages* *asdf-packages*))
+
+(loop for pkg in *quicklisp-packages*
+ do (ql:quickload (symbol-name pkg)))
+
+(do-defsystem :name "resume"
+ :version "1"
+ :maintainer "Carlos Konstanski <me@ckons.org>"
+ :author "Carlos Konstanski <me@ckons.org>"
+ :description "resume"
+ :long-description "resume is a web application written in Common Lisp based on the Hunchentoot web server. The client-side code is written in ClojureScript. Purpose: public website for the Violin and Viola Studio in Pocatello ID."
+ :depends-on *all-packages*
+ :components ((:module core
+ :components ((:file "core")))
+ (:module sql
+ :depends-on (core)
+ :components ((:file "generics")
+ (:file "user-session" :depends-on ("generics"))
+ (:file "user-session-pkg" :depends-on ("generics" "user-session"))
+ (:file "user" :depends-on ("generics"))
+ (:file "role-group" :depends-on ("generics"))
+ (:file "user-role" :depends-on ("generics"))
+ (:file "registration" :depends-on ("generics"))
+ (:file "about-us" :depends-on ("generics"))
+ (:file "contact-us" :depends-on ("generics"))
+ (:file "auth-pkg" :depends-on ("user-session-pkg" "user" "role-group" "user-role" "registration"))
+ (:file "general-pkg" :depends-on ("about-us"))
+ (:file "contact-pkg" :depends-on ("contact-us"))))
+ (:module service
+ :depends-on (sql)
+ :components ((:file "generics")
+ (:file "base-service")
+ (:file "rest-service" :depends-on ("base-service"))
+ (:file "auth-service" :depends-on ("rest-service"))
+ (:file "generic-form" :depends-on ("rest-service"))
+ (:file "menu-service" :depends-on ("base-service"))
+ (:file "home-service" :depends-on ("rest-service"))
+ (:file "login-service" :depends-on ("generic-form" "rest-service"))
+ (:file "logout-service" :depends-on ("rest-service"))
+ (:file "profile-service" :depends-on ("generic-form" "auth-service"))
+ (:file "password-service" :depends-on ("generic-form" "auth-service"))
+ (:file "about-us-service" :depends-on ("generic-form" "auth-service"))
+ (:file "contact-us-service" :depends-on ("generic-form" "auth-service"))
+ (:file "users-service" :depends-on ("generic-form" "auth-service"))
+ (:file "messages-service" :depends-on ("generic-form" "auth-service"))))
+ (:module webapps
+ :depends-on (service)
+ :components ((:file "generics")
+ (:file "webapp-loader" :depends-on ("generics"))
+ (:module resume
+ :depends-on ("webapp-loader")
+ :components ((:file "site")))))))
diff --git a/lisp/service/about-us-service.lisp b/lisp/service/about-us-service.lisp
new file mode 100644
index 0000000..c479efe
--- /dev/null
+++ b/lisp/service/about-us-service.lisp
@@ -0,0 +1,66 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defclass about-us-service (rest-service)
+ ((category :initarg :category
+ :initform nil
+ :accessor category))
+ (:documentation ""))
+
+(defun about-us-json (category)
+ (with-noauth (instance about-us-service)
+ (setf (category instance) category)))
+
+(defclass about-us/view-service (about-us-service)
+ ((content :initarg :content
+ :initform nil
+ :accessor content)
+ (admin-p :initarg :admin-p
+ :initform nil
+ :accessor admin-p)
+ (location :initform nil))
+ (:documentation ""))
+
+(defun about-us-view-json (category)
+ (with-noauth (instance about-us/view-service)
+ (setf (category instance) category)
+ (with-valid-user (user "about-us-modify")
+ (setf (admin-p instance) nil)
+ (setf (admin-p instance) t))
+ (with-resume-database
+ (let* ((general-pkg (make-instance 'general-pkg))
+ (about-us (get-about-us general-pkg category)))
+ (when about-us
+ (setf (content instance) (content about-us)))))))
+
+(defclass about-us/modify-service (about-us/view-service auth-service)
+ ((form :initarg :form
+ :initform nil
+ :accessor form)
+ (admin-p :initform t)
+ (location-p :initform nil))
+ (:documentation ""))
+
+(defun about-us-modify-json (category)
+ (with-auth (instance about-us/modify-service "about-us-modify")
+ (with-resume-database
+ (let* ((general-pkg (make-instance 'general-pkg))
+ (about-us (get-about-us general-pkg category)))
+ (setf (form instance) (make-form "about-us-modify-form"
+ nil
+ nil
+ `((:label "Content" :name "txt-content" :field-type "textarea" :value ,(content about-us) :required "required")
+ (:label "Modify" :field-type "button" :onclick "on_about_us_modify_submit_clicked()"))))))))
+
+(defun about-us-modify-submit-json (category content)
+ (with-auth (instance about-us/modify-service "about-us-modify")
+ (with-resume-database
+ (let* ((general-pkg (make-instance 'general-pkg))
+ (about-us (get-about-us general-pkg category)))
+ (when about-us
+ (setf (content about-us) content)
+ (update-record general-pkg about-us))))
+ (setf (content instance) content)
+ (setf (message instance) "About Us text saved successfully.")))
diff --git a/lisp/service/auth-service.lisp b/lisp/service/auth-service.lisp
new file mode 100644
index 0000000..8450e64
--- /dev/null
+++ b/lisp/service/auth-service.lisp
@@ -0,0 +1,48 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defclass auth-service (rest-service)
+ ()
+ (:documentation ""))
+
+(defmethod initialize-instance :after ((auth-service auth-service) &key roles)
+ (with-valid-user (session roles)
+ (progn
+ (setf (location auth-service) "/home")
+ (setf (message auth-service) nil)
+ (setf (errormsg auth-service) "You are not authorized to access this resource."))
+ t))
+
+(defmacro with-auth ((instance auth-service roles) &body body)
+ `(let ((,instance (make-instance ',auth-service :roles ,roles)))
+ (when (null (errormsg ,instance))
+ ,@body)
+ (when (location-p ,instance)
+ (setf (session-value :message) nil)
+ (setf (session-value :errormsg) nil))
+ (org-ckons-json::objects-to-json `(,,instance))))
+
+(defmacro with-auth-raw ((instance auth-service roles) &body body)
+ `(let ((,instance (make-instance ',auth-service :roles ,roles)))
+ (when (location-p ,instance)
+ (setf (session-value :message) nil)
+ (setf (session-value :errormsg) nil))
+ (when (null (errormsg ,instance))
+ ,@body)))
+
+(defmacro with-noauth ((instance rest-service) &body body)
+ `(let ((,instance (make-instance ',rest-service)))
+ ,@body
+ (when (location-p ,instance)
+ (setf (session-value :message) nil)
+ (setf (session-value :errormsg) nil))
+ (org-ckons-json::objects-to-json `(,,instance))))
+
+(defmacro with-noauth-raw ((instance rest-service) &body body)
+ `(let ((,instance (make-instance ',rest-service)))
+ (when (location-p ,instance)
+ (setf (session-value :message) nil)
+ (setf (session-value :errormsg) nil))
+ ,@body))
diff --git a/lisp/service/base-service.lisp b/lisp/service/base-service.lisp
new file mode 100644
index 0000000..bd38c5a
--- /dev/null
+++ b/lisp/service/base-service.lisp
@@ -0,0 +1,21 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defclass base-service ()
+ ()
+ (:documentation ""))
+
+(defmacro loop-intersect-slots ((slot record other-object) &body body)
+ `(loop for ,slot in (intersect-slots ,record (org-ckons-core::map-slot-names ,other-object))
+ do (when (slot-is-field-p ,slot)
+ ,@body)))
+
+(defmethod copy-from-record ((base-service base-service) (record record))
+ (loop-intersect-slots (slot record base-service)
+ (setf (slot-value base-service slot) (slot-value record slot))))
+
+(defmethod copy-to-record ((base-service base-service) (record record))
+ (loop-intersect-slots (slot record base-service)
+ (setf (slot-value record slot) (slot-value base-service slot))))
diff --git a/lisp/service/contact-us-service.lisp b/lisp/service/contact-us-service.lisp
new file mode 100644
index 0000000..43f6da3
--- /dev/null
+++ b/lisp/service/contact-us-service.lisp
@@ -0,0 +1,84 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defclass contact-us-service (rest-service)
+ ()
+ (:documentation ""))
+
+(defun contact-us-form ()
+ (make-form "contact-us-form"
+ nil
+ t
+ `((:name "first_name" :label "First Name" :field-type "text" :required "required")
+ (:name "last_name" :label "Last Name" :field-type "text" :required "required")
+ (:name "email" :label "Email" :field-type "text" :required "required")
+ (:name "phone" :label "Phone" :field-type "text")
+ (:name "comments" :label "Message" :field-type "textarea" :required "required")
+ (:label "Submit" :field-type "button" :onclick "on_contact_us_email_submit_clicked()"))))
+
+(defun contact-us-json ()
+ (with-noauth (instance contact-us-service)
+ t))
+
+(defclass contact-us/view-service (contact-us-service)
+ ((content :initarg :content
+ :initform nil
+ :accessor content)
+ (form :initarg :form
+ :initform nil
+ :accessor form)
+ (location-p :initform nil))
+ (:documentation ""))
+
+(defun contact-us-view-json ()
+ (with-noauth (instance contact-us/view-service)
+ (setf (form instance) (contact-us-form))))
+
+(defclass contact-us/email-service (contact-us/view-service)
+ ()
+ (:documentation ""))
+
+(defun contact-us-email-json (first_name last_name email phone comments)
+ (declare (special first_name last_name email phone comments))
+ (with-noauth (instance contact-us/email-service)
+ (with-resume-database
+ (let ((contact-pkg (make-instance 'contact-pkg))
+ (contact-us-post (make-instance 'contact-us-post)))
+ (loop for param in (sb-introspect:function-lambda-list #'contact-us-email-json)
+ do (setf (slot-value contact-us-post param) (symbol-value param)))
+ (insert-contact-us-post contact-pkg contact-us-post)
+ (handler-case
+ (let ((text-message (format nil
+ "A contact-us form submission was received.~%~%Name: ~a ~a~%Email: ~a~%Phone: ~a~%~%Message: ~a~%"
+ (first_name contact-us-post)
+ (last_name contact-us-post)
+ (email contact-us-post)
+ (phone contact-us-post)
+ (comments contact-us-post)))
+ (html-message (org-ckons-http::html5
+ `(html
+ ((p) "A contact-us form submission was received.")
+ ((p)
+ ,(format nil
+ "Name: ~a ~a<br/>Email: ~a<br/>Phone: ~a"
+ (first_name contact-us-post)
+ (last_name contact-us-post)
+ (email contact-us-post)
+ (phone contact-us-post)))
+ ((p)
+ ,(format nil "Message: ~a" (comments contact-us-post)))))))
+ (org-ckons-core::sendmail (mail-mx *webapp*)
+ (mail-postmaster *webapp*)
+ (mail-info *webapp*)
+ (format nil "~a contact-us form" (name *webapp*))
+ text-message
+ :html-message html-message
+ :reply-to (mail-postmaster *webapp*)
+ :ssl (mail-ssl *webapp*)
+ :authentication (mail-authentication *webapp*))
+ (setf (session-value :message) "Form submitted successfully."))
+ (error (e)
+ (declare (ignore e))
+ (setf (session-value :errormsg) "Error submitting form.")))))))
diff --git a/lisp/service/generic-form.lisp b/lisp/service/generic-form.lisp
new file mode 100644
index 0000000..c9742a4
--- /dev/null
+++ b/lisp/service/generic-form.lisp
@@ -0,0 +1,87 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defclass generic-form (base-service)
+ ((name :initarg :name
+ :initform nil
+ :accessor name)
+ (http-method :initarg :http-method
+ :initform "POST"
+ :accessor http-method)
+ (action :initarg :action
+ :initform nil
+ :accessor action)
+ (required-p :initarg :required-p
+ :initform nil
+ :accessor required-p)
+ (form-fields :initarg :form-fields
+ :initform nil
+ :accessor form-fields))
+ (:documentation ""))
+
+(defclass form-field (base-service)
+ ((name :initarg :name
+ :initform nil
+ :accessor name)
+ (label :initarg :label
+ :initform nil
+ :accessor label)
+ (value :initarg :value
+ :initform nil
+ :accessor value)
+ (checked :initarg :checked
+ :initform nil
+ :accessor checked)
+ (field-type :initarg :field-type
+ :initform nil
+ :accessor field-type)
+ (required :initarg :required
+ :initform nil
+ :accessor required)
+ (dismiss :initarg :dismiss
+ :initform nil
+ :accessor dismiss)
+ (options :initarg :options
+ :initform nil
+ :accessor options)
+ (onclick :initarg :onclick
+ :initform nil
+ :accessor onclick)
+ (onchange :initarg :onchange
+ :initform nil
+ :accessor onchange))
+ (:documentation ""))
+
+(defclass option (base-service)
+ ((label :initarg :label
+ :initform nil
+ :accessor label)
+ (value :initarg :value
+ :initform nil
+ :accessor value))
+ (:documentation ""))
+
+(defun make-form (name action required-p fields)
+ (make-instance 'generic-form
+ :name name
+ :action action
+ :required-p required-p
+ :form-fields (mapcar (lambda (field)
+ (make-instance 'form-field
+ :name (getf field :name)
+ :label (getf field :label)
+ :value (getf field :value)
+ :checked (getf field :checked)
+ :field-type (getf field :field-type)
+ :required (getf field :required)
+ :dismiss (getf field :dismiss)
+ :options (mapcar (lambda (option)
+ (make-instance 'option
+ :label (getf option :label)
+ :value (getf option :value)))
+ (getf field :options))
+ :onclick (getf field :onclick)
+ :onchange (getf field :onchange)))
+ fields)))
diff --git a/lisp/service/generics.lisp b/lisp/service/generics.lisp
new file mode 100644
index 0000000..caca29b
--- /dev/null
+++ b/lisp/service/generics.lisp
@@ -0,0 +1,15 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defgeneric copy-from-record (base-service record)
+ (:documentation "Copies the fields from `record' to
+`base-service'."))
+
+(defgeneric copy-to-record (base-service record)
+ (:documentation "Copies the fields from `base-service' to
+`record'."))
+
+(defgeneric sanitize-rest-json (rest-service)
+ (:documentation ""))
diff --git a/lisp/service/home-service.lisp b/lisp/service/home-service.lisp
new file mode 100644
index 0000000..8e8a740
--- /dev/null
+++ b/lisp/service/home-service.lisp
@@ -0,0 +1,18 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defclass home-service (rest-service)
+ ((content :initarg :content
+ :initform nil
+ :accessor content))
+ (:documentation ""))
+
+(defmethod initialize-instance :after ((home-service home-service) &key)
+ t)
+
+(defun home-json (&optional message errormsg)
+ (with-noauth (instance home-service)
+ (when (not (org-ckons-core::null-or-empty-p message)) (setf (message instance) message))
+ (when (not (org-ckons-core::null-or-empty-p errormsg)) (setf (errormsg instance) errormsg))))
diff --git a/lisp/service/login-service.lisp b/lisp/service/login-service.lisp
new file mode 100644
index 0000000..5f7dd2c
--- /dev/null
+++ b/lisp/service/login-service.lisp
@@ -0,0 +1,64 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defclass login-service (rest-service)
+ ((form :initarg :form
+ :initform nil
+ :accessor form)
+ (title :initarg :title
+ :initform nil
+ :accessor title))
+ (:documentation ""))
+
+(defclass login-forgot-service (login-service)
+ ()
+ (:documentation ""))
+
+(defmethod initialize-instance :after ((login-service login-service) &key)
+ (setf (title login-service) "Login")
+ (setf (form login-service) (make-form "login-form"
+ nil
+ t
+ '((:name "username" :label "Username" :field-type "text" :required "required")
+ (:name "pwd" :label "Password" :field-type "password" :required "required")
+ (:label "Login" :field-type "button" :onclick "on_login_submit_clicked()")))))
+
+(defmethod initialize-instance :after ((login-forgot-service login-forgot-service) &key)
+ (setf (title login-forgot-service) "Reset Password")
+ (setf (form login-forgot-service) (make-form "login-forgot-form"
+ nil
+ t
+ '((:name "username" :label "Username" :field-type "text" :required "required")
+ (:label "Send password reset email" :field-type "button" :onclick "on_login_forgot_submit_clicked()")))))
+
+(defun login-json ()
+ (with-noauth (instance login-service)
+ t))
+
+(defun login-forgot-json ()
+ (with-noauth (instance login-forgot-service)
+ t))
+
+(defclass login-authenticate-service (rest-service)
+ ((location-p :initform nil))
+ (:documentation ""))
+
+(defun login-authenticate-json (username pwd)
+ (with-noauth (instance login-authenticate-service)
+ (if (or (org-ckons-core::null-or-empty-p username)
+ (org-ckons-core::null-or-empty-p pwd))
+ (setf (session-value :errormsg) "Login failed.")
+ (with-resume-database
+ (let* ((auth-pkg (make-instance 'auth-pkg))
+ (user (get-active-user-by-username-pwd auth-pkg username pwd)))
+ (cond (user
+ (set-user user)
+ (setf (session-value :message) "Successfully logged in.")
+ (setf (session-value :errormsg) nil))
+ (t
+ (setf (session-value :message) nil)
+ (setf (session-value :errormsg) "Login failed."))))))
+ (setf (message instance) (session-value :message))
+ (setf (errormsg instance) (session-value :errormsg))))
diff --git a/lisp/service/logout-service.lisp b/lisp/service/logout-service.lisp
new file mode 100644
index 0000000..a3c3948
--- /dev/null
+++ b/lisp/service/logout-service.lisp
@@ -0,0 +1,14 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defclass logout-service (rest-service)
+ ((location-p :initform nil))
+ (:documentation ""))
+
+(defun logout-json ()
+ (with-noauth (instance logout-service)
+ (set-user (make-default-user))
+ (setf (location instance) "/home")
+ (setf (message instance) "You are now logged out.")))
diff --git a/lisp/service/menu-service.lisp b/lisp/service/menu-service.lisp
new file mode 100644
index 0000000..e7af0c3
--- /dev/null
+++ b/lisp/service/menu-service.lisp
@@ -0,0 +1,96 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defparameter *menu-config* '((:id "a_menu_home" :label "Home" :handler "/home" :permissions "_Public")
+ (:id "a_menu_lessons" :label "About the Studio" :handler "/lessons" :permissions "_Public")
+ (:id "a_menu_gigs" :label "For Hire" :handler "/gigs" :permissions "_Public")
+ (:id "a_menu_programming" :label "Software Consulting" :handler "/programming" :permissions "_Public")
+ (:id "a_menu_contact_us" :label "Contact Me" :handler "/contact-us" :permissions "_Public")
+ (:id "a_menu_messages" :label "Messages" :handler "/messages" :permissions "messages-view")
+ (:id "a_menu_users" :label "Users" :handler "/users" :permissions "users-view")))
+
+(defparameter *menu-user-config* '((:id "a_menu_login" :label "Login" :handler "/login" :permissions "_Public")
+ (:id "a_menu_profile" :label "Edit Profile" :handler "/profile" :permissions "_Public")
+ (:id "a_menu_password" :label "Change Password" :handler "/password" :permissions "_Public")
+ (:id "a_menu_logout" :label "Logout" :handler "/logout" :permissions "_Public")))
+
+(defclass menuitem ()
+ ((id :initarg :id
+ :initform nil
+ :accessor id)
+ (label :initarg :label
+ :initform nil
+ :accessor label)
+ (handler :initarg :handler
+ :initform nil
+ :accessor handler)
+ (permissions :initarg :permissions
+ :initform nil
+ :accessor permissions)
+ (children :initarg :children
+ :initform nil
+ :accessor children))
+ (:documentation ""))
+
+(defclass menu-service (base-service)
+ ((menuitems :initarg :menuitems
+ :initform nil
+ :accessor menuitems)
+ (location-p :initarg :location-p
+ :initform nil
+ :accessor location-p))
+ (:documentation ""))
+
+(defclass menu-user-service (menu-service)
+ ((label :initarg :label
+ :initform nil
+ :accessor label)
+ (location-p :initarg :location-p
+ :initform nil
+ :accessor location-p))
+ (:documentation ""))
+
+(defmethod initialize-instance :after ((menu-service menu-service) &key user menu-config)
+ (with-resume-database
+ (let ((auth-pkg (make-instance 'auth-pkg))
+ (user-logged-in-p (and user (not (= (id user) 0))))
+ roles)
+ (setf roles (when user (get-all-roles auth-pkg user)))
+ (setf (menuitems menu-service)
+ (remove-if (lambda (menuitem)
+ (find-if (lambda (x)
+ (string= (id menuitem) x))
+ (if user-logged-in-p
+ '("a_menu_login")
+ '("a_menu_logout" "a_menu_profile" "a_menu_password"))))
+ (mapcar (lambda (x)
+ (make-instance 'menuitem
+ :id (getf x :id)
+ :label (getf x :label)
+ :handler (getf x :handler)
+ :permissions (getf x :permissions)))
+ (remove-if 'null (mapcar (lambda (x)
+ (when (find-if (lambda (y)
+ (string= (getf x :permissions) y))
+ (mapcar (lambda (z)
+ (role_name z))
+ roles))
+ x))
+ menu-config))))))))
+
+(defun menu-json ()
+ (org-ckons-json::objects-to-json `(,(make-instance 'menu-service
+ :user (get-user)
+ :menu-config *menu-config*))))
+
+(defun menu-user-json ()
+ (let* ((user (get-user))
+ (instance (make-instance 'menu-user-service
+ :user user
+ :menu-config *menu-user-config*)))
+ (setf (label instance) (if user
+ (format nil "~a ~a" (first_name user) (last_name user))
+ "No User Found"))
+ (org-ckons-json::objects-to-json `(,instance))))
diff --git a/lisp/service/messages-service.lisp b/lisp/service/messages-service.lisp
new file mode 100644
index 0000000..9dc047a
--- /dev/null
+++ b/lisp/service/messages-service.lisp
@@ -0,0 +1,57 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defclass messages-service (auth-service)
+ ((title :initarg :title
+ :initform nil
+ :accessor title)
+ (form :initarg :form
+ :initform nil
+ :accessor form))
+ (:documentation ""))
+
+(defun messages-json ()
+ (with-auth (instance messages-service "messages-view")
+ (setf (title instance) "Messages Administration")
+ (setf (form instance) (make-form "messages-select-mode-form"
+ nil
+ nil
+ `((:name "read" :field-type "hidden" :required "required" :value ,(session-value :messages-read))
+ (:label "View Unread" :field-type "button" :onclick "on_messages_mode_clicked('unread')")
+ (:label "View Read" :field-type "button" :onclick "on_messages_mode_clicked('read')"))))))
+
+(defclass messages/results-service (messages-service)
+ ((results :initarg :results
+ :initform nil
+ :accessor results)
+ (location-p :initform nil))
+ (:documentation ""))
+
+(defun messages-results-json (read)
+ (with-auth (instance messages/results-service "messages-view")
+ (when read (setf (session-value :messages-read) read))
+ (with-resume-database
+ (let ((contact-pkg (make-instance 'contact-pkg)))
+ (setf (results instance) (get-contact-us-posts contact-pkg
+ (id (get-user))
+ (string= (session-value :messages-read) "read")))))
+ (sanitize-rest-json instance)
+ (loop for result in (results instance)
+ do (sanitize-json result))))
+
+(defclass messages/mark-service (messages-service)
+ ((location-p :initform nil))
+ (:documentation ""))
+
+(defun messages-mark-json (read id)
+ (with-auth (instance messages/mark-service "messages-view")
+ (with-resume-database
+ (handler-case
+ (let ((contact-pkg (make-instance 'contact-pkg)))
+ (mark-contact-us-post contact-pkg id (id (get-user)) (string= read "read"))
+ (setf (session-value :message) (format nil "Message marked ~a successfully." read)))
+ (error (e)
+ (declare (ignore e))
+ (setf (session-value :errormsg) (format nil "Error marking message ~a." read)))))))
diff --git a/lisp/service/password-service.lisp b/lisp/service/password-service.lisp
new file mode 100644
index 0000000..31b51b8
--- /dev/null
+++ b/lisp/service/password-service.lisp
@@ -0,0 +1,50 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defclass password-service (auth-service user)
+ ((form :initarg :form
+ :initform nil
+ :accessor form)
+ (title :initarg :title
+ :initform nil
+ :accessor title))
+ (:documentation ""))
+
+(defun password-json ()
+ (with-auth (instance password-service "profile-modify")
+ (let ((user (get-user)))
+ (setf (title instance) "Change Password")
+ (setf (form instance) (make-form "password-form"
+ nil
+ t
+ `((:name "id" :label "" :field-type "hidden" :value ,(id user) :required "required")
+ (:name "pwd" :label "Password" :field-type "password" :required "required")
+ (:name "pwd2" :label "Password (again)" :field-type "password" :required "required")
+ (:label "Change Password" :field-type "button" :onclick "on_password_submit_clicked()")))))))
+
+(defclass password/modify-service (password-service)
+ ((form :initarg :form
+ :initform nil
+ :accessor form)
+ (location-p :initform nil))
+ (:documentation ""))
+
+(defun password-submit-json (id pwd pwd2)
+ (declare (special pwd pwd2))
+ (with-auth (instance password/modify-service "profile-modify")
+ (setf (title instance) "Change Password")
+ (with-resume-database
+ (let ((user (get-user)))
+ (if (and (= id (id user))
+ (string= pwd pwd2))
+ (let ((auth-pkg (make-instance 'auth-pkg)))
+ (loop for param in (remove-if (lambda (x)
+ (intersection `(,x) '(id pwd2)))
+ (sb-introspect:function-lambda-list #'password-submit-json))
+ do (setf (slot-value user param) (symbol-value param)))
+ (update-password auth-pkg user)
+ (set-user user)
+ (setf (session-value :message) "Password saved successfully."))
+ (setf (session-value :errormsg) "An error occured."))))))
diff --git a/lisp/service/profile-service.lisp b/lisp/service/profile-service.lisp
new file mode 100644
index 0000000..9e37bbf
--- /dev/null
+++ b/lisp/service/profile-service.lisp
@@ -0,0 +1,61 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defclass profile-service (auth-service user)
+ ((title :initarg :title
+ :initform nil
+ :accessor title))
+ (:documentation ""))
+
+(defun profile-json ()
+ (with-auth (instance profile-service "profile-modify")
+ (setf (title instance) "Profile")
+ (sanitize-rest-json instance)))
+
+(defun profile-view-json ()
+ (with-auth (instance profile-service "profile-modify")
+ (let ((user (get-user)))
+ (copy-from-record instance user))
+ (sanitize-rest-json instance)))
+
+(defclass profile/modify-service (profile-service)
+ ((form :initarg :form
+ :initform nil
+ :accessor form)
+ (location-p :initform nil))
+ (:documentation ""))
+
+(defun profile-modify-json ()
+ (with-auth (instance profile/modify-service "profile-modify")
+ (let ((user (get-user)))
+ (sanitize-rest-json instance)
+ (setf (title instance) "Profile - Modify")
+ (setf (form instance) (make-form "profile-modify-form"
+ nil
+ t
+ `((:name "id" :label "" :field-type "hidden" :value ,(id user) :required "required")
+ (:name "username" :label "Username" :field-type "text" :value ,(username user) :required "required")
+ (:name "first_name" :label "First Name" :field-type "text" :value ,(first_name user) :required "required")
+ (:name "last_name" :label "Last Name" :field-type "text" :value ,(last_name user) :required "required")
+ (:name "email" :label "Email" :field-type "text" :value ,(email user) :required "required")
+ (:name "phone" :label "Phone" :field-type "text" :value ,(phone user))
+ (:label "Modify Profile" :field-type "button" :onclick "on_profile_modify_submit_clicked()")))))))
+
+(defun profile-modify-submit-json (id username first_name last_name email phone)
+ (declare (special username first_name last_name email phone))
+ (with-auth (instance profile/modify-service "profile-modify")
+ (with-resume-database
+ (let ((user (get-user)))
+ (if (= (id user) id)
+ (let ((auth-pkg (make-instance 'auth-pkg)))
+ (loop for param in (remove-if (lambda (x)
+ (intersection `(,x) '(id)))
+ (sb-introspect:function-lambda-list #'profile-modify-submit-json))
+ do (setf (slot-value user param) (symbol-value param)))
+ (copy-from-record instance user)
+ (update-user auth-pkg user)
+ (set-user user)
+ (setf (session-value :message) "Profile saved successfully."))
+ (setf (session-value :errormsg) "An error occured."))))))
diff --git a/lisp/service/rest-service.lisp b/lisp/service/rest-service.lisp
new file mode 100644
index 0000000..92274d6
--- /dev/null
+++ b/lisp/service/rest-service.lisp
@@ -0,0 +1,41 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defclass rest-service (base-service)
+ ((location :initarg :location
+ :initform nil
+ :accessor location)
+ (location-p :initarg :location-p
+ :initform t
+ :accessor location-p)
+ (errormsg :initarg :errormsg
+ :initform nil
+ :accessor errormsg)
+ (message :initarg :message
+ :initform nil
+ :accessor message))
+ (:documentation ""))
+
+(defmethod initialize-instance :after ((rest-service rest-service) &key)
+ (when (location-p rest-service)
+ (if (message rest-service)
+ (setf (session-value :message) (message rest-service))
+ (setf (message rest-service) (session-value :message)))
+ (if (errormsg rest-service)
+ (setf (session-value :errormsg) (errormsg rest-service))
+ (setf (errormsg rest-service) (session-value :errormsg)))
+ (when (null (location rest-service))
+ (setf (location rest-service) (type-to-path rest-service)))))
+
+(defun location-json (&optional (location "/home"))
+ (format nil "{\"location\":\"~a\"}" location))
+
+(defun type-to-path (rest-type)
+ (concatenate 'string "/" (ppcre:regex-replace "-service$" (string-downcase (type-of rest-type)) "")))
+
+(defmethod sanitize-rest-json ((rest-service rest-service))
+ (loop for slot in (intersection '(org-ckons-session::*session-key *table *where-expression pwd)
+ (org-ckons-core::map-slot-names rest-service))
+ do (setf (slot-value rest-service slot) nil)))
diff --git a/lisp/service/users-service.lisp b/lisp/service/users-service.lisp
new file mode 100644
index 0000000..a626971
--- /dev/null
+++ b/lisp/service/users-service.lisp
@@ -0,0 +1,282 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defclass users-service (auth-service)
+ ((title :initarg :title
+ :initform nil
+ :accessor title))
+ (:documentation ""))
+
+(defun users-json ()
+ (with-auth (instance users-service "users-view")
+ (setf (title instance) "Users")))
+
+(defclass users/view-service (users-service user)
+ ((users :initarg :users
+ :initform nil
+ :accessor users)
+ (location-p :initform nil))
+ (:documentation ""))
+
+(defun users-view-json ()
+ (with-auth (instance users/view-service "users-view")
+ (with-resume-database
+ (let ((auth-pkg (make-instance 'auth-pkg)))
+ (setf (users instance) (get-all-users auth-pkg))))
+ (sanitize-rest-json instance)
+ (loop for user in (users instance)
+ do (sanitize-json user))))
+
+(defclass users/add-service (users/view-service)
+ ((form :initarg :form
+ :initform nil
+ :accessor form))
+ (:documentation ""))
+
+(defun role-checkboxes (role-groups &optional active-role-groups)
+ (remove-if 'null
+ (mapcar (lambda (role-group)
+ (when (not (intersection `(,(name role-group)) `("_Public" "profile-admin") :test 'string=))
+ (let ((checked (when (intersection `(,(name role-group))
+ (mapcar (lambda (x)
+ (name x))
+ active-role-groups)
+ :test 'string=)
+ '(:checked "checked" :value "on"))))
+ (remove-if 'null `(:name ,(format nil "chk_~a" (name role-group)) :label ,(name role-group) :field-type "checkbox" ,@checked)))))
+ role-groups)))
+
+(defun users-add-json ()
+ (with-auth (instance users/add-service "users-modify")
+ (setf (title instance) "Users - Add")
+ (with-resume-database
+ (let* ((auth-pkg (make-instance 'auth-pkg))
+ (user (get-user))
+ (role-groups (get-all-role-groups auth-pkg user)))
+ (setf (form instance) (make-form "users-add-form"
+ nil
+ t
+ `((:name "first_name" :label "First Name" :field-type "text" :required "required")
+ (:name "last_name" :label "Last Name" :field-type "text" :required "required")
+ (:name "email" :label "Email" :field-type "text" :required "required")
+ ,@(role-checkboxes role-groups)
+ (:label "Add User" :field-type "button" :onclick "on_users_add_submit_clicked()"))))))))
+
+(defun users-add-submit-json (role_groups first_name last_name email)
+ (declare (special role_groups first_name last_name email))
+ (with-auth (instance users/add-service "users-modify")
+ (with-resume-database
+ (let ((registration (make-instance 'registration)))
+ (loop for param in (sb-introspect:function-lambda-list #'users-add-submit-json)
+ do (setf (slot-value registration param) (symbol-value param)))
+ (let* ((auth-pkg (make-instance 'auth-pkg))
+ (id (insert-registration auth-pkg registration)))
+ (setf registration (get-registration-by-id auth-pkg id)))
+ (handler-case
+ (let ((text-message (format nil
+ "Hello ~a ~a and welcome to the ~a website!~%~%A user account registration has been created for you. It expires in 3 days.~%~%Please click the following link to complete the registration:~%~%~a://~a/register?hash=~a~%"
+ (name *webapp*)
+ (first_name registration)
+ (last_name registration)
+ (scheme *webapp*)
+ (url *webapp*)
+ (hash registration)))
+ (html-message (org-ckons-http::html5
+ `(html
+ ((p)
+ ,(format nil
+ "Hello ~a ~a and welcome to ~a!"
+ (name *webapp*)
+ (first_name registration)
+ (last_name registration)))
+ ((p) "A user account registration has been created for you. It expires in 3 days.")
+ ((p) "Please click the following link to complete the registration:")
+ ((p)
+ ((a :href ,(format nil
+ "~a://~a/register?hash=~a"
+ (scheme *webapp*)
+ (url *webapp*)
+ (hash registration)))
+ ,(format nil
+ "~a://~a/register?hash=~a"
+ (scheme *webapp*)
+ (url *webapp*)
+ (hash registration))))))))
+ (org-ckons-core::sendmail (mail-mx *webapp*)
+ (mail-info *webapp*)
+ (email registration)
+ (format nil "~a website registration" (name *webapp*))
+ text-message
+ :html-message html-message
+ :reply-to (mail-postmaster *webapp*)
+ :ssl (mail-ssl *webapp*)
+ :authentication (mail-authentication *webapp*))
+ (setf (session-value :message) (format nil "Email sent successfully to ~a" (email registration))))
+ (error (e)
+ (setf (session-value :errormsg) (format nil "Error sending email to ~a. Registration failed. ~a" (email registration) e))))))))
+
+(defclass users/register-service (rest-service)
+ ((title :initarg :title
+ :initform nil
+ :accessor title)
+ (hash :initarg :hash
+ :initform nil
+ :accessor hash))
+ (:documentation ""))
+
+(defun users-register-json (hash)
+ (with-noauth (instance users/register-service)
+ (setf (title instance) "New User Registration")
+ (setf (hash instance) hash)))
+
+(defclass users/register/form-service (rest-service)
+ ((form :initarg :form
+ :initform nil
+ :accessor form)
+ (location-p :initform nil))
+ (:documentation ""))
+
+(defun users-register-form-json (hash)
+ (with-noauth (instance users/register/form-service)
+ (with-resume-database
+ (let* ((auth-pkg (make-instance 'auth-pkg))
+ (registration (get-registration-by-hash auth-pkg hash)))
+ (registrations-gc auth-pkg)
+ (if registration
+ (progn
+ (setf (form instance)
+ (make-form "users-register-form"
+ nil
+ t
+ `((:name "hash" :field-type "hidden" :value ,(hash registration) :required "required")
+ (:name "username" :label "Username" :field-type "text" :required "required")
+ (:name "pwd" :label "Password" :field-type "password" :required "required")
+ (:name "pwd2" :label "Password (again)" :field-type "password" :required "required")
+ (:name "phone" :label "Phone" :field-type "text")
+ (:label "Register" :field-type "button" :onclick "on_users_register_submit_clicked()"))))
+ (setf (session-value :message) "Registered successfully."))
+ (setf (session-value :errormsg) "Error: invalid registration."))))))
+
+(defclass users/register/submit-service (rest-service)
+ ((location-p :initform nil))
+ (:documentation ""))
+
+(defun users-register-submit-json (hash username pwd pwd2 phone)
+ (with-noauth (instance users/register/submit-service)
+ (with-resume-database
+ (let ((auth-pkg (make-instance 'auth-pkg))
+ registration)
+ (registrations-gc auth-pkg)
+ (setf registration (get-registration-by-hash auth-pkg hash))
+ (org-ckons-json::objects-to-json
+ `(,(if registration
+ (if (string= pwd pwd2)
+ (let ((user (make-instance 'user
+ :username username
+ :pwd pwd
+ :first_name (first_name registration)
+ :last_name (last_name registration)
+ :email (email registration)
+ :phone phone
+ :active t)))
+ (if (insert-user auth-pkg user)
+ (progn
+ (loop for role-group in (union '("profile-admin")
+ (cl-ppcre:split "\\|" (role_groups registration))
+ :test 'string=)
+ do (insert-user-role-group auth-pkg (make-instance 'user-role
+ :user_id (id user)
+ :role_group_name role-group)))
+ (delete-registration auth-pkg hash)
+ (setf (session-value :message) "Registration completed successfully."))
+ (setf (session-value :errormsg) "Error while completing registration.")))
+ (setf (session-value :errormsg) "Error: passwords do not match."))
+ (setf (session-value :errormsg) "Error while completing registration."))))))))
+
+(defclass users/modify-service (users/add-service)
+ ()
+ (:documentation ""))
+
+(defun users-modify-json (id)
+ (with-auth (instance users/modify-service "users-modify")
+ (setf (title instance) "Users - Modify")
+ (with-resume-database
+ (let* ((auth-pkg (make-instance 'auth-pkg))
+ (me (get-user))
+ (user (get-user-by-id auth-pkg id))
+ (role-groups (get-all-role-groups auth-pkg me))
+ (active-role-groups (get-active-role-groups auth-pkg user)))
+ (if user
+ (setf (form instance) (make-form "users-modify-form"
+ nil
+ t
+ `((:name "id" :field-type "hidden" :value ,(id user) :required "required")
+ (:name "username" :label "Username" :field-type "text" :value ,(username user) :required "required")
+ (:name "first_name" :label "First Name" :field-type "text" :value ,(first_name user) :required "required")
+ (:name "last_name" :label "Last Name" :field-type "text" :value ,(last_name user) :required "required")
+ (:name "email" :label "Email" :field-type "text" :value ,(email user) :required "required")
+ (:name "phone" :label "Phone" :field-type "text" :value ,(phone user))
+ ,@(role-checkboxes role-groups active-role-groups)
+ (:label "Modify User" :field-type "button" :onclick "on_users_modify_submit_clicked()"))))
+ (setf (session-value :errormsg) "Error: could not modify user. Not found."))))))
+
+(defun users-modify-submit-json (id role_groups username first_name last_name email phone)
+ (declare (special role_groups username first_name last_name email phone))
+ (with-auth (instance users/modify-service "users-modify")
+ (with-resume-database
+ (let* ((auth-pkg (make-instance 'auth-pkg))
+ (user (get-active-user-by-id auth-pkg id)))
+ (if user
+ (progn
+ (loop for param in (remove-if (lambda (x)
+ (intersection `(,x) '(id role_groups)))
+ (sb-introspect:function-lambda-list #'users-modify-submit-json))
+ do (setf (slot-value user param) (symbol-value param)))
+ (update-user auth-pkg user)
+ (delete-role-groups auth-pkg user)
+ (loop for role-group in (union '("profile-admin")
+ (cl-ppcre:split "\\|" role_groups)
+ :test 'string=)
+ do (insert-user-role-group auth-pkg (make-instance 'user-role
+ :user_id (id user)
+ :role_group_name role-group)))
+ (setf (session-value :message) "User saved successfully."))
+ (setf (session-value :errormsg) "An error occured."))))))
+
+(defclass users/toggle-service (users/add-service)
+ ()
+ (:documentation ""))
+
+(defun users-toggle-active-json (id)
+ (with-auth (instance users/toggle-service "users-modify")
+ (with-resume-database
+ (let* ((auth-pkg (make-instance 'auth-pkg))
+ (user (get-user-by-id auth-pkg id)))
+ (if user
+ (if (= (id user) (id (get-user)))
+ (setf (session-value :errormsg) "Error: you may not toggle your own active state.")
+ (progn
+ (user-toggle-active auth-pkg user)
+ (setf (session-value :errormsg) nil)
+ (setf (session-value :message) "User active state toggled successfully.")))
+ (setf (session-value :errormsg) "Error: could not toggle the active state of the user: not found."))))))
+
+(defclass users/delete-service (users/add-service)
+ ()
+ (:documentation ""))
+
+(defun users-delete-json (id)
+ (with-auth (instance users/delete-service "users-modify")
+ (with-resume-database
+ (let* ((auth-pkg (make-instance 'auth-pkg))
+ (user (get-user-by-id auth-pkg id)))
+ (if user
+ (if (= (id user) (id (get-user)))
+ (setf (session-value :errormsg) "Error: you may not delete yourself.")
+ (progn
+ (deactivate-user auth-pkg user)
+ (setf (session-value :errormsg) nil)
+ (setf (session-value :message) "User deleted successfully.")))
+ (setf (session-value :errormsg) "Error: could not delete user: not found."))))))
diff --git a/lisp/sql/about-us.lisp b/lisp/sql/about-us.lisp
new file mode 100644
index 0000000..43c7216
--- /dev/null
+++ b/lisp/sql/about-us.lisp
@@ -0,0 +1,18 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defclass about-us (postgres-record)
+ ((id :initarg :id
+ :initform nil
+ :accessor id)
+ (category :initarg :category
+ :initform nil
+ :accessor category)
+ (content :initarg :content
+ :initform nil
+ :accessor content)
+ (*table :initform "general.about_us")
+ (*where-expression :initform "id = ~a"))
+ (:documentation "Holds the data for a general.about_us type."))
diff --git a/lisp/sql/auth-pkg.lisp b/lisp/sql/auth-pkg.lisp
new file mode 100644
index 0000000..8bb6aef
--- /dev/null
+++ b/lisp/sql/auth-pkg.lisp
@@ -0,0 +1,205 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defparameter *authenticated-user-session-key* "authenticated-user")
+
+(defclass auth-pkg (record-pkg)
+ ()
+ (:documentation ""))
+
+(defmethod insert-user ((auth-pkg auth-pkg) user)
+ (setf (*table user) (format nil
+ "auth.insert_user('~a', '~a', '~a', '~a', '~a', '~a', '~a')"
+ (username user)
+ (pwd user)
+ (first_name user)
+ (last_name user)
+ (email user)
+ (phone user)
+ (active user)))
+ (setf (id user) (caar (call-pg-function auth-pkg user)))
+ (id user))
+
+(defmethod update-user ((auth-pkg auth-pkg) user)
+ (setf (*table user) (format nil
+ "auth.update_user(~a, '~a', '~a', '~a', '~a', '~a')"
+ (id user)
+ (username user)
+ (first_name user)
+ (last_name user)
+ (email user)
+ (phone user)))
+ (caar (call-pg-function auth-pkg user)))
+
+(defmethod get-all-active-users ((auth-pkg auth-pkg))
+ (let ((user (make-instance 'user)))
+ (setf (*table user) "auth.get_all_active_users()")
+ (get-records auth-pkg user "first_name asc, last_name asc")))
+
+(defmethod get-all-users ((auth-pkg auth-pkg))
+ (let ((user (make-instance 'user)))
+ (setf (*table user) "auth.get_all_users()")
+ (get-records auth-pkg user "first_name asc, last_name asc")))
+
+(defmethod get-active-user-by-username-pwd ((auth-pkg auth-pkg) username pwd)
+ (let ((user (make-instance 'user)))
+ (setf (*table user) (format nil
+ "auth.get_active_user_by_username_pwd('~a', '~a')"
+ username
+ pwd))
+ (get-record auth-pkg user)))
+
+(defmethod get-active-user-by-id ((auth-pkg auth-pkg) id)
+ (let ((user (make-instance 'user)))
+ (setf (*table user) (format nil "auth.get_active_user_by_id(~a)" id))
+ (get-record auth-pkg user)))
+
+(defmethod get-user-by-id ((auth-pkg auth-pkg) id)
+ (let ((user (make-instance 'user)))
+ (setf (*table user) (format nil "auth.get_user_by_id(~a)" id))
+ (get-record auth-pkg user)))
+
+(defmethod user-toggle-active ((auth-pkg auth-pkg) (user user))
+ (setf (*table user) (format nil "auth.user_toggle_active(~a)" (id user)))
+ (call-pg-function auth-pkg user))
+
+(defmethod deactivate-user ((auth-pkg auth-pkg) (user user))
+ (setf (*table user) (format nil "auth.user_delete(~a)" (id user)))
+ (call-pg-function auth-pkg user))
+
+(defmethod get-all-roles ((auth-pkg auth-pkg) (user user))
+ (let ((user-role (make-instance 'user-role
+ :*table (format nil
+ "auth.get_all_roles_for_user(~a)"
+ (id user)))))
+ (get-records auth-pkg user-role nil)))
+
+(defmethod has-role ((auth-pkg auth-pkg) (user user) role)
+ (let ((user-role (make-instance 'user-role)))
+ (setf (*table user-role) (format nil (*table user-role) (id user) role))
+ (get-record auth-pkg user-role)))
+
+(defmethod get-all-role-groups ((auth-pkg auth-pkg) (user user))
+ (let ((role-group (make-instance 'role-group)))
+ (setf (*table role-group) (format nil (*table role-group) (id user)))
+ (get-records auth-pkg role-group nil)))
+
+(defmethod get-role-group-by-name ((auth-pkg auth-pkg) (user user) name)
+ (find-if (lambda (x)
+ (string= name (name x)))
+ (get-all-role-groups auth-pkg user)))
+
+(defmethod get-active-role-groups ((auth-pkg auth-pkg) (user user))
+ (let ((role-group (make-instance 'role-group
+ :*table (format nil
+ "auth.get_active_role_groups_for_user(~a)"
+ (id user)))))
+ (get-records auth-pkg role-group nil)))
+
+(defmethod insert-user-role-group ((auth-pkg auth-pkg) (user-role user-role))
+ (setf (*table user-role) (format nil
+ "auth.insert_user_role_group(~a, '~a')"
+ (user_id user-role)
+ (role_group_name user-role)))
+ (caar (call-pg-function auth-pkg user-role)))
+
+(defmethod insert-registration ((auth-pkg auth-pkg) (registration registration))
+ (setf (*table registration) (format nil
+ "auth.insert_registration('~a', '~a', '~a', '~a')"
+ (first_name registration)
+ (last_name registration)
+ (email registration)
+ (role_groups registration)))
+ (setf (id registration) (caar (call-pg-function auth-pkg registration)))
+ (id registration))
+
+(defmethod delete-role-groups ((auth-pkg auth-pkg) user)
+ (let ((role-group (make-instance 'role-group
+ :*table (format nil
+ "auth.delete_role_groups_for_user(~a)"
+ (id user)))))
+ (call-pg-function auth-pkg role-group)))
+
+(defmethod get-registration-by-id ((auth-pkg auth-pkg) id)
+ (let ((registration (make-instance 'registration
+ :*table (format nil "auth.get_registration_by_id(~a)" id))))
+ (get-record auth-pkg registration)))
+
+(defmethod get-registration-by-hash ((auth-pkg auth-pkg) hash)
+ (let ((registration (make-instance 'registration
+ :*table (format nil "auth.get_registration_by_hash('~a')" hash))))
+ (get-record auth-pkg registration)))
+
+(defmethod registrations-gc ((auth-pkg auth-pkg))
+ (let ((registration (make-instance 'registration :*table "auth.registrations_gc()")))
+ (call-pg-function auth-pkg registration)))
+
+(defmethod delete-registration ((auth-pkg auth-pkg) hash)
+ (let ((registration (make-instance 'registration
+ :*table (format nil "auth.delete_registration('~a')" hash))))
+ (call-pg-function auth-pkg registration)))
+
+(defmethod update-profile ((auth-pkg auth-pkg) (user user))
+ (setf (*table user) (format nil
+ "auth.update_profile(~a, '~a', '~a', '~a', '~a', '~a')"
+ (id user)
+ (username user)
+ (first_name user)
+ (last_name user)
+ (email user)
+ (phone user)))
+ (call-pg-function auth-pkg user))
+
+(defmethod update-password ((auth-pkg auth-pkg) (user user))
+ (setf (*table user) (format nil "auth.update_password(~a, '~a')" (id user) (pwd user)))
+ (call-pg-function auth-pkg user))
+
+(defmacro with-valid-user ((session-name roles) error-body &body body)
+ "Runs `body' if there is a valid authenticated `user' in the
+`user-session' whose roles match `roles', otherwise runs
+`error-body'. If a valid `user' exists, it will be bound to
+`session-name'."
+ `(let ((,session-name (get-session-object *authenticated-user-session-key*)))
+ (if ,session-name
+ (let* ((auth-pkg (make-instance 'auth-pkg))
+ (has-all-roles-p (let ((has-all-roles-p t))
+ (with-resume-database
+ (loop for role in (if (listp ,roles) ,roles (list ,roles))
+ do (when (not (has-role auth-pkg ,session-name role))
+ (setf has-all-roles-p nil)))
+ has-all-roles-p))))
+ (if has-all-roles-p
+ ,@body
+ ,error-body))
+ ,error-body)))
+
+(defun make-default-user ()
+ "Convenience function for making a new instance of `user' that has
+its session key set to `authenticated-user', but has no privileges."
+ (make-instance 'user
+ :*session-key *authenticated-user-session-key*
+ :id 0
+ :first_name "Guest"
+ :last_name "User"))
+
+(defun ensure-user-exists ()
+ "Ensures that there is an `authenticated-user' in the user session,
+even if it's just a guest user."
+ (let ((user (get-session-object *authenticated-user-session-key*)))
+ (unless user
+ (setf user (make-default-user))
+ (set-user user))))
+
+(defun get-user ()
+ "Convenience function for getting the `authenticated-user' from the
+user session."
+ (get-session-object *authenticated-user-session-key*))
+
+(defun set-user (user)
+ "Convenience function for setting the `authenticated-user' into the
+user session."
+ (setf (*table user) "auth.users")
+ (setf (pwd user) nil)
+ (set-session-object *authenticated-user-session-key* user))
diff --git a/lisp/sql/contact-pkg.lisp b/lisp/sql/contact-pkg.lisp
new file mode 100644
index 0000000..dce6384
--- /dev/null
+++ b/lisp/sql/contact-pkg.lisp
@@ -0,0 +1,32 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defclass contact-pkg (record-pkg)
+ ()
+ (:documentation ""))
+
+(defmethod get-contact-us-posts ((contact-pkg contact-pkg) user-id read-p)
+ (let ((contact-us-post (make-instance 'contact-us-post)))
+ (setf (*table contact-us-post) (format nil (*table contact-us-post) user-id (if read-p "t" "f")))
+ (get-records contact-pkg contact-us-post nil)))
+
+(defmethod insert-contact-us-post ((contact-pkg contact-pkg) (contact-us-post contact-us-post))
+ (setf (*table contact-us-post) (format nil
+ "contact.insert_contact_us_post('~a', '~a', '~a', '~a', '~a')"
+ (first_name contact-us-post)
+ (last_name contact-us-post)
+ (email contact-us-post)
+ (phone contact-us-post)
+ (comments contact-us-post)))
+ (caar (call-pg-function contact-pkg contact-us-post)))
+
+(defmethod mark-contact-us-post ((contact-pkg contact-pkg) contact-us-post-id user-id read-p)
+ (let ((contact-us-post (make-instance 'contact-us-post
+ :*table (format nil
+ "contact.mark_contact_us_post(~a, ~a, '~a')"
+ contact-us-post-id
+ user-id
+ (if read-p "t" "f")))))
+ (call-pg-function contact-pkg contact-us-post)))
diff --git a/lisp/sql/contact-us.lisp b/lisp/sql/contact-us.lisp
new file mode 100644
index 0000000..07854bc
--- /dev/null
+++ b/lisp/sql/contact-us.lisp
@@ -0,0 +1,50 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defclass contact-us (postgres-record)
+ ((id :initarg :id
+ :initform nil
+ :accessor id)
+ (content :initarg :content
+ :initform nil
+ :accessor content)
+ (*table :initform "contact.contact_us")
+ (*where-expression :initform "id = ~a"))
+ (:documentation "Holds the data for a contact.contact_us type."))
+
+(defclass contact-us-post (postgres-record)
+ ((id :initarg :id
+ :initform nil
+ :accessor id)
+ (first_name :initarg :first_name
+ :initform nil
+ :accessor first_name)
+ (last_name :initarg :last_name
+ :initform nil
+ :accessor last_name)
+ (email :initarg :email
+ :initform nil
+ :accessor email)
+ (phone :initarg :phone
+ :initform nil
+ :accessor phone)
+ (submitted :initarg :submitted
+ :initform nil)
+ (comments :initarg :comments
+ :initform nil
+ :accessor comments)
+ (*table :initform "contact.get_contact_us_posts_by_id_and_read(~a, '~a')")
+ (*where-expression :initform nil))
+ (:documentation "Holds the data for a contact.contact_us_posts type."))
+
+(defmethod submitted ((contact-us-post contact-us-post))
+ (slot-value contact-us-post 'submitted))
+
+(defmethod (setf submitted) (value (contact-us-post contact-us-post))
+ (handler-case
+ (setf (slot-value contact-us-post 'submitted) (simple-date-to-date value))
+ (error (e)
+ (declare (ignore e))
+ (setf (slot-value contact-us-post 'submitted) nil))))
diff --git a/lisp/sql/general-pkg.lisp b/lisp/sql/general-pkg.lisp
new file mode 100644
index 0000000..0c7421c
--- /dev/null
+++ b/lisp/sql/general-pkg.lisp
@@ -0,0 +1,11 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defclass general-pkg (record-pkg)
+ ()
+ (:documentation ""))
+
+(defmethod get-about-us ((general-pkg general-pkg) category)
+ (get-record general-pkg (make-instance 'about-us :category category)))
diff --git a/lisp/sql/generics.lisp b/lisp/sql/generics.lisp
new file mode 100644
index 0000000..3118823
--- /dev/null
+++ b/lisp/sql/generics.lisp
@@ -0,0 +1,143 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defmacro with-resume-database (&body body)
+ `(with-database (getf (databases *webapp*) :db-resume) ,@body))
+
+(defgeneric insert-user (auth-pkg user)
+ (:documentation "Inserts a new user into the database."))
+
+(defgeneric update-user (auth-pkg user)
+ (:documentation "Updates a user in the database."))
+
+(defgeneric get-all-active-users (record-pkg)
+ (:documentation "Calls `get_all_active_users'."))
+
+(defgeneric get-all-users (record-pkg)
+ (:documentation "Calls `get_all_users'."))
+
+(defgeneric get-active-user-by-username-pwd (record-pkg username pwd)
+ (:documentation "Calls `get_active_user_by_username_pwd'."))
+
+(defgeneric get-active-user-by-id (record-pkg id)
+ (:documentation "Calls `get_active_user_by_id'."))
+
+(defgeneric get-user-by-id (record-pkg id)
+ (:documentation "Calls `get_user_by_id'."))
+
+(defgeneric user-toggle-active (record-pkg user)
+ (:documentation "Calls `user_toggle_active'."))
+
+(defgeneric user-delete (record-pkg user)
+ (:documentation "Calls `user_delete'."))
+
+(defgeneric get-all-roles (record-pkg record)
+ (:documentation "Returns all the roles for a `user'."))
+
+(defgeneric has-role (record-pkg record role)
+ (:documentation "Returns `t' when the user exists and has the
+specified `role', `nil' otherwise."))
+
+(defgeneric get-all-role-groups (auth-pkg user)
+ (:documentation "Returns all the role-groups for a `user'."))
+
+(defgeneric get-role-group-by-name (auth-pkg user name)
+ (:documentation "Returns a role-group by `name' for a `user'."))
+
+(defgeneric get-active-role-groups (auth-pkg user)
+ (:documentation "Returns all the role-groups for a `user' that are
+actually present in `auth.users_role_groups', in other words the
+role-groups that are assigned to the user without any superuser
+magic."))
+
+(defgeneric insert-user-role-group (auth-pkg user-role)
+ (:documentation "Idempotently inserts a new record into
+auth.user_role_groups based on the user ID and the role group name."))
+
+(defgeneric delete-role-groups (auth-pkg user)
+ (:documentation "Delete all role-group assignments for a user."))
+
+(defgeneric update-profile (record-pkg record)
+ (:documentation ""))
+
+(defgeneric update-password (auth-pkg user)
+ (:documentation ""))
+
+(defgeneric get-user-sessions (record-pkg)
+ (:documentation "Gets all user session records."))
+
+(defgeneric get-user-session (record-pkg &optional sessionid)
+ (:documentation ""))
+
+(defgeneric update-timestamp (record-pkg record)
+ (:documentation "Updates the timestamp of the `user-session'."))
+
+(defgeneric get-user-session-objects (record-pkg record)
+ (:documentation "Get all user session objects associated with a user
+session."))
+
+(defgeneric get-user-session-object (record-pkg session-key)
+ (:documentation ""))
+
+(defgeneric flush-user-session-object (record-pkg session-key)
+ (:documentation ""))
+
+(defgeneric create-user-session (record-pkg)
+ (:documentation "Creates a new user session and returns the
+sessionid."))
+
+(defgeneric get-about-us (record-pkg category)
+ (:documentation "Gets the one and only general.about_us record with the given
+`category'."))
+
+(defgeneric get-contact-us-posts (record-pkg user-id read-p)
+ (:documentation "Gets contact.contact_us_posts records."))
+
+(defgeneric insert-contact-us-post (contact-pkg contact-us-post)
+ (:documentation "Inserts a new contact-us post into
+contact.contact_us_posts. Returns the ID of the new record."))
+
+(defgeneric mark-contact-us-post (contact-pkg contact-us-post-id user-id read-p)
+ (:documentation "Marks or unmarks a contact-us post as read."))
+
+(defgeneric insert-registration (auth-pkg registration)
+ (:documentation "Inserts a new registration into
+auth.registrations. Returns the ID of the new record."))
+
+(defgeneric get-registration-by-id (auth-pkg id)
+ (:documentation "Gets the record from auth.registrations with the
+given `id'."))
+
+(defgeneric get-registration-by-hash (auth-pkg hash)
+ (:documentation "Gets the record from auth.registrations with the
+given `hash'."))
+
+(defgeneric registrations-gc (auth-pkg)
+ (:documentation "Garbage collects registrations that are more than 3
+days old."))
+
+(defgeneric delete-registration (auth-pkg hash)
+ (:documentation "Deletes a registration with the given `hash'."))
+
+(defgeneric event_date (record)
+ (:documentation "Reader for the event_date field. Converts a
+local-time::timestamp to a SQL date string."))
+
+(defgeneric (setf event_date) (value record)
+ (:documentation "Writer for the event_date field."))
+
+(defgeneric submitted (record)
+ (:documentation "Reader for the submitted field. Converts a
+local-time::timestamp to a SQL date string."))
+
+(defgeneric (setf submitted) (value record)
+ (:documentation "Writer for the submitted field."))
+
+(defgeneric created (record)
+ (:documentation "Reader for the created field. Converts a
+local-time::timestamp to a SQL date string."))
+
+(defgeneric (setf created) (value record)
+ (:documentation "Writer for the created field."))
diff --git a/lisp/sql/registration.lisp b/lisp/sql/registration.lisp
new file mode 100644
index 0000000..5ca227c
--- /dev/null
+++ b/lisp/sql/registration.lisp
@@ -0,0 +1,42 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defclass registration (postgres-record)
+ ((id :initarg :id
+ :initform nil
+ :accessor id)
+ (hash :initarg :hash
+ :initform nil
+ :accessor hash)
+ (first_name :initarg :first_name
+ :initform nil
+ :accessor first_name)
+ (last_name :initarg :last_name
+ :initform nil
+ :accessor last_name)
+ (email :initarg :email
+ :initform nil
+ :accessor email)
+ (role_groups :initarg :role_groups
+ :initform nil
+ :accessor role_groups)
+ (created :initarg :created
+ :initform nil)
+ (valid_for :initarg :valid_for
+ :initform nil
+ :accessor valid_for)
+ (*table :initform "auth.registrations")
+ (*where-expression :initform "id = ~a"))
+ (:documentation "Holds the data for an auth.registrations type."))
+
+(defmethod created ((registration registration))
+ (slot-value registration 'created))
+
+(defmethod (setf created) (value (registration registration))
+ (handler-case
+ (setf (slot-value registration 'created) (simple-date-to-date value))
+ (error (e)
+ (declare (ignore e))
+ (setf (slot-value registration 'created) nil))))
diff --git a/lisp/sql/role-group.lisp b/lisp/sql/role-group.lisp
new file mode 100644
index 0000000..4cdc99b
--- /dev/null
+++ b/lisp/sql/role-group.lisp
@@ -0,0 +1,24 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defclass role-group (postgres-record)
+ ((user_role_group_id :initarg :user_role_group_id
+ :initform nil
+ :accessor user_role_group_id)
+ (user_id :initarg :user_id
+ :initform nil
+ :accessor user_id)
+ (role_group_id :initarg :role_group_id
+ :initform nil
+ :accessor role_group_id)
+ (name :initarg :name
+ :initform nil
+ :accessor name)
+ (description :initarg :description
+ :initform nil
+ :accessor description)
+ (*table :initform "auth.get_all_role_groups_for_user(~a)")
+ (*where-expression :initform nil))
+ (:documentation "Holds the data for an auth.role_group_t type."))
diff --git a/lisp/sql/user-role.lisp b/lisp/sql/user-role.lisp
new file mode 100644
index 0000000..077f496
--- /dev/null
+++ b/lisp/sql/user-role.lisp
@@ -0,0 +1,36 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defclass user-role (postgres-record)
+ ((user_role_group_id :initarg :user_role_group_id
+ :initform nil
+ :accessor user_role_group_id)
+ (user_id :initarg :user_id
+ :initform nil
+ :accessor user_id)
+ (role_group_id :initarg :role_group_id
+ :initform nil
+ :accessor role_group_id)
+ (role_group_name :initarg :role_group_name
+ :initform nil
+ :accessor role_group_name)
+ (role_group_description :initarg :role_group_description
+ :initform nil
+ :accessor role_group_description)
+ (role_group_role_id :initarg :role_group_role_id
+ :initform nil
+ :accessor role_group_role_id)
+ (role_id :initarg :role_id
+ :initform nil
+ :accessor role_id)
+ (role_name :initarg :role_name
+ :initform nil
+ :accessor role_name)
+ (role_description :initarg :role_description
+ :initform nil
+ :accessor role_description)
+ (*table :initform "auth.has_role(~a, '~a')")
+ (*where-expression :initform nil))
+ (:documentation "Holds the data for an auth.user_role_t type."))
diff --git a/lisp/sql/user-session-pkg.lisp b/lisp/sql/user-session-pkg.lisp
new file mode 100644
index 0000000..45c47d2
--- /dev/null
+++ b/lisp/sql/user-session-pkg.lisp
@@ -0,0 +1,116 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defvar *user-session-mutex* (sb-thread:make-mutex :name "*user-session-mutex*"))
+
+(defclass user-session-pkg (record-pkg)
+ ()
+ (:documentation "Database-backed user session API."))
+
+(defmethod get-user-sessions ((user-session-pkg user-session-pkg))
+ (get-records user-session-pkg (make-instance 'user-session) "sessionid ASC"))
+
+(defmethod get-user-session ((user-session-pkg user-session-pkg) &optional (sessionid *sessionid*))
+ (when sessionid
+ (get-record user-session-pkg (make-instance 'user-session :sessionid sessionid))))
+
+(defmethod update-timestamp ((user-session-pkg user-session-pkg) (user-session user-session))
+ (setf (datetime user-session) (get-universal-time))
+ (update-record user-session-pkg user-session))
+
+(defmethod get-user-session-objects ((user-session-pkg user-session-pkg) (user-session user-session))
+ (get-records user-session-pkg (make-instance 'user-session-object :user_session_id (id user-session)) "session_key ASC"))
+
+(defmethod get-user-session-object ((user-session-pkg user-session-pkg) session-key)
+ (when (and *sessionid* session-key)
+ (let ((user-session (get-user-session user-session-pkg)))
+ (when user-session
+ (get-record user-session-pkg (make-instance 'user-session-object :user_session_id (id user-session) :session_key session-key))))))
+
+(defmethod flush-user-session-object ((user-session-pkg user-session-pkg) session-key)
+ (when (and *sessionid* session-key)
+ (let ((user-session-object (get-user-session-object user-session-pkg session-key)))
+ (when user-session-object
+ (delete-record user-session-pkg user-session-object)))))
+
+(defmethod create-user-session ((user-session-pkg user-session-pkg))
+ (let* ((sessionid (org-ckons-session::generate-sessionid))
+ (user-session (make-instance 'user-session :sessionid sessionid :datetime (get-universal-time))))
+ (insert-record user-session-pkg user-session)
+ sessionid))
+
+(defun get-session-object (session-key)
+ "Returns the object stored in the user session under the given
+`session-key'."
+ (with-resume-database
+ (let* ((user-session-pkg (make-instance 'user-session-pkg))
+ (user-session-object (get-user-session-object user-session-pkg session-key))
+ object)
+ (when user-session-object
+ (setf object (org-ckons-serializable::deserialize (serialization user-session-object)))
+ (setf (org-ckons-session::*session-key object) session-key))
+ object)))
+
+(defun set-session-object (session-key object)
+ "Sets the object into the user-session under the given
+`session-key'. Will not write anything if the session given by
+`*sessionid*' does not exist."
+ (with-resume-database
+ (let* ((user-session-pkg (make-instance 'user-session-pkg))
+ (user-session-object (get-user-session-object user-session-pkg session-key)))
+ (if user-session-object
+ ;; overwrite existing session object with current serialization
+ (progn
+ (setf (serialization user-session-object) (org-ckons-serializable::serialize object :package-name (package-name #.*package*)))
+ (update-record user-session-pkg user-session-object))
+ ;; insert a new object into the session
+ (let ((user-session (get-user-session user-session-pkg)))
+ (when user-session
+ (setf user-session-object (make-instance 'user-session-object
+ :user_session_id (id user-session)
+ :session_key session-key
+ :serialization (org-ckons-serializable::serialize object :package-name (package-name #.*package*))))
+ (insert-record user-session-pkg user-session-object)))))))
+
+(defun flush-session-object (session-key)
+ "Removes the object from the user session under the given
+`session-key'."
+ (with-resume-database
+ (let ((user-session-pkg (make-instance 'user-session-pkg)))
+ (flush-user-session-object user-session-pkg *sessionid* session-key))))
+
+(defun ensure-user-session-exists (&optional force-new-sessionid-p)
+ "Ensures that the user has a valid sessionid cookie. Returns the
+`sessionid'. If the session does exist, update its timestamp."
+ (sb-thread:with-mutex (*user-session-mutex*)
+ (run-garbage-collect-cycle)
+ (with-resume-database
+ (let* ((user-session-pkg (make-instance 'user-session-pkg))
+ (sessionid (when (not force-new-sessionid-p)
+ (org-ckons-session::get-sessionid-from-request)))
+ (user-session (get-user-session user-session-pkg sessionid)))
+ (if user-session
+ (update-timestamp user-session-pkg user-session)
+ (progn
+ (setf sessionid (create-user-session user-session-pkg))
+ (org-ckons-session::set-sessionid-cookie *header-register* sessionid)))
+ sessionid))))
+
+(defun run-garbage-collect-cycle ()
+ "Goes through all the user sessions, expiring any that have remained
+inactive for a period of time determined by the `*session-timeout*'
+variable."
+ (when (> (- (get-universal-time) org-ckons-session::*gc-last-cycle-timestamp*) org-ckons-session::*gc-interval*)
+ (setf org-ckons-session::*gc-last-cycle-timestamp* (get-universal-time))
+ (with-resume-database
+ (let ((user-session-pkg (make-instance 'user-session-pkg)))
+ (loop for user-session in (get-user-sessions user-session-pkg)
+ do (let ((inactive-time (- org-ckons-session::*gc-last-cycle-timestamp* (datetime user-session))))
+ (when (and (> inactive-time org-ckons-session::*session-timeout*)
+ (sessionid user-session))
+ (org-ckons-core::logger (format nil "Deleting expired session: id = [~a] ; sessionid = [~a]" (id user-session) (sessionid user-session)))
+ (loop for user-session-object in (get-user-session-objects user-session-pkg user-session)
+ do (delete-record user-session-pkg user-session-object))
+ (delete-record user-session-pkg user-session))))))))
diff --git a/lisp/sql/user-session.lisp b/lisp/sql/user-session.lisp
new file mode 100644
index 0000000..47ff31c
--- /dev/null
+++ b/lisp/sql/user-session.lisp
@@ -0,0 +1,35 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defclass user-session (postgres-record)
+ ((id :initarg :id
+ :initform nil
+ :accessor id)
+ (sessionid :initarg :sessionid
+ :initform nil
+ :accessor sessionid)
+ (datetime :initarg :datetime
+ :initform nil
+ :accessor datetime)
+ (*table :initform "auth.user_sessions")
+ (*where-expression :initform "id = ~a"))
+ (:documentation "Holds the data for a auth.user_session record."))
+
+(defclass user-session-object (postgres-record)
+ ((id :initarg :id
+ :initform nil
+ :accessor id)
+ (user_session_id :initarg :user_session_id
+ :initform nil
+ :accessor user_session_id)
+ (session_key :initarg :session_key
+ :initform nil
+ :accessor session_key)
+ (serialization :initarg :serialization
+ :initform nil
+ :accessor serialization)
+ (*table :initform "auth.user_session_objects")
+ (*where-expression :initform "id = ~a"))
+ (:documentation "Holds the data for a auth.user_session record."))
diff --git a/lisp/sql/user.lisp b/lisp/sql/user.lisp
new file mode 100644
index 0000000..89d9c7d
--- /dev/null
+++ b/lisp/sql/user.lisp
@@ -0,0 +1,50 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defclass user (org-ckons-session::session-object postgres-record)
+ ((id :initarg :id
+ :initform nil
+ :accessor id)
+ (username :initarg :username
+ :initform nil
+ :accessor username)
+ (pwd :initarg :pwd
+ :initform nil
+ :accessor pwd)
+ (first_name :initarg :first_name
+ :initform nil
+ :accessor first_name)
+ (last_name :initarg :last_name
+ :initform nil
+ :accessor last_name)
+ (email :initarg :email
+ :initform nil
+ :accessor email)
+ (phone :initarg :phone
+ :initform nil
+ :accessor phone)
+ (active :initarg :active
+ :initform nil
+ :accessor active)
+ (created :initarg :created
+ :initform nil)
+ (*table :initform "auth.users")
+ (*where-expression :initform "id = ~a")
+ (org-ckons-session::*session-key :initform "user"))
+ (:documentation "Holds the data for a user record."))
+
+(defmethod created ((user user))
+ (slot-value user 'created))
+
+(defmethod (setf created) (value (user user))
+ (handler-case
+ (setf (slot-value user 'created) (simple-date-to-date value))
+ (error (e)
+ (declare (ignore e))
+ (setf (slot-value user 'created) nil))))
+
+(defmethod sanitize-json ((user user))
+ (setf (pwd user) nil)
+ (call-next-method))
diff --git a/lisp/webapps/generics.lisp b/lisp/webapps/generics.lisp
new file mode 100644
index 0000000..33145ba
--- /dev/null
+++ b/lisp/webapps/generics.lisp
@@ -0,0 +1,12 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defgeneric get-site-file-path (webapp)
+ (:documentation "Builds a full filesystem path to a webapp's site
+file."))
+
+(defgeneric get-pages-file-paths (webapp)
+ (:documentation ""))
+
diff --git a/lisp/webapps/resume/cljs-out b/lisp/webapps/resume/cljs-out
new file mode 120000
index 0000000..7f17999
--- /dev/null
+++ b/lisp/webapps/resume/cljs-out
@@ -0,0 +1 @@
+clojurescript/resume/resources/public/cljs-out \ No newline at end of file
diff --git a/lisp/webapps/resume/clojurescript/resume/.gitignore b/lisp/webapps/resume/clojurescript/resume/.gitignore
new file mode 100644
index 0000000..77c3a20
--- /dev/null
+++ b/lisp/webapps/resume/clojurescript/resume/.gitignore
@@ -0,0 +1,16 @@
+target
+classes
+resources
+checkouts
+pom.xml
+pom.xml.asc
+*.jar
+*.class
+.lein-*
+.nrepl-port
+.rebel_readline_history
+.hgignore
+.hg
+figwheel-main.edn
+figwheel_server.log
+.rebel_readline_history
diff --git a/lisp/webapps/resume/clojurescript/resume/dev.cljs.edn b/lisp/webapps/resume/clojurescript/resume/dev.cljs.edn
new file mode 100644
index 0000000..b2a7ff0
--- /dev/null
+++ b/lisp/webapps/resume/clojurescript/resume/dev.cljs.edn
@@ -0,0 +1,2 @@
+^{:watch-dirs ["src"]}
+{:main resume.core}
diff --git a/lisp/webapps/resume/clojurescript/resume/project.clj b/lisp/webapps/resume/clojurescript/resume/project.clj
new file mode 100644
index 0000000..a19d784
--- /dev/null
+++ b/lisp/webapps/resume/clojurescript/resume/project.clj
@@ -0,0 +1,24 @@
+(defproject resume.core "0.1.0-SNAPSHOT"
+ :description "FIXME"
+ :url "FIXME"
+ :license "public domain"
+ :min-lein-version "2.7.1"
+ :dependencies [[org.clojure/clojure "LATEST"]
+ [org.clojure/clojurescript "LATEST"]
+ [cljs-ajax "LATEST"]
+ [prismatic/dommy "LATEST"]
+ [hiccups "LATEST"]
+ [cljsjs/showdown "LATEST"]
+ [com.andrewmcveigh/cljs-time "LATEST"]
+ [cljsjs/react "LATEST"]
+ [cljsjs/react-dom "LATEST"]
+ [cljsjs/react-dom-server "LATEST"]
+ [reagent "LATEST"]]
+ :source-paths ["src"]
+ :aliases {"fig:build" ["trampoline" "run" "-m" "figwheel.main" "-b" "dev" "-r"]
+ "fig:min" ["run" "-m" "figwheel.main" "-O" "advanced" "-bo" "dev"]}
+ :profiles {:dev {:dependencies [[com.bhauman/figwheel-main "LATEST"]
+ [org.slf4j/slf4j-nop "LATEST"]
+ [com.bhauman/rebel-readline-cljs "LATEST"]]
+ :resource-paths ["target"]
+ :clean-targets ^{:protect false} ["target"]}})
diff --git a/lisp/webapps/resume/clojurescript/resume/src/core.cljs b/lisp/webapps/resume/clojurescript/resume/src/core.cljs
new file mode 100644
index 0000000..7010a88
--- /dev/null
+++ b/lisp/webapps/resume/clojurescript/resume/src/core.cljs
@@ -0,0 +1,1073 @@
+(ns resume.core
+ (:require-macros [hiccups.core :as hiccups :refer [html]])
+ (:require [ajax.core :refer [GET POST PUT DELETE raw-response-format]]
+ [dommy.core :as dommy]
+ [hiccups.runtime :as hiccupsrt]
+ [cljsjs.showdown :as showdown]
+ [clojure.string :as str]
+ [cljs-time.format :as time-format]
+ [reagent.core :as r]
+ [reagent.dom :as rd]
+ [reagent.dom.client :as rdc]
+ [org-ckons-cljs.notifications.react :as ck-notifications]
+ [org-ckons-cljs.form.core :as ck-form]))
+
+;; declarations
+
+(declare date-sql-to-pretty)
+(declare markdown-to-html)
+(declare reduce-checkboxes)
+(declare comp-app)
+(declare start-render)
+(declare start-location)
+(declare reset-location)
+(declare start)
+(declare notifications)
+(declare auth-notifications)
+(declare comp-menu-main)
+(declare comp-menu-user)
+(declare handler-menu)
+(declare handler-menu-user)
+(declare render-menu)
+(declare comp-home)
+(declare handler-home)
+(declare render-home)
+(declare template-login)
+(declare handler-login)
+(declare render-login)
+(declare on-login-submit-clicked)
+(declare handler-login-authenticate)
+(declare render-login-authenticate)
+(declare handler-logout)
+(declare render-logout)
+(declare template-profile)
+(declare handler-profile)
+(declare render-profile)
+(declare template-profile-view)
+(declare handler-profile-view)
+(declare render-profile-view)
+(declare on-profile-modify-clicked)
+(declare template-profile-modify)
+(declare handler-profile-modify)
+(declare render-profile-modify)
+(declare on-profile-modify-submit-clicked)
+(declare handler-profile-modify-submit)
+(declare render-profile-modify-submit)
+(declare template-password)
+(declare handler-password)
+(declare render-password)
+(declare on-password-submit-clicked)
+(declare handler-password-submit)
+(declare render-password-submit)
+(declare template-about-us)
+(declare handler-about-us)
+(declare render-lessons)
+(declare template-about-us-view)
+(declare handler-about-us-view)
+(declare render-about-us-view)
+(declare on-about-us-modify-clicked)
+(declare template-about-us-modify)
+(declare handler-about-us-modify)
+(declare render-about-us-modify)
+(declare on-about-us-modify-submit-clicked)
+(declare handler-about-us-modify-submit)
+(declare render-about-us-modify-submit)
+(declare template-contact-us)
+(declare handler-contact-us)
+(declare render-contact-us)
+(declare template-contact-us-view)
+(declare handler-contact-us-view)
+(declare render-contact-us-view)
+(declare on-contact-us-email-submit-clicked)
+(declare handler-contact-us-email-submit)
+(declare render-contact-us-email-submit)
+(declare template-messages)
+(declare handler-messages)
+(declare render-messages)
+(declare on-messages-mode-clicked)
+(declare template-messages-results)
+(declare handler-messages-results)
+(declare render-messages-results)
+(declare on-messages-mark)
+(declare handler-messages-mark)
+(declare render-messages-mark)
+(declare template-users)
+(declare handler-users)
+(declare render-users)
+(declare template-users-view)
+(declare handler-users-view)
+(declare render-users-view)
+(declare on-users-add-clicked)
+(declare template-users-add)
+(declare handler-users-add)
+(declare render-users-add)
+(declare on-users-add-submit-clicked)
+(declare handler-users-add-submit)
+(declare render-users-add-submit)
+(declare template-users-register)
+(declare handler-users-register)
+(declare render-users-register)
+(declare template-users-register-form)
+(declare handler-users-register-form)
+(declare handler-users-register-form-impl)
+(declare render-users-register-form)
+(declare on-users-register-submit-clicked)
+(declare template-users-register-submit)
+(declare handler-users-register-submit)
+(declare render-users-register-submit)
+(declare on-users-modify-clicked)
+(declare template-users-modify)
+(declare handler-users-modify)
+(declare render-users-modify)
+(declare on-users-modify-submit-clicked)
+(declare handler-users-modify-submit)
+(declare render-users-modify-submit)
+(declare on-users-toggle-active-clicked)
+(declare handler-users-toggle-active)
+(declare render-users-toggle-active)
+(declare on-users-delete-clicked)
+(declare handler-users-delete)
+(declare render-users-delete)
+(declare on-menu-clicked)
+(declare handler-location)
+(declare goto-location)
+(declare goto-register)
+(declare reset-app)
+(declare reset-about-us-category)
+
+(enable-console-print!)
+
+(defonce jquery (js* "$"))
+(defonce sql-formatter (time-format/formatter "yyyy-MM-dd HH:mm:ss"))
+(defonce pretty-formatter (time-format/formatters :rfc822))
+(defonce location-state (r/atom "/home"))
+(defonce about-us-category-state (r/atom nil))
+(defonce menu-main-state (r/atom []))
+(defonce menu-user-state (r/atom []))
+(defonce menu-user-label (r/atom nil))
+
+;; helper functions
+
+(defn date-sql-to-pretty [sql-date]
+ (first (str/split (time-format/unparse pretty-formatter (time-format/parse sql-formatter (first (str/split sql-date ".")))) " Z")))
+
+(defn markdown-to-html [markdown]
+ (let [converter (js/showdown.Converter.)]
+ (.makeHtml converter markdown)))
+
+(defn reduce-checkboxes [selector]
+ "Reduces the names of all checked checkboxes to a pipe-separated
+ list. Assumes that the checkboxes are named via the convention
+ `chk_something-more'. The important thing is the underscore
+ separating the throwaway prefix and the remaining useful
+ bit. `selector' will likely be something like: [id^='chk_']"
+ (reduce (fn [x y]
+ (cond (and x y) (str x "|" y)
+ (and x (not y)) x
+ (and (not x) y) y
+ :else ""))
+ (map (fn [elem]
+ (let [this (jquery (str "#" (dommy/attr elem :id)))]
+ (when (-> this (.prop "checked"))
+ (second (str/split (-> this (.prop "id")) "_")))))
+ (.toArray (jquery selector)))))
+
+;; body
+
+(defn comp-app []
+ [:div {:class "container-fluid"}
+ [:div {:class "banner"}
+ [:table {:width "100%" :height "100%"}
+ [:tbody
+ [:tr
+ [:td {:class "banner-menu"}
+ [comp-menu-user]]
+ [:td {:class "banner-title"} "Bogen-" [:i "Herr"]]
+ [:td {:class "banner-menu"} " "]]]]]
+ [comp-menu-main]
+ [ck-notifications/comp-errormsg]
+ [ck-notifications/comp-message]
+ [:div {:id "body"}]
+ [:div {:id "footer"}
+ [:hr]
+ "Carlos Konstanski (970) 294-9708"
+ [:br]
+ [:a {:href "https://github.com/ckonstanski/resume/tree/master"
+ :target "_blank"}
+ "Source code on GitHub"]]])
+
+;; start the react app
+
+(defn start-render []
+ (let [app-root (rdc/create-root (js/document.getElementById "app"))]
+ (rdc/render app-root [comp-app])))
+
+(defn start-location []
+ (cond (str/starts-with? @location-state "/register/")
+ (goto-register (str/replace-first @location-state "/register/" ""))
+ :else
+ (goto-location @location-state)))
+
+(defn reset-location [url]
+ (reset! location-state url))
+
+(defn reset-about-us-category [category]
+ (reset! about-us-category-state category))
+
+(defn ^:dev/after-load start
+ ([]
+ (start-render)
+ (start-location))
+ ([url]
+ (reset-location url)
+ (start-render)
+ (start-location)))
+
+;; notifications
+
+(defn notifications [jsonobj]
+ (ck-notifications/reset-message (get jsonobj "message"))
+ (ck-notifications/reset-errormsg (get jsonobj "errormsg")))
+
+(defn auth-notifications [jsonobj]
+ (cond (empty? (get jsonobj "errormsg"))
+ (notifications jsonobj)
+ :else
+ (do
+ (render-home "" (get jsonobj "errormsg"))
+ (render-menu))))
+
+;; menu
+
+(defn comp-menu-main []
+ [:div {:class "well"}
+ [:ul {:class "nav nav-pills"}
+ (for [menuitem @menu-main-state]
+ [:li {:key (str "li_" (get menuitem "id"))
+ :class "nav-item"}
+ [:a {:class (cond (= (str/upper-case (get menuitem "handler"))
+ (str/upper-case @location-state))
+ "nav-link active"
+ :else
+ "nav-link")
+ :key (get menuitem "id")
+ :on-click #(on-menu-clicked (get menuitem "handler"))}
+ (get menuitem "label")]])]])
+
+(defn comp-menu-user []
+ [:div {:class "dropdown"}
+ [:button {:class "btn btn-primary dropdown-toggle"
+ :type "button"
+ :id "button-menu-user"
+ :data-toggle "dropdown"
+ :aria-haspopup "true"
+ :aria-expanded "false"}
+ @menu-user-label]
+ [:div {:class "dropdown-menu" :aria-labelledby "button-menu-user"}
+ (for [menuitem @menu-user-state]
+ [:a {:key (get menuitem "id")
+ :class "dropdown-item"
+ :on-click #(on-menu-clicked (get menuitem "handler"))}
+ (get menuitem "label")])]])
+
+(defn handler-menu [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (reset! menu-main-state (get jsonobj "menuitems"))))
+
+(defn handler-menu-user [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (reset! menu-user-state (get jsonobj "menuitems"))
+ (reset! menu-user-label (get jsonobj "label"))))
+
+(defn render-menu []
+ (GET "/menu" {:handler handler-menu})
+ (GET "/menu/user" {:handler handler-menu-user}))
+
+;; home
+
+(hiccups/defhtml template-home [jsonobj]
+ [:div
+ [:h2 {:style "text-align: center"} "Welcome To Carlos Konstanski's Music Studio"]
+ [:h3 {:style "text-align: center"}
+ [:i "a.k.a. der Bogenherr" [:br] "a.k.a. Dr. Divertimento"]]
+ [:h2 {:style "text-align: center"} "Study the Violin, Viola and Viola d'Amore With Me!"]
+ [:div {:style "text-align: center"}
+ [:img {:style "width: 100%"
+ :src "/static/images/instrument-cabinet.jpg"}]]])
+
+(defn handler-home [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (notifications jsonobj)
+ (dommy/set-html! (dommy/sel1 :#body) (template-home jsonobj))))
+
+(defn render-home
+ ([]
+ (GET "/home" {:handler handler-home}))
+ ([message errormsg]
+ (POST "/home"
+ {:format :raw
+ :params {:message message
+ :errormsg errormsg}
+ :handler handler-home})))
+
+;; login
+
+(hiccups/defhtml template-login [jsonobj]
+ [:h3 {:style "text-align: center"} (get jsonobj "title")]
+ (ck-form/template-generic-form (get jsonobj "form") (namespace ::x))
+ [:div {:style "text-align: center"}
+ [:a {:href ""} "Forgot password?"]])
+
+(defn handler-login [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (notifications jsonobj)
+ (dommy/set-html! (dommy/sel1 :#body) (template-login jsonobj))))
+
+(defn render-login []
+ (GET "/login" {:handler handler-login}))
+
+;; login-authenticate
+
+(defn on-login-submit-clicked []
+ (when (-> (jquery "#login-form")
+ (.get "0")
+ (.checkValidity))
+ (render-login-authenticate)))
+
+(defn handler-login-authenticate [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (cond (get jsonobj "errormsg")
+ (goto-location "/login")
+ (get jsonobj "message")
+ (do
+ (reset-location "/home")
+ (render-home)
+ (render-menu)))))
+
+(defn render-login-authenticate []
+ (POST "/login/authenticate"
+ {:format :raw
+ :params {:username (dommy/value (dommy/sel1 :#username))
+ :pwd (dommy/value (dommy/sel1 :#pwd))}
+ :handler handler-login-authenticate}))
+
+;; logout
+
+(defn handler-logout [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (render-home "You are now logged out" "")
+ (reset-location "/home")
+ (render-menu)))
+
+(defn render-logout []
+ (GET "/logout" {:handler handler-logout}))
+
+;; profile
+
+(hiccups/defhtml template-profile [jsonobj]
+ [:h3 {:style "text-align: center"} (get jsonobj "title")]
+ [:div {:id "content"}]
+ [:div {:id "modify"
+ :class "modal fade"
+ :role "dialog"
+ :tabindex "-1"}
+ [:div {:class "modal-dialog modal-lg"}
+ [:div {:class "modal-content"}
+ [:div {:class "modal-header"}
+ [:h5 {:class "modal-title"} "Profile - Modify"]
+ [:button {:type "button"
+ :class "close"
+ :data-dismiss "modal"}
+ "&times;"]]
+ [:div {:id "modify-body"
+ :class "modal-body"
+ :style "height: 460px;"}]
+ [:div {:class "modal-footer"}
+ [:button {:type "submit"
+ :class "btn btn-danger btn-default"
+ :data-dismiss "modal"}
+ [:span {:class "glyphicon glyphicon-remove"}]
+ "Cancel"]]]]])
+
+(defn handler-profile [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (notifications jsonobj)
+ (dommy/set-html! (dommy/sel1 :#body) (template-profile jsonobj))
+ (render-profile-view)))
+
+(defn render-profile []
+ (GET "/profile" {:handler handler-profile}))
+
+;; profile-view
+
+(hiccups/defhtml template-profile-view [jsonobj]
+ [:div {:style "text-align: right"}
+ [:img {:src "/static/images/edit.png"
+ :style "cursor:pointer; cursor:hand"
+ :onclick (str (namespace ::x) ".on_profile_modify_clicked()")}]]
+ [:div {:class "container"}
+ [:div {:class "row"}
+ [:div {:class "col" :style "text-align: right"} "Username:"]
+ [:div {:class "col-10"} (get jsonobj "username")]]
+ [:div {:class "row"}
+ [:div {:class "col" :style "text-align: right"} "First Name:"]
+ [:div {:class "col-10"} (get jsonobj "first_name")]]
+ [:div {:class "row"}
+ [:div {:class "col" :style "text-align: right"} "Last Name:"]
+ [:div {:class "col-10"} (get jsonobj "last_name")]]
+ [:div {:class "row"}
+ [:div {:class "col" :style "text-align: right"} "Email:"]
+ [:div {:class "col-10"} (get jsonobj "email")]]
+ [:div {:class "row"}
+ [:div {:class "col" :style "text-align: right"} "Phone:"]
+ [:div {:class "col-10"} (get jsonobj "phone")]]])
+
+(defn handler-profile-view [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (dommy/set-html! (dommy/sel1 :#content) (template-profile-view jsonobj))))
+
+(defn render-profile-view []
+ (GET "/profile/view" {:handler handler-profile-view}))
+
+;; profile-modify
+
+(defn on-profile-modify-clicked []
+ (render-profile-modify))
+
+(hiccups/defhtml template-profile-modify [jsonobj]
+ (ck-form/template-generic-form (get jsonobj "form") (namespace ::x)))
+
+(defn handler-profile-modify [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (auth-notifications jsonobj)
+ (dommy/set-html! (dommy/sel1 :#modify-body) (template-profile-modify jsonobj))
+ (.modal (jquery "#modify"))))
+
+(defn render-profile-modify []
+ (POST "/profile/modify" {:handler handler-profile-modify}))
+
+;; profile-modify-submit
+
+(defn on-profile-modify-submit-clicked []
+ (when (-> (jquery "#profile-modify-form")
+ (.get "0")
+ (.checkValidity))
+ (.modal (jquery "#modify") "hide")
+ (render-profile-modify-submit)))
+
+(defn handler-profile-modify-submit [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (auth-notifications jsonobj)
+ (on-menu-clicked "/profile")))
+
+(defn render-profile-modify-submit []
+ (POST "/profile/modify/submit"
+ {:format :raw
+ :params {:id (dommy/value (dommy/sel1 :#id))
+ :username (dommy/value (dommy/sel1 :#username))
+ :first_name (dommy/value (dommy/sel1 :#first_name))
+ :last_name (dommy/value (dommy/sel1 :#last_name))
+ :email (dommy/value (dommy/sel1 :#email))
+ :phone (dommy/value (dommy/sel1 :#phone))}
+ :handler handler-profile-modify-submit}))
+
+;; password
+
+(hiccups/defhtml template-password [jsonobj]
+ [:h3 {:style "text-align: center"} (get jsonobj "title")]
+ (ck-form/template-generic-form (get jsonobj "form") (namespace ::x)))
+
+(defn handler-password [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (notifications jsonobj)
+ (dommy/set-html! (dommy/sel1 :#body) (template-password jsonobj))))
+
+(defn render-password []
+ (GET "/password" {:handler handler-password}))
+
+;; password-submit
+
+(defn on-password-submit-clicked []
+ (when (-> (jquery "#password-form")
+ (.get "0")
+ (.checkValidity))
+ (render-password-submit)))
+
+(defn handler-password-submit [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (auth-notifications jsonobj)
+ (on-menu-clicked "/password")))
+
+(defn render-password-submit []
+ (POST "/password/submit"
+ {:format :raw
+ :params {:id (dommy/value (dommy/sel1 :#id))
+ :pwd (dommy/value (dommy/sel1 :#pwd))
+ :pwd2 (dommy/value (dommy/sel1 :#pwd2))}
+ :handler handler-password-submit}))
+
+;; about-us (lessons, gigs, programming)
+
+(hiccups/defhtml template-about-us [jsonobj]
+ [:h1 {:style "text-align: center"}
+ (cond (= @about-us-category-state "lessons") "About the Studio"
+ (= @about-us-category-state "gigs") "Hire Me to Play"
+ (= @about-us-category-state "programming") "Hire Me to Write Software")]
+ [:div {:id "content"}]
+ [:div {:id "modify"
+ :class "modal fade"
+ :role "dialog"
+ :tabindex "-1"}
+ [:div {:class "modal-dialog modal-lg"}
+ [:div {:class "modal-content"}
+ [:div {:class "modal-header"}
+ [:h5 [:span {:id "modify-title"}]]
+ [:button {:type "button"
+ :class "close"
+ :data-dismiss "modal"}
+ "&times;"]]
+ [:div {:id "modify-body"
+ :class "modal-body"
+ :style "height: 460px;"}]
+ [:div {:class "modal-footer"}
+ [:button {:type "submit"
+ :class "btn btn-danger btn-default"
+ :data-dismiss "modal"}
+ [:span {:class "glyphicon glyphicon-remove"}]
+ "Cancel"]]]]])
+
+(defn handler-about-us [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (reset-about-us-category (get jsonobj "category"))
+ (notifications jsonobj)
+ (dommy/set-html! (dommy/sel1 :#body) (template-about-us jsonobj))
+ (render-about-us-view)))
+
+(defn render-lessons []
+ (GET "/lessons" {:handler handler-about-us}))
+
+(defn render-gigs []
+ (GET "/gigs" {:handler handler-about-us}))
+
+(defn render-programming []
+ (GET "/programming" {:handler handler-about-us}))
+
+;; about-us-view
+
+(hiccups/defhtml template-about-us-view [jsonobj]
+ (when (get jsonobj "adminP")
+ [:div {:style "text-align: right;"}
+ [:img {:src "/static/images/edit.png"
+ :style "cursor:pointer; cursor:hand"
+ :onclick (str (namespace ::x) ".on_about_us_modify_clicked()")}]])
+ [:div {:id "markdown"}])
+
+(defn handler-about-us-view [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (dommy/set-html! (dommy/sel1 :#content) (template-about-us-view jsonobj))
+ (dommy/set-html! (dommy/sel1 :#markdown) (markdown-to-html (get jsonobj "content")))))
+
+(defn render-about-us-view []
+ (GET (str "/" @about-us-category-state "/view")
+ {:handler handler-about-us-view}))
+
+;; about-us-modify
+
+(defn on-about-us-modify-clicked []
+ (render-about-us-modify))
+
+(hiccups/defhtml template-about-us-modify [jsonobj]
+ (ck-form/template-generic-form (get jsonobj "form") (namespace ::x)))
+
+(defn handler-about-us-modify [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (auth-notifications jsonobj)
+ (dommy/set-html! (dommy/sel1 :#modify-title)
+ (str (cond (= @about-us-category-state "lessons") "About the Studio"
+ (= @about-us-category-state "gigs") "For Hire"
+ (= @about-us-category-state "programming") "Software Consulting")
+ " - Modify"))
+ (dommy/set-html! (dommy/sel1 :#modify-body) (template-about-us-modify jsonobj))
+ (.modal (jquery "#modify"))))
+
+(defn render-about-us-modify []
+ (POST (str "/" @about-us-category-state "/modify")
+ {:handler handler-about-us-modify}))
+
+;; about-us-modify-submit
+
+(defn on-about-us-modify-submit-clicked []
+ (when (-> (jquery "#about-us-modify-form")
+ (.get "0")
+ (.checkValidity))
+ (.modal (jquery "#modify") "hide")
+ (render-about-us-modify-submit)))
+
+(defn handler-about-us-modify-submit [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (auth-notifications jsonobj)
+ (dommy/set-html! (dommy/sel1 :#content) (template-about-us-view jsonobj))
+ (dommy/set-html! (dommy/sel1 :#markdown) (markdown-to-html (get jsonobj "content")))))
+
+(defn render-about-us-modify-submit []
+ (POST (str "/" @about-us-category-state "/modify/submit")
+ {:format :raw
+ :params {:content (dommy/value (dommy/sel1 :#txt-content))}
+ :handler handler-about-us-modify-submit}))
+
+;; contact-us
+
+(hiccups/defhtml template-contact-us [jsonobj]
+ [:h1 {:style "text-align: center"} "Reach out to me by filling out the form."]
+ [:div {:id "content"}]
+ [:table {:width "100%"}
+ [:tr
+ [:td {:style "width: 100%; text-align: center;"}
+ [:h3 "Located on the edge of the ISU campus!"]]]
+ [:tr
+ [:td {:style "width: 100%; text-align: center;"}
+ [:iframe {:src "https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3225.263770603599!2d-112.4266916!3d42.8635091!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x53554f343c440407%3A0xf13bf897ecb4f7e0!2sBogenherr%20Violin%20and%20Viola%20Studio!5e1!3m2!1sen!2sus!4v1783097048097!5m2!1sen!2sus"
+ :style "width: 600px; height: 450px; border: 0px;"
+ :allowfullscreen ""
+ :loading "lazy"
+ :referrerpolicy "strict-origin-when-cross-origin"}]]]])
+
+(defn handler-contact-us [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (notifications jsonobj)
+ (dommy/set-html! (dommy/sel1 :#body) (template-contact-us jsonobj))
+ (render-contact-us-view)))
+
+(defn render-contact-us []
+ (GET "/contact-us" {:handler handler-contact-us}))
+
+;; contact-us-view
+
+(hiccups/defhtml template-contact-us-view [jsonobj]
+ (ck-form/template-generic-form (get jsonobj "form") (namespace ::x)))
+
+(defn handler-contact-us-view [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (notifications jsonobj)
+ (dommy/set-html! (dommy/sel1 :#content) (template-contact-us-view jsonobj))))
+
+(defn render-contact-us-view []
+ (GET "/contact-us/view" {:handler handler-contact-us-view}))
+
+;; contact-us-submit
+
+(defn on-contact-us-email-submit-clicked []
+ (when (-> (jquery "#contact-us-form")
+ (.get "0")
+ (.checkValidity))
+ (render-contact-us-email-submit)))
+
+(defn handler-contact-us-email-submit [response]
+ (on-menu-clicked "/contact-us"))
+
+(defn render-contact-us-email-submit []
+ (POST "/contact-us/email"
+ {:format :raw
+ :params {:first_name (dommy/value (dommy/sel1 :#first_name))
+ :last_name (dommy/value (dommy/sel1 :#last_name))
+ :email (dommy/value (dommy/sel1 :#email))
+ :phone (dommy/value (dommy/sel1 :#phone))
+ :comments (dommy/value (dommy/sel1 :#comments))}
+ :handler handler-contact-us-email-submit}))
+
+;; messages
+
+(hiccups/defhtml template-messages [jsonobj]
+ [:h3 {:style "text-align: center"} (get jsonobj "title")]
+ (ck-form/template-generic-form (get jsonobj "form") (namespace ::x))
+ [:div {:id "results"}])
+
+(defn handler-messages [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (notifications jsonobj)
+ (dommy/set-html! (dommy/sel1 :#body) (template-messages jsonobj))
+ (when (not (empty? (dommy/value (dommy/sel1 :#read))))
+ (render-messages-results))))
+
+(defn render-messages []
+ (GET "/messages" {:handler handler-messages}))
+
+;; messages-results
+
+(defn on-messages-mode-clicked [read]
+ (dommy/set-value! (dommy/sel1 :#read) read)
+ (render-messages-results))
+
+(hiccups/defhtml template-messages-results [jsonobj]
+ (let [results (get jsonobj "results")
+ keys (remove (fn [x]
+ (not (get (first results) x)))
+ (keys (first results)))]
+ (cond (empty? results)
+ [:h5 {:style "text-align: center"} "No results found."]
+ :else
+ [:table {:class "table table-hover"}
+ [:thead
+ [:tr
+ (for [key keys]
+ [:th key])
+ [:th
+ (str "Mark " (cond (= (dommy/value (dommy/sel1 :#read)) "read")
+ "unread"
+ :else
+ "read"))]]]
+ [:tbody
+ (for [rec results]
+ (let [onclick "void()"]
+ [:tr
+ (for [key keys]
+ [:td {:onclick onclick} (get rec key)])
+ [:td [:img {:src (str "/static/images/"
+ (cond (= (dommy/value (dommy/sel1 :#read)) "read")
+ "edit-undo.png"
+ :else
+ "edit-redo.png"))
+ :style "cursor: pointer; cursor: hand"
+ :onclick (str (namespace ::x)
+ ".on_messages_mark("
+ (cond (= (dommy/value (dommy/sel1 :#read)) "read")
+ "'unread'"
+ :else
+ "'read'")
+ "," (get rec "id") ")")}]]]))]])))
+
+(defn handler-messages-results [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (auth-notifications jsonobj)
+ (dommy/set-html! (dommy/sel1 :#results) (template-messages-results jsonobj))))
+
+(defn render-messages-results []
+ (POST "/messages/results"
+ {:format :raw
+ :params {:read (dommy/value (dommy/sel1 :#read))}
+ :handler handler-messages-results}))
+
+;; messages-mark
+
+(defn on-messages-mark [read id]
+ (render-messages-mark read id))
+
+(defn handler-messages-mark [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (auth-notifications jsonobj)
+ (on-menu-clicked "/messages")))
+
+(defn render-messages-mark [read id]
+ (POST "/messages/mark"
+ {:format :raw
+ :params {:read read
+ :id id}
+ :handler handler-messages-mark}))
+
+;; users
+
+(hiccups/defhtml template-users [jsonobj]
+ [:h3 {:style "text-align: center"} (get jsonobj "title")]
+ [:div {:id "content"}]
+ [:div {:id "modify"
+ :class "modal fade"
+ :role "dialog"
+ :tabindex "-1"}
+ [:div {:class "modal-dialog modal-lg"}
+ [:div {:class "modal-content"}
+ [:div {:class "modal-header"}
+ [:h5 [:span {:id "modify-title"}]]
+ [:button {:type "button"
+ :class "close"
+ :data-dismiss "modal"}
+ "&times;"]]
+ [:div {:id "modify-body"
+ :class "modal-body"
+ :style "height: 450px;"}]
+ [:div {:class "modal-footer"}
+ [:button {:type "submit"
+ :class "btn btn-danger btn-default"
+ :data-dismiss "modal"}
+ [:span {:class "glyphicon glyphicon-remove"}]
+ "Cancel"]]]]])
+
+(defn handler-users [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (notifications jsonobj)
+ (dommy/set-html! (dommy/sel1 :#body) (template-users jsonobj))
+ (render-users-view)))
+
+(defn render-users []
+ (GET "/users" {:handler handler-users}))
+
+;; users-view
+
+(hiccups/defhtml template-users-view [jsonobj]
+ [:div {:style "text-align: right"}
+ [:img {:src "/static/images/add.png"
+ :style "cursor:pointer; cursor:hand"
+ :onclick (str (namespace ::x) ".on_users_add_clicked()")}]]
+ [:table {:class "table table-striped table-hover table-sm"}
+ [:thead
+ [:tr
+ [:th {:scope "col"} "Active?"]
+ [:th {:scope "col"} "Last Name"]
+ [:th {:scope "col"} "First Name"]
+ [:th {:scope "col"} "Username"]
+ [:th {:scope "col"} "Email"]
+ [:th {:scope "col"} "Phone"]
+ [:th {:scope "col"} "Del"]]]
+ [:tbody
+ (for [user (get jsonobj "users")]
+ (let [onclick (str (namespace ::x) ".on_users_modify_clicked(" (get user "id") ")")
+ toggle-active (str (namespace ::x) ".on_users_toggle_active_clicked(" (get user "id") ")")]
+ [:tr
+ [:td [:img {:src (cond (get user "active")
+ "/static/images/yes.png"
+ :else
+ "/static/images/no.png")
+ :onclick toggle-active}]]
+ [:td {:onclick onclick} (get user "last_name")]
+ [:td {:onclick onclick} (get user "first_name")]
+ [:td {:onclick onclick} (get user "username")]
+ [:td {:onclick onclick} (get user "email")]
+ [:td {:onclick onclick} (get user "phone")]
+ [:td [:img {:src "/static/images/delete.png"
+ :onclick (str (namespace ::x) ".on_users_delete_clicked(" (get user "id") ", '" (get user "first_name") "', '" (get user "last_name") "')")}]]]))]])
+
+(defn handler-users-view [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (dommy/set-html! (dommy/sel1 :#content) (template-users-view jsonobj))))
+
+(defn render-users-view []
+ (GET "/users/view" {:handler handler-users-view}))
+
+;; users-add
+
+(defn on-users-add-clicked []
+ (render-users-add))
+
+(hiccups/defhtml template-users-add [jsonobj]
+ (ck-form/template-generic-form (get jsonobj "form") (namespace ::x)))
+
+(defn handler-users-add [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (auth-notifications jsonobj)
+ (dommy/set-html! (dommy/sel1 :#modify-title) (get jsonobj "title"))
+ (dommy/set-html! (dommy/sel1 :#modify-body) (template-users-add jsonobj))
+ (.modal (jquery "#modify"))))
+
+(defn render-users-add []
+ (POST "/users/add" {:handler handler-users-add}))
+
+;; users-add-submit
+
+(defn on-users-add-submit-clicked []
+ (when (-> (jquery "#users-add-form")
+ (.get "0")
+ (.checkValidity))
+ (.modal (jquery "#modify") "hide")
+ (render-users-add-submit)))
+
+(defn handler-users-add-submit [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (auth-notifications jsonobj)
+ (on-menu-clicked "/users")))
+
+(defn render-users-add-submit []
+ (POST "/users/add/submit"
+ {:format :raw
+ :params {:role_groups (reduce-checkboxes "[id^='chk_']")
+ :first_name (dommy/value (dommy/sel1 :#first_name))
+ :last_name (dommy/value (dommy/sel1 :#last_name))
+ :email (dommy/value (dommy/sel1 :#email))}
+ :handler handler-users-add-submit}))
+
+;; users-register
+
+(hiccups/defhtml template-users-register [jsonobj]
+ [:h3 {:style "text-align: center"} (get jsonobj "title")]
+ [:div {:id "content"}])
+
+(defn handler-users-register [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (dommy/set-html! (dommy/sel1 :#body) (template-users-register jsonobj))
+ (render-users-register-form (get jsonobj "hash"))))
+
+(defn render-users-register [hash]
+ (POST "/users/register"
+ {:format :raw
+ :params {:hash hash}
+ :handler handler-users-register}))
+
+;; users-register-form
+
+(hiccups/defhtml template-users-register-form [jsonobj]
+ (cond (get jsonobj "errormsg")
+ [:a {:href (str "javascript:" (namespace ::x) ".reset_app()")}
+ "Click here to return to the home page."]
+ :else
+ (do
+ [:p (str "Welcome " (get jsonobj "firstName") " " (get jsonobj "lastName") " to the new user registration page. Please fill out the form below to complete your registration.")]
+ [:p "Create a new username and password for your login to the website. Phone number is optional."]
+ [:p (str "Your email address is recorded as " (get jsonobj "email") ". This is where notifications will be sent. If this is not the desired email address, you can change it later by visiting the Edit Profile page.")]
+ [:p (str "This registration will expire in " (get jsonobj "validFor") ". Please submit this form before that time.")]
+ (ck-form/template-generic-form (get jsonobj "form") (namespace ::x)))))
+
+(defn handler-users-register-form [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (notifications jsonobj)
+ (dommy/set-html! (dommy/sel1 :#content) (template-users-register-form jsonobj))))
+
+(defn render-users-register-form [hash]
+ (POST "/users/register/form"
+ {:format :raw
+ :params {:hash hash}
+ :handler handler-users-register-form}))
+
+;; users-register-submit
+
+(defn on-users-register-submit-clicked []
+ (when (-> (jquery "#users-register-form")
+ (.get "0")
+ (.checkValidity))
+ (render-users-register-submit)))
+
+(hiccups/defhtml template-users-register-submit [jsonobj]
+ [:a {:href (str "javascript:" (namespace ::x) ".reset_app()")}
+ "Click here to start using the website!"])
+
+(defn handler-users-register-submit [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (notifications jsonobj)
+ (cond (get jsonobj "errormsg")
+ (do
+ (dommy/set-value! (dommy/sel1 :#pwd) "")
+ (dommy/set-value! (dommy/sel1 :#pwd2) ""))
+ :else
+ (dommy/set-html! (dommy/sel1 :#content) (template-users-register-submit jsonobj)))))
+
+(defn render-users-register-submit []
+ (POST "/users/register/submit"
+ {:format :raw
+ :params {:hash (dommy/value (dommy/sel1 :#hash))
+ :username (dommy/value (dommy/sel1 :#username))
+ :pwd (dommy/value (dommy/sel1 :#pwd))
+ :pwd2 (dommy/value (dommy/sel1 :#pwd2))
+ :phone (dommy/value (dommy/sel1 :#phone))}
+ :handler handler-users-register-submit}))
+
+;; users-modify
+
+(defn on-users-modify-clicked [id]
+ (render-users-modify id))
+
+(hiccups/defhtml template-users-modify [jsonobj]
+ (ck-form/template-generic-form (get jsonobj "form") (namespace ::x)))
+
+(defn handler-users-modify [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (auth-notifications jsonobj)
+ (dommy/set-html! (dommy/sel1 :#modify-title) (get jsonobj "title"))
+ (dommy/set-html! (dommy/sel1 :#modify-body) (template-users-modify jsonobj))
+ (.modal (jquery "#modify"))))
+
+(defn render-users-modify [id]
+ (POST "/users/modify"
+ {:format :raw
+ :params {:id id}
+ :handler handler-users-modify}))
+
+;; users-modify-submit
+
+(defn on-users-modify-submit-clicked []
+ (when (-> (jquery "#users-modify-form")
+ (.get "0")
+ (.checkValidity))
+ (.modal (jquery "#modify") "hide")
+ (render-users-modify-submit)))
+
+(defn handler-users-modify-submit [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (auth-notifications jsonobj)
+ (on-menu-clicked "/users")))
+
+(defn render-users-modify-submit []
+ (POST "/users/modify/submit"
+ {:format :raw
+ :params {:id (dommy/value (dommy/sel1 :#id))
+ :role_groups (reduce-checkboxes "[id^='chk_']")
+ :username (dommy/value (dommy/sel1 :#username))
+ :first_name (dommy/value (dommy/sel1 :#first_name))
+ :last_name (dommy/value (dommy/sel1 :#last_name))
+ :email (dommy/value (dommy/sel1 :#email))
+ :phone (dommy/value (dommy/sel1 :#phone))}
+ :handler handler-users-modify-submit}))
+
+;; users-toggle-active
+
+(defn on-users-toggle-active-clicked [id]
+ (render-users-toggle-active id))
+
+(defn handler-users-toggle-active [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (auth-notifications jsonobj)
+ (on-menu-clicked "/users")))
+
+(defn render-users-toggle-active [id]
+ (POST "/users/toggle-active"
+ {:format :raw
+ :params {:id id}
+ :handler handler-users-toggle-active}))
+
+;; users-delete
+
+(defn on-users-delete-clicked [id first-name last-name]
+ (when (js/confirm (str "Are you sure you want to delete " first-name " " last-name "? This action cannot be undone."))
+ (render-users-delete id)))
+
+(defn handler-users-delete [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (auth-notifications jsonobj)
+ (on-menu-clicked "/users")))
+
+(defn render-users-delete [id]
+ (DELETE (str "/users/delete/" id)
+ {:format :raw
+ :handler handler-users-delete}))
+
+;; location
+
+(defn on-menu-clicked [handler]
+ (reset-location handler)
+ (render-menu)
+ (cond (= handler "/home") (render-home)
+ (= handler "/login") (render-login)
+ (= handler "/logout") (render-logout)
+ (= handler "/profile") (render-profile)
+ (= handler "/password") (render-password)
+ (= handler "/lessons") (render-lessons)
+ (= handler "/gigs") (render-gigs)
+ (= handler "/programming") (render-programming)
+ (= handler "/contact-us") (render-contact-us)
+ (= handler "/messages") (render-messages)
+ (= handler "/users") (render-users)))
+
+(defn handler-location [response]
+ (let [jsonobj (js->clj (js/JSON.parse response))]
+ (on-menu-clicked (get jsonobj "location"))
+ (notifications jsonobj)))
+
+(defn goto-location [location]
+ (POST "/location"
+ {:format :raw
+ :params {:location location}
+ :handler handler-location}))
+
+(defn reset-app []
+ (set! (.-location js/document) "/"))
+
+(defn goto-register [hash]
+ (reset-location "/users/register")
+ (render-menu)
+ (render-users-register hash))
diff --git a/lisp/webapps/resume/conf/.gitignore b/lisp/webapps/resume/conf/.gitignore
new file mode 100644
index 0000000..14fa7a6
--- /dev/null
+++ b/lisp/webapps/resume/conf/.gitignore
@@ -0,0 +1 @@
+options.lisp
diff --git a/lisp/webapps/resume/conf/options.lisp.example b/lisp/webapps/resume/conf/options.lisp.example
new file mode 100644
index 0000000..dd570ee
--- /dev/null
+++ b/lisp/webapps/resume/conf/options.lisp.example
@@ -0,0 +1,13 @@
+(:name "resume"
+ :url "resume.org"
+ :document-root "resume"
+ :title "Resume"
+ :meta-description "The Resume website written in Common Lisp/Hunchentoot and ClojureScript."
+ :databases (:db-resume ("127.0.0.1" "resume" "resume" "password"))
+ :mail-mx "mail.ckons.org"
+ :mail-from "postmaster@ckons.org"
+ :mail-postmaster "postmaster@ckons.org"
+ :mail-webmaster "webmaster@ckons.org"
+ :mail-info "info@ckons.org"
+ :mail-login-notify "me@ckons.org"
+ :mail-authentication ("me@ckons.org" "password"))
diff --git a/lisp/webapps/resume/site.lisp b/lisp/webapps/resume/site.lisp
new file mode 100644
index 0000000..3e31ac1
--- /dev/null
+++ b/lisp/webapps/resume/site.lisp
@@ -0,0 +1,206 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defmacro .base (&optional (start-url "/home"))
+ `(org-ckons-http::html5
+ `(html
+ (head
+ ((meta :name "viewport" :content "width=device-width, initial-scale=1, shrink-to-fit=no"))
+ ((meta :charset "utf-8"))
+ ((title) ,(title *webapp*))
+ ,@(mapcar (lambda (css)
+ `((link :rel "stylesheet" :href ,(getf css :href) :integrity ,(getf css :integrity) :crossorigin ,(getf css :crossorigin))))
+ '((:href "https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css" :integrity "sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" :crossorigin "anonymous")
+ (:href "/static/css/stylesheet.css" :crossorigin "anonymous")))
+ ,@(mapcar (lambda (js)
+ `((script :type "text/javascript" :src ,(getf js :src) :integrity ,(getf js :integrity) :crossorigin ,(getf js :crossorigin))))
+ '((:src "https://code.jquery.com/jquery-3.7.1.slim.min.js" :integrity "sha256-kmHvs0B+OpCW5GVHUNjv9rOmY0IvSIRcf7zGUDTDQM8=" :crossorigin "anonymous")))
+ ((script :type "text/javascript" :src "/cljs-out/dev-main.js")))
+ ((body :onload ,(format nil "resume.core.start(~a)" ,(if start-url
+ (format nil "'~a'" start-url)
+ "null")))
+ ((div :id "app"))
+ ,@(mapcar (lambda (js)
+ `((script :type "text/javascript" :src ,(getf js :src) :integrity ,(getf js :integrity) :crossorigin ,(getf js :crossorigin))))
+ '((:src "https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js" :integrity "sha384-Fy6S3B9q64WdZWQUiU+q4/2Lc9npb8tCaSX9FK7E8HnRr0Jz8D6OP9dO5Vg3Q9ct" :crossorigin "anonymous")))))))
+
+(defmacro .location ()
+ `(location-json location))
+
+(defmacro .home-get ()
+ `(home-json))
+
+(defmacro .home-post ()
+ `(home-json message errormsg))
+
+(defmacro .menu ()
+ `(menu-json))
+
+(defmacro .menu-user ()
+ `(menu-user-json))
+
+(defmacro .login ()
+ `(login-json))
+
+(defmacro .login-authenticate ()
+ `(login-authenticate-json username pwd))
+
+(defmacro .login-forgot ()
+ `(login-forgot-json))
+
+(defmacro .logout ()
+ `(logout-json))
+
+(defmacro .profile ()
+ `(profile-json))
+
+(defmacro .profile-view ()
+ `(profile-view-json))
+
+(defmacro .profile-modify ()
+ `(profile-modify-json))
+
+(defmacro .profile-modify-submit ()
+ `(profile-modify-submit-json id username first_name last_name email phone))
+
+(defmacro .password ()
+ `(password-json))
+
+(defmacro .password-submit ()
+ `(password-submit-json id pwd pwd2))
+
+(defmacro .lessons ()
+ `(about-us-json "lessons"))
+
+(defmacro .lessons-view ()
+ `(about-us-view-json "lessons"))
+
+(defmacro .lessons-modify ()
+ `(about-us-modify-json "lessons"))
+
+(defmacro .lessons-modify-submit ()
+ `(about-us-modify-submit-json "lessons" content))
+
+(defmacro .gigs ()
+ `(about-us-json "gigs"))
+
+(defmacro .gigs-view ()
+ `(about-us-view-json "gigs"))
+
+(defmacro .gigs-modify ()
+ `(about-us-modify-json "gigs"))
+
+(defmacro .gigs-modify-submit ()
+ `(about-us-modify-submit-json "gigs" content))
+
+(defmacro .programming ()
+ `(about-us-json "programming"))
+
+(defmacro .programming-view ()
+ `(about-us-view-json "programming"))
+
+(defmacro .programming-modify ()
+ `(about-us-modify-json "programming"))
+
+(defmacro .programming-modify-submit ()
+ `(about-us-modify-submit-json "programming" content))
+
+(defmacro .contact-us ()
+ `(contact-us-json))
+
+(defmacro .contact-us-view ()
+ `(contact-us-view-json))
+
+(defmacro .contact-us-email ()
+ `(contact-us-email-json first_name last_name email phone comments))
+
+(defmacro .messages ()
+ `(messages-json))
+
+(defmacro .messages-results ()
+ `(messages-results-json read))
+
+(defmacro .messages-mark ()
+ `(messages-mark-json read id))
+
+(defmacro .users ()
+ `(users-json))
+
+(defmacro .users-view ()
+ `(users-view-json))
+
+(defmacro .users-add ()
+ `(users-add-json))
+
+(defmacro .users-add-submit ()
+ `(users-add-submit-json role_groups first_name last_name email))
+
+(defmacro .users-register ()
+ `(users-register-json hash))
+
+(defmacro .users-register-form ()
+ `(users-register-form-json hash))
+
+(defmacro .users-register-submit ()
+ `(users-register-submit-json hash username pwd pwd2 phone))
+
+(defmacro .users-modify ()
+ `(users-modify-json id))
+
+(defmacro .users-modify-submit ()
+ `(users-modify-submit-json id role_groups username first_name last_name email phone))
+
+(defmacro .users-toggle-active ()
+ `(users-toggle-active-json id))
+
+(defmacro .users-delete ()
+ `(users-delete-json id))
+
+(define-endpoint ("/" :method :get) () .base)
+(define-endpoint ("/location" :method :post) (&post (location :parameter-type 'string)) .location)
+(define-endpoint ("/home" :method :get) () .home-get)
+(define-endpoint ("/home" :method :post) (&post (message :parameter-type 'string) (errormsg :parameter-type 'string)) .home-post)
+(define-endpoint ("/menu" :method :get) () .menu)
+(define-endpoint ("/menu/user" :method :get) () .menu-user)
+(define-endpoint ("/login" :method :get) () .login)
+(define-endpoint ("/login/authenticate" :method :post) (&post (username :parameter-type 'string) (pwd :parameter-type 'string)) .login-authenticate)
+(define-endpoint ("/login/forgot" :method :get) () .login-forgot)
+(define-endpoint ("/logout" :method :get) () .logout)
+(define-endpoint ("/profile" :method :get) () .profile)
+(define-endpoint ("/profile/view" :method :get) () .profile-view)
+(define-endpoint ("/profile/modify" :method :post) () .profile-modify)
+(define-endpoint ("/profile/modify/submit" :method :post) (&post (id :parameter-type 'integer) (username :parameter-type 'string) (first_name :parameter-type 'string) (last_name :parameter-type 'string) (email :parameter-type 'string) (phone :parameter-type 'string)) .profile-modify-submit)
+(define-endpoint ("/password" :method :get) () .password)
+(define-endpoint ("/password/submit" :method :post) (&post (id :parameter-type 'integer) (pwd :parameter-type 'string) (pwd2 :parameter-type 'string)) .password-submit)
+(define-endpoint ("/lessons" :method :get) () .lessons)
+(define-endpoint ("/lessons/view" :method :get) () .lessons-view)
+(define-endpoint ("/lessons/modify" :method :post) () .lessons-modify)
+(define-endpoint ("/lessons/modify/submit" :method :post) (&post (content :parameter-type 'string)) .lessons-modify-submit)
+(define-endpoint ("/gigs" :method :get) () .gigs)
+(define-endpoint ("/gigs/view" :method :get) () .gigs-view)
+(define-endpoint ("/gigs/modify" :method :post) () .gigs-modify)
+(define-endpoint ("/gigs/modify/submit" :method :post) (&post (content :parameter-type 'string)) .gigs-modify-submit)
+(define-endpoint ("/programming" :method :get) () .programming)
+(define-endpoint ("/programming/view" :method :get) () .programming-view)
+(define-endpoint ("/programming/modify" :method :post) () .programming-modify)
+(define-endpoint ("/programming/modify/submit" :method :post) (&post (content :parameter-type 'string)) .programming-modify-submit)
+(define-endpoint ("/contact-us" :method :get) () .contact-us)
+(define-endpoint ("/contact-us/view" :method :get) () .contact-us-view)
+(define-endpoint ("/contact-us/email" :method :post) (&post (first_name :parameter-type 'string) (last_name :parameter-type 'string) (email :parameter-type 'string) (phone :parameter-type 'string) (comments :parameter-type 'string)) .contact-us-email)
+(define-endpoint ("/messages" :method :get) () .messages)
+(define-endpoint ("/messages/results" :method :post) (&post (read :parameter-type 'string)) .messages-results)
+(define-endpoint ("/messages/mark" :method :post) (&post (read :parameter-type 'string) (id :parameter-type 'integer)) .messages-mark)
+(define-endpoint ("/users" :method :get) () .users)
+(define-endpoint ("/users/view" :method :get) () .users-view)
+(define-endpoint ("/users/add" :method :post) () .users-add)
+(define-endpoint ("/users/add/submit" :method :post) (&post (role_groups :parameter-type 'string) (first_name :parameter-type 'string) (last_name :parameter-type 'string) (email :parameter-type 'string)) .users-add-submit)
+(define-endpoint ("/register/:hash" :method :get) (&path (hash 'string)) .base (format nil "/register/~a" hash))
+(define-endpoint ("/users/register" :method :post) (&post (hash :parameter-type 'string)) .users-register)
+(define-endpoint ("/users/register/form" :method :post) (&post (hash :parameter-type 'string)) .users-register-form)
+(define-endpoint ("/users/register/submit" :method :post) (&post (hash :parameter-type 'string) (username :parameter-type 'string) (pwd :parameter-type 'string) (pwd2 :parameter-type 'string) (phone :parameter-type 'string)) .users-register-submit)
+(define-endpoint ("/users/modify" :method :post) (&post (id :parameter-type 'integer)) .users-modify)
+(define-endpoint ("/users/modify/submit" :method :post) (&post (id :parameter-type 'integer) (role_groups :parameter-type 'string) (username :parameter-type 'string) (first_name :parameter-type 'string) (last_name :parameter-type 'string) (email :parameter-type 'string) (phone :parameter-type 'string)) .users-modify-submit)
+(define-endpoint ("/users/toggle-active" :method :post) (&post (id :parameter-type 'integer)) .users-toggle-active)
+(define-endpoint ("/users/delete/:id" :method :delete) (&path (id 'integer)) .users-delete)
diff --git a/lisp/webapps/resume/static/css/stylesheet.css b/lisp/webapps/resume/static/css/stylesheet.css
new file mode 100644
index 0000000..659aa54
--- /dev/null
+++ b/lisp/webapps/resume/static/css/stylesheet.css
@@ -0,0 +1,117 @@
+body {
+ background-color: #161012;
+ color: white;
+ font-size: 14px;
+}
+
+hr {
+ border: 1px #ffd081 solid;
+ width: 200px;
+}
+
+a {
+ color: #ffd081;
+}
+
+.container-fluid {
+ width: 100%;
+ margin: 20px auto;
+}
+
+.table {
+ color: white;
+}
+
+.nav-link {
+ color: white;
+ cursor: pointer;
+ cursor: hand;
+}
+
+.nav-pills {
+ .nav-link.active,
+ .show > .nav-link {
+ background-color: #59200e;
+ }
+}
+
+.well {
+ background-color: #2c180d;
+ border: 1px black solid;
+ text-align: center;
+}
+
+.btn,
+.btn-primary,
+.dropdown-toggle {
+ color: white;
+ background-color: #59200e;
+ border-color: #59200e;
+ &:hover {
+ color: white;
+ background-color: #59200e;
+ border-color: #59200e;
+ }
+}
+
+.show > .btn-primary.dropdown-toggle {
+ color: white;
+ background-color: #59200e;
+ border-color: #59200e;
+}
+
+.dropdown-menu {
+ color: white;
+ background-color: #59200e;
+}
+
+.dropdown-item {
+ color: white;
+ background-color: #59200e;
+}
+
+.banner {
+ height: 90px;
+ background-image: url("/static/images/violin-strip.jpg");
+ background-repeat: repeat;
+ background-position: left;
+}
+
+.banner-title {
+ width: *;
+ color: #59200e;
+ font-size: 28px;
+ font-weight: bold;
+ text-align: center;
+}
+
+.banner-menu {
+ width: 150px;
+ text-align: left;
+ vertical-align: top;
+}
+
+.form-control {
+ width: 600px;
+ background-color: #ffd081;
+}
+
+.contact-info {
+ color: #59200e;
+ font-size: 15px;
+ font-weight: bold;
+ line-height: 0.4;
+}
+
+.modal-content {
+ background-color: #161012;
+}
+
+#body {
+ min-height: 500px;
+}
+
+#footer {
+ font-size: 14px;
+ text-align: center;
+}
diff --git a/lisp/webapps/resume/static/images/add.png b/lisp/webapps/resume/static/images/add.png
new file mode 100644
index 0000000..1055df8
--- /dev/null
+++ b/lisp/webapps/resume/static/images/add.png
Binary files differ
diff --git a/lisp/webapps/resume/static/images/classical-bow.jpg b/lisp/webapps/resume/static/images/classical-bow.jpg
new file mode 100644
index 0000000..fce1392
--- /dev/null
+++ b/lisp/webapps/resume/static/images/classical-bow.jpg
Binary files differ
diff --git a/lisp/webapps/resume/static/images/delete.png b/lisp/webapps/resume/static/images/delete.png
new file mode 100644
index 0000000..ec140b7
--- /dev/null
+++ b/lisp/webapps/resume/static/images/delete.png
Binary files differ
diff --git a/lisp/webapps/resume/static/images/document-open.png b/lisp/webapps/resume/static/images/document-open.png
new file mode 100644
index 0000000..20b7ca8
--- /dev/null
+++ b/lisp/webapps/resume/static/images/document-open.png
Binary files differ
diff --git a/lisp/webapps/resume/static/images/down.png b/lisp/webapps/resume/static/images/down.png
new file mode 100644
index 0000000..e2d15bf
--- /dev/null
+++ b/lisp/webapps/resume/static/images/down.png
Binary files differ
diff --git a/lisp/webapps/resume/static/images/download.png b/lisp/webapps/resume/static/images/download.png
new file mode 100644
index 0000000..3973b3c
--- /dev/null
+++ b/lisp/webapps/resume/static/images/download.png
Binary files differ
diff --git a/lisp/webapps/resume/static/images/edit-redo.png b/lisp/webapps/resume/static/images/edit-redo.png
new file mode 100644
index 0000000..3d4db6c
--- /dev/null
+++ b/lisp/webapps/resume/static/images/edit-redo.png
Binary files differ
diff --git a/lisp/webapps/resume/static/images/edit-undo.png b/lisp/webapps/resume/static/images/edit-undo.png
new file mode 100644
index 0000000..367fc5a
--- /dev/null
+++ b/lisp/webapps/resume/static/images/edit-undo.png
Binary files differ
diff --git a/lisp/webapps/resume/static/images/edit.png b/lisp/webapps/resume/static/images/edit.png
new file mode 100644
index 0000000..550dacd
--- /dev/null
+++ b/lisp/webapps/resume/static/images/edit.png
Binary files differ
diff --git a/lisp/webapps/resume/static/images/emblem-favorite.png b/lisp/webapps/resume/static/images/emblem-favorite.png
new file mode 100644
index 0000000..6535d5d
--- /dev/null
+++ b/lisp/webapps/resume/static/images/emblem-favorite.png
Binary files differ
diff --git a/lisp/webapps/resume/static/images/emblem-nowrite.png b/lisp/webapps/resume/static/images/emblem-nowrite.png
new file mode 100644
index 0000000..997fedf
--- /dev/null
+++ b/lisp/webapps/resume/static/images/emblem-nowrite.png
Binary files differ
diff --git a/lisp/webapps/resume/static/images/instrument-cabinet.jpg b/lisp/webapps/resume/static/images/instrument-cabinet.jpg
new file mode 100644
index 0000000..0500829
--- /dev/null
+++ b/lisp/webapps/resume/static/images/instrument-cabinet.jpg
Binary files differ
diff --git a/lisp/webapps/resume/static/images/no.png b/lisp/webapps/resume/static/images/no.png
new file mode 100644
index 0000000..1dc1b7c
--- /dev/null
+++ b/lisp/webapps/resume/static/images/no.png
Binary files differ
diff --git a/lisp/webapps/resume/static/images/sort-alpha.png b/lisp/webapps/resume/static/images/sort-alpha.png
new file mode 100644
index 0000000..0e4a831
--- /dev/null
+++ b/lisp/webapps/resume/static/images/sort-alpha.png
Binary files differ
diff --git a/lisp/webapps/resume/static/images/up.png b/lisp/webapps/resume/static/images/up.png
new file mode 100644
index 0000000..55bab86
--- /dev/null
+++ b/lisp/webapps/resume/static/images/up.png
Binary files differ
diff --git a/lisp/webapps/resume/static/images/violin-strip.jpg b/lisp/webapps/resume/static/images/violin-strip.jpg
new file mode 100644
index 0000000..6149a87
--- /dev/null
+++ b/lisp/webapps/resume/static/images/violin-strip.jpg
Binary files differ
diff --git a/lisp/webapps/resume/static/images/yes.png b/lisp/webapps/resume/static/images/yes.png
new file mode 100644
index 0000000..b9cadf5
--- /dev/null
+++ b/lisp/webapps/resume/static/images/yes.png
Binary files differ
diff --git a/lisp/webapps/webapp-loader.lisp b/lisp/webapps/webapp-loader.lisp
new file mode 100644
index 0000000..41a00da
--- /dev/null
+++ b/lisp/webapps/webapp-loader.lisp
@@ -0,0 +1,177 @@
+;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+(declaim (optimize (speed 0) (safety 3) (debug 3)))
+
+(in-package :resume)
+
+(defvar *acceptor* nil)
+(defvar *dispatch-table* '(#'dispatch-easy-handlers #'default-dispatcher))
+(defvar *webapps* (make-hash-table :test 'equal))
+(defvar *webapp* nil)
+(defvar *uri* nil)
+(defvar *header-register* nil)
+(defvar *sessionid* nil)
+(defparameter *port* 3014)
+(defparameter *server-root* (namestring (asdf:system-relative-pathname (intern (package-name #.*package*)) "./"))
+ "The location of the web server root on the filesystem.")
+
+(defclass webapp ()
+ ((name :initarg :name
+ :initform nil
+ :accessor name
+ :documentation "The name of the webapp as used in the code. A
+string used as the key to any webapp config lookup.")
+ (scheme :initarg :scheme
+ :initform nil
+ :accessor scheme)
+ (url :initarg :url
+ :initform nil
+ :accessor url
+ :documentation "The domain portion of the URL to the
+root of the webapp.")
+ (document-root :initarg :document-root
+ :initform nil
+ :accessor document-root
+ :documentation "The absolute filesystem path to the
+webapp's top-level directory which is inside the webapps folder.")
+ (title :initarg :title
+ :initform nil
+ :accessor title
+ :documentation "The default title that shows up in
+the browser title bar.")
+ (meta-description :initarg :meta-description
+ :initform nil
+ :accessor meta-description
+ :documentation "The text that goes into the META DESCRIPTION
+tag, and anywhere else we want to put this text so that it will show
+up in Google.")
+ (databases :initarg :databases
+ :initform nil
+ :accessor databases)
+ (mail-mx :initarg :mail-mx
+ :initform nil
+ :accessor mail-mx)
+ (mail-from :initarg :mail-from
+ :initform nil
+ :accessor mail-from)
+ (mail-postmaster :initarg :mail-postmaster
+ :initform nil
+ :accessor mail-postmaster)
+ (mail-webmaster :initarg :mail-webmaster
+ :initform nil
+ :accessor mail-webmaster)
+ (mail-info :initarg :mail-info
+ :initform nil
+ :accessor mail-info)
+ (mail-login-notify :initarg :mail-login-notify
+ :initform nil
+ :accessor mail-login-notify)
+ (mail-authentication :initarg :mail-authentication
+ :initform nil
+ :accessor mail-authentication)
+ (mail-ssl :initarg :mail-ssl
+ :initform nil
+ :accessor mail-ssl))
+ (:documentation ""))
+
+(defmethod get-site-file-path ((webapp webapp))
+ (format nil "~a/site" (document-root webapp)))
+
+(defmethod get-pages-file-paths ((webapp webapp))
+ (mapcar (lambda (pages-file)
+ (ppcre:regex-replace-all "\\.lisp$" (format nil "~a" pages-file) ""))
+ (remove-if (lambda (x) (equal x "shared"))
+ (org-ckons-core::shell-wrapper (format nil "find '~a' -maxdepth 1 -type f -iname 'pages*.lisp' |sort" (document-root webapp))))))
+
+(defun make-server-path (relative-path)
+ "Makes a relative filesystem path into a full one, using
+`*server-root*' as the base."
+ (make-document-root-path *server-root* relative-path))
+
+(defun make-document-root-path (document-root relative-path)
+ "Makes a relative filesystem path into a full one, using
+`document-root' as the base."
+ (concatenate 'string document-root relative-path))
+
+(defun make-webapp-path (relative-path)
+ "Makes an absolute filesystem path to a location in the webapps
+folder."
+ (concatenate 'string *server-root* "webapps/" relative-path))
+
+(defun get-options-files ()
+ (mapcar (lambda (webapp-directory)
+ (format nil "~a/conf/options.lisp" webapp-directory))
+ (remove-if (lambda (x) (or (org-ckons-core::match-it "webapps/$" x)
+ (org-ckons-core::match-it "webapps/shared$" x)
+ (org-ckons-core::match-it "webapps/CVS$" x)
+ (org-ckons-core::match-it "webapps/\\.$" x)
+ (org-ckons-core::match-it "webapps/\\.\\.$" x)))
+ (org-ckons-core::shell-wrapper (format nil "find '~a' -maxdepth 1 -type d |sort" (make-webapp-path ""))))))
+
+(defun set-webapp (webapp)
+ "Sets a `webapp' object in `*webapps*'. The lookup key is the webapp
+name. If a webapp already exists under this key, it gets overwritten
+with the new one."
+ (setf (gethash (name webapp) *webapps*) webapp))
+
+(defun get-webapp (key)
+ "Gets the webapp object stored under the key `key'."
+ (gethash key *webapps*))
+
+(defun populate-webapps ()
+ (loop for options-file in (get-options-files)
+ do (with-open-file (input options-file :direction :input)
+ (let* ((form (read input)))
+ (set-webapp (make-instance 'webapp
+ :name (getf form :name)
+ :scheme (getf form :scheme)
+ :url (getf form :url)
+ :document-root (make-webapp-path (getf form :document-root))
+ :title (getf form :title)
+ :meta-description (getf form :meta-description)
+ :databases (getf form :databases)
+ :mail-mx (getf form :mail-mx)
+ :mail-from (getf form :mail-from)
+ :mail-postmaster (getf form :mail-postmaster)
+ :mail-webmaster (getf form :mail-webmaster)
+ :mail-info (getf form :mail-info)
+ :mail-login-notify (getf form :mail-login-notify)
+ :mail-authentication (getf form :mail-authentication)
+ :mail-ssl (getf form :mail-ssl)))))))
+
+(defun resume ()
+ "Call this to start the server."
+ (when (null *acceptor*)
+ (let ((package (string-downcase (package-name #.*package*))))
+ (populate-webapps)
+ (setf (log-manager) (make-instance 'log-manager :message-class 'formatted-message))
+ (start-messenger 'text-file-messenger :filename (format nil "/var/log/lisp/~a.log" package))
+ (setf *session-secret* (org-ckons-session::generate-sessionid))
+ (populate-webapps)
+ (setf *acceptor* (start (make-instance 'easy-routes:easy-routes-acceptor
+ :port *port*
+ :document-root (make-server-path (format nil "webapps/~a/" package))
+ :name (format nil "~a-acceptor" package)))))))
+
+(defmacro with-request-wrapper (uri page-function &rest args)
+ (let ((package (string-downcase (package-name #.*package*))))
+ `(let (output)
+ (let* ((*webapp* (get-webapp ,package))
+ (*uri* ,uri)
+ (*header-register* (make-instance 'org-ckons-session::header-register))
+ (*sessionid* (ensure-user-session-exists)))
+ (ensure-user-exists)
+ (setf output (,page-function ,@args))
+ (org-ckons-session::ship-headers *header-register*))
+ output)))
+
+(defmacro define-endpoint (template-and-options var-list page-function &rest args)
+ "Does the grunt work of creating an `easy-routes' route for each page
+you wish to publish."
+ (let ((name (gensym))
+ (uri (first template-and-options))
+ (method (getf (rest template-and-options) :method)))
+ `(progn
+ (org-ckons-core::logger (format nil "Publishing page. URL = [~a], method = [~a]" ,uri ,method))
+ (easy-routes:defroute ,name ,template-and-options
+ ,var-list
+ (with-request-wrapper ,uri ,page-function ,@args)))))
diff --git a/sql/functions/auth.delete_registration.sql b/sql/functions/auth.delete_registration.sql
new file mode 100644
index 0000000..5adf705
--- /dev/null
+++ b/sql/functions/auth.delete_registration.sql
@@ -0,0 +1,14 @@
+drop function auth.delete_registration (
+ p_hash text
+);
+
+create or replace function auth.delete_registration (
+ p_hash text
+)
+returns void
+as $$
+begin
+ delete from auth.registrations r
+ where r.hash = ('\x' || p_hash)::bytea;
+end;
+$$ language plpgsql;
diff --git a/sql/functions/auth.delete_role_groups_for_user.sql b/sql/functions/auth.delete_role_groups_for_user.sql
new file mode 100644
index 0000000..1b14f0d
--- /dev/null
+++ b/sql/functions/auth.delete_role_groups_for_user.sql
@@ -0,0 +1,16 @@
+drop function auth.delete_role_groups_for_user (
+ p_user_id bigint
+);
+
+create or replace function auth.delete_role_groups_for_user (
+ p_user_id bigint
+)
+returns void
+as $$
+begin
+ delete from auth.users_role_groups urg
+ where user_id = p_user_id;
+
+ return;
+end;
+$$ language plpgsql;
diff --git a/sql/functions/auth.get_active_role_groups_for_user.sql b/sql/functions/auth.get_active_role_groups_for_user.sql
new file mode 100644
index 0000000..41f7712
--- /dev/null
+++ b/sql/functions/auth.get_active_role_groups_for_user.sql
@@ -0,0 +1,25 @@
+drop function auth.get_active_role_groups_for_user (
+ p_user_id bigint
+);
+
+create or replace function auth.get_active_role_groups_for_user (
+ p_user_id bigint
+)
+returns setof auth.role_groups_t
+as $$
+begin
+ return query
+ select urg.id as user_role_group_id,
+ urg.user_id,
+ rg.id as role_group_id,
+ rg.name,
+ rg.description
+ from auth.users_role_groups urg
+ inner join auth.role_groups rg
+ on urg.role_group_id = rg.id
+ where urg.user_id = p_user_id
+ order by rg.name asc;
+
+ return;
+end;
+$$ language plpgsql;
diff --git a/sql/functions/auth.get_active_user_by_id.sql b/sql/functions/auth.get_active_user_by_id.sql
new file mode 100644
index 0000000..7f635a0
--- /dev/null
+++ b/sql/functions/auth.get_active_user_by_id.sql
@@ -0,0 +1,27 @@
+drop function auth.get_active_user_by_id (
+ p_user_id bigint
+);
+
+create or replace function auth.get_active_user_by_id (
+ p_user_id bigint
+)
+returns setof auth.users
+as $$
+begin
+ return query
+ select u.id,
+ u.username,
+ u.pwd,
+ u.first_name,
+ u.last_name,
+ u.email,
+ u.phone,
+ u.active,
+ u.created
+ from auth.users u
+ where u.id = p_user_id
+ and u.active = true;
+
+ return;
+end;
+$$ language plpgsql;
diff --git a/sql/functions/auth.get_active_user_by_username_pwd.sql b/sql/functions/auth.get_active_user_by_username_pwd.sql
new file mode 100644
index 0000000..138234a
--- /dev/null
+++ b/sql/functions/auth.get_active_user_by_username_pwd.sql
@@ -0,0 +1,30 @@
+drop function auth.get_active_user_by_username_pwd (
+ p_username character varying,
+ p_pwd character varying
+);
+
+create or replace function auth.get_active_user_by_username_pwd (
+ p_username character varying,
+ p_pwd character varying
+)
+returns setof auth.users
+as $$
+begin
+ return query
+ select u.id,
+ u.username,
+ u.pwd,
+ u.first_name,
+ u.last_name,
+ u.email,
+ u.phone,
+ u.active,
+ u.created
+ from auth.users u
+ where u.username = p_username
+ and u.pwd = digest(p_pwd, 'sha512')
+ and u.active = true;
+
+ return;
+end;
+$$ language plpgsql;
diff --git a/sql/functions/auth.get_all_active_users.sql b/sql/functions/auth.get_all_active_users.sql
new file mode 100644
index 0000000..243be63
--- /dev/null
+++ b/sql/functions/auth.get_all_active_users.sql
@@ -0,0 +1,25 @@
+drop function auth.get_all_active_users ();
+
+create or replace function auth.get_all_active_users ()
+returns setof auth.users
+as $$
+begin
+ return query
+ select u.id,
+ u.username,
+ u.pwd,
+ u.first_name,
+ u.last_name,
+ u.email,
+ u.phone,
+ u.active,
+ u.created
+ from auth.users u
+ where u.active = true::bool
+ order by u.last_name asc,
+ u.first_name asc,
+ u.id asc;
+
+ return;
+end;
+$$ language plpgsql;
diff --git a/sql/functions/auth.get_all_inactive_users.sql b/sql/functions/auth.get_all_inactive_users.sql
new file mode 100644
index 0000000..eee0ab9
--- /dev/null
+++ b/sql/functions/auth.get_all_inactive_users.sql
@@ -0,0 +1,24 @@
+drop function auth.get_all_users ();
+
+create or replace function auth.get_all_users ()
+returns setof auth.users
+as $$
+begin
+ return query
+ select u.id,
+ u.username,
+ u.pwd,
+ u.first_name,
+ u.last_name,
+ u.email,
+ u.phone,
+ u.active,
+ u.created
+ from auth.users u
+ order by u.last_name asc,
+ u.first_name asc,
+ u.id asc;
+
+ return;
+end;
+$$ language plpgsql;
diff --git a/sql/functions/auth.get_all_role_groups_for_user.sql b/sql/functions/auth.get_all_role_groups_for_user.sql
new file mode 100644
index 0000000..f8c111d
--- /dev/null
+++ b/sql/functions/auth.get_all_role_groups_for_user.sql
@@ -0,0 +1,36 @@
+drop function auth.get_all_role_groups_for_user (
+ p_user_id bigint
+);
+
+create or replace function auth.get_all_role_groups_for_user (
+ p_user_id bigint
+)
+returns setof auth.role_groups_t
+as $$
+begin
+ if auth.superuser_p(p_user_id) = true::bool then
+ return query
+ select null::bigint as user_role_group_id,
+ p_user_id as user_id,
+ id as role_group_id,
+ name,
+ description
+ from auth.role_groups
+ order by name asc;
+ else
+ return query
+ select urg.id as user_role_group_id,
+ urg.user_id,
+ rg.id as role_group_id,
+ rg.name,
+ rg.description
+ from auth.users_role_groups urg
+ inner join auth.role_groups rg
+ on urg.role_group_id = rg.id
+ where urg.user_id = p_user_id
+ order by rg.name asc;
+ end if;
+
+ return;
+end;
+$$ language plpgsql;
diff --git a/sql/functions/auth.get_all_roles_for_user.sql b/sql/functions/auth.get_all_roles_for_user.sql
new file mode 100644
index 0000000..712bd4b
--- /dev/null
+++ b/sql/functions/auth.get_all_roles_for_user.sql
@@ -0,0 +1,163 @@
+drop function auth.get_all_roles_for_user (
+ p_user_id bigint
+);
+
+create or replace function auth.get_all_roles_for_user (
+ p_user_id bigint
+)
+returns setof auth.user_roles_t
+as $$
+begin
+ if auth.superuser_p(p_user_id) = true::bool then
+ return query
+ with all_user_roles as (
+ select null::bigint as user_role_group_id,
+ p_user_id as user_id,
+ null::bigint as role_group_id,
+ null as role_group_name,
+ null as role_group_description,
+ null::bigint as role_group_role_id,
+ null::bigint as role_id,
+ '_Public' as role_name,
+ null as role_description,
+ null::bigint as role_id_exclude,
+ null::bigint as role_id_append
+ union
+ select null::bigint as user_role_group_id,
+ p_user_id as user_id,
+ rg.id as role_group_id,
+ rg.name as role_group_name,
+ rg.description as role_group_description,
+ rgr.id as role_group_role_id,
+ r.id as role_id,
+ r.name as role_name,
+ r.description as role_description,
+ null::bigint as role_id_exclude,
+ null::bigint as role_id_append
+ from auth.role_groups rg
+ inner join auth.role_groups_roles rgr
+ on rgr.role_group_id = rg.id
+ inner join auth.roles r
+ on rgr.role_id = r.id
+ )
+ select a.user_role_group_id,
+ a.user_id,
+ a.role_group_id,
+ a.role_group_name,
+ a.role_group_description,
+ a.role_group_role_id,
+ a.role_id,
+ a.role_name,
+ a.role_description
+ from (
+ select r.user_role_group_id as user_role_group_id,
+ r.user_id as user_id,
+ r.role_group_id as role_group_id,
+ r.role_group_name as role_group_name,
+ r.role_group_description as role_group_description,
+ r.role_group_role_id as role_group_role_id,
+ r.role_id as role_id,
+ r.role_name as role_name,
+ r.role_description as role_description,
+ r.role_id_exclude as role_id_exclude,
+ r.role_id_append as role_id_append
+ from all_user_roles r
+ ) a
+ order by role_name asc;
+ else
+ return query
+ with all_user_roles as (
+ select null::bigint as user_role_group_id,
+ p_user_id as user_id,
+ null::bigint as role_group_id,
+ null as role_group_name,
+ null as role_group_description,
+ null::bigint as role_group_role_id,
+ null::bigint as role_id,
+ '_Public' as role_name,
+ null as role_description,
+ null::bigint as role_id_exclude,
+ null::bigint as role_id_append
+ union
+ select urg.id as user_role_group_id,
+ urg.user_id,
+ rg.id as role_group_id,
+ rg.name as role_group_name,
+ rg.description as role_group_description,
+ rgr.id as role_group_role_id,
+ r.id as role_id,
+ r.name as role_name,
+ r.description as role_description,
+ null::bigint as role_id_exclude,
+ null::bigint as role_id_append
+ from auth.users_role_groups urg
+ inner join auth.role_groups rg
+ on urg.role_group_id = rg.id
+ inner join auth.role_groups_roles rgr
+ on rgr.role_group_id = rg.id
+ inner join auth.roles r
+ on rgr.role_id = r.id
+ union
+ select null::bigint as user_role_group_id,
+ ure.user_id,
+ null::bigint as role_group_id,
+ null as role_group_name,
+ null role_group_description,
+ null::bigint as role_group_role_id,
+ r.id as role_id,
+ r.name as role_name,
+ r.description role_description,
+ ure.role_id as role_id_exclude,
+ null::bigint as role_id_append
+ from auth.users_roles_exclude ure
+ inner join auth.roles r
+ on ure.role_id = r.id
+ where ure.user_id = p_user_id
+ union
+ select null::bigint as user_role_group_id,
+ ura.user_id,
+ null::bigint as role_group_id,
+ null as role_group_name,
+ null role_group_description,
+ null::bigint as role_group_role_id,
+ r.id as role_id,
+ r.name as role_name,
+ r.description role_description,
+ null::bigint as role_id_exclude,
+ ura.role_id as role_id_append
+ from auth.users_roles_append ura
+ inner join auth.roles r
+ on ura.role_id = r.id
+ where ura.user_id = p_user_id
+ )
+ select a.user_role_group_id,
+ a.user_id,
+ a.role_group_id,
+ a.role_group_name,
+ a.role_group_description,
+ a.role_group_role_id,
+ a.role_id,
+ a.role_name,
+ a.role_description
+ from (
+ select r.user_role_group_id as user_role_group_id,
+ r.user_id as user_id,
+ r.role_group_id as role_group_id,
+ r.role_group_name as role_group_name,
+ r.role_group_description as role_group_description,
+ r.role_group_role_id as role_group_role_id,
+ r.role_id as role_id,
+ r.role_name as role_name,
+ r.role_description as role_description,
+ r.role_id_exclude as role_id_exclude,
+ r.role_id_append as role_id_append
+ from all_user_roles r
+ where user_id = p_user_id
+ ) a
+ where a.role_id_exclude is null
+ order by role_name asc;
+ end if;
+
+ return;
+end;
+$$ language plpgsql;
diff --git a/sql/functions/auth.get_registration_by_hash.sql b/sql/functions/auth.get_registration_by_hash.sql
new file mode 100644
index 0000000..87d622e
--- /dev/null
+++ b/sql/functions/auth.get_registration_by_hash.sql
@@ -0,0 +1,25 @@
+drop function auth.get_registration_by_hash (
+ p_hash text
+);
+
+create or replace function auth.get_registration_by_hash (
+ p_hash text
+)
+returns setof auth.registrations_t
+as $$
+begin
+ return query
+ select r.id,
+ substring(r.hash::text from 3),
+ r.first_name,
+ r.last_name,
+ r.email,
+ r.role_groups,
+ r.created,
+ (r.created - (now() - interval '3 days'))::interval as valid_for
+ from auth.registrations r
+ where r.hash = ('\x' || p_hash)::bytea;
+
+ return;
+end;
+$$ language plpgsql;
diff --git a/sql/functions/auth.get_registration_by_id.sql b/sql/functions/auth.get_registration_by_id.sql
new file mode 100644
index 0000000..dc9fdc1
--- /dev/null
+++ b/sql/functions/auth.get_registration_by_id.sql
@@ -0,0 +1,25 @@
+drop function auth.get_registration_by_id (
+ p_id bigint
+);
+
+create or replace function auth.get_registration_by_id (
+ p_id bigint
+)
+returns setof auth.registrations_t
+as $$
+begin
+ return query
+ select r.id,
+ substring(r.hash::text from 3),
+ r.first_name,
+ r.last_name,
+ r.email,
+ r.role_groups,
+ r.created,
+ (r.created - (now() - interval '3 days'))::interval as valid_for
+ from auth.registrations r
+ where r.id = p_id;
+
+ return;
+end;
+$$ language plpgsql;
diff --git a/sql/functions/auth.get_user_by_id.sql b/sql/functions/auth.get_user_by_id.sql
new file mode 100644
index 0000000..d745c49
--- /dev/null
+++ b/sql/functions/auth.get_user_by_id.sql
@@ -0,0 +1,26 @@
+drop function auth.get_user_by_id (
+ p_user_id bigint
+);
+
+create or replace function auth.get_user_by_id (
+ p_user_id bigint
+)
+returns setof auth.users
+as $$
+begin
+ return query
+ select u.id,
+ u.username,
+ u.pwd,
+ u.first_name,
+ u.last_name,
+ u.email,
+ u.phone,
+ u.active,
+ u.created
+ from auth.users u
+ where u.id = p_user_id;
+
+ return;
+end;
+$$ language plpgsql;
diff --git a/sql/functions/auth.has_role.sql b/sql/functions/auth.has_role.sql
new file mode 100644
index 0000000..1d6cdbd
--- /dev/null
+++ b/sql/functions/auth.has_role.sql
@@ -0,0 +1,28 @@
+drop function auth.has_role (
+ p_user_id bigint,
+ p_role_name character varying
+);
+
+create or replace function auth.has_role (
+ p_user_id bigint,
+ p_role_name character varying
+)
+returns setof auth.user_roles_t
+as $$
+begin
+ return query
+ select user_role_group_id,
+ user_id,
+ role_group_id,
+ role_group_name,
+ role_group_description,
+ role_group_role_id,
+ role_id,
+ role_name,
+ role_description
+ from auth.get_all_roles_for_user(p_user_id)
+ where role_name = p_role_name;
+
+ return;
+end;
+$$ language plpgsql;
diff --git a/sql/functions/auth.insert_registration.sql b/sql/functions/auth.insert_registration.sql
new file mode 100644
index 0000000..0f0b73d
--- /dev/null
+++ b/sql/functions/auth.insert_registration.sql
@@ -0,0 +1,42 @@
+drop function auth.insert_registration (
+ p_first_name character varying,
+ p_last_name character varying,
+ p_email character varying,
+ p_role_groups text
+);
+
+create or replace function auth.insert_registration (
+ p_first_name character varying,
+ p_last_name character varying,
+ p_email character varying,
+ p_role_groups text
+)
+returns bigint
+as $$
+declare
+ l_id bigint;
+begin
+ insert into auth.registrations (
+ hash,
+ first_name,
+ last_name,
+ email,
+ role_groups
+ ) values (
+ digest(concat(cast(current_timestamp as text), random()::text), 'sha512'),
+ p_first_name,
+ p_last_name,
+ p_email,
+ p_role_groups
+ );
+
+ select id into l_id
+ from auth.registrations
+ where first_name = p_first_name
+ and last_name = p_last_name
+ and email = p_email
+ and role_groups = p_role_groups;
+
+ return l_id;
+end;
+$$ language plpgsql;
diff --git a/sql/functions/auth.insert_user.sql b/sql/functions/auth.insert_user.sql
new file mode 100644
index 0000000..597fd39
--- /dev/null
+++ b/sql/functions/auth.insert_user.sql
@@ -0,0 +1,59 @@
+drop function auth.insert_user(
+ p_username character varying,
+ p_pwd character varying,
+ p_first_name character varying,
+ p_last_name character varying,
+ p_email character varying,
+ p_phone character varying,
+ p_active boolean
+);
+
+create or replace function auth.insert_user (
+ p_username character varying,
+ p_pwd character varying,
+ p_first_name character varying,
+ p_last_name character varying,
+ p_email character varying,
+ p_phone character varying,
+ p_active boolean
+)
+returns bigint
+as $$
+declare
+ l_id bigint;
+begin
+ select u.id into l_id
+ from auth.users u
+ where u.username = p_username
+ and u.pwd = digest(p_pwd, 'sha512');
+
+ if(l_id is null) then
+ insert into auth.users (
+ username,
+ pwd,
+ first_name,
+ last_name,
+ email,
+ phone,
+ active
+ ) values (
+ p_username,
+ digest(p_pwd, 'sha512'),
+ p_first_name,
+ p_last_name,
+ p_email,
+ p_phone,
+ p_active
+ );
+
+ select u.id into l_id
+ from auth.users u
+ where u.username = p_username
+ and u.pwd = digest(p_pwd, 'sha512');
+
+ return l_id;
+ else
+ return null;
+ end if;
+end;
+$$ language plpgsql;
diff --git a/sql/functions/auth.insert_user_role_group.sql b/sql/functions/auth.insert_user_role_group.sql
new file mode 100644
index 0000000..ee8b21e
--- /dev/null
+++ b/sql/functions/auth.insert_user_role_group.sql
@@ -0,0 +1,44 @@
+drop function auth.insert_user_role_group (
+ p_user_id bigint,
+ p_role_group_name character varying
+);
+
+create or replace function auth.insert_user_role_group (
+ p_user_id bigint,
+ p_role_group_name character varying
+)
+returns bigint
+as $$
+declare
+ l_role_group_id bigint;
+ l_user_role_group_id bigint;
+begin
+ select rg.id into l_role_group_id
+ from auth.role_groups rg
+ where rg.name = p_role_group_name;
+
+ if(l_role_group_id is not null) then
+ select urg.id into l_user_role_group_id
+ from auth.users_role_groups urg
+ where urg.user_id = p_user_id
+ and urg.role_group_id = l_role_group_id;
+
+ if(l_user_role_group_id is null) then
+ insert into auth.users_role_groups (
+ user_id,
+ role_group_id
+ ) values (
+ p_user_id,
+ l_role_group_id
+ );
+
+ select urg.id into l_user_role_group_id
+ from auth.users_role_groups urg
+ where urg.user_id = p_user_id
+ and urg.role_group_id = l_role_group_id;
+ end if;
+ end if;
+
+ return l_user_role_group_id;
+end;
+$$ language plpgsql;
diff --git a/sql/functions/auth.registrations_gc.sql b/sql/functions/auth.registrations_gc.sql
new file mode 100644
index 0000000..c1e1d90
--- /dev/null
+++ b/sql/functions/auth.registrations_gc.sql
@@ -0,0 +1,11 @@
+drop function auth.registrations_gc ();
+
+create or replace function auth.registrations_gc ()
+returns void
+as $$
+begin
+ delete
+ from auth.registrations
+ where created < now() - interval '3 days';
+end;
+$$ language plpgsql;
diff --git a/sql/functions/auth.superuser_p.sql b/sql/functions/auth.superuser_p.sql
new file mode 100644
index 0000000..f6865ec
--- /dev/null
+++ b/sql/functions/auth.superuser_p.sql
@@ -0,0 +1,34 @@
+drop function auth.superuser_p (
+ p_user_id bigint
+);
+
+create or replace function auth.superuser_p (
+ p_user_id bigint
+)
+returns bool
+as $$
+declare
+ l_user_id bigint;
+begin
+ with roles as (
+ select urg.user_id,
+ rg.id as role_group_id,
+ rg.name
+ from auth.users_role_groups urg
+ inner join auth.role_groups_roles rgr
+ on urg.role_group_id = rgr.role_group_id
+ inner join auth.role_groups rg
+ on rgr.role_group_id = rg.id
+ where urg.user_id = p_user_id
+ and rg.name = 'superuser'
+ )
+ select user_id into l_user_id
+ from roles;
+
+ if(l_user_id is null) then
+ return false::bool;
+ else
+ return true::bool;
+ end if;
+end;
+$$ language plpgsql;
diff --git a/sql/functions/auth.update_password.sql b/sql/functions/auth.update_password.sql
new file mode 100644
index 0000000..8dc730c
--- /dev/null
+++ b/sql/functions/auth.update_password.sql
@@ -0,0 +1,19 @@
+drop function auth.update_password (
+ p_id bigint,
+ p_pwd character varying
+);
+
+create or replace function auth.update_password (
+ p_id bigint,
+ p_pwd character varying
+)
+returns bigint
+as $$
+begin
+ update auth.users
+ set pwd = digest(p_pwd, 'sha512')
+ where id = p_id;
+
+ return p_id;
+end;
+$$ language plpgsql;
diff --git a/sql/functions/auth.update_profile.sql b/sql/functions/auth.update_profile.sql
new file mode 100644
index 0000000..8e08c4b
--- /dev/null
+++ b/sql/functions/auth.update_profile.sql
@@ -0,0 +1,32 @@
+drop function auth.update_profile (
+ p_id bigint,
+ p_username character varying,
+ p_first_name character varying,
+ p_last_name character varying,
+ p_email character varying,
+ p_phone character varying
+);
+
+create or replace function auth.update_profile (
+ p_id bigint,
+ p_username character varying,
+ p_first_name character varying,
+ p_last_name character varying,
+ p_email character varying,
+ p_phone character varying
+)
+returns bigint
+as $$
+begin
+ update auth.users
+ set username = p_username,
+ pwd = p_pwd,
+ first_name = p_first_name,
+ last_name = p_last_name,
+ email = p_email,
+ phone = p_phone
+ where id = p_id;
+
+ return p_id;
+end;
+$$ language plpgsql;
diff --git a/sql/functions/auth.update_user.sql b/sql/functions/auth.update_user.sql
new file mode 100644
index 0000000..312bf2e
--- /dev/null
+++ b/sql/functions/auth.update_user.sql
@@ -0,0 +1,45 @@
+drop function auth.update_user(
+ p_id bigint,
+ p_username character varying,
+ p_first_name character varying,
+ p_last_name character varying,
+ p_email character varying,
+ p_phone character varying
+);
+
+create or replace function auth.update_user (
+ p_id bigint,
+ p_username character varying,
+ p_first_name character varying,
+ p_last_name character varying,
+ p_email character varying,
+ p_phone character varying
+)
+returns bigint
+as $$
+declare
+ l_id bigint;
+begin
+ select u.id into l_id
+ from auth.users u
+ where u.id = p_id;
+
+ if(l_id is not null) then
+ update auth.users
+ set username = p_username,
+ first_name = p_first_name,
+ last_name = p_last_name,
+ email = p_email,
+ phone = p_phone
+ where id = p_id;
+
+ select u.id into l_id
+ from auth.users u
+ where u.id = p_id;
+
+ return l_id;
+ else
+ return null;
+ end if;
+end;
+$$ language plpgsql;
diff --git a/sql/functions/auth.upsert_user.sql b/sql/functions/auth.upsert_user.sql
new file mode 100644
index 0000000..28e94a0
--- /dev/null
+++ b/sql/functions/auth.upsert_user.sql
@@ -0,0 +1,66 @@
+drop function auth.upsert_user (
+ p_id bigint,
+ p_username character varying,
+ p_pwd character varying,
+ p_first_name character varying,
+ p_last_name character varying,
+ p_email character varying,
+ p_phone character varying,
+ p_active boolean
+);
+
+create or replace function auth.upsert_user (
+ p_id bigint,
+ p_username character varying,
+ p_pwd character varying,
+ p_first_name character varying,
+ p_last_name character varying,
+ p_email character varying,
+ p_phone character varying,
+ p_active boolean
+)
+returns bigint
+as $$
+begin
+ if(p_id is null) then
+ insert into auth.users (
+ username,
+ pwd,
+ first_name,
+ last_name,
+ email,
+ phone,
+ active
+ ) values (
+ p_username,
+ digest(p_pwd, 'sha512'),
+ p_first_name,
+ p_last_name,
+ p_email,
+ p_phone,
+ p_active
+ );
+
+ select id into p_id
+ from auth.users
+ where username = p_username
+ and pwd = digest(p_pwd, 'sha512')
+ and first_name = p_first_name
+ and last_name = p_last_name
+ and email = p_email
+ and phone = p_phone;
+ else
+ update auth.users
+ set username = p_username,
+ pwd = digest(p_pwd, 'sha512'),
+ first_name = p_first_name,
+ last_name = p_last_name,
+ email = p_email,
+ phone = p_phone,
+ active = p_active
+ where id = p_id;
+ end if;
+
+ return p_id;
+end;
+$$ language plpgsql;
diff --git a/sql/functions/auth.user_delete.sql b/sql/functions/auth.user_delete.sql
new file mode 100644
index 0000000..8cb399e
--- /dev/null
+++ b/sql/functions/auth.user_delete.sql
@@ -0,0 +1,50 @@
+drop function auth.user_delete (
+ p_id bigint
+);
+
+create or replace function auth.user_delete (
+ p_id bigint
+)
+returns bool
+as $$
+declare
+ l_id bigint;
+ l_result bool;
+begin
+ if p_id is null then
+ l_result := false::bool;
+ else
+ select id into l_id
+ from auth.users
+ where id = p_id;
+
+ if l_id is null or l_id != p_id then
+ l_result := false::bool;
+ else
+ delete
+ from auth.users_roles_append
+ where user_id = l_id;
+
+ delete
+ from auth.users_roles_exclude
+ where user_id = l_id;
+
+ delete
+ from auth.users_role_groups
+ where user_id = l_id;
+
+ delete
+ from contact.contact_us_posts_read
+ where user_id = l_id;
+
+ delete
+ from auth.users
+ where id = l_id;
+
+ l_result := true::bool;
+ end if;
+ end if;
+
+ return l_result;
+end;
+$$ language plpgsql;
diff --git a/sql/functions/auth.user_toggle_active.sql b/sql/functions/auth.user_toggle_active.sql
new file mode 100644
index 0000000..adc1b62
--- /dev/null
+++ b/sql/functions/auth.user_toggle_active.sql
@@ -0,0 +1,31 @@
+drop function auth.user_toggle_active (
+ p_id bigint
+);
+
+create or replace function auth.user_toggle_active (
+ p_id bigint
+)
+returns bigint
+as $$
+declare
+ l_id bigint;
+begin
+ select u.id into l_id
+ from auth.users u
+ where u.id = p_id;
+
+ if(l_id is not null) then
+ update auth.users
+ set active = not active
+ where id = p_id;
+
+ select u.id into l_id
+ from auth.users u
+ where u.id = p_id;
+
+ return l_id;
+ else
+ return null;
+ end if;
+end;
+$$ language plpgsql;
diff --git a/sql/functions/contact.get_contact_us_posts_by_id_and_read.sql b/sql/functions/contact.get_contact_us_posts_by_id_and_read.sql
new file mode 100644
index 0000000..646fe0f
--- /dev/null
+++ b/sql/functions/contact.get_contact_us_posts_by_id_and_read.sql
@@ -0,0 +1,30 @@
+drop function contact.get_contact_us_posts_by_id_and_read (
+ p_user_id bigint,
+ p_read bool
+);
+
+create or replace function contact.get_contact_us_posts_by_id_and_read (
+ p_user_id bigint,
+ p_read bool
+)
+returns setof contact.contact_us_posts
+as $$
+begin
+ return query
+ select c.id,
+ c.first_name,
+ c.last_name,
+ c.email,
+ c.phone,
+ c.submitted,
+ c.comments
+ from contact.contact_us_posts c
+ left outer join contact.contact_us_posts_read cr
+ on c.id = cr.contact_us_post_id
+ where (p_read = false and cr.user_id is null)
+ or (p_read = true and cr.user_id = p_user_id)
+ order by c.submitted desc;
+
+ return;
+end;
+$$ language plpgsql;
diff --git a/sql/functions/contact.insert_contact_us_post.sql b/sql/functions/contact.insert_contact_us_post.sql
new file mode 100644
index 0000000..739f084
--- /dev/null
+++ b/sql/functions/contact.insert_contact_us_post.sql
@@ -0,0 +1,45 @@
+drop function contact.insert_contact_us_post (
+ p_first_name character varying,
+ p_last_name character varying,
+ p_email character varying,
+ p_phone character varying,
+ p_comments text
+);
+
+create or replace function contact.insert_contact_us_post (
+ p_first_name character varying,
+ p_last_name character varying,
+ p_email character varying,
+ p_phone character varying,
+ p_comments text
+)
+returns bigint
+as $$
+declare
+ l_id bigint;
+begin
+ insert into contact.contact_us_posts (
+ first_name,
+ last_name,
+ email,
+ phone,
+ comments
+ ) values (
+ p_first_name,
+ p_last_name,
+ p_email,
+ p_phone,
+ p_comments
+ );
+
+ select id into l_id
+ from contact.contact_us_posts
+ where first_name = p_first_name
+ and last_name = p_last_name
+ and email = p_email
+ and phone = p_phone
+ and comments = p_comments;
+
+ return l_id;
+end;
+$$ language plpgsql;
diff --git a/sql/functions/contact.mark_contact_us_post.sql b/sql/functions/contact.mark_contact_us_post.sql
new file mode 100644
index 0000000..7bcc001
--- /dev/null
+++ b/sql/functions/contact.mark_contact_us_post.sql
@@ -0,0 +1,38 @@
+drop function contact.mark_contact_us_post (
+ p_contact_us_post_id int,
+ p_user_id int,
+ p_read_p bool
+);
+
+create or replace function contact.mark_contact_us_post (
+ p_contact_us_post_id int,
+ p_user_id int,
+ p_read_p bool
+)
+returns void
+as $$
+declare
+ l_id bigint;
+begin
+ if(p_read_p) then
+ select id into l_id
+ from contact.contact_us_posts_read
+ where contact_us_post_id = p_contact_us_post_id
+ and user_id = p_user_id;
+
+ if(l_id is null) then
+ insert into contact.contact_us_posts_read (
+ contact_us_post_id,
+ user_id
+ ) values (
+ p_contact_us_post_id,
+ p_user_id
+ );
+ end if;
+ else
+ delete from contact.contact_us_posts_read
+ where contact_us_post_id = p_contact_us_post_id
+ and user_id = p_user_id;
+ end if;
+end;
+$$ language plpgsql;
diff --git a/sql/schemas/schemas.sql b/sql/schemas/schemas.sql
new file mode 100644
index 0000000..c2108a9
--- /dev/null
+++ b/sql/schemas/schemas.sql
@@ -0,0 +1,6 @@
+-- set search_path = auth,general,contact,public;
+
+create schema auth;
+create schema general;
+create schema contact;
+create schema resume;
diff --git a/sql/tables/auth.registrations.sql b/sql/tables/auth.registrations.sql
new file mode 100644
index 0000000..91eb3a1
--- /dev/null
+++ b/sql/tables/auth.registrations.sql
@@ -0,0 +1,11 @@
+drop table auth.registrations cascade;
+
+create table auth.registrations (
+ id serial8 primary key,
+ hash bytea not null,
+ first_name character varying(255) not null,
+ last_name character varying(255) not null,
+ email character varying(255) not null,
+ role_groups text,
+ created timestamp without time zone not null default now()
+);
diff --git a/sql/tables/auth.role_groups.sql b/sql/tables/auth.role_groups.sql
new file mode 100644
index 0000000..8b6d225
--- /dev/null
+++ b/sql/tables/auth.role_groups.sql
@@ -0,0 +1,7 @@
+drop table auth.role_groups cascade;
+
+create table auth.role_groups (
+ id serial8 primary key,
+ name character varying(255) not null,
+ description character varying(255)
+);
diff --git a/sql/tables/auth.role_groups_roles.sql b/sql/tables/auth.role_groups_roles.sql
new file mode 100644
index 0000000..7ebab21
--- /dev/null
+++ b/sql/tables/auth.role_groups_roles.sql
@@ -0,0 +1,7 @@
+drop table auth.role_groups_roles cascade;
+
+create table auth.role_groups_roles (
+ id serial8 primary key,
+ role_group_id bigint not null references auth.role_groups(id),
+ role_id bigint not null references auth.roles(id)
+);
diff --git a/sql/tables/auth.roles.sql b/sql/tables/auth.roles.sql
new file mode 100644
index 0000000..f5873fe
--- /dev/null
+++ b/sql/tables/auth.roles.sql
@@ -0,0 +1,7 @@
+drop table auth.roles cascade;
+
+create table auth.roles (
+ id serial8 primary key,
+ name character varying(255) not null,
+ description character varying(255)
+);
diff --git a/sql/tables/auth.user_session_objects.sql b/sql/tables/auth.user_session_objects.sql
new file mode 100644
index 0000000..aedfb42
--- /dev/null
+++ b/sql/tables/auth.user_session_objects.sql
@@ -0,0 +1,8 @@
+drop table auth.user_session_objects cascade;
+
+create table auth.user_session_objects (
+ id serial8 primary key,
+ user_session_id bigint not null references auth.user_sessions(id),
+ session_key character varying(255),
+ serialization text
+);
diff --git a/sql/tables/auth.user_sessions.sql b/sql/tables/auth.user_sessions.sql
new file mode 100644
index 0000000..2629d62
--- /dev/null
+++ b/sql/tables/auth.user_sessions.sql
@@ -0,0 +1,7 @@
+drop table auth.user_sessions cascade;
+
+create table auth.user_sessions (
+ id serial8 primary key,
+ sessionid character varying(255) not null,
+ datetime bigint not null
+);
diff --git a/sql/tables/auth.users.sql b/sql/tables/auth.users.sql
new file mode 100644
index 0000000..8eecf76
--- /dev/null
+++ b/sql/tables/auth.users.sql
@@ -0,0 +1,13 @@
+drop table auth.users cascade;
+
+create table auth.users (
+ id serial8 primary key,
+ username character varying(255) not null,
+ pwd bytea,
+ first_name character varying(255) not null,
+ last_name character varying(255) not null,
+ email character varying(255) not null,
+ phone character varying(255),
+ active bool,
+ created timestamp without time zone not null default now()
+);
diff --git a/sql/tables/auth.users_role_groups.sql b/sql/tables/auth.users_role_groups.sql
new file mode 100644
index 0000000..44cfe4a
--- /dev/null
+++ b/sql/tables/auth.users_role_groups.sql
@@ -0,0 +1,7 @@
+drop table auth.users_role_groups cascade;
+
+create table auth.users_role_groups (
+ id serial8 primary key,
+ user_id bigint not null references auth.users(id),
+ role_group_id bigint not null references auth.role_groups(id)
+);
diff --git a/sql/tables/auth.users_roles_append.sql b/sql/tables/auth.users_roles_append.sql
new file mode 100644
index 0000000..f6603e6
--- /dev/null
+++ b/sql/tables/auth.users_roles_append.sql
@@ -0,0 +1,7 @@
+drop table auth.users_roles_append cascade;
+
+create table auth.users_roles_append (
+ id serial8 primary key,
+ user_id bigint not null references auth.users(id),
+ role_id bigint not null references auth.roles(id)
+);
diff --git a/sql/tables/auth.users_roles_exclude.sql b/sql/tables/auth.users_roles_exclude.sql
new file mode 100644
index 0000000..e47385e
--- /dev/null
+++ b/sql/tables/auth.users_roles_exclude.sql
@@ -0,0 +1,7 @@
+drop table auth.users_roles_exclude cascade;
+
+create table auth.users_roles_exclude (
+ id serial8 primary key,
+ user_id bigint not null references auth.users(id),
+ role_id bigint not null references auth.roles(id)
+);
diff --git a/sql/tables/contact.contact_us.sql b/sql/tables/contact.contact_us.sql
new file mode 100644
index 0000000..0957e3c
--- /dev/null
+++ b/sql/tables/contact.contact_us.sql
@@ -0,0 +1,6 @@
+drop table contact.contact_us cascade;
+
+create table contact.contact_us (
+ id serial8 primary key,
+ content text
+);
diff --git a/sql/tables/contact.contact_us_posts.sql b/sql/tables/contact.contact_us_posts.sql
new file mode 100644
index 0000000..5e208cb
--- /dev/null
+++ b/sql/tables/contact.contact_us_posts.sql
@@ -0,0 +1,11 @@
+drop table contact.contact_us_posts cascade;
+
+create table contact.contact_us_posts (
+ id serial8 primary key,
+ first_name character varying(255) not null,
+ last_name character varying(255) not null,
+ email character varying(255) not null,
+ phone character varying(255),
+ submitted timestamp without time zone not null default now(),
+ comments text
+);
diff --git a/sql/tables/contact.contact_us_posts_read.sql b/sql/tables/contact.contact_us_posts_read.sql
new file mode 100644
index 0000000..80be27c
--- /dev/null
+++ b/sql/tables/contact.contact_us_posts_read.sql
@@ -0,0 +1,7 @@
+drop table contact.contact_us_posts_read cascade;
+
+create table contact.contact_us_posts_read (
+ id serial8 primary key,
+ contact_us_post_id bigint not null references contact.contact_us_posts(id),
+ user_id bigint not null references auth.users(id)
+);
diff --git a/sql/tables/general.about_us.sql b/sql/tables/general.about_us.sql
new file mode 100644
index 0000000..b199daf
--- /dev/null
+++ b/sql/tables/general.about_us.sql
@@ -0,0 +1,7 @@
+drop table general.about_us cascade;
+
+create table general.about_us (
+ id serial8 primary key,
+ category character varying(255),
+ content text
+);
diff --git a/sql/tables/resume.contactinfo.sql b/sql/tables/resume.contactinfo.sql
new file mode 100644
index 0000000..85bcd2c
--- /dev/null
+++ b/sql/tables/resume.contactinfo.sql
@@ -0,0 +1,11 @@
+drop table resume.contactinfo cascade;
+
+create table resume.contactinfo (
+ id serial8 primary key,
+ address character varying(255) not null,
+ city character varying(255) not null,
+ state int not null references resume.states(id),
+ phone character varying(255) not null,
+ email character varying(255) not null,
+ visastatus_id int not null references resume.visastatus(id)
+);
diff --git a/sql/tables/resume.education.sql b/sql/tables/resume.education.sql
new file mode 100644
index 0000000..638a50c
--- /dev/null
+++ b/sql/tables/resume.education.sql
@@ -0,0 +1,12 @@
+drop table resume.education cascade;
+
+create table resume.education (
+ id serial8 primary key,
+ school character varying(255) not null,
+ start_date timestamp without time zone not null default now(),
+ end_date timestamp without time zone not null default now(),
+ major character varying(255) not null,
+ minor character varying(255) not null,
+ degree character varying(255) not null,
+ progress character varying(255) not null
+);
diff --git a/sql/tables/resume.job.sql b/sql/tables/resume.job.sql
new file mode 100644
index 0000000..6e1dc3d
--- /dev/null
+++ b/sql/tables/resume.job.sql
@@ -0,0 +1,10 @@
+drop table resume.job cascade;
+
+create table resume.job (
+ id serial8 primary key,
+ company character varying(255) not null,
+ location character varying(255) not null,
+ start_date timestamp without time zone not null default now(),
+ end_date timestamp without time zone not null default now(),
+ overview text
+);
diff --git a/sql/tables/resume.job_skill.sql b/sql/tables/resume.job_skill.sql
new file mode 100644
index 0000000..3b16b53
--- /dev/null
+++ b/sql/tables/resume.job_skill.sql
@@ -0,0 +1,7 @@
+drop table resume.job_skill cascade;
+
+create table resume.job_skill (
+ id serial8 primary key,
+ job_id int not null references resume.job(id),
+ skill_id int not null references resume.skill(id)
+);
diff --git a/sql/tables/resume.language.sql b/sql/tables/resume.language.sql
new file mode 100644
index 0000000..0f5c452
--- /dev/null
+++ b/sql/tables/resume.language.sql
@@ -0,0 +1,11 @@
+drop table resume.language cascade;
+
+create table resume.language (
+ id serial8 primary key,
+ name character varying(255) not null,
+ understanding_listening character varying(2) not null,
+ understanding_reading character varying(2) not null,
+ speaking_interaction character varying(2) not null,
+ speaking_production character varying(2) not null,
+ writing character varying(2) not null
+);
diff --git a/sql/tables/resume.resume.sql b/sql/tables/resume.resume.sql
new file mode 100644
index 0000000..2d3c8bf
--- /dev/null
+++ b/sql/tables/resume.resume.sql
@@ -0,0 +1,7 @@
+drop table resume.resume cascade;
+
+create table resume.resume (
+ id serial8 primary key,
+ user_id int not null references auth.users(id),
+ name character varying(255) not null
+);
diff --git a/sql/tables/resume.resume_contactinfo.sql b/sql/tables/resume.resume_contactinfo.sql
new file mode 100644
index 0000000..a51e33c
--- /dev/null
+++ b/sql/tables/resume.resume_contactinfo.sql
@@ -0,0 +1,7 @@
+drop table resume.resume_contactinfo cascade;
+
+create table resume.resume_contactinfo (
+ id serial8 primary key,
+ resume_id int not null references resume.resume(id),
+ contactinfo_id int not null references resume.contactinfo(id)
+);
diff --git a/sql/tables/resume.resume_education.sql b/sql/tables/resume.resume_education.sql
new file mode 100644
index 0000000..739169d
--- /dev/null
+++ b/sql/tables/resume.resume_education.sql
@@ -0,0 +1,7 @@
+drop table resume.resume_education cascade;
+
+create table resume.resume_education (
+ id serial8 primary key,
+ resume_id int not null references resume.resume(id),
+ education_id int not null references resume.education(id)
+);
diff --git a/sql/tables/resume.resume_job.sql b/sql/tables/resume.resume_job.sql
new file mode 100644
index 0000000..4418242
--- /dev/null
+++ b/sql/tables/resume.resume_job.sql
@@ -0,0 +1,7 @@
+drop table resume.resume_job cascade;
+
+create table resume.resume_job (
+ id serial8 primary key,
+ resume_id int not null references resume.resume(id),
+ job_id int not null references resume.job(id)
+);
diff --git a/sql/tables/resume.resume_language.sql b/sql/tables/resume.resume_language.sql
new file mode 100644
index 0000000..15a63e3
--- /dev/null
+++ b/sql/tables/resume.resume_language.sql
@@ -0,0 +1,7 @@
+drop table resume.resume_language cascade;
+
+create table resume.resume_language (
+ id serial8 primary key,
+ resume_id int not null references resume.resume(id),
+ language_id int not null references resume.language(id)
+);
diff --git a/sql/tables/resume.skill.sql b/sql/tables/resume.skill.sql
new file mode 100644
index 0000000..5085cf1
--- /dev/null
+++ b/sql/tables/resume.skill.sql
@@ -0,0 +1,6 @@
+drop table resume.skill cascade;
+
+create table resume.skill (
+ id serial8 primary key,
+ skill text not null
+);
diff --git a/sql/tables/resume.states.sql b/sql/tables/resume.states.sql
new file mode 100644
index 0000000..b52cb57
--- /dev/null
+++ b/sql/tables/resume.states.sql
@@ -0,0 +1,7 @@
+drop table resume.states cascade;
+
+create table resume.states (
+ id serial8 primary key,
+ state character varying(255) not null,
+ abbr character varying(2) not null
+);
diff --git a/sql/tables/resume.visastatus.sql b/sql/tables/resume.visastatus.sql
new file mode 100644
index 0000000..5008972
--- /dev/null
+++ b/sql/tables/resume.visastatus.sql
@@ -0,0 +1,6 @@
+drop table resume.visastatus cascade;
+
+create table resume.visastatus (
+ id serial8 primary key,
+ status character varying(255) not null
+);
diff --git a/sql/types/auth.registrations_t.sql b/sql/types/auth.registrations_t.sql
new file mode 100644
index 0000000..cde66d0
--- /dev/null
+++ b/sql/types/auth.registrations_t.sql
@@ -0,0 +1,12 @@
+drop type auth.registrations_t cascade;
+
+create type auth.registrations_t as (
+ id bigint,
+ hash text,
+ first_name character varying,
+ last_name character varying,
+ email character varying,
+ role_groups text,
+ created timestamp without time zone,
+ valid_for interval
+);
diff --git a/sql/types/auth.role_groups_t.sql b/sql/types/auth.role_groups_t.sql
new file mode 100644
index 0000000..615c481
--- /dev/null
+++ b/sql/types/auth.role_groups_t.sql
@@ -0,0 +1,9 @@
+drop type auth.role_groups_t cascade;
+
+CREATE TYPE auth.role_groups_t AS (
+ user_role_group_id bigint,
+ user_id bigint,
+ role_group_id bigint,
+ name character varying,
+ description character varying
+);
diff --git a/sql/types/auth.user_roles_t.sql b/sql/types/auth.user_roles_t.sql
new file mode 100644
index 0000000..e6b8443
--- /dev/null
+++ b/sql/types/auth.user_roles_t.sql
@@ -0,0 +1,13 @@
+drop type auth.user_roles_t cascade;
+
+CREATE TYPE auth.user_roles_t AS (
+ user_role_group_id bigint,
+ user_id bigint,
+ role_group_id bigint,
+ role_group_name character varying,
+ role_group_description character varying,
+ role_group_role_id bigint,
+ role_id bigint,
+ role_name character varying,
+ role_description character varying
+);