Browse Source

Add basic login with database-stored sessions

master
parent
commit
0b2c29d0f2
5 changed files with 193 additions and 28 deletions
  1. +3
    -1
      project.clj
  2. +49
    -0
      resources/login.html
  3. +47
    -9
      src/clj/character_suite/server.clj
  4. +40
    -0
      src/clj/character_suite/session/sql.clj
  5. +54
    -18
      src/clj/character_suite/store.clj

+ 3
- 1
project.clj View File

@@ -22,7 +22,9 @@
[environ "1.0.0"]
[org.apache.pdfbox/pdfbox "1.8.10"]
[org.clojure/java.jdbc "0.3.5"]
[org.xerial/sqlite-jdbc "3.7.2"]]
[org.xerial/sqlite-jdbc "3.7.2"]
[com.lambdaworks/scrypt "1.4.0"]
[enlive "1.1.6"]]

:plugins [[lein-cljsbuild "1.0.5"]
[lein-environ "1.0.0"]


+ 49
- 0
resources/login.html View File

@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/css/bootstrap.min.css" rel="stylesheet">
<link href="/css/selectize.bootstrap3.css" rel="stylesheet" type="text/css">
<link href="/css/style.css" rel="stylesheet" type="text/css">
</head>
<body>
<div class="container">
<div class="header clearfix">
<nav>
<ul class="nav nav-pills pull-right">
</ul>
</nav>
<h3 class="text-muted">RPG Character Suite</h3>
</div>


<div id="app" class="row marketing">
<h2>Login</h2>
</div>

<div class="row marketing">
<div class="col-lg-12">
<form method="post" action="/login">
<div class="form-group">
<label for="username">Username:</label>
<input name="username" class="form-control" />
</div>

<div class="form-group">
<label for="password">Password:</label>
<input type="password" name="password" class="form-control" />
</div>

<input type="hidden" class="return-url" name="return-url" />
<button type="submit" class="btn btn-primary">Login</button>
</form>
</div>
</div> <!-- /marketing -->

<script src="/js/jquery.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
</div> <!-- /container -->
</body>
</html>

+ 47
- 9
src/clj/character_suite/server.clj View File

@@ -22,8 +22,11 @@
[ring.middleware.params :refer [wrap-params]]
[ring.middleware.reload :refer [wrap-reload]]
[ring.middleware.session :refer [wrap-session]]
[ring.util.response :refer [file-response resource-response]]
[character-suite.store :as store])
[ring.util.response :refer [redirect]]
[character-suite.session.sql :as session-sql]
[character-suite.store :as store]
[net.cgrand.enlive-html :refer [deftemplate set-attr]]
[net.cgrand.reload :refer [auto-reload]])
(:gen-class))

(defn- get-api-characters-handler [request]
@@ -49,14 +52,48 @@
:headers {"Content-Type" "text/plain"}
:session (assoc (:session request) :character (:id saved-character))}))

(defn index-handler [request]
(file-response "resources/public/index.html"))
(deftemplate login-page (io/resource "login.html") [request]
[:input.return-url] (let [return-url-param (or (get (:form-params request) "return-url")
"")
return-url (if (empty? return-url-param)
(:uri request)
return-url-param)]
(set-attr :value return-url)))

(defn- run-handler
"The \"handler\" can be a function, but also a URL for static content."
[handler request]
(if (fn? handler)
(handler request)
handler))

(defn logged-in-handler [handler]
(fn [request]
(let [session (:session request)]
(if (:logged-in? session)
(run-handler handler request)
(login-page request)))))

(defn- post-login-handler [request]
(let [username (get (:form-params request) "username")
password (get (:form-params request) "password")
return-url (or (get (:form-params request) "return-url")
"/")]
(if (store/check-credentials (env :database-url) username password)
(assoc (redirect return-url)
:session (assoc (:session request) :logged-in? true))
(login-page request))))

(defn- get-logout-handler [request]
(assoc (redirect "/") :session nil))

(defroutes app-routes
(GET "/api/characters" [] get-api-characters-handler)
(GET "/characters/:id/pdf" [id] get-character-pdf-handler)
(POST "/api/characters" [] post-characters-handler)
(GET "/" [] (io/resource "index.html"))
(GET "/api/characters" [] (logged-in-handler get-api-characters-handler))
(GET "/characters/:id/pdf" [id] (logged-in-handler get-character-pdf-handler))
(POST "/api/characters" [] (logged-in-handler post-characters-handler))
(POST "/login" [] post-login-handler)
(GET "/logout" [] get-logout-handler)
(GET "/" [] (logged-in-handler (io/resource "index.html")))
(resources "/")
(resources "/react" {:root "react"})
(not-found "404 Not Found"))
@@ -64,7 +101,7 @@
(def app
(-> app-routes
wrap-params
wrap-session
(wrap-session {:store (session-sql/sql-store (env :database-url))})
wrap-exceptions))

