base frontend website with authentication

This commit is contained in:
2018-10-03 00:00:07 +02:00
parent 507e941540
commit 4c033ac86c
21 changed files with 1358 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
;; Copyright (C) 2015-2018 Dyne.org foundation
;; Sourcecode designed, written and maintained by
;; Denis Roio <jaromil@dyne.org>
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU Affero 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 Affero General Public License for more details.
;; You should have received a copy of the GNU Affero General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
(ns toaster.config
(:require [clojure.pprint :refer [pprint]]
[clojure.string :refer [upper-case]]
[clojure.java.io :as io]
[clojure.walk :refer [keywordize-keys]]
[auxiliary.core :as aux]
[taoensso.timbre :as log]
[failjure.core :as f]
[schema.core :as s]
[yaml.core :as yaml]
[cheshire.core :refer :all]))
;; (s/defschema Config
;; {s/Keyword
;; (s/optional-key :webserver) {:port s/Num
;; :host s/Str
;; :anti-forgery s/Bool
;; :ssl-redirect s/Bool}
;; (s/optional-key :just-auth) {:email-server s/Str
;; :email-user s/Str
;; :email-pass s/Str
;; :email-address s/Str}
;; }
;; :appname s/Str
;; :paths [s/Str]
;; :filename s/Str
;; })
(def run-mode (atom :web))
(def default-settings {:webserver
{:anti-forgery false
:ssl-redirect false}})
(defn yaml-read [path]
(if (.exists (io/as-file path))
(-> path yaml/from-file keywordize-keys)))
(defn- config-read
"Read configurations from standard locations, overriding defaults or
system-wide with user specific paths. Requires the application name
and optionally default values."
([appname] (config-read appname {}))
([appname defaults & flags]
(let [home (System/getenv "HOME")
pwd (System/getenv "PWD" )
file (str appname ".yaml")
conf (-> {:appname appname
:filename file
:paths [(str "/etc/" appname "/" file)
(str home "/." appname "/" file)
(str pwd "/" file)
;; TODO: this should be resources
(str pwd "/resources/" file)
(str pwd "/test-resources/" file)]}
(conj defaults))]
(loop [[p & paths] (:paths conf)
res defaults]
(let [res (merge res
(if (.exists (io/as-file p))
(conj res (yaml-read p))))]
(if (empty? paths) (conj conf {(keyword appname) res})
(recur paths res)))))))
(defn- spy "Print out a config structure nicely formatted"
[edn]
(if (log/may-log? :debug)
(binding [*out* *err*] (pprint edn)))
edn)
(defn q [conf path] ;; query a variable inside the config
{:pre [(coll? path)]}
;; (try ;; adds an extra check every time configuration is read
;; (s/validate Config conf)
;; (catch Exception ex
;; (f/fail (log/spy :error ["Invalid configuration: " conf ex]))))
(get-in conf path))
(defn load-config [name default]
(log/info (str "Loading configuration: " name))
(->> (config-read name default)))

View File

