A website for producing interactive charts without writing a single line of code. Built with Common Lisp and Python. https://charts.craigoates.net
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

256 lines
8.8 KiB

;; (in-package :cl-user)
(defpackage hot-line.web
(:use #:cl
#:caveman2
#:hot-line.config
#:hot-line.view
#:hot-line.db
#:datafly
#:sxql
#:app-constants
#:local-time
#:sqlite
#:cl-pass
;; #:validation
#:authentication
#:user-management
#:hermetic
#:storage-management
#:convert
#:pagination
#:routing
#:storage)
(:export :*web*))
(in-package :hot-line.web)
;; for @route annotation
(syntax:use-syntax :annot)
;;
;; Application
(defclass <web> (<app>) ())
(defvar *web* (make-instance '<web>))
(clear-routing-rules *web*)
;;
;; Routing rules
(defroute "/" ()
(cond ((not (hermetic:logged-in-p))
(render "/index.html"
`(:token ,(authentication:csrf-token))))
(t (render "/index.html"
`(:user ,(authentication:get-current-user)
:token ,(authentication:csrf-token)
:roles ,(authentication:get-user-roles))))))
(defroute "/about" ()
(cond ((not (hermetic:logged-in-p))
(render "about.html"
`(:token ,(authentication:csrf-token))))
(t (render "about.html"
`(:user ,(authentication:get-current-user)
:token ,(authentication:csrf-token)
:roles ,(authentication:get-user-roles))))))
(defroute "/privacy" ()
(cond ((not (hermetic:logged-in-p))
(render "privacy.html"
`(:token ,(authentication:csrf-token))))
(t (render "privacy.html"
`(:user ,(authentication:get-current-user)
:token ,(authentication:csrf-token)
:roles ,(authentication:get-user-roles))))))
(defroute "/navigation" ()
(cond ((not (hermetic:logged-in-p))
(render "/nav-menu.html"
`(:token ,(authentication:csrf-token))))
(t (render "/nav-menu.html"
`(:user ,(authentication:get-current-user)
:token ,(authentication:csrf-token)
:roles ,(authentication:get-user-roles))))))
(defroute ("/sign-up" :method :get) ()
(if (hermetic:logged-in-p)
`(301 (:location "/dashboard"))
(render "sign-up.html" `(:token ,(authentication:csrf-token)))))
(defroute ("/sign-up" :method :post) (&key method)
(destructuring-bind (&key authenticity-token &allow-other-keys)
(authentication:request-params
(lack.request:request-body-parameters ningle:*request*))
(cond ((not (string= authenticity-token (authentication:csrf-token)))
'(403 (:content-type "text/plain") ("Denied")))
((hermetic:logged-in-p)
'(301 (:location "/dashboard")))
((string= "sign-up-user" method)
(routing:sign-up-user
(lack.request:request-body-parameters ningle:*request*)))
(t `(400 (:content-type "text/plain")
(,(format nil "Unknown method ~S" method)))))))
;; Admin/User Section
(defroute "/login" ()
(if (hermetic:logged-in-p)
`(301 (:location "/dashboard"))
(render "user/log-in.html"
`(:token ,(authentication:csrf-token)))))
(defroute ("/login" :method :post) (&key method)
(cond ((string= "login" method)
(routing:attempt-login (lack.request:request-body-parameters ningle:*request*)))
(t `(400 (:content-type "text/plain")
(,(format nil "Unknown method ~S" method))))))
(defroute ("/logout" :method :post) (&key method)
(cond ((string= "logout" method)
(routing:log-out (lack.request:request-body-parameters ningle:*request*)))
(t `(400 (:content-type "text/plain")
(,(format nil "Unknown method ~S" method))))))
(defroute ("/users") ()
(cond ((not (hermetic:logged-in-p))
(on-exception *web* 404))
((equal +true+ (user::is-administrator-p
(authentication:get-current-user)))
(render "user/index.html"
`(:user ,(authentication:get-current-user)
:users ,(user-management:get-all-users)
:user-count
,(user-management:get-total-user-count)
:categories
,(db-management:get-distinct-column-totals
"user" "administrator")
:token ,(authentication:csrf-token)
:roles ,(authentication:get-user-roles))))
(t (on-exception *web* 404))))
(defroute ("/user/add") ()
(cond ((not (hermetic:logged-in-p))
(on-exception *web* 404))
((equal +true+ (user::is-administrator-p
(authentication:get-current-user)))
(render "user/add.html"
`(:user ,(authentication:get-current-user)
:token ,(authentication:csrf-token)
:roles ,(authentication:get-user-roles))))
(t (on-exception *web* 404))))
(defroute ("/user/edit/:username") (&key username)
(cond ((or (not (hermetic:logged-in-p))
(null (user-management:user-in-db-p :username username)))
(on-exception *web* 404))
((or (and (not (null (user-management:user-in-db-p
:username username)))
(equal +true+ (user::is-administrator-p
(authentication:get-current-user))))
(string= username (user::username-of
(authentication:get-current-user))))
`(200 () (, (render "user/edit.html"
`(:user-to-edit ,(user-management:user-in-db-p
:username username)
:user ,(authentication:get-current-user)
:roles ,(authentication:get-user-roles)
:token ,(authentication:csrf-token)
:roles ,(authentication:get-user-roles)
:session ,ningle:*session*)))))
(t (on-exception *web* 404))))
(defroute ("/user" :method :post) (&key method)
(destructuring-bind
(&key authenticity-token &allow-other-keys)
(authentication:request-params
(lack.request:request-body-parameters ningle:*request*))
(cond ((not (string= authenticity-token (authentication:csrf-token)))
'(403 (:content-type "text/plain") ("Denied")))
((not (hermetic:logged-in-p))
'(303 (:location "/login")))
((string= "add" method)
(routing:add-user (lack.request:request-body-parameters ningle:*request*)))
((string= "update-role" method)
(routing:update-role (lack.request:request-body-parameters ningle:*request*)))
((string= "update-display-name" method)
(routing:update-display-name (lack.request:request-body-parameters ningle:*request*)))
((string= "update-password" method)
(routing:update-password (lack.request:request-body-parameters ningle:*request*)))
((string= "delete-user" method)
(routing:delete-user (lack.request:request-body-parameters ningle:*request*)))
(t `(400 (:content-type "text/plain")
(,(format nil "Unknown method ~S" method)))))))
(defroute "/dashboard" ()
(if (not (hermetic:logged-in-p))
'(303 (:location "/login"))
(let* ((current-user (authentication:get-current-user))
(username (user::username-of current-user)))
(render "user/dashboard.html"
`(:user ,current-user
:token ,(authentication:csrf-token)
:roles ,(authentication:get-user-roles)
:storage-files ,(reverse
(storage:get-file-names
(storage:get-files-in-directory
username ""))))))))
(defroute "/storage/download/:username/:filename" (&key username filename)
(if (and (hermetic:logged-in-p)
(string= username (user::username-of (authentication:get-current-user))))
(if (storage:file-exists-p username "" filename)
`(200 (:content-type "octet/stream")
,(storage:open-binary-file username "" filename)))
(on-exception *web* 404)))
(defroute ("/storage" :method :POST) (&key method)
(destructuring-bind
(&key filename authenticity-token &allow-other-keys)
(authentication:request-params
(lack.request:request-body-parameters ningle:*request*))
(cond ((not (string= authenticity-token (authentication:csrf-token)))
'(403 (:content-type "text/plain") ("Denied")))
((validation:string-is-nil-or-empty-p filename)
(hot-line.web::on-exception hot-line.web:*web* 404))
((not (hermetic:logged-in-p))
'(303 (:location "/login")))
((string= "delete-storage-file" method)
(routing:delete-storage-file
(lack.request:request-body-parameters ningle:*request*)))
(t (on-exception *web* 404)))))
;; This is where the chart stuff starts...
(defroute "/chart/add" ()
(if (not (hermetic:logged-in-p))
'(303 (:location "/login"))
(progn
(let* ((current-user (authentication:get-current-user))
(username (user::username-of current-user)))
(render "chart/add.html"
`(:user ,current-user
:token ,(authentication:csrf-token)
:roles ,(authentication:get-user-roles)))))))
(defroute ("/chartify" :method :post) (&key method)
(destructuring-bind
(&key authenticity-token &allow-other-keys)
(authentication:request-params
(lack.request:request-body-parameters ningle:*request*))
(cond ((not (string= authenticity-token (authentication:csrf-token)))
'(403 (:content-type "text/plain") ("Denied")))
((not (hermetic:logged-in-p))
'(303 (:location "/login")))
((string= "create-chart" method)
(routing:create-chart
(lack.request:request-body-parameters ningle:*request*)))
(t `(400 (:content-type "text/plain")
(,(format nil "Unknown method ~S" method)))))))
;;
;; Error pages
(defmethod on-exception ((app <web>) (code (eql 404)))
(declare (ignore app))
(merge-pathnames #P"_errors/404.html"
*template-directory*))