(in-package #:cl-user) (defpackage #:ritherdon-archive.web (:use #:cl #:caveman2 #:ritherdon-archive.config #:ritherdon-archive.view #:ritherdon-archive.db #:datafly #:sxql #:cl-pass #:app-constants #:status-codes ; (HTTP Status Codes) #:hermetic #:auth #:utils #:validation #:user #:nera-db #:files) (:export #:*web*)) (in-package #:ritherdon-archive.web) ;; for @route annotation (syntax:use-syntax :annot) ;; ;; Application (defclass () ()) (defvar *web* (make-instance ')) (clear-routing-rules *web*) ;; ;; Routing rules (defroute "/" () (cond ((not (uiop:file-exists-p (ritherdon-archive.config:database-name))) (redirect "/setup")) ((string= "archive" (site-settings::home-page-of (nera:get-site-settings))) (redirect "/archive")) ((string= "login" (site-settings::home-page-of (nera:get-site-settings))) (redirect "/login")) ((string= "pages" (site-settings::home-page-of (nera:get-site-settings))) (redirect "/pages")) ((string= "sign-up" (site-settings::home-page-of (nera:get-site-settings))) (redirect "/sign-up")) (t (render #P"index.html" (append (if (hermetic:logged-in-p) (auth:auth-user-data)) `(:alert ,(utils:get-and-reset-alert) :nav-menu ,(nera:nav-menu-slugs) :system-data ,(nera:system-data) :content ,(storage:open-text-file "" "pages" (site-settings::home-page-of (nera:get-site-settings))))))))) (defroute "/setup" () ;; If there is no database, there is no user, hence no more checks. (cond ((not (uiop:file-exists-p (ritherdon-archive.config:database-name))) (render #P"initial-setup.html" `(:token ,(auth:csrf-token)))) (t `(,+service-unavailable+ (:location "/"))))) (defroute ("/run-setup" :method :POST) () (destructuring-bind (&key authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (cond ((not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied"))) ((uiop:file-exists-p (ritherdon-archive.config:database-name)) `(,+service-unavailable+ (:location "/"))) (t (nera-db:init-db (lack.request:request-body-parameters ningle:*request*)) (storage:init-storage) ;; Redirect to /setup-complete? (utils:set-alert "All set-up. Log-in to continue.") (redirect "/login"))))) (defroute ("/sign-up" :method :GET) () (if (= +false+ (site-settings::enable-sign-up-p (nera:get-site-settings))) (progn (utils:set-alert "This feature has been disabled.") (redirect "/")) (hermetic:auth (:logged-in) ;; Hermetic Authorised (Logged in users can't sign-up). (progn (utils:set-alert "You need to be signed out to create a new account.") (redirect "/dashboard")) ;; Hermetic Not Authorised (let ((alert (utils:get-and-reset-alert))) (render "sign-up.html" `(:token ,(auth:csrf-token) :system-data ,(nera:system-data) :alert ,alert)))))) (defroute ("/sign-up" :method :POST) () (destructuring-bind (&key username display-name password password-check authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (cond ((not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied"))) ((not (null (nera:get-user username))) (utils:set-alert "Username already taken.") (redirect "/sign-up")) ((not (string= password password-check)) (utils:set-alert "Passwords don't match.") (redirect "/sign-up")) ((find t (mapcar #'utils:string-is-nil-or-empty? `(,username ,display-name ,password))) (utils:set-alert "Incomplete form. Please fill out every section.") (redirect "/sign-up")) (t (hermetic:auth (:logged-in) ;; Authorised (Logged in user can't sign-up). (progn (utils:set-alert "Logged in users are not allowed to create account.") (redirect "/dashboard")) ;; Not Authorised (progn (nera-db:create-user username display-name password +false+) (utils:set-alert "Account created.") (redirect "/login"))))))) (defroute ("/login" :method :GET) () (hermetic:auth (:logged-in) ;; Authorised (redirect "/dashboard") ;; Not Authorised (let ((alert (utils:get-and-reset-alert))) (render "user/log-in.html" `(:token ,(auth:csrf-token) :system-data ,(nera:system-data) :alert ,alert))))) (defroute ("/login" :method :POST) () (destructuring-bind (&key username password authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (if (not (string= authenticity-token (csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied")) (let ((params (list :|username| username :|password| password))) (hermetic:login params ;; Successful log-in attempt. (progn (setf ;; Set session Id. to the logged in user. (gethash :id ningle:*session*) (nera:get-user-id username) ;; Set the users password (for session) (gethash :password ningle:*session*) password) (redirect "/dashboard")) ;; Failed log-in attempt. (progn (utils:set-alert "Incorrect details provided.") (redirect "/login")) ;; No user found. (progn (utils:set-alert "Can't find that user.") (redirect "/login"))))))) (defroute ("/logout" :method :POST) () (destructuring-bind (&key authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (if (not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied")) (hermetic:auth (:logged-in) (hermetic:logout ;; Successful log-out. (progn (auth:flash-gethash :id ningle:*session*) (redirect "/")) ;; Failed log-out (progn (utils:set-alert "Error: Unable to log out.") (redirect "/"))))))) (defroute ("/dashboard" :method :GET) () (hermetic:auth (:logged-in) ;; Authorised (let ((alert (utils:get-and-reset-alert))) (render #P"user/dashboard.html" (append (auth:auth-user-data) `(:alert ,alert :storage-files ,(nera:latest-storage-editted-files 10 t) :pages ,(nera:latest-editted-pages 10 t) :archive-entries ,(nera::latest-archive-editted-entries 10 t) :system-data ,(nera:system-data))))) ;; Not Authorised (progn (utils:set-alert "You are not logged in.") (redirect "/login")))) (defroute ("/site-settings" :method :GET) () (hermetic:auth (:administrator) ;; Authorised (render #P"user/site-settings.html" (append (auth:auth-user-data) `(:alert ,(utils:get-and-reset-alert) :pages ,(nera:get-all-pages) :system-data ,(nera:system-data) :site-wide-snippet ,(storage:open-text-file "" "snippets" "site-wide-snippet.html") ;; Should this be replaced by `SYSTEM-DATA'? :settings ,(nera:get-site-settings)))) ;; Not Authorised (progn (utils:set-alert "You are not authorised to view this page.") (redirect "/login")))) (defroute ("/site-settings/update-sign-up" :method :POST) () (destructuring-bind (&key enable-sign-up authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (if (not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied")) (hermetic:auth (:administrator) ;; Authorised (progn (nera:update-enable-sign-on-settings enable-sign-up) (utils:set-alert "Enable Sign-Up setting saved.") (redirect "/site-settings")) ;; Not Authorised (progn (utils:set-alert "You are not authorised to view this page.") (redirect "/login")))))) (defroute ("/site-settings/update-nav-menu" :method :POST) (&key _parsed) (destructuring-bind (&key authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (if (not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied")) (hermetic:auth (:administrator) ;; Authorised (progn (nera:update-nav-menu (cdr (assoc "NAV-MENU" _parsed :test #'string=))) (utils:set-alert "Nav. menu updated." "success") (redirect "/site-settings")) ;; Not Authorised (progn (utils:set-alert "You are not authorised to view this page.") (redirect "/login")))))) (defroute ("/site-settings/update-home-page" :method :POST) () (destructuring-bind (&key set-home-page authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (if (not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied")) (hermetic:auth (:administrator) ;; Authorised (cond ((or (string= set-home-page "Select Page") (utils:string-is-nil-or-empty? set-home-page)) (utils:set-alert "No value provided. Home page not changed.") (redirect "/site-settings")) (t (nera:set-home-page set-home-page) (utils:set-alert "Home page set.") (redirect "/site-settings"))) ;; Not Authorised (progn (utils:set-alert "You are not authorised to view this page.") (redirect "/login")))))) (defroute ("/site-settings/update-site-name" :method :POST) () (destructuring-bind (&key site-name authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (if (not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied")) (hermetic:auth (:administrator) ;; Authorised (cond ((utils:string-is-nil-or-empty? site-name) (utils:set-alert "No value provided. Site name not changed.") (redirect "/site-settings")) (t (nera:update-site-name site-name) (utils:set-alert "Site name updated.") (redirect "/site-settings"))) ;; Not Authorised (progn (utils:set-alert "You are not authorised to view this page.") (redirect "/login")))))) (defroute ("/site-settings/update-code-snippet" :method :POST) () (destructuring-bind (&key code-snippet authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (if (not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied")) (hermetic:auth (:administrator) ;; Authorised (progn (storage:store-text "" "snippets" "site-wide-snippet.html" code-snippet) (utils:set-alert "Site-Wide snippet updated.") (redirect "/site-settings")) ;; Not Authorised (progn (utils:set-alert "You are not authorised to view this page.") (redirect "/login")))))) (defroute ("/site-settings/update-favicon" :method :POST) () (destructuring-bind (&key favicon-file authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (if (not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied")) (hermetic:auth (:administrator) ;; Authorised (cond ((utils:string-is-nil-or-empty? (cadr favicon-file)) (utils:set-alert "No file provided. Favicon not uploaded.") (redirect "/site-settings")) (t (storage:store-with-raw-path "static/images/favicon.png" favicon-file) (utils:set-alert "Favicon saved.") (utils:run-bash-command (format nil "convert ~a -resize 192x192\\> ~a" (merge-pathnames #P"static/images/favicon.png" ritherdon-archive.config:*application-root*) (merge-pathnames #P"static/images/favicon.png" ritherdon-archive.config:*application-root*))) (redirect "/site-settings"))) ;; Not Authorised (progn (utils:set-alert "You are not authorised to view this page.") (redirect "/login")))))) (defroute ("/site-settings/update-enable-site-logo" :method :POST) () (destructuring-bind (&key enable-site-logo authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (if (not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied")) (hermetic:auth (:administrator) ;; Authorised (progn (nera:update-enable-site-logo-setting enable-site-logo) (utils:set-alert "Enable Site Logo setting saved.") (redirect "/site-settings")) ;; Not Authorised (progn (utils:set-alert "You are not authorised to view this page.") (redirect "/login")))))) (defroute ("/site-settings/update-site-logo" :method :POST) () (destructuring-bind (&key site-logo authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (if (not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied")) (hermetic:auth (:administrator) ;; Authorised (cond ((utils:string-is-nil-or-empty? (cadr site-logo)) (utils:set-alert "No file provided. Site logo not uploaded.") (redirect "/site-settings")) (t (storage:store-with-raw-path "static/images/site-logo.png" site-logo) (utils:set-alert "Site logo saved.") (utils:run-bash-command (format nil "convert ~a -resize 500x500\\> ~a" (merge-pathnames #P"static/images/site-logo.png" ritherdon-archive.config:*application-root*) (merge-pathnames #P"static/images/site-logo.png" ritherdon-archive.config:*application-root*))) (redirect "/site-settings"))) ;; Not Authorised (progn (utils:set-alert "You are not authorised to view this page.") (redirect "/login")))))) (defroute ("/users" :method :GET) () (hermetic:auth (:administrator) ;; Authorised (let ((alert (utils:get-and-reset-alert))) (render #P"user/index.html" (append (auth:auth-user-data) `(:alert ,alert :system-data ,(nera:system-data) :users ,(nera:get-all-users))))) ;; Not Authorised (progn (utils:set-alert "You are not authorised to view this page.") (redirect "/login")))) (defroute ("/user/admin/create" :method :POST) () (destructuring-bind (&key username display-name password password-check authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (cond ((not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied"))) ((not (string= password password-check)) (utils:set-alert "Passwords don't match.") (redirect "/users")) ((find t (mapcar #'utils:string-is-nil-or-empty? `(,username ,display-name ,password))) (utils:set-alert "Incomplete form. Please fill out every section.") (redirect "/users")) ((not (null (nera:get-user username))) (utils:set-alert "Username already taken.") (redirect "/users")) (t (hermetic:auth (:administrator) ;; Authorised (progn (nera-db:create-user username display-name password +false+) (utils:set-alert "Account created.") (redirect "/users")) ;; Not Authorised (progn (utils:set-alert "You are not authorised to view that page.") (redirect "/"))))))) (defroute ("/user/edit" :method :GET) () (hermetic:auth (:logged-in) ;; Authorised (let ((alert (utils:get-and-reset-alert))) (render #P"user/edit.html" (append (auth:auth-user-data) `(:alert ,alert :system-data ,(nera:system-data))))) ;; Not Authorised (progn (utils:set-alert "You are not logged in.") (redirect "/login")))) (defroute ("/user/edit" :method :POST) () (destructuring-bind (&key display-name new-password password-check authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (cond ((not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied"))) ((not (string= new-password password-check)) (utils:set-alert "Passwords don't match.") (redirect "/user/edit")) (t (hermetic:auth (:logged-in) ;; Authorised (progn (nera-db:update-user (user::username-of (auth:get-current-user)) display-name new-password) (utils:set-alert "User details updated.") (redirect "/dashboard")) ;; Not Authorised (progn (utils:set-alert "You are not logged in.") (redirect "/login"))))))) (defroute ("/user/admin/edit-password" :method :POST) () (destructuring-bind (&key username password authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (cond ((not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied"))) (t (hermetic:auth (:administrator) ;; Authorised (cond ((utils:string-is-nil-or-empty? username) (utils:set-alert "Username not provided. No change made.") (redirect "/users")) ((utils:string-is-nil-or-empty? password) (utils:set-alert "Password not provided. No change made.") (redirect "/users")) ((null (nera:get-user username)) (utils:set-alert "Unable to find user. Unable to delete account") (redirect "/users")) (t (nera:update-user (user::username-of (nera:get-user username)) :new-password password) (utils:set-alert "Password changed.") (redirect "/users"))) ;; Not Authorised (progn (utils:set-alert "You are not authorised to view that page.") (redirect "/"))))))) (defroute ("/user/admin/delete" :method :POST) () (destructuring-bind (&key username authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (cond ((not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied"))) (t (hermetic:auth (:administrator) ;; Authorised (cond ((utils:string-is-nil-or-empty? username) (utils:set-alert "Username not provided. Unable to delete account.") (redirect "/users")) ((null (nera:get-user username)) (utils:set-alert "Unable to find user. Unable to delete account") (redirect "/users")) (t (nera:delete-user (user::username-of (nera:get-user username))) (utils:set-alert "Account deleted.") (redirect "/users"))) ;; Not Authorised (progn (utils:set-alert "You are not logged in.") (redirect "/login"))))))) (defroute ("/user/delete" :method :POST) () (destructuring-bind (&key authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (cond ((not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied"))) (t (hermetic:auth (:logged-in) ;; Authorised (progn (nera-db:delete-user (user::username-of (auth:get-current-user))) (hermetic:logout ;; Successful log-out -- after account deleted ;; (session data cleared). (progn (auth:flash-gethash :id ningle:*session*) (redirect "/")) ;; Failed log-out -- after account deleted ;; (session data persits). (progn (utils:set-alert "Error: Unable to delete session data.") (redirect "/")))) ;; Not Authorised (progn (utils:set-alert "You are not logged in.") (redirect "/login"))))))) (defroute ("/create/page" :method :GET) () (hermetic:auth (:logged-in) ;; Authorised (let ((alert (utils:get-and-reset-alert))) (render "/user/create-page.html" (append (auth:auth-user-data) `(:alert ,alert :system-data ,(nera:system-data))))) ;; Not Authorised (progn (utils:set-alert "You are not logged in.") (redirect "/login")))) (defroute ("/create/page" :method :POST) () (destructuring-bind (&key title page-content authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (cond ((not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied"))) (t (hermetic:auth (:logged-in) ;; Authorised (cond ((utils:string-is-nil-or-empty? title) (render "/user/create-page.html" (append (auth:auth-user-data) `(:alert "Title not provided. Unable to save page." :title ,title :data ,page-content)))) ((storage:file-exists-p "" "pages" (utils:slugify title)) (render "/user/create-page.html" (append (auth:auth-user-data) `(:alert "Page with that title already exists. Unable to save page." :title ,title :data ,page-content)))) (t (storage:store-text "" ; `USERNAME' blank because it's not used/needed. "pages" (utils:slugify title) page-content) (nera:create-page title (utils:slugify title) +false+ +true+) (utils:set-alert "Page created.") (redirect "/dashboard"))) ;; Not Authorised (progn (utils:set-alert "You are not logged in.") (redirect "/login"))))))) (defroute ("/user/pages" :method :GET) () (hermetic:auth (:logged-in) ;; Authorised (let ((alert (utils:get-and-reset-alert))) (render "/user/pages.html" (append (auth:auth-user-data) `(:alert ,alert :system-data ,(nera:system-data) :pages ,(nera:get-all-pages))))) ;; Not Authorised (progn (utils:set-alert "You are not logged in.") (redirect "/login")))) (defroute ("/pages" :method :GET) () (let ((alert (utils:get-and-reset-alert))) (render #P"pages.html" (append (if (hermetic:logged-in-p) (auth:auth-user-data)) `(:alert ,alert :system-data ,(nera:system-data) :pages ,(nera:get-all-pages)))))) (defroute ("/view/page/:slug" :method :GET) (&key slug) (let ((alert (utils:get-and-reset-alert))) (if (storage:file-exists-p "" "pages" slug) (render #P"page.html" (append (if (hermetic:logged-in-p) (auth:auth-user-data)) `(:alert ,alert :db-data ,(nera:get-page slug) :system-data ,(nera:system-data) :data ,(storage:open-text-file "" "pages" slug)))) (on-exception *web* 404)))) (defroute ("/edit/page/:slug" :method :GET) (&key slug) (hermetic:auth (:logged-in) ;; Authorised (let ((alert (utils:get-and-reset-alert))) (render "/user/edit-page.html" (append (auth:auth-user-data) `(:alert ,alert :system-data ,(nera:system-data) :db-data ,(nera:get-page slug) :data ,(storage:open-text-file "" "pages" slug))))) ;; Not Authorised (progn (utils:set-alert "You are not logged in.") (redirect "/login")))) (defroute ("/edit/page" :method :POST) () (destructuring-bind (&key slug page-content authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (cond ((not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied"))) (t (hermetic:auth (:logged-in) ;; Authorised (cond ((utils:string-is-nil-or-empty? slug) (utils:set-alert "Page cannot be found.") (redirect "/user/pages")) (t (storage:store-text "" ; `USERNAME' blank because it's not used/needed. "pages" slug page-content) (utils:set-alert "Page updated.") (redirect "/user/pages"))) ;; Not Authorised (progn (utils:set-alert "You are not logged in.") (redirect "/login"))))))) (defroute ("/rename/page" :method :POST) () (destructuring-bind (&key id slug new-title authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (cond ((not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied"))) (t (hermetic:auth (:logged-in) ;; Authorised (cond ((utils:string-is-nil-or-empty? slug) (utils:set-alert "Cannot find file. Unable to save changes.") (redirect "/user/pages")) ((utils:string-is-nil-or-empty? new-title) (utils:set-alert "No title provided. Unable to save changes.") (redirect (format nil "/edit/page/~a" slug))) ((utils:string-is-nil-or-empty? id) (utils:set-alert "No Id. found. Cannot find page in database.") (redirect "/user/pages")) (t (storage:rename-content-file "" ; `USERNAME' blank because it's not used/needed. "pages" slug (utils:slugify new-title)) (nera:update-page id new-title (utils:slugify new-title)) (utils:set-alert "File name changed.") (redirect (format nil "/edit/page/~a" (utils:slugify new-title))))) ;; Not Authorised (progn (utils:set-alert "You are not logged in.") (redirect "/login"))))))) (defroute ("/page/set-nav-menu" :method :POST) () (destructuring-bind (&key slug show-in-nav authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (cond ((not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied"))) (t (hermetic:auth (:logged-in) ;; Authorised (cond ((utils:string-is-nil-or-empty? slug) (utils:set-alert "Cannot find page in database.") (redirect "/user/pages")) (t (nera:update-nav-menu slug (if (null show-in-nav) +false+ +true+)) (utils:set-alert "Updated name changed.") (redirect (format nil "/edit/page/~a" slug)))) ;; Not Authorised (progn (utils:set-alert "You are not logged in.") (redirect "/login"))))))) (defroute ("/page/delete" :method :POST) () (destructuring-bind (&key slug authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (cond ((not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied"))) (t (hermetic:auth (:administrator) ;; Authorised (cond ((utils:string-is-nil-or-empty? slug) (utils:set-alert "Cannot find page in database. Nothing deleted.") (redirect "/user/pages")) ((null (storage:file-exists-p "" "pages" slug)) (utils:set-alert "Unable to find page. Nothing deleted.") (redirect "/user/pages")) ((eql (pages::can-delete-p (nera:get-page slug)) +false+) (utils:set-alert "File not allowed to be deleted.") (redirect "/user/pages")) (t (storage:remove-file "" ; `USERNAME' blank because it's not used/needed. "pages" slug) (nera:delete-page :slug slug) (utils:set-alert "Page deleted.") (redirect "/user/pages"))) ;; Not Authorised (progn (utils:set-alert "You are not authorised to delete page.") (redirect "/login"))))))) (defroute ("/storage/view/media/:slug" :method :GET) (&key slug) (if (nera:get-storage-file :slug slug) `(200 (:content-type ,(files::file-type-of (nera:get-storage-file :slug slug))) ,(storage:open-binary-file "" "media" (files::name-of (nera:get-storage-file :slug slug)))) (on-exception *web* 404))) (defroute ("/storage/thumb/archive/:slug" :method :GET) (&key slug) (if (nera:get-archive-entry :slug slug) (let ((archive-entry (nera:get-archive-entry :slug slug))) `(200 (:content-type ,(archive::thumbnail-file-type-of archive-entry)) ,(storage:open-binary-file "" "archive" (archive::thumbnail-slug-of archive-entry)))) (on-exception *web* 404))) (defroute ("/storage/manage" :method :GET) () (hermetic:auth (:logged-in) ;; Authorised (let ((alert (utils:get-and-reset-alert))) (render #P"/user/storage.html" (append (auth:auth-user-data) `(:alert ,alert :files ,(nera:get-all-storage-files) :system-data ,(nera:system-data))))) ;; Not Authorised (progn (utils:set-alert "You are not logged in.") (redirect "/login")))) (defroute ("/storage/multi-upload" :method :POST) () (destructuring-bind (&key authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (if (not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied")) (let ((files (utils:separate-files-in-web-request (lack.request:request-body-parameters ningle:*request*) "STORAGE-FILES"))) (hermetic:auth (:logged-in) ;; Authorised (cond ((utils:string-is-nil-or-empty? (caddr (car files))) (utils:set-alert "No files provided.") (redirect "/storage/manage")) (t (loop :for item :in files :do (storage:store-file "" "media" (utils:format-filename (caddr item)) (cdr item)) (nera:add-storage-file (caddr item) (utils:slugify (caddr item)) (cadddr item)) (when (str:contains? "image" (cadddr item) :ignore-case t) (utils:create-thumbnail "media" (utils:format-filename (caddr item)) nil) (nera:add-storage-file (format nil "thumbnail-~a" (caddr item)) (utils:slugify (format nil "thumbnail-~a" (caddr item))) (cadddr item)))) (utils:set-alert "Multi-File upload complete.") (redirect "/storage/manage"))) ;; Not Authorised (progn (utils:set-alert "You are not authorised to view this page.") (redirect "/login"))))))) (defroute ("/storage/rename/:slug" :method :POST) (&key slug) (destructuring-bind (&key new-file-name authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (if (not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied")) (hermetic:auth (:logged-in) ;; Authorised (cond ((utils:string-is-nil-or-empty? new-file-name) (utils:set-alert "No file name provided. Nothing updated.") (redirect "/storage/manage")) ((not (null (nera:get-storage-file :filename new-file-name))) (utils:set-alert "File with that name already exists. File not saved.") (redirect "/storage/manage")) (t (storage:rename-content-file "" "media" (files::slug-of (nera:get-storage-file :slug slug)) (utils:slugify new-file-name)) ;; Rename the thumbnail if there is one (not all files are images). (when (storage:file-exists-p "" "media" (format nil "thumbnail-~a" slug)) (storage:rename-content-file "" "media" (format nil "thumbnail-~a" (files::slug-of (nera:get-storage-file :slug slug))) (utils:slugify (format nil "thumbnail-~a" new-file-name)))) (nera:rename-storage-file (files::name-of (nera:get-storage-file :slug slug)) new-file-name) (utils:set-alert "File renamed.") (redirect "/storage/manage"))) ;; Not Authorised (progn (utils:set-alert "You are not authorised to view this page.") (redirect "/login")))))) (defroute ("/storage/delete/:slug" :method :POST) (&key slug) (destructuring-bind (&key authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (cond ((not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied"))) (t (hermetic:auth (:administrator) ;; Authorised (cond ((null (nera:get-storage-file :slug slug)) (utils:set-alert "Unable to find file. Nothing deleted.") (redirect "/storage/manage")) (t (storage:remove-file "" "media" (files::name-of (nera:get-storage-file :slug slug))) (when (storage:file-exists-p "" "media" (format nil "thumbnail-~a" slug)) (storage:remove-file "" "media" (format nil "thumbnail-~a" slug))) (nera:delete-storage-file :slug slug) (utils:set-alert "File deleted.") (redirect "/storage/manage"))) ;; Not Authorised (progn (utils:set-alert "You are not authorised to delete page.") (redirect "/login"))))))) (defroute ("/archive" :method :GET) () (let ((alert (utils:get-and-reset-alert))) (render #P"archive.html" (append (if (hermetic:logged-in-p) (auth:auth-user-data)) `(:alert ,alert :system-data ,(nera:system-data) :archive-entries ,(nera:get-all-archive-entries)))))) (defroute ("/view/archive/:slug" :method :GET) (&key slug) (let ((alert (utils:get-and-reset-alert))) (if (storage:file-exists-p "" "archive" slug) (render #P"archive-entry.html" (append (if (hermetic:logged-in-p) (auth:auth-user-data)) `(:alert ,alert :db-data ,(nera:get-archive-entry :slug slug) :system-data ,(nera:system-data) :data ,(storage:open-text-file "" "archive" slug)))) (on-exception *web* 404)))) (defroute ("/user/archive" :method :GET) () (hermetic:auth (:logged-in) ;; Authorised (let ((alert (utils:get-and-reset-alert))) (render "/user/archive.html" (append (auth:auth-user-data) `(:alert ,alert :system-data ,(nera:system-data) :archive-entries ,(nera:get-all-archive-entries))))) ;; Not Authorised (progn (utils:set-alert "You are not logged in.") (redirect "/login")))) (defroute ("/create/archive-entry" :method :GET) () (hermetic:auth (:logged-in) ;; Authorised (let ((alert (utils:get-and-reset-alert))) (render "/user/create-archive.html" (append (auth:auth-user-data) `(:alert ,alert :system-data ,(nera:system-data))))) ;; Not Authorised (progn (utils:set-alert "You are not logged in.") (redirect "/login")))) (defroute ("/create/archive-entry" :method :POST) () (destructuring-bind (&key title keywords thumbnail-file page-content authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (cond ((not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied"))) (t (hermetic:auth (:logged-in) ;; Authorised (cond ((find t (mapcar #'utils:string-is-nil-or-empty? `(,title ,keywords))) (render "/user/create-archive.html" (append (auth:auth-user-data) `(:alert "Data is missing. Unable to create entry." :title ,title :keywords ,keywords :data ,page-content)))) ((not (null (nera:get-archive-entry :title title))) (render "/user/create-archive.html" (append (auth:auth-user-data) `(:alert "Entry with that title already exists. Unable to create entry." :title ,title :keywords ,keywords :data ,page-content)))) ((not (str:contains? "image" (caddr thumbnail-file) :ignore-case t)) (render "/user/create-archive.html" (append (auth:auth-user-data) `(:alert "File uploaded is not an image. Entry not created." :title ,title :keywords ,keywords :data ,page-content)))) (t (nera:create-archive-entry title (utils:create-timestamp-id) (format nil "~a.html" (utils:slugify title)) (utils:format-filename (cadr thumbnail-file)) ; File Name (caddr thumbnail-file) ; File Type (utils:format-keywords keywords)) ;; parse info and enter into meilisearch database. ;; Use placeholder thumbnail if one not detected? (storage:store-text "" "archive" (format nil "~a.html" (utils:slugify title)) page-content) (storage:store-file "" "archive" (utils:format-filename (cadr thumbnail-file)) thumbnail-file) (utils:create-thumbnail ; Overwrites the original here. "archive" (utils:slugify (cadr thumbnail-file)) t) (utils:set-alert "Archive entry created. Great Success!") (redirect "/dashboard"))) ;; Not Authorised (progn (utils:set-alert "You are not logged in.") (redirect "/login"))))))) (defroute ("/edit/archive/:slug" :method :GET) (&key slug) (if (nera:get-archive-entry :slug slug) (progn (hermetic:auth (:logged-in) ;; Authorised (let ((alert (utils:get-and-reset-alert))) (render "/user/edit-archive.html" (append (auth:auth-user-data) `(:alert ,alert :system-data ,(nera:system-data) :db-data ,(nera:get-archive-entry :slug slug) :data ,(storage:open-text-file "" "archive" slug))))) ;; Not Authorised (progn (utils:set-alert "You are not logged in.") (redirect "/login")))) (on-exception *web* 404))) (defroute ("/edit/archive-thumbnail" :method :POST) () (destructuring-bind (&key archive-slug thumbnail-file authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (cond ((not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied"))) (t (hermetic:auth (:administrator) ;; Authorised (cond ((utils:string-is-nil-or-empty? archive-slug) (utils:set-alert "No archive slug provided. Thumbnail not updated.") (redirect "/user/archive")) ((null (nera:get-archive-entry :slug archive-slug)) (utils:set-alert "Cannot file thumbnail's archive entry. Nothing updated.") (redirect "/user/archive")) ((not (str:contains? "image" (caddr thumbnail-file) :ignore-case t)) (utils:set-alert "File uploaded is not an image. Thumbnail not updated.") (redirect (format nil "/edit/archive/~a" archive-slug))) (t (storage:remove-file "" "archive" (archive::thumbnail-slug-of (nera:get-archive-entry :slug archive-slug))) (storage:store-file "" "archive" (cadr thumbnail-file) thumbnail-file) (nera:update-archive-entry-property :slug archive-slug :property 'archive::thumbnail-file-type-of :value (caddr thumbnail-file)) (nera:update-archive-entry-property :slug archive-slug :property 'archive::thumbnail-slug-of :value (cadr thumbnail-file)) (utils:create-thumbnail "archive" (archive::thumbnail-slug-of (nera:get-archive-entry :slug archive-slug)) t) ;; Integrate updating Meilisearch here. (utils:set-alert "Thumbnail updated.") (redirect (format nil "/edit/archive/~a" archive-slug)))) ;; Not Authorised (progn (utils:set-alert "You are not authorised to delete this archive entry.") (redirect "/login"))))))) (defroute ("/rename/archive-entry" :method :POST) () (destructuring-bind (&key id archive-slug new-title authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (cond ((not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied"))) (t (hermetic:auth (:administrator) ;; Authorised (cond ((find t (mapcar #'utils:string-is-nil-or-empty? `(,id ,archive-slug ,new-title))) (utils:set-alert "Missing data. Archive entry not renamed.") (redirect "/user/archive")) ((null (nera:get-archive-entry :slug archive-slug)) (utils:set-alert "Cannot file archive entry. Archive entry not updated.") (redirect "/user/archive")) (t (storage:rename-content-file "" "archive" (archive::slug-of (nera:get-archive-entry :id (parse-integer id))) (utils:format-filename (format nil "~a.html" new-title))) (nera:update-archive-entry-property :slug archive-slug :property 'archive::title-of :value new-title) (nera:update-archive-entry-property :slug archive-slug :property 'archive::slug-of :value (utils:format-filename (format nil "~a.html" new-title))) ;; Integrate updating Meilisearch here. (utils:set-alert "Archive entry updated.") (redirect (format nil "/edit/archive/~a" (utils:format-filename (format nil "~a.html" new-title)))))) ;; Not Authorised (progn (utils:set-alert "You are not authorised to delete this archive entry.") (redirect "/login"))))))) (defroute ("/edit/archive-keywords" :method :POST) () (destructuring-bind (&key archive-slug new-keywords authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (cond ((not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied"))) (t (hermetic:auth (:administrator) ;; Authorised (cond ((find t (mapcar #'utils:string-is-nil-or-empty? `(,archive-slug ,new-keywords))) (utils:set-alert "Missing data. Keywords not updated.") (redirect "/user/archive")) ((null (nera:get-archive-entry :slug archive-slug)) (utils:set-alert "Cannot file archive entry. Keywords not updated.") (redirect "/user/archive")) (t (nera:update-archive-entry-property :slug archive-slug :property 'archive::keywords-of :value (utils:format-keywords new-keywords)) ;; Integrate updating Meilisearch here. (utils:set-alert "Archive entry updated.") (redirect (format nil "/edit/archive/~a" archive-slug)))) ;; Not Authorised (progn (utils:set-alert "You are not authorised to delete this archive entry.") (redirect "/login"))))))) (defroute ("/edit/archive" :method :POST) () (destructuring-bind (&key slug page-content authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (cond ((not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied"))) (t (hermetic:auth (:logged-in) ;; Authorised (cond ((utils:string-is-nil-or-empty? slug) (utils:set-alert "Archive entry cannot be found.") (redirect "/user/archive")) (t (storage:store-text "" "archive" slug page-content) (utils:set-alert "Archive entry updated.") (redirect (format nil "/edit/archive/~a" slug)))) ;; Not Authorised (progn (utils:set-alert "You are not logged in.") (redirect "/login"))))))) (defroute ("/archive/delete/entry" :method :POST) () (destructuring-bind (&key slug authenticity-token &allow-other-keys) (utils:request-params (lack.request:request-body-parameters ningle:*request*)) (cond ((not (string= authenticity-token (auth:csrf-token))) `(,+forbidden+ (:content-type "text/plain") ("Denied"))) (t (hermetic:auth (:administrator) ;; Authorised (cond ((utils:string-is-nil-or-empty? slug) (utils:set-alert "Cannot find archive entry in database. Nothing deleted.") (redirect "/user/archive")) ((and (null (storage:file-exists-p "" "archive" slug)) (null (nera:get-archive-entry :slug slug))) (utils:set-alert "Unable to find archive entry. Nothing deleted.") (redirect "/user/archive")) ((and (null (storage:file-exists-p "" "archive" slug)) (not (null (nera:get-archive-entry :slug slug)))) (nera:delete-archive-entry :slug slug) (utils:set-alert "Couldn't find archive entry files. Deleted from database only.") (redirect "/user/archive")) ((and (not (null (storage:file-exists-p "" "archive" slug))) (null (nera:get-archive-entry :slug slug))) (utils:set-alert "Couldn't find archive entry in database. Deleted files only.") ;; The thumbnail in not know because it was linked to the ;; text file via the database. You will need to delete the ;; thumbnail manually at this point but the system has ;; already failed here anyway so the extra work was already ;; needed. (storage:remove-file "" "archive" slug) (redirect "/user/archive")) (t (storage:remove-file "" "archive" slug) (storage:remove-file "" "archive" (archive::thumbnail-slug-of (nera:get-archive-entry :slug slug))) (nera:delete-archive-entry :slug slug) (utils:set-alert "Archive entry deleted.") (redirect "/user/archive"))) ;; Not Authorised (progn (utils:set-alert "You are not authorised to delete this archive entry.") (redirect "/login"))))))) ;; ;; Error pages (defmethod on-exception ((app ) (code (eql 404))) (declare (ignore app)) (merge-pathnames #P"_errors/404.html" *template-directory*))