@@ -0,0 +1,167 @@
;; Copyright (C) 2015-2018 Dyne.org foundation
;; Sourcecode designed, written and maintained by
;; Denis Roio <jaromil@dyne.org>
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU Affero 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 Affero General Public License for more details.
;; You should have received a copy of the GNU Affero General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
(ns toaster.handler
(:require
[clojure.string :as str]
[clojure.java.io :as io]
[clojure.data.json :as json]
[compojure.core :refer :all]
[compojure.handler :refer :all]
[compojure.route :as route]
[compojure.response :as response]
[ring.adapter.jetty :refer :all]
[ring.middleware.session :refer :all]
[ring.middleware.accept :refer [wrap-accept]]
[ring.middleware.defaults :refer [wrap-defaults site-defaults]]
[failjure.core :as f]
[taoensso.timbre :as log]
[just-auth.core :as auth]
[toaster.session :as s]
[toaster.config :as conf]
[toaster.webpage :as web]
[toaster.ring :as ring])
(:import java.io.File)
(:gen-class))
(defonce config (conf/load-config "toaster" conf/default-settings))
(defroutes app-routes
(GET "/" request (web/render "Hello World!"));; web/readme))
;; NEW ROUTES HERE
;; JUST-AUTH ROUTES
(GET "/login" request
(f/attempt-all
[acct (s/check-account request)]
(web/render acct
[:div
[:h1 (str "Already logged in with account: "
(:email acct))]
[:h2 [:a {:href "/logout"} "Logout"]]])
(f/when-failed [e]
(web/render web/login-form))))
(POST "/login" request
(f/attempt-all
[username (s/param request :username)
password (s/param request :password)
logged (auth/sign-in
@ring/auth username password {})]
;; TODO: pass :ip-address in last argument map
(let [session {:session {:config config
:auth logged}}]
(conj session
(web/render
logged
[:div
[:h1 "Logged in: " username]
(web/render-yaml session)])))
(f/when-failed [e]
(web/render-error-page
(str "Login failed: " (f/message e))))))
(GET "/session" request
(-> (:session request) web/render-yaml web/render))
(GET "/logout" request
(conj {:session {:config config}}
(web/render [:h1 "Logged out."])))
(GET "/signup" request
(web/render web/signup-form))
(POST "/signup" request
(f/attempt-all
[name (s/param request :name)
email (s/param request :email)
password (s/param request :password)
repeat-password (s/param request :repeat-password)
activation {:activation-uri
(get-in request [:headers "host"])}]
(web/render
(if (= password repeat-password)
(f/try*
(f/if-let-ok?
[signup (auth/sign-up @ring/auth
name
email
password
activation
[])]
[:div
[:h2 (str "Account created: "
name " &lt;" email "&gt;")]
[:h3 "Account pending activation."]]
(web/render-error
(str "Failure creating account: "
(f/message signup)))))
(web/render-error
"Repeat password didnt match")))
(f/when-failed [e]
(web/render-error-page
(str "Sign-up failure: " (f/message e))))))
(GET "/activate/:email/:activation-id"
[email activation-id :as request]
(let [activation-uri
(str "http://"
(get-in request [:headers "host"])
"/activate/" email "/" activation-id)]
(web/render
[:div
(f/if-let-failed?
[act (auth/activate-account
@ring/auth email
{:activation-link activation-uri})]
(web/render-error
[:div
[:h1 "Failure activating account"]
[:h2 (f/message act)]
[:p (str "Email: " email " activation-id: " activation-id)]])
[:h1 (str "Account activated - " email)])])))
;; -- end of JUST-AUTH
(POST "/" request
;; generic endpoint for canceled operations
(web/render (s/check-account request)
[:div {:class (str "alert alert-danger") :role "alert"}
(s/param request :message)]))
(route/resources "/")
(route/not-found (web/render-error-page "Page Not Found"))
) ;; end of routes
(def app
(-> (wrap-defaults app-routes ring/app-defaults)
(wrap-accept {:mime ["text/html"]
;; preference in language, fallback to english
:language ["en" :qs 0.5
"it" :qs 1
"nl" :qs 1
"hr" :qs 1]})
(wrap-session)))
;; for uberjar
(defn -main []
(println "Starting standalone jetty server on http://localhost:6060")
(run-jetty app {:port 6060
:host "localhost"
:join? true}))

View File

@@ -0,0 +1,93 @@
;; Copyright (C) 2015-2017 Dyne.org foundation
;; Sourcecode designed, written and maintained by
;; Denis Roio <jaromil@dyne.org>
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU Affero 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 Affero General Public License for more details.
;; You should have received a copy of the GNU Affero General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
(ns toaster.ring
(:require
[clojure.java.io :as io]
[toaster.config :as conf]
[taoensso.timbre :as log]
[failjure.core :as f]
[clj-storage.db.mongo :refer [get-mongo-db create-mongo-store]]
[just-auth.core :as auth]
[just-auth.db.just-auth :as auth-db]
[auxiliary.translation :as trans]
[compojure.core :refer :all]
[compojure.handler :refer :all]
[ring.middleware.session :refer :all]
[ring.middleware.accept :refer [wrap-accept]]
[ring.middleware.defaults :refer [wrap-defaults site-defaults]]))
(def config (atom {}))
(def db (atom {}))
(def accts (atom {}))
(def auth (atom {}))
(defn init []
(log/merge-config! {:level :debug
;; #{:trace :debug :info :warn :error :fatal :report}
;; Control log filtering by
;; namespaces/patterns. Useful for turning off
;; logging in noisy libraries, etc.:
;; :ns-whitelist ["agiladmin.*" "just-auth.*"]
:ns-blacklist ["org.eclipse.jetty.*"
"org.mongodb.driver.cluster"]})
;; load configuration
(reset! config (conf/load-config
(or (System/getenv "toaster_conf") "toaster")
conf/default-settings))
(let [justauth-conf (get-in @config [:toaster :just-auth])]
;; connect database (TODO: take parameters from configuration)
(reset! db (get-mongo-db (:mongo-url justauth-conf)))
;; create authentication stores in db
(f/attempt-all
[auth-conf (get-in @config [:toaster :just-auth])
auth-stores (auth-db/create-auth-stores @db)]
[(trans/init "lang/auth-en.yml" "lang/english.yaml")
(reset! accts auth-stores)
(reset! auth (auth/email-based-authentication
auth-stores
;; TODO: replace with email taken from config
(dissoc (:just-auth (:toaster (conf/load-config
"toaster" conf/default-settings)))
:mongo-url :mongo-user :mongo-pass)
{:criteria #{:email :ip-address}
:type :block
:time-window-secs 10
:threshold 5}))]
;; (select-keys auth-stores [:account-store
;; :password-recovery-store])
(f/when-failed [e]
(log/error (str (trans/locale [:init :failure])
" - " (f/message e))))))
(log/info (str (trans/locale [:init :success])))
(log/debug @auth))
(def app-defaults
(-> site-defaults
(assoc-in [:cookies] true)
(assoc-in [:security :anti-forgery]
(get-in @config [:webserver :anti-forgery]))
(assoc-in [:security :ssl-redirect]
(get-in @config [:webserver :ssl-redirect]))
(assoc-in [:security :hsts] true)))

