Merge branch '2-allow-users-to-save-favorites-food-items' into 'master'

Resolve "allow users to save favorites food items"

Closes #2

See merge request aretherecookies/server!1
This commit is contained in:
Bart Akeley 2020-03-29 19:39:52 +00:00
commit f42bfe4afa
6 changed files with 133 additions and 58 deletions

View file

@ -11,9 +11,9 @@ CREATE TYPE CATEGORY AS ENUM ('beverages', 'desserts', 'entrees', 'other');
DROP TABLE IF EXISTS food_items CASCADE;
CREATE TABLE food_items (
id uuid PRIMARY KEY,
name VARCHAR(100),
place_id VARCHAR(50),
category CATEGORY,
name VARCHAR(100) NOT NULL,
place_id VARCHAR(50) NOT NULL,
category CATEGORY NOT NULL,
images VARCHAR,
loc geography(POINT,4326)
);
@ -22,7 +22,7 @@ DROP TABLE IF EXISTS images CASCADE;
CREATE TABLE images (
filename VARCHAR(200) PRIMARY KEY,
food_item_id uuid REFERENCES food_items (id) ON DELETE CASCADE,
username VARCHAR(100),
username VARCHAR(100) NOT NULL,
date timestamp (1) with time zone
);
@ -30,10 +30,17 @@ DROP TABLE IF EXISTS quantities CASCADE;
CREATE TABLE quantities (
food_item_id uuid REFERENCES food_items (id) ON DELETE CASCADE,
date timestamp (1) with time zone,
quantity QUANTITY,
PRIMARY KEY(food_item_id, date)
quantity QUANTITY NOT NULL,
PRIMARY KEY (food_item_id, date)
);
DROP TABLE IF EXISTS faves CASCADE;
CREATE TABLE faves (
food_item_id uuid REFERENCES food_items (id) ON DELETE CASCADE,
username VARCHAR(200) NOT NULL,
date timestamp (1) with time zone NOT NULL,
PRIMARY KEY (food_item_id, username)
);
CREATE INDEX IF NOT EXISTS food_loc_index ON food_items USING GIST ( loc );

View file

@ -4,18 +4,19 @@
quantity-handler
add-food-item-handler
add-image-handler
get-images-handler]]
[aretherecookies.auth :refer [facebook-auth-backend
google-auth-backend]]
get-images-handler
faves-get-handler
faves-put-handler
faves-delete-handler]]
[aretherecookies.auth :refer [auth0-auth-backend]]
[environ.core :refer [env]]
[compojure.handler :refer [api]]
[compojure.core :refer :all]
[compojure.core :refer [defroutes GET POST PUT DELETE]]
[ring.adapter.jetty :as jetty]
[ring.middleware.anti-forgery :refer :all]
[ring.middleware.json :refer [wrap-json-body]]
[ring.middleware.multipart-params :refer [wrap-multipart-params]]
[ring.middleware.defaults :refer [wrap-defaults api-defaults]]
[buddy.auth :refer [throw-unauthorized]]
[buddy.auth.middleware :refer [wrap-authentication wrap-authorization]]))
(defroutes app-routes
@ -25,7 +26,10 @@
(POST "/quantity" [] quantity-handler)
(POST "/addfooditem" [] add-food-item-handler)
(POST "/images/:foodItemId" [] add-image-handler)
(GET "/images/:foodItemId" [] get-images-handler))
(GET "/images/:foodItemId" [] get-images-handler)
(POST "/faves" [] faves-get-handler)
(PUT "/fave" [] faves-put-handler)
(DELETE "/fave" [] faves-delete-handler))
(def app-config (assoc-in api-defaults [:security :anti-forgery] false))
@ -35,11 +39,9 @@
[& [port]]
(let [port (Integer. (or port (env :port) 3000))]
(->
(api #'app)
(wrap-authorization facebook-auth-backend)
(wrap-authorization google-auth-backend)
(wrap-authentication facebook-auth-backend)
(wrap-authentication google-auth-backend)
wrap-multipart-params
(wrap-json-body {:keywords? true})
(jetty/run-jetty {:port port :join? false}))))
(api #'app)
(wrap-authentication auth0-auth-backend)
(wrap-authorization auth0-auth-backend)
wrap-multipart-params
(wrap-json-body {:keywords? true})
(jetty/run-jetty {:port port :join? false}))))

View file

@ -2,16 +2,20 @@
(:require [buddy.auth.backends :as backends]
[clj-http.client :as client]))
; TODO implement this as LRU cache to eliminate memory leaking
(def tokens (atom {}))
(def facebook-me-url "https://graph.facebook.com/me?access_token=")
(def google-userinfo-url "https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=")
(def auth0-userinfo-url "https://aretherecookies.auth0.com/userinfo")
(defn verify-token
"perform rest requesting using token and return boolean if successful"
[token url]
(= 200 (:status (client/get (str url token) {:accept :json}))))
(try
(let [headers {:authorization (str "Bearer " token)}
res (if-not (empty? token) (client/get url {:accept :json :headers headers}) {})
status (:status res)]
(= 200 status))
(catch Exception e (print (str "Caught: " e)))))
(defn cache-token
"swap the new token into our cache map atom and return the token"
@ -21,7 +25,7 @@
(defn verify-and-cache-token
"if a REST request is successful against url using token, cache the token and return it"
[token url]
(if (verify-token token url) (cache-token token)))
(if (verify-token token url) (cache-token token) nil))
(defn url-backend
"return a buddy auth backend that validates tokens agianst given url"
@ -29,8 +33,5 @@
(fn [_ token] (get @tokens (keyword token) (verify-and-cache-token token url))))
(def facebook-auth-backend
(backends/token {:token-name "facebook-token" :authfn (url-backend facebook-me-url)}))
(def google-auth-backend
(backends/token {:token-name "google-token" :authfn (url-backend google-userinfo-url)}))
(def auth0-auth-backend
(backends/token {:token-name "auth0-token" :authfn (url-backend auth0-userinfo-url)}))

