diff --git a/src/clj/game/core/set_up.clj b/src/clj/game/core/set_up.clj index 5fc111277e..ac90bae71f 100644 --- a/src/clj/game/core/set_up.clj +++ b/src/clj/game/core/set_up.clj @@ -79,7 +79,7 @@ (defn- init-game-state "Initialises the game state" - [{:keys [players gameid timer spectatorhands save-replay room]}] + [{:keys [players gameid timer spectatorhands api-access save-replay room]}] (let [corp (some #(when (corp? %) %) players) runner (some #(when (runner? %) %) players) corp-deck (create-deck (:deck corp)) @@ -101,6 +101,7 @@ (t/now) {:timer timer :spectatorhands spectatorhands + :api-access api-access :save-replay save-replay} (new-corp (:user corp) corp-identity corp-options (map #(assoc % :zone [:deck]) corp-deck) corp-deck-id corp-quote) (new-runner (:user runner) runner-identity runner-options (map #(assoc % :zone [:deck]) runner-deck) runner-deck-id runner-quote))))) diff --git a/src/clj/tasks/index.clj b/src/clj/tasks/index.clj index effafb33e9..2ed1dec40d 100644 --- a/src/clj/tasks/index.clj +++ b/src/clj/tasks/index.clj @@ -7,7 +7,8 @@ (def ^:const indexes (let [case-insensitive-index-opts {:collation {:locale "en" :strength (int 2)}}] - [["cards" (array-map :code 1)] + [["api-keys" (array-map :api-key 1)] + ["cards" (array-map :code 1)] ["cards" (array-map :previous-versions 1)] ["cards" (array-map :type 1)] ["decks" (array-map :username 1)] diff --git a/src/clj/web/api.clj b/src/clj/web/api.clj index 2c5e8b226f..8029fe625b 100644 --- a/src/clj/web/api.clj +++ b/src/clj/web/api.clj @@ -10,6 +10,8 @@ [web.admin :as admin] [web.tournament :as tournament] [web.decks :as decks] + [web.api-keys :as api-keys] + [web.game-api :as game-api] [compojure.route :as route] [ring.middleware.params :refer [wrap-params]] [ring.middleware.keyword-params :refer [wrap-keyword-params]] @@ -54,7 +56,8 @@ (POST "/register" [] auth/register-handler) (POST "/login" [] auth/login-handler) (POST "/forgot" [] auth/forgot-password-handler) - (POST "/reset/:token" [] auth/reset-password-handler)) + (POST "/reset/:token" [] auth/reset-password-handler) + (GET "/game/deck" [] game-api/deck-handler)) (defroutes admin-routes (POST "/admin/announce" [] admin/announce-create-handler) @@ -94,7 +97,11 @@ (GET "/data/decks" [] decks/decks-handler) (POST "/data/decks" [] decks/decks-create-handler) (PUT "/data/decks" [] decks/decks-save-handler) - (DELETE "/data/decks/:id" [] decks/decks-delete-handler)) + (DELETE "/data/decks/:id" [] decks/decks-delete-handler) + + (GET "/data/api-keys" [] api-keys/api-keys-handler) + (POST "/data/api-keys" [] api-keys/api-keys-create-handler) + (DELETE "/data/api-keys/:id" [] api-keys/api-keys-delete-handler)) (defroutes tournament-routes (GET "/tournament-auth/:username" [] tournament/auth)) @@ -106,8 +113,8 @@ (defroutes routes (wrap-routes private-routes wrap-anti-forgery) - (wrap-routes public-CSRF-routes wrap-anti-forgery) - public-routes) + public-routes + (wrap-routes public-CSRF-routes wrap-anti-forgery)) (defn wrap-return-favicon [handler] (fn [req] diff --git a/src/clj/web/api_keys.clj b/src/clj/web/api_keys.clj new file mode 100644 index 0000000000..9abaee98b1 --- /dev/null +++ b/src/clj/web/api_keys.clj @@ -0,0 +1,43 @@ +(ns web.api-keys + (:require [web.mongodb :refer [object-id]] + [web.utils :refer [response]] + [web.ws :as ws] + [monger.collection :as mc] + [monger.result :refer [acknowledged?]]) + (:import org.bson.types.ObjectId)) + +(defn api-keys-handler [{db :system/db + {username :username} :user}] + (if username + (response 200 (mc/find-maps db "api-keys" {:username username})) + (response 401 {:message "Unauthorized"}))) + +(defn api-keys-create-handler [{db :system/db + {username :username} :user}] + (if username + (let [new-key (java.util.UUID/randomUUID) + new-entry (mc/insert db "api-keys" + {:username username + :date (java.util.Date.) + :api-key new-key})] + (if (acknowledged? new-entry) + (if (acknowledged? (mc/update db "users" {:username username} {"$set" {:has-api-keys true}})) + (response 201 {:message "Created API Key"}) + (response 500 {:message "Failed to update user info"})) + (response 500 {:message "Failed to create API Key"}))) + (response 401 {:message "Unauthorized"}))) + +(defn api-keys-delete-handler [{db :system/db + {username :username} :user + {id :id} :params}] + (try + (if (and username id) + (if (acknowledged? (mc/remove db "api-keys" {:_id (object-id id) :username username})) + (let [key-count (mc/count db "api-keys" {:username username})] + (when-not (pos? key-count) + (mc/update db "users" {:username username} {"$set" {:has-api-keys false}})) + (response 200 {:message "Deleted"})) + (response 403 {:message "Forbidden"})) + (response 401 {:message "Unauthorized"})) + (catch Exception ex + (response 409 {:message "Unknown API Key"})))) diff --git a/src/clj/web/auth.clj b/src/clj/web/auth.clj index 4f8b292dfd..23a286b35c 100644 --- a/src/clj/web/auth.clj +++ b/src/clj/web/auth.clj @@ -50,7 +50,7 @@ (defn wrap-user [handler] (fn [{db :system/db :keys [cookies] :as req}] - (let [user-keys [:_id :username :emailhash :isadmin :ismoderator :tournament-organizer :special :options :stats] + (let [user-keys [:_id :username :emailhash :isadmin :ismoderator :tournament-organizer :special :options :stats :has-api-keys] auth-cookie (get cookies "session") user (when auth-cookie (unsign-token (:value auth-cookie))) diff --git a/src/clj/web/game_api.clj b/src/clj/web/game_api.clj new file mode 100644 index 0000000000..d6d12b290c --- /dev/null +++ b/src/clj/web/game_api.clj @@ -0,0 +1,49 @@ +(ns web.game-api + (:require [web.mongodb :refer [object-id]] + [web.decks :as decks] + [web.lobby :as lobby] + [web.utils :refer [response]] + [monger.collection :as mc])) + +(defn- make-link [host path] (str host path)) + +(defn- make-card-info [host card] + {:title (:title (:card card)) + :qty (:qty card) + :image (make-link host (get-in card [:card :images :en :default :stock]))}) + +(defn- get-deck-id [username game] + (if (:started game) + (let [state @(:state game) + side (if (= username (get-in state [:runner :user :username])) :runner :corp)] + (get-in state [side :deck-id])) + (:_id (:deck (first (filter #(= username (get-in % [:user :username])) (:players game))))))) + +(defn- get-deck [db username game] + (if-let [deck-id (get-deck-id username game)] + (decks/update-deck (mc/find-one-as-map db "decks" {:_id (object-id deck-id) :username username})) + nil)) + +(defn deck-handler [{db :system/db + scheme :scheme + headers :headers}] + (if-let [api-key (get headers "x-jnet-api")] + (let [api-uuid (try + (java.util.UUID/fromString api-key) + (catch Exception e nil)) + api-record (mc/find-one-as-map db "api-keys" {:api-key api-uuid} ["username"]) + username (:username api-record)] + (if username + (let [game (lobby/game-for-username username) + allow-access (:api-access game)] + (if (and game allow-access) + (if-let [deck (get-deck db username game)] + (let [host (str (name scheme) "://" (get headers "host"))] + (response 200 {:name (:name deck) + :identity {:title (get-in deck [:identity :title]) + :image (make-link host (get-in deck [:identity :images :en :default :stock]))} + :cards (map #(make-card-info host %) (:cards deck))})) + (response 204 {:message "No deck selected"})) + (response 403 {:message "No game for key or API Access not enabled"}))) + (response 404 {:message "Unknown X-JNet-API key"}))) + (response 400 {:message "No X-JNet-API header specified"}))) diff --git a/src/clj/web/lobby.clj b/src/clj/web/lobby.clj index 5dc8d0b7f1..4bd1877ca8 100644 --- a/src/clj/web/lobby.clj +++ b/src/clj/web/lobby.clj @@ -37,18 +37,27 @@ [client-id] (get @all-games (get @client-gameids client-id))) +(defn- username-is-player + [username game] + (some #(= username (get-in % [:user :username])) (:players game))) + +(defn game-for-username + "Returns the game map the given username is playing (but not spectating)" + [username] + (first (filter #(username-is-player username %) (vals @all-games)))) + (defn lobby-clients "Returns a seq of all client-ids playing or spectating a gameid." [gameid] (let [game (game-for-id gameid)] (keep :ws-id (concat (:players game) (:spectators game))))) -(def lobby-only-keys [:messages :spectators :mute-spectators :spectatorhands :timer]) - (let [public-lobby-updates (atom {}) game-lobby-updates (atom {}) send-ready (atom true)] + (def lobby-only-keys [:messages :spectators :mute-spectators :spectatorhands :timer :api-access]) + (defn- game-public-view "Strips private server information from a game map, preparing to send the game to clients." [gameid game] @@ -251,7 +260,7 @@ (defmethod ws/-msg-handler :lobby/create [{{{:keys [username] :as user} :user} :ring-req client-id :client-id - {:keys [title format timer allow-spectator save-replay + {:keys [title format timer allow-spectator save-replay api-access spectatorhands password room side]} :?data}] (let [gameid (java.util.UUID/randomUUID) game {:date (java.util.Date.) @@ -259,6 +268,7 @@ :title title :allow-spectator allow-spectator :save-replay save-replay + :api-access api-access :spectatorhands spectatorhands :mute-spectators false :password (when (not-empty password) (bcrypt/encrypt password)) diff --git a/src/cljs/nr/account.cljs b/src/cljs/nr/account.cljs index bbd14fab83..ba162e64b2 100644 --- a/src/cljs/nr/account.cljs +++ b/src/cljs/nr/account.cljs @@ -5,7 +5,7 @@ [goog.dom :as gdom] [nr.auth :refer [valid-email?] :as auth] [nr.appstate :refer [app-state]] - [nr.ajax :refer [POST GET PUT]] + [nr.ajax :refer [POST GET PUT DELETE]] [nr.avatar :refer [avatar]] [nr.translations :refer [tr]] [nr.utils :refer [non-game-toast set-scroll-top store-scroll-top]] @@ -191,6 +191,44 @@ (reagent-modals/close-modal!))} (tr [:settings.cancel "Cancel"])]]]]))) +(defn- update-api-keys-response [response s] + (let [status (:status response)] + (if (or (= 200 status) + (= 201 status)) + (do + (go (swap! s assoc :api-keys (:json ( (:date d) js/Date. js/moment (.format "DD-MMM-YYYY, HH:mm "))] + [:span.title (:api-key d "")]]))]] + [:button {:on-click #(do (.preventDefault %) + (create-api-key s))} + (tr [:settings.create-api-key "Create API Key"])]])) + (defn account-content [user s scroll-top] (r/create-class { @@ -441,6 +479,8 @@ :on-click #(remove-user-from-block-list % s)} "X" ] [:span.blocked-user-name (str " " bu)]]))] + [api-keys s] + [:section [:button.float-right (tr [:settings.update-profile "Update Profile"])] [:span.flash-message (:flash-message @s)]]]])})) @@ -480,6 +520,8 @@ (when (= 200 (:status response)) (swap! state assoc :email (:email (:json response)))))) + (go (swap! state assoc :api-keys (:json (