View File

@@ -0,0 +1,60 @@
(ns toaster.session
(:refer-clojure :exclude [get])
(:require
[toaster.config :as conf]
[taoensso.timbre :as log]
[failjure.core :as f]
[just-auth.core :as auth]
[toaster.ring :as ring]
[toaster.webpage :as web]))
(defn param [request param]
(let [value
(get-in request
(conj [:params] param))]
(if (nil? value)
(f/fail (str "Parameter not found: " param))
value)))
;; TODO: not working?
(defn get [req arrk]
{:pre (coll? arrk)}
(if-let [value (get-in req (conj [:session] arrk))]
value
(f/fail (str "Value not found in session: " (str arrk)))))
(defn check-config [request]
;; reload configuration from file all the time if in debug mode
(if-let [session (:session request)]
(if (contains? session :config)
(:config session)
(conf/load-config "toaster" conf/default-settings))
(f/fail "Session not found. ")))
(defn check-account [request]
;; check if login is present in session
(f/attempt-all
[login (get-in request [:session :auth :email])
user (auth/get-account @ring/auth login)]
user
(f/when-failed [e]
(->> e f/message
(str "Unauthorized access. ")
f/fail))))
(defn check-database []
(if-let [db @ring/db]
db
(f/fail "No connection to database. ")))
(defn check [request fun]
(f/attempt-all
[db (check-database)
config (check-config request)
account (check-account request)]
(fun request config account)
(f/when-failed [e]
(web/render
[:div
(web/render-error (f/message e))
web/login-form]))))

View File