View file

@ -77,16 +77,16 @@
(defn create-food-item [{:keys [:name :placeId :category :quantity :latitude :longitude]}]
(let [food-item (first (insert-food-item
@pooled-db
{:name name
:placeId placeId
:category category
:longitude longitude
:latitude latitude}))
@pooled-db
{:name name
:placeId placeId
:category category
:longitude longitude
:latitude latitude}))
quantity (first (insert-quantity {:foodItemId (:id food-item) :quantity quantity}))]
(merge
food-item
(rename-keys (select-keys quantity [:date :quantity]) {:date :lastupdated}))))
food-item
(rename-keys (select-keys quantity [:date :quantity]) {:date :lastupdated}))))
(defn get-images
"query database for a list of images for a food item id"
@ -96,4 +96,19 @@
(defn add-image
"update the list of images for given food item id"
[image]
(insert-image @pooled-db image))
(insert-image @pooled-db image))
(defn get-faves
"select all faves associated with a username"
[username]
(select-from-faves @pooled-db {:username username}))
(defn add-faves
"add a food item id to faves for this username"
[username foodItemIds]
(map (fn [foodItemId] (insert-into-faves @pooled-db {:username username :food_item_id foodItemId})) foodItemIds))
(defn delete-faves
"remove a list of food item ids from faves for this username"
[username foodItemIds]
(delete-from-faves @pooled-db {:username username :food_item_ids foodItemIds}))

View file

@ -3,7 +3,10 @@
insert-quantity
create-food-item
get-images
add-image]]
add-image
get-faves
add-faves
delete-faves]]
[aretherecookies.parsers :refer [parse-special-types
parse-location]]
[aretherecookies.aws :refer [put-s3
@ -45,9 +48,9 @@
(hash-map
:filter (:filter body)
:fooditems (->>
(query-food-items body)
(map parse-location)
(map (update-key-to-s3 :thumbimage)))))))
(query-food-items body)
(map parse-location)
(map (update-key-to-s3 :thumbimage)))))))
(defn quantity-handler
@ -60,7 +63,7 @@
(insert-quantity {:foodItemId foodItemId :quantity quantity})
:value-fn parse-special-types)))
(defn key-found?
(defn key-found?
[obj key]
(not (str/blank? (str (get obj key)))))
@ -68,8 +71,8 @@
"returns a list of required keys which were not found on the given food item"
[foodItem]
(remove
#(key-found? foodItem %)
[:name :placeId :latitude :longitude :category :quantity]))
#(key-found? foodItem %)
[:name :placeId :latitude :longitude :category :quantity]))
(defn add-food-item-handler
@ -81,7 +84,7 @@
(if (> (count missing-keys) 0)
(bad-request {"missingkeys" missing-keys})
(as->
(create-food-item food-item) %
(create-food-item food-item) %
(parse-location %)
(safe-json %)))))
@ -91,15 +94,15 @@
[req]
(let [foodItemId (get-in req [:params :foodItemId])]
(->>
(get-images foodItemId)
(map #(update % :url build-s3-url))
safe-json)))
(get-images foodItemId)
(map #(update % :url build-s3-url))
safe-json)))
(defn add-image-handler
"given foodItemId from route and photo from multipart form post uploads image into s3 and adds URL into food item record"
[req]
;(if-not (authenticated? req) (throw-unauthorized))
; (if-not (authenticated? req) (throw-unauthorized))
(let [username (get-in req [:params :username] "Bart Akeley")
foodItemId (get-in req [:params :foodItemId])
image (get-in req [:params :photo :tempfile])
@ -107,9 +110,38 @@
(println "/addimage ---->" img-name)
(put-s3 img-name image)
(->>
(add-image {:food_item_id foodItemId
:filename img-name
:username username})
first
(update-key-to-s3 :url)
safe-json)))
(add-image {:food_item_id foodItemId
:filename img-name
:username username})
first
(update-key-to-s3 :url)
safe-json)))
(defn faves-get-handler
"return all the faves for a given username"
[req]
(if-not (authenticated? req) (throw-unauthorized))
(let [email (get-in req [:body :email])]
(println "/faves/ ----> POST " email)
(->> (get-faves email) safe-json)))
(defn faves-put-handler
"add a food item to faves for a user"
[req]
(if-not (authenticated? req) (throw-unauthorized))
(let [email (get-in req [:body :email])
foodItemIds (get-in req [:body :foodItemIds])]
(println "/fave/" email "----> PUT" foodItemIds)
(->> (add-faves email foodItemIds) safe-json)))
(defn faves-delete-handler
"add a food item to faves for a user"
[req]
(if-not (authenticated? req) (throw-unauthorized))
(let [email (get-in req [:body :email])
foodItemIds (get-in req [:body :foodItemIds])]
(println "/faves/" email "----> DELETE" foodItemIds)
(->> (delete-faves email foodItemIds) safe-json)))

View file

@ -88,4 +88,22 @@ RETURNING
filename as url,
date,
username,
food_item_id
food_item_id
-- :name select-from-faves
SELECT food_item_id, date
FROM faves
WHERE username=:v:username
ORDER BY date DESC
-- :name insert-into-faves
INSERT INTO faves (food_item_id, username, date)
VALUES (:v:food_item_id::uuid, :v:username, current_timestamp)
ON CONFLICT DO NOTHING
RETURNING food_item_id, date
-- :name delete-from-faves
DELETE FROM faves WHERE username=:v:username AND food_item_id IN (:v*:food_item_ids::uuid) RETURNING food_item_id, date