(def http-handler
@@ -78,6 +115,7 @@
(run-jetty http-handler {:port port :join? false})))

(defn run-auto-reload [& [port]]
(auto-reload *ns*)
(start-figwheel))

(defn run [& [port]]


+ 40
- 0
src/clj/character_suite/session/sql.clj View File

@@ -0,0 +1,40 @@
(ns character-suite.session.sql
(:require [ring.middleware.session.store :refer [SessionStore]]
[clojure.java.jdbc :refer :all]
[clojure.edn :as edn])
(:import java.util.UUID))

(def ^:dynamic *session-ttl* (* 60 60 24 7))

(defn- new-session-id []
(str (UUID/randomUUID)))

(deftype SqlStore [db-spec]
SessionStore
(read-session [store session-id]
(let [now-ts (quot (System/currentTimeMillis) 1000)
raw-session (-> (query db-spec
["SELECT contents FROM sessions WHERE id = ? AND expirationts > ?"
session-id
now-ts])
first
:contents)]
(if (nil? raw-session)
nil
(edn/read-string raw-session))))
(write-session [store key data]
(let [session-id (or key (new-session-id))
now-ts (quot (System/currentTimeMillis)
1000)
expiration-ts (+ now-ts *session-ttl*)]
(delete! db-spec :sessions ["id = ?" session-id])
(insert! db-spec :sessions {:id session-id
:contents (pr-str data)
:expirationts expiration-ts})
session-id))
(delete-session [store session-id]
(delete! db-spec :sessions ["id = ?" session-id])
nil))

(defn sql-store [db-spec]
(SqlStore. db-spec))

+ 54
- 18
src/clj/character_suite/store.clj View File

@@ -3,20 +3,13 @@
[clojure.java.jdbc :refer :all]
[clojure.java.io :as io]
[clojure.edn :as edn])
(:import java.util.UUID))

(defn create-db [db-spec]
(db-do-commands db-spec
(create-table-ddl :characters
[:id :text]
[:rulesystem :text]
[:props :text])
(create-table-ddl :users
[:id :integer]
[:username :text]
[:password :text])))
(:import java.util.UUID
com.lambdaworks.crypto.SCryptUtil))

(def ^:dynamic *character-path* "characters")
(def ^:dynamic *cpu-cost* 16384)
(def ^:dynamic *ram-cost* 8)
(def ^:dynamic *parallelism* 1)

(defn- load-deprecated-character [uuid]
(let [uuid (or uuid "2461aee0-cdc9-49de-bb3f-5c81f5189ace")
@@ -36,13 +29,48 @@
:rulesystem (name (:rule-system character))
:props (pr-str (:props character))}))))

(defn- create-characters-table [db-spec]
(db-do-commands db-spec
(create-table-ddl :characters
[:id :text]
[:rulesystem :text]
[:props :text]))
(migrate-characters db-spec))

(defn- create-default-user [db-spec]
(insert! db-spec :users {:username "admin"
:password (SCryptUtil/scrypt "admin"
*cpu-cost*
*ram-cost*
*parallelism*)}))

(defn- create-users-table [db-spec]
(db-do-commands db-spec
(create-table-ddl :users
[:id :integer]
[:username :text]
[:password :text]))
(create-default-user db-spec))

(defn- create-sessions-table [db-spec]
(db-do-commands db-spec
(create-table-ddl :sessions
[:id :text]
[:contents :text]
[:expirationts :integer])))

(def migrations
[["characters" create-characters-table]
["users" create-users-table]
["sessions" create-sessions-table]])

(defn ensure-db [db-spec]
(let [md-results (with-db-metadata [md db-spec]
(metadata-result (.getTables md nil nil "characters" (into-array ["TABLE"]))))
already-there? (> (count md-results) 0)]
(when-not already-there?
(create-db db-spec)
(migrate-characters db-spec))))
(doseq [[table migration-fn] migrations]
(let [md-results (with-db-metadata [md db-spec]
(metadata-result (.getTables md nil nil table (into-array ["TABLE"]))))
already-there? (> (count md-results) 0)]
(when-not already-there?
(migration-fn db-spec)))))

(defn- character-from-database [row]
{:id (:id row)
@@ -66,3 +94,11 @@
:rulesystem (name (:rule-system character))
:props (pr-str (:props character))})
return-character))

(defn check-credentials [db-spec username password]
(if-let [hashedp (-> (query db-spec
["SELECT password FROM users WHERE username = ?"
username])
first
:password)]
(SCryptUtil/check password hashedp)))

Loading…
Cancel
Save