@@ -0,0 +1,343 @@
;; Copyright (C) 2015-2018 Dyne.org foundation
;; Sourcecode designed, written and maintained by
;; Denis Roio <jaromil@dyne.org>
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU Affero 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 Affero General Public License for more details.
;; You should have received a copy of the GNU Affero General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
(ns toaster.webpage
(:require [clojure.java.io :as io]
[clojure.data.json :as json]
[clojure.data.csv :as csv]
[yaml.core :as yaml]
[toaster.config :as conf]
[taoensso.timbre :as log]
[failjure.core :as f]
[toaster.ring :as ring]
[hiccup.page :as page]
[hiccup.form :as hf]))
(declare render)
(declare render-head)
(declare navbar-guest)
(declare navbar-account)
(declare render-footer)
(declare render-yaml)
(declare render-edn)
(declare render-error)
(declare render-error-page)
(declare render-static)
(defn q [req]
"wrapper to retrieve parameters"
;; TODO: sanitise and check for irregular chars
(get-in req (conj [:params] req)))
(defn button
([url text] (button url text [:p]))
([url text field] (button url text field "btn-secondary btn-lg"))
([url text field type]
(hf/form-to [:post url]
field ;; can be an hidden key/value field (project,
;; person, etc using hf/hidden-field)
(hf/submit-button {:class (str "btn " type)} text))))
(defn button-cancel-submit [argmap]
[:div
{:class
(str "row col-md-6 btn-group btn-group-lg "
(:btn-group-class argmap))
:role "group"}
(button
(:cancel-url argmap) "Cancel"
(:cancel-params argmap)
"btn-primary btn-lg btn-danger col-md-3")
(button
(:submit-url argmap) "Submit"
(:submit-params argmap)
"btn-primary btn-lg btn-success col-md-3")])
(defn reload-session [request]
;; TODO: validation of all data loaded via prismatic schema
(conf/load-config "toaster" conf/default-settings)
)
(defn render
([body]
{:headers {"Content-Type"
"text/html; charset=utf-8"}
:body (page/html5
(render-head)
[:body ;; {:class "static"}
navbar-guest
[:div {:class "container-fluid"} body]
(render-footer)])})
([account body]
{:headers {"Content-Type"
"text/html; charset=utf-8"}
:body (page/html5
(render-head)
[:body (if (empty? account)
navbar-guest
navbar-account)
[:div {:class "container-fluid"} body]
(render-footer)])}))
(defn render-error
"render an error message without ending the page"
[err]
[:div {:class "alert alert-danger" :role "alert"}
[:span {:class "far fa-meh"
:aria-hidden "true" :style "padding: .5em"}]
[:span {:class "sr-only"} "Error:" ]
err])
(defn render-error-page
([] (render-error-page {} "Unknown"))
([err] (render-error-page {} err))
([session error]
(render
[:div {:class "container-fluid"}
(render-error error)
(if-not (empty? session)
[:div {:class "config"}
[:h2 "Environment dump:"]
(render-yaml session)])])))
(defn render-head
([] (render-head
"toaster" ;; default title
"toaster"
"https://toaster.dyne.org")) ;; default desc
([title desc url]
[:head [:meta {:charset "utf-8"}]
[:meta {:http-equiv "X-UA-Compatible" :content "IE=edge,chrome=1"}]
[:meta
{:name "viewport"
:content "width=device-width, initial-scale=1, maximum-scale=1"}]
[:title title]
;; javascript scripts
(page/include-js "/static/js/jquery-3.2.1.min.js")
(page/include-js "/static/js/bootstrap.min.js")
;; cascade style sheets
(page/include-css "/static/css/bootstrap.min.css")
(page/include-css "/static/css/json-html.css")
(page/include-css "/static/css/highlight-tomorrow.css")
(page/include-css "/static/css/formatters-styles/html.css")
(page/include-css "/static/css/formatters-styles/annotated.css")
(page/include-css "/static/css/fa-regular.min.css")
(page/include-css "/static/css/fontawesome.min.css")
(page/include-css "/static/css/toaster.css")]))
(def navbar-guest
[:nav
{:class "navbar navbar-default navbar-fixed-top navbar-expand-md navbar-expand-lg"}
[:div {:class "navbar-header"}
[:button {:class "navbar-toggle" :type "button"
:data-toggle "collapse"
:data-target "#navbarResponsive"
:aria-controls "navbarResponsive"
:aria-expanded "false"
:aria-label "Toggle navigation"}
[:span {:class "sr-only"} "Toggle navigation"]
[:span {:class "icon-bar"}]
[:span {:class "icon-bar"}]
[:span {:class "icon-bar"}]]
[:a {:class "navbar-brand far fa-handshake" :href "/"} "toaster"]]
[:div {:class "collapse navbar-collapse" :id "navbarResponsive"}
[:ul {:class "nav navbar-nav hidden-sm hidden-md ml-auto"}
;; --
[:li {:class "divider" :role "separator"}]
[:li {:class "nav-item"}
[:a {:class "nav-link far fa-address-card"
:href "/login"} " Login"]]
]]])
(def navbar-account
[:nav {:class "navbar navbar-default navbar-fixed-top navbar-expand-lg"}
[:div {:class "navbar-header"}
[:button {:class "navbar-toggle" :type "button"
:data-toggle "collapse" :data-target "#navbarResponsive"
:aria-controls "navbarResponsive" :aria-expanded "false"
:aria-label "Toggle navigation"}
[:span {:class "sr-only"} "Toggle navigation"]
[:span {:class "icon-bar"}]
[:span {:class "icon-bar"}]
[:span {:class "icon-bar"}]]
[:a {:class "navbar-brand far fa-handshake" :href "/"} "toaster"]]
[:div {:class "collapse navbar-collapse" :id "navbarResponsive"}
[:ul {:class "nav navbar-nav hidden-sm hidden-md ml-auto"}
;; --
[:li {:class "divider" :role "separator"}]
;; LIST OF RELEVANT LINKS AFTER LOGIN
;; [:li {:class "nav-item"}
;; [:a {:class "nav-link far fa-address-card"
;; :href "/persons/list"} " Persons"]]
;; [:li {:class "nav-item"}
;; [:a {:class "nav-link far fa-paper-plane"
;; :href "/projects/list"} " Projects"]]
;; [:li {:class "nav-item"}
;; [:a {:class "nav-link far fa-plus-square"
;; :href "/timesheets"} " Upload"]]
;; [:li {:class "nav-item"}
;; [:a {:class "nav-link far fa-save"
;; :href "/reload"} " Reload"]]
;; --
[:li {:role "separator" :class "divider"} ]
[:li {:class "nav-item"}
[:a {:class "nav-link far fa-file-code"
:href "/config"} " Configuration"]]
]]])
(defn render-footer []
[:footer {:class "row" :style "margin: 20px"}
[:hr]
[:div {:class "footer col-lg-3"}
[:img {:src "/static/img/AGPLv3.png" :style "margin-top: 2.5em"
:alt "Affero GPLv3 License"
:title "Affero GPLv3 License"} ]]
[:div {:class "footer col-lg-3"}
[:a {:href "https://www.dyne.org"}
[:img {:src "/static/img/swbydyne.png"
:alt "Software by Dyne.org"
:title "Software by Dyne.org"}]]]
])
(defn render-static [body]
(page/html5 (render-head)
[:body {:class "fxc static"}
navbar-guest
[:div {:class "container"} body]
(render-footer)
]))
;; highlight functions do no conversion, take the format they highlight
;; render functions take edn and convert to the highlight format
;; download functions all take an edn and convert it in target format
;; edit functions all take an edn and present an editor in the target format
(defn render-yaml
"renders an edn into an highlighted yaml"
[data]
[:span
[:pre [:code {:class "yaml"}
(yaml/generate-string data)]]
[:script "hljs.initHighlightingOnLoad();"]])
(defn highlight-yaml
"renders a yaml text in highlighted html"
[data]
[:span
[:pre [:code {:class "yaml"}
data]]
[:script "hljs.initHighlightingOnLoad();"]])
(defn highlight-json
"renders a json text in highlighted html"
[data]
[:span
[:pre [:code {:class "json"}
data]]
[:script "hljs.initHighlightingOnLoad();"]])
(defn download-csv
"takes an edn, returns a csv plain/text for download"
[data]
{:headers {"Content-Type"
"text/plain; charset=utf-8"}
:body (with-out-str (csv/write-csv *out* data))})
(defn edit-edn
"renders an editor for the edn in yaml format"
[data]
[:div;; {:class "form-group"}
[:textarea {:class "form-control"
:rows "20" :data-editor "yaml"
:id "config" :name "editor"}
(yaml/generate-string data)]
[:script {:src "/static/js/ace.js"
:type "text/javascript" :charset "utf-8"}]
[:script {:type "text/javascript"}
(slurp (io/resource "public/static/js/ace-embed.js"))]
;; code to embed the ace editor on all elements in page
;; that contain the attribute "data-editor" set to the
;; mode language of choice
[:input {:class "btn btn-success btn-lg pull-top"
:type "submit" :value "submit"}]])
;; (defonce readme
;; (slurp (io/resource "public/static/README.html")))
(defonce login-form
[:div
[:h1 "Login for your account"
[:form {:action "/login"
:method "post"}
[:input {:type "text" :name "username"
:placeholder "Username"
:class "form-control"
:style "margin-top: 1em"}]
[:input {:type "password" :name "password"
:placeholder "Password"
:class "form-control"
:style "margin-top: 1em"}]
[:input {:type "submit" :value "Login"
:class "btn btn-primary btn-lg btn-block"
:style "margin-top: 1em"}]]]])
(defonce signup-form
[:div
[:h1 "Sign Up for a toaster account"
[:form {:action "/signup"
:method "post"}
[:input {:type "text" :name "name"
:placeholder "Name"
:class "form-control"
:style "margin-top: 1em"}]
[:input {:type "text" :name "email"
:placeholder "Email"
:class "form-control"
:style "margin-top: 1em"}]
[:input {:type "password" :name "password"
:placeholder "Password"
:class "form-control"
:style "margin-top: 1em"}]
[:input {:type "password" :name "repeat-password"
:placeholder "Repeat password"
:class "form-control"
:style "margin-top: 1em"}]
[:input {:type "submit" :value "Sign In"
:class "btn btn-primary btn-lg btn-block"
:style "margin-top: 1em"}]]]])