diff options
| -rw-r--r-- | dynamic/api.scm | 157 | ||||
| -rw-r--r-- | dynamic/capabilities/comments.scm | 40 | ||||
| -rw-r--r-- | dynamic/capabilities/rsvp.scm | 280 | ||||
| -rw-r--r-- | dynamic/logging.scm | 37 | ||||
| -rw-r--r-- | dynamic/schema-rsvp.sql | 1 | ||||
| -rw-r--r-- | dynamic/util.scm | 57 | ||||
| -rw-r--r-- | haunt/static/image/hourglass.gif | bin | 0 -> 21999 bytes | |||
| -rw-r--r-- | haunt/static/js/rsvp.js | 183 |
8 files changed, 535 insertions, 220 deletions
diff --git a/dynamic/api.scm b/dynamic/api.scm index 4a41040..b53be8a 100644 --- a/dynamic/api.scm +++ b/dynamic/api.scm @@ -14,163 +14,26 @@ ;;; along with this program. If not, see ;;; <http://www.gnu.org/licenses/>. -(add-to-load-path (dirname (current-filename))) -;; (add-to-load-path "/home/jakob/Blog/dynamic") - -(use-modules (base64) - (captcha) - (ice-9 binary-ports) - (json) +(use-modules (dynamic capabilities rsvp) + (dynamic logging) (srfi srfi-1) - (srfi srfi-11) - (srfi srfi-13) - (srfi srfi-26) - (squee) - (rnrs bytevectors) (ice-9 match) (web server) (web request) (web response) (web uri)) -;; Globals. - -(define challenges (make-hash-table)) -;; (define conn (connect-to-postgres-paramstring "dbname=jakob_comments")) -(define conn (connect-to-postgres-paramstring "dbname=jakob_rsvp")) - -;; Util. - -(define (acons-list k v alist) - "Add V to K to alist as list" - (let ((value (assoc-ref alist k))) - (if value - (let ((alist (alist-delete k alist))) - (acons k (cons v value) alist)) - (acons k (list v) alist)))) - -(define (list->alist lst) - "Build a alist of list based on a list of key and values. - - Multiple values can be associated with the same key" - (let next ((lst lst) - (out '())) - (if (null? lst) - out - (next (cdr lst) (acons-list (caar lst) (cdar lst) out))))) - -(define (decode-form bv) - "Convert BV querystring or form data to an alist" - (define string (if (string? bv) bv (utf8->string bv))) - (define pairs (map (cut string-split <> #\=) - ;; semi-colon and amp can be used as pair separator - (append-map (cut string-split <> #\;) - (string-split string #\&)))) - (list->alist (map (match-lambda - ((key value) - (cons (uri-decode key) (uri-decode value)))) pairs))) - -(define (request-path-components request) - (split-and-decode-uri-path (uri-path (request-uri request)))) - (define (not-found request) + "Build a (somewhat) descriptive response for a non-existent resource." (values (build-response #:code 404) (string-append "Resource not found: " (uri->string (request-uri request))))) -;; - -(define (get-comments request body) - (values '((content-type . (application/json))) - (scm->json-string - '((title . "whoa buddy") - (author . "Jakob Kreuze") - (date . "2022-03-27") - (text . "bad take, bad take!"))))) - -(define (put-comment request body) - (display (decode-form body)) - (newline) - (values '((content-type . (text/plain))) "Hello hacker!")) - -(define (make-challenge request body) - (let-values (((uuid value image) (new-captcha))) - (hash-set! challenges uuid value) - (hash-for-each (lambda (x y) (display x) (newline)) challenges) - (values `((content-type . (application/base64)) - (access-control-allow-origin . "*") - (x-captcha-id . ,uuid)) - (base64-encode image)))) - -;; (define (valid-invite-code invitation) -;; (and (= (string-length)))) - -(define (generate-vanity-code) - (call-with-input-file "/dev/urandom" - (lambda (port) - (base64-encode (get-bytevector-n port 9))))) - -(define (post-event-rsvp request body) - (let* ((params (json-string->scm (utf8->string body))) - (invitation-code (assoc-ref params "id")) - (name (assoc-ref params "name")) - (email (assoc-ref params "email")) - (attending (assoc-ref params "rsvp")) - (guests (assoc-ref params "guests"))) - (cond ((not (and invitation-code name email attending guests)) - (values (build-response #:code 400 #:headers '((Access-Control-Allow-Origin . "*"))) - (scm->json-string - `((success . #f) - (error . "Invalid form data"))))) - ((> 1 (length (exec-query conn "SELECT event_id FROM invitations WHERE vanity = $1" (list invitation-code)))) - (values (build-response #:code 400 #:headers '((Access-Control-Allow-Origin . "*"))) - (scm->json-string - `((success . #f) - (error . "Invalid invitation code"))))) - (else - (let* ((receipt-code (generate-vanity-code)) - (event-id (caar (exec-query conn "SELECT event_id FROM invitations WHERE vanity = $1" (list invitation-code))))) - (exec-query conn "INSERT INTO rsvps (vanity, event_id, fullname, email, attending, guests) VALUES ($1, $2, $3, $4, $5, $6)" (list receipt-code event-id name email attending guests)) - (values '((content-type . (application/json)) - (Access-Control-Allow-Origin . "*")) - (scm->json-string - `((receipt . ,receipt-code))))))))) - -(define (get-event-info request body) - (define (format-rsvp rsvp) - (match rsvp - ((name email guests) - `((name . ,name) - (email . ,email) - (guests . ,guests))))) - (let* ((params (decode-form (uri-query (request-uri request)))) - (invitation-code (car (assoc-ref params "i"))) - (invitation (exec-query conn "SELECT event_id, capabilities FROM invitations WHERE vanity = $1" (list invitation-code)))) - (if (> 1 (length invitation)) - (values (build-response #:code 400 #:headers '((Access-Control-Allow-Origin . "*"))) - (scm->json-string - `((success . #f) - (error . "Invalid invitation code")))) - (let* ((capabilities (cadar invitation)) - (event (exec-query conn "SELECT * FROM events WHERE id = $1" (list (caar invitation)))) - (rsvps (exec-query conn "SELECT fullname, email, guests FROM rsvps WHERE event_id = $1" (list (caar invitation))))) - (match (car event) - ((i_ title description date location) - (values '((content-type . (application/json)) - (Access-Control-Allow-Origin . "*")) - (scm->json-string - `((title . ,title) - (description . ,description) - (image . #f) - (date . ,date) - (location . ,location) - ,@(if (= 1 (logand (string->number capabilities) 1)) - `((rsvps . ,(list->vector (map format-rsvp rsvps)))) - '())))))))))) - (define (handle-api-request request body endpoint) - (display (cons (request-method request) endpoint)) - (newline) + "Route handler for the API server." + (let ((method (request-method request)) + (originating-ip (assoc-ref (request-headers request) 'X-Forwarded-For))) + (log-append! 'info (format #f "~a ~a (~a)" method endpoint originating-ip))) ((match (cons (request-method request) endpoint) ;; ('(GET "challenge") make-challenge) ;; ('(GET "comments") get-comments) @@ -181,9 +44,11 @@ request body)) (define (main-request-handler request body) - (let ((path (request-path-components request))) + "Server entry-point; parse `request' and defer to routing system." + (let* ((path-encoded (uri-path (request-uri request))) + (path (split-and-decode-uri-path path-encoded))) (if (string= "api" (first path)) (handle-api-request request body (drop path 1)) (not-found request)))) -(run-server main-request-handler 'http '(#:port 8081)) +(run-server main-request-handler) diff --git a/dynamic/capabilities/comments.scm b/dynamic/capabilities/comments.scm new file mode 100644 index 0000000..d7dc1fa --- /dev/null +++ b/dynamic/capabilities/comments.scm @@ -0,0 +1,40 @@ +;;; Copyright © 2019 - 2022 Jakob L. Kreuze <zerodaysfordays@sdf.org> +;;; +;;; This program is free software; you can redistribute it and/or +;;; modify it under the terms of the GNU General Public License as +;;; published by the Free Software Foundation; either version 3 of the +;;; License, or (at your option) any later version. +;;; +;;; This program is distributed in the hope that it will be useful, +;;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +;;; General Public License for more details. +;;; +;;; You should have received a copy of the GNU General Public License +;;; along with this program. If not, see +;;; <http://www.gnu.org/licenses/>. + +(define challenges (make-hash-table)) +(define conn (connect-to-postgres-paramstring "dbname=jakob_comments")) + +(define (get-comments request body) + (values '((content-type . (application/json))) + (scm->json-string + '((title . "whoa buddy") + (author . "Jakob Kreuze") + (date . "2022-03-27") + (text . "bad take, bad take!"))))) + +(define (put-comment request body) + (display (decode-form body)) + (newline) + (values '((content-type . (text/plain))) "Hello hacker!")) + +(define (make-challenge request body) + (let-values (((uuid value image) (new-captcha))) + (hash-set! challenges uuid value) + (hash-for-each (lambda (x y) (display x) (newline)) challenges) + (values `((content-type . (application/base64)) + (access-control-allow-origin . "*") + (x-captcha-id . ,uuid)) + (base64-encode image)))) diff --git a/dynamic/capabilities/rsvp.scm b/dynamic/capabilities/rsvp.scm new file mode 100644 index 0000000..527f7a1 --- /dev/null +++ b/dynamic/capabilities/rsvp.scm @@ -0,0 +1,280 @@ +;;; Copyright © 2019 - 2022 Jakob L. Kreuze <zerodaysfordays@sdf.org> +;;; +;;; This program is free software; you can redistribute it and/or +;;; modify it under the terms of the GNU General Public License as +;;; published by the Free Software Foundation; either version 3 of the +;;; License, or (at your option) any later version. +;;; +;;; This program is distributed in the hope that it will be useful, +;;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +;;; General Public License for more details. +;;; +;;; You should have received a copy of the GNU General Public License +;;; along with this program. If not, see +;;; <http://www.gnu.org/licenses/>. + +(define-module (dynamic capabilities rsvp) + #:use-module (base64) + #:use-module (dynamic util) + #:use-module (ice-9 binary-ports) + #:use-module (ice-9 match) + #:use-module (json) + #:use-module (rnrs bytevectors) + #:use-module (squee) + #:use-module (srfi srfi-1) + #:use-module (srfi srfi-9) + #:use-module (web request) + #:use-module (web response) + #:use-module (web uri) + #:export (get-event-info post-event-rsvp)) + +;; How many bytes of entropy to use when generating vanity ID's. +(define %vanity-length (make-parameter 9)) + +;; Path where event header images are stored. +(define %event-image-path-fmt (make-parameter "/home/jakob/event-images/~a.png")) + +;; Global handle to the RSVP database. +(define conn (connect-to-postgres-paramstring "dbname=jakob_rsvp")) + + + +(define (generate-vanity-code) + "Generate a random vanity ID. + +A vanity ID is used in the RSVP system for creating unique URLs for invitations. +It is a base64 string, encoding `%vanity-length' bytes of randomness." + (call-with-input-file "/dev/urandom" + (lambda (port) (base64-encode (get-bytevector-n port 9))))) + +(define (valid-invite-code invitation) + "Check database to see if `invitation' exists." + (and (= (string-length invitation) (base64-length (%vanity-length))) + (positive? + (length + (exec-query conn "SELECT * FROM invitations WHERE vanity = $1" + (list invitation)))))) + +(define (valid-receipt-code receipt) + "Check database to see if `receipt'." + (and (= (string-length receipt) (base64-length (%vanity-length))) + (positive? + (length + (exec-query conn "SELECT * FROM rsvps WHERE vanity = $1" + (list receipt)))))) + + + +(define-record-type <rsvp-create> + (make-rsvp-create-parameters) + rsvp-create-parameters? + (invitation-code rsvp-create-code set-rsvp-create-code!) + (name rsvp-create-name set-rsvp-create-name!) + (email rsvp-create-email set-rsvp-create-email!) + (attending rsvp-create-attending set-rsvp-create-attending!) + (guests rsvp-create-guests set-rsvp-create-guests!)) + +(define (params->rsvp-create params) + "Parse `params', an alist, into a `<rsvp-create>'." + (let ((res (make-rsvp-create-parameters))) + (set-rsvp-create-code! res (assoc-ref params "id")) + (set-rsvp-create-name! res (assoc-ref params "name")) + (set-rsvp-create-email! res (assoc-ref params "email")) + (set-rsvp-create-attending! res (assoc-ref params "rsvp")) + (set-rsvp-create-guests! res (assoc-ref params "guests")) + (if (any not + (list (rsvp-create-code res) + (rsvp-create-name res) + (rsvp-create-email res) + (rsvp-create-attending res) + (rsvp-create-guests res))) + #f + res))) + +(define (invitation->event-id vanity-code) + "For valid `vanity-code', find the corresponding event ID and capabilities." + (car + (exec-query conn "SELECT event_id, capabilities FROM invitations WHERE vanity = $1" + (list vanity-code)))) + +(define (create-new-event-rsvp params) + "Handler for RSVP'ing to an event." + (let ((params (params->rsvp-create params))) + (cond ((not params) + (values (build-response #:code 400 #:headers '((Access-Control-Allow-Origin . "*"))) + (scm->json-string + `((success . #f) + (error . "Invalid form data"))))) + ((not (valid-invite-code (rsvp-create-code params))) + (values (build-response #:code 400 #:headers '((Access-Control-Allow-Origin . "*"))) + (scm->json-string + `((success . #f) + (error . "Invalid invitation code"))))) + (else + (let ((receipt-code (generate-vanity-code)) + (event-id (car (invitation->event-id (rsvp-create-code params))))) + (exec-query conn + "INSERT INTO rsvps (vanity, invitation_id, event_id, fullname, email, attending, guests) VALUES ($1, $2, $3, $4, $5, $6, $7)" + (list receipt-code + (rsvp-create-code params) + event-id + (rsvp-create-name params) + (rsvp-create-email params) + (rsvp-create-attending params) + (rsvp-create-guests params))) + (values '((content-type . (application/json)) + (Access-Control-Allow-Origin . "*")) + (scm->json-string + `((receipt . ,receipt-code))))))))) + + + +(define-record-type <rsvp-update> + (make-rsvp-update-parameters) + rsvp-update-parameters? + (invitation-code rsvp-update-code set-rsvp-update-code!) + (name rsvp-update-name set-rsvp-update-name!) + (email rsvp-update-email set-rsvp-update-email!) + (attending rsvp-update-attending set-rsvp-update-attending!) + (guests rsvp-update-guests set-rsvp-update-guests!)) + +(define (params->rsvp-update params) + "Parse `params', an alist, into a `<rsvp-update>'." + (let ((res (make-rsvp-update-parameters))) + (set-rsvp-update-code! res (assoc-ref params "update")) + (set-rsvp-update-name! res (assoc-ref params "name")) + (set-rsvp-update-email! res (assoc-ref params "email")) + (set-rsvp-update-attending! res (assoc-ref params "rsvp")) + (set-rsvp-update-guests! res (assoc-ref params "guests")) + (if (any not + (list (rsvp-update-code res) + (rsvp-update-name res) + (rsvp-update-email res) + (rsvp-update-attending res) + (rsvp-update-guests res))) + #f + res))) + +(define (update-event-rsvp params) + "Handler for updating an RSVP to an event." + (let ((params (params->rsvp-update params))) + (cond ((not params) + (values (build-response #:code 400 #:headers '((Access-Control-Allow-Origin . "*"))) + (scm->json-string + `((success . #f) + (error . "Invalid form data"))))) + ((not (valid-receipt-code (rsvp-update-code params))) + (values (build-response #:code 400 #:headers '((Access-Control-Allow-Origin . "*"))) + (scm->json-string + `((success . #f) + (error . "Invalid receipt code"))))) + (else + (exec-query conn + "UPDATE rsvps SET fullname = $2, email = $3, attending = $4, guests = $5 WHERE vanity = $1" + (list + (rsvp-update-code params) + (rsvp-update-name params) + (rsvp-update-email params) + (rsvp-update-attending params) + (rsvp-update-guests params))) + (values '((content-type . (application/json))) + (scm->json-string + `((receipt . ,(rsvp-update-code params))))))))) + + + +(define (post-event-rsvp request body) + "Entry point for RSVP create/update. We dispatch on the parameters." + (let* ((params (json-string->scm (utf8->string body)))) + (cond ((assoc-ref params "id") (create-new-event-rsvp params)) + ((assoc-ref params "update") (update-event-rsvp params)) + (else (values (build-response #:code 400 #:headers '((Access-Control-Allow-Origin . "*"))) + (scm->json-string + `((success . #f) + (error . "Invalid invite/update code")))))))) + + + +(define (get-event-image event-id) + "Return, as base64, the header image for `event-id'." + (call-with-input-file (format #f (%event-image-path-fmt) event-id) + (lambda (port) + (base64-encode (get-bytevector-all port))))) + +(define (get-event-invitation invitation-code) + "Handler for reading information about an event." + (define (format-rsvp rsvp) + (match rsvp + ((name email attending guests) + `((name . ,name) + (email . ,email) + (attending . ,attending) + (guests . ,guests))))) + (let* ((invitation (invitation->event-id invitation-code)) + (capabilities (cadr invitation)) + (event (exec-query conn "SELECT * FROM events WHERE id = $1" (list (car invitation)))) + (rsvps (exec-query conn "SELECT fullname, email, attending, guests FROM rsvps WHERE event_id = $1" (list (car invitation))))) + (match (car event) + ((i_ title description date location) + (values '((content-type . (application/json)) + (Access-Control-Allow-Origin . "*")) + (scm->json-string + `((title . ,title) + (description . ,description) + (image . ,(get-event-image (car invitation))) + (date . ,date) + (location . ,location) + ,@(if (= 1 (logand (string->number capabilities) 1)) + `((rsvps . ,(list->vector (map format-rsvp rsvps)))) + '())))))))) + +(define (get-event-receipt receipt-code) + "Handler for reading information about an event, with receipt info." + (define (format-rsvp rsvp) + (match rsvp + ((name email attending guests) + `((name . ,name) + (email . ,email) + (attending . ,attending) + (guests . ,guests))))) + (let* ((rsvp (exec-query conn "SELECT invitation_id, fullname, email, attending, guests FROM rsvps WHERE vanity = $1" (list receipt-code))) + (invitation (invitation->event-id (caar rsvp))) + (capabilities (cadr invitation)) + (event (exec-query conn "SELECT * FROM events WHERE id = $1" (list (car invitation)))) + (rsvps (exec-query conn "SELECT fullname, email, attending, guests FROM rsvps WHERE event_id = $1" (list (car invitation))))) + (match (car event) + ((i_ title description date location) + (values '((content-type . (application/json)) + (Access-Control-Allow-Origin . "*")) + (scm->json-string + `((title . ,title) + (description . ,description) + (image . ,(get-event-image (car invitation))) + (date . ,date) + (location . ,location) + (name . ,(list-ref (car rsvp) 1)) + (email . ,(list-ref (car rsvp) 2)) + (attending . ,(list-ref (car rsvp) 3)) + (guests . ,(list-ref (car rsvp) 4)) + ,@(if (= 1 (logand (string->number capabilities) 1)) + `((rsvps . ,(list->vector (map format-rsvp rsvps)))) + '())))))))) + +(define (get-event-info request body) + "Entry point to `get-event-receipt'/`get-event-invitation'." + (let* ((query-string (uri-query (request-uri request))) + (params (if query-string + (decode-form query-string) + '())) + (invitation-code (assoc-ref params "i")) + (receipt-code (assoc-ref params "r"))) + (cond ((and receipt-code (valid-receipt-code (car receipt-code))) + (get-event-receipt (car receipt-code))) + ((and invitation-code (valid-invite-code (car invitation-code))) + (get-event-invitation (car invitation-code))) + (else + (values (build-response #:code 400 #:headers '((Access-Control-Allow-Origin . "*"))) + (scm->json-string + `((success . #f) + (error . "Invalid invitation or receipt code")))))))) diff --git a/dynamic/logging.scm b/dynamic/logging.scm new file mode 100644 index 0000000..dab2464 --- /dev/null +++ b/dynamic/logging.scm @@ -0,0 +1,37 @@ +;;; Copyright © 2019 - 2022 Jakob L. Kreuze <zerodaysfordays@sdf.org> +;;; +;;; This program is free software; you can redistribute it and/or +;;; modify it under the terms of the GNU General Public License as +;;; published by the Free Software Foundation; either version 3 of the +;;; License, or (at your option) any later version. +;;; +;;; This program is distributed in the hope that it will be useful, +;;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +;;; General Public License for more details. +;;; +;;; You should have received a copy of the GNU General Public License +;;; along with this program. If not, see +;;; <http://www.gnu.org/licenses/>. + +(define-module (dynamic logging) + #:use-module (ice-9 format) + #:use-module (srfi srfi-19) + #:export (log-append!)) + +(define %log-file-name (make-parameter "/var/log/jakob-dynamic.log")) +(define %log-level (make-parameter 'info)) + +(define (dominates message-level baseline-level) + "Determine if `a' should be shown for baseline log level `b'." + (define log-level-hierarchy '(debug info warn error)) + (>= (list-index log-level-hierarchy message-level) + (list-index log-level-hierarchy baseline-level))) + +(define (log-append! level message) + "Append `message', at `level', to the log buffer." + (when (dominates level (%log-level)) + (call-with-output-file (%log-file-name) + (lambda (port) + (let ((now (date->string (current-date) "~4"))) + (format port "[~a] ~a: ~a~%" now level message)))))) diff --git a/dynamic/schema-rsvp.sql b/dynamic/schema-rsvp.sql index 42c8c41..3e6a21f 100644 --- a/dynamic/schema-rsvp.sql +++ b/dynamic/schema-rsvp.sql @@ -20,6 +20,7 @@ CREATE TABLE IF NOT EXISTS invitations ( CREATE TABLE IF NOT EXISTS rsvps ( id SERIAL, vanity char(12) NOT NULL, + invitation_id char(12) NOT NULL, event_id bigint NOT NULL, fullname varchar(128) NOT NULL, email varchar(256) NOT NULL, diff --git a/dynamic/util.scm b/dynamic/util.scm new file mode 100644 index 0000000..29a40db --- /dev/null +++ b/dynamic/util.scm @@ -0,0 +1,57 @@ +;;; Copyright © 2019 - 2022 Jakob L. Kreuze <zerodaysfordays@sdf.org> +;;; +;;; This program is free software; you can redistribute it and/or +;;; modify it under the terms of the GNU General Public License as +;;; published by the Free Software Foundation; either version 3 of the +;;; License, or (at your option) any later version. +;;; +;;; This program is distributed in the hope that it will be useful, +;;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +;;; General Public License for more details. +;;; +;;; You should have received a copy of the GNU General Public License +;;; along with this program. If not, see +;;; <http://www.gnu.org/licenses/>. + +(define-module (dynamic util) + #:use-module (ice-9 match) + #:use-module (rnrs bytevectors) + #:use-module (srfi srfi-1) + #:use-module (srfi srfi-26) + #:use-module (web uri) + #:export (base64-length + decode-form)) + +(define (acons-list k v alist) + "Add V to K to alist as list" + (let ((value (assoc-ref alist k))) + (if value + (let ((alist (alist-delete k alist))) + (acons k (cons v value) alist)) + (acons k (list v) alist)))) + +(define (list->alist lst) + "Build a alist of list based on a list of key and values. + + Multiple values can be associated with the same key" + (let next ((lst lst) + (out '())) + (if (null? lst) + out + (next (cdr lst) (acons-list (caar lst) (cdar lst) out))))) + +(define (decode-form bv) + "Convert BV querystring or form data to an alist" + (define string (if (string? bv) bv (utf8->string bv))) + (define pairs (map (cut string-split <> #\=) + ;; semi-colon and amp can be used as pair separator + (append-map (cut string-split <> #\;) + (string-split string #\&)))) + (list->alist (map (match-lambda + ((key value) + (cons (uri-decode key) (uri-decode value)))) pairs))) + +(define (base64-length n) + "The length of the base64 string encoding `n' bytes." + (inexact->exact (* 4 (ceiling (/ n 3.0))))) diff --git a/haunt/static/image/hourglass.gif b/haunt/static/image/hourglass.gif Binary files differnew file mode 100644 index 0000000..e1bf222 --- /dev/null +++ b/haunt/static/image/hourglass.gif diff --git a/haunt/static/js/rsvp.js b/haunt/static/js/rsvp.js index c7b4cd8..f0a4353 100644 --- a/haunt/static/js/rsvp.js +++ b/haunt/static/js/rsvp.js @@ -17,15 +17,89 @@ * <http://www.gnu.org/licenses/>. */ +// Common nodes. +const guestView = document.getElementById("guest-view"); +const addEntry = document.getElementById("add-entry"); +const removeEntry = document.getElementById("remove-entry"); +const submitButton = document.getElementById("submit-form"); +const form = document.getElementById("rsvp-input"); + +// Parse out the HTML form into something we can send to the backend. +function submitForm() { + let data = new FormData(form); + if (data.get("name").trim().length == 0) { + alert("You have to provide something for \"Name\"."); + return; + } else if (data.get("rsvp") === null) { + alert("You have to choose an option for \"RSVP Status\"."); + return; + } + + let res = { guests: [] }; + if (urlParams.get("i")) { + res["id"] = urlParams.get("i"); + } else if (urlParams.get("r")) { + res["update"] = urlParams.get("r"); + } + for (let pair of data.entries()) { + if (pair[0].startsWith("guest-")) { + res.guests.push(pair[1]); + } else if (pair[0] === "rsvp") { + const radioButtons = document.querySelectorAll('input[name="rsvp"]'); + for (const radioButton of radioButtons) { + if (radioButton.checked) { + res.rsvp = radioButton.id; + break; + } + } + } else { + res[pair[0]] = pair[1]; + } + } + res.guests = res.guests.join(","); + + let xhr = new XMLHttpRequest(); + xhr.open("POST", `${location.protocol}//${location.host}/api/rsvp`); + xhr.send(JSON.stringify(res)); + xhr.onload = function(data) { + if (xhr.status != 200) { + alert("Failed to submit form."); + } else { + document.getElementById("rsvp").innerHTML = ""; + let resp = JSON.parse(xhr.response); + let elem = document.createElement("p"); + elem.innerHTML = "Thanks for registering! Please bookmark or save the following link:"; + document.getElementById("rsvp").appendChild(elem); + + elem = document.createElement("a"); + elem.href = [location.protocol, '//', location.host, location.pathname].join('') + `?r=${resp.receipt}`; + elem.textContent = elem.href; + document.getElementById("rsvp").appendChild(elem); + + elem = document.createElement("p"); + elem.innerHTML = "This will enable you to edit your RSVP later."; + document.getElementById("rsvp").appendChild(elem); + } + } + xhr.onerror = function(error) { + throw new Error(`Request failed: ${error}`); + } +} + +// Immediately send off an XHR to load info from the backend. const urlParams = new URLSearchParams(window.location.search); -if (!urlParams.get("i")) { +if (!urlParams.get("i") && !urlParams.get("r")) { document.getElementById("loading").remove(); let elem = document.createElement("p"); elem.innerHTML = "No invitation code provided."; document.getElementById("rsvp").appendChild(elem); } else { let xhr = new XMLHttpRequest(); - xhr.open("GET", `http://localhost:8081/api/rsvp/event-info?i=${urlParams.get("i")}`); + if (urlParams.get("i")) { + xhr.open("GET", `${location.protocol}//${location.host}/api/rsvp/event-info?i=${urlParams.get("i")}`); + } else { + xhr.open("GET", `${location.protocol}//${location.host}/api/rsvp/event-info?r=${urlParams.get("r")}`); + } xhr.send(); xhr.onload = function(data) { document.getElementById("loading").remove(); @@ -42,6 +116,13 @@ if (!urlParams.get("i")) { elem.innerHTML = response.title; document.getElementById("event-info").appendChild(elem); + if (response.image !== "#f") { + elem = document.createElement("img"); + elem.src = `data:image/png;base64,${response.image}`; + elem.style = "float: right; max-size: 200px; margin: 16px;"; + document.getElementById("event-info").appendChild(elem); + } + elem = document.createElement("p"); elem.innerHTML = `Where: ${response.location}`; document.getElementById("event-info").appendChild(elem); @@ -63,7 +144,6 @@ if (!urlParams.get("i")) { document.getElementById("rsvp-global").appendChild(elem); for (let rsvp of response.rsvps) { - console.log(rsvp.name); let container = document.createElement("tr"); let field = document.createElement("td"); field.textContent = rsvp.name; @@ -72,7 +152,7 @@ if (!urlParams.get("i")) { field.textContent = rsvp.email; container.append(field); field = document.createElement("td"); - field.textContent = rsvp.guests.replace(',', ', '); + field.textContent = rsvp.guests.replaceAll(',', ', '); container.append(field); field = document.createElement("td"); field.textContent = rsvp.attending; @@ -80,8 +160,28 @@ if (!urlParams.get("i")) { elem.appendChild(container); } } - - console.log(response); + + // Pre-filled info if this is an edit. + if (response.hasOwnProperty("name")) { + document.getElementById("name").value = response.name; + } + if (response.hasOwnProperty("email")) { + document.getElementById("email").value = response.email; + } + if (response.hasOwnProperty("attending")) { + document.getElementById(response.attending).checked = true; + } + if (response.hasOwnProperty("guests") && response.guests.trim() != "") { + let i = 0; + for (let guest of response.guests.split(",")) { + let elem = document.createElement("input"); + elem.type = "text"; + elem.name = `guest-${i}`; + elem.value = guest; + guestView.appendChild(elem); + i++; + } + } } } xhr.onerror = function(error) { @@ -89,12 +189,9 @@ if (!urlParams.get("i")) { } } -const guestView = document.getElementById("guest-view"); -const addEntry = document.getElementById("add-entry"); -const removeEntry = document.getElementById("remove-entry"); -const submitButton = document.getElementById("submit-form"); -const form = document.getElementById("rsvp-input"); - +// Attach event listeners to the UI. +submitButton.addEventListener("click", submitForm); +submitButton.addEventListener("submit", submitForm); addEntry.addEventListener("click", () => { let i = guestView.childNodes.length; let elem = document.createElement("input"); @@ -102,70 +199,8 @@ addEntry.addEventListener("click", () => { elem.name = `guest-${i}`; guestView.appendChild(elem); }); - removeEntry.addEventListener("click", () => { if (guestView.lastChild) { guestView.lastChild.remove(); } }); - -function submitForm() { - let data = new FormData(form); - if (data.get("name").trim().length == 0) { - alert("You have to provide something for \"Name\"."); - return; - } else if (data.get("rsvp") === null) { - alert("You have to choose an option for \"RSVP Status\"."); - return; - } - - let res = { id: urlParams.get("i"), guests: [] }; - for (let pair of data.entries()) { - console.log(pair[0]); - if (pair[0].startsWith("guest-")) { - res.guests.push(pair[1]); - } else if (pair[0] === "rsvp") { - const radioButtons = document.querySelectorAll('input[name="rsvp"]'); - for (const radioButton of radioButtons) { - if (radioButton.checked) { - res.rsvp = radioButton.id; - break; - } - } - } else { - res[pair[0]] = pair[1]; - } - } - res.guests = res.guests.join(","); - console.log(res); - - let xhr = new XMLHttpRequest(); - xhr.open("POST", "http://localhost:8081/api/rsvp"); - xhr.send(JSON.stringify(res)); - xhr.onload = function(data) { - if (xhr.status != 200) { - alert("Failed to submit form."); - } else { - document.getElementById("rsvp").innerHTML = ""; - let resp = JSON.parse(xhr.response); - let elem = document.createElement("p"); - elem.innerHTML = "Thanks for registering! Please bookmark or save the following link:"; - document.getElementById("rsvp").appendChild(elem); - - elem = document.createElement("a"); - elem.href = [location.protocol, '//', location.host, location.pathname].join('') + `?r=${resp.receipt}`; - elem.textContent = elem.href; - document.getElementById("rsvp").appendChild(elem); - - elem = document.createElement("p"); - elem.innerHTML = "This will enable you to edit your RSVP later."; - document.getElementById("rsvp").appendChild(elem); - } - } - xhr.onerror = function(error) { - throw new Error(`Request failed: ${error}`); - } -} - -submitButton.addEventListener("click", submitForm); -submitButton.addEventListener("submit", submitForm); |