summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorsostler <sbostler@gmail.com>2010-04-29 03:11:35 -0400
committersostler <sbostler@gmail.com>2010-04-29 03:11:35 -0400
commit210d0294b59759c7cccd3d1f7408627cecc7f86a (patch)
tree1fa9a192f66d4c78de0e268b56acec5a6ad6f37a /src
parent34869a3f8fb0ac6ed6c17db4a90e28c705829f0d (diff)
Password reset feature
Diffstat (limited to 'src')
-rw-r--r--src/admin.clj22
-rw-r--r--src/email.clj8
-rw-r--r--src/image_utils.clj3
-rw-r--r--src/site.clj176
-rw-r--r--src/user.clj42
-rwxr-xr-xsrc/utils.clj59
6 files changed, 187 insertions, 123 deletions
diff --git a/src/admin.clj b/src/admin.clj
index f0b6453..76e690a 100644
--- a/src/admin.clj
+++ b/src/admin.clj
@@ -4,6 +4,7 @@
(:use compojure
email
scheduled-agent
+ user
utils))
;; Muting
@@ -105,9 +106,6 @@ WHERE mute_id = ?
AND cancelled = false
")
-(defn- assert-update [res ok err]
- (if (zero? (first res)) err ok))
-
(defn cancel-mute! [mute-id admin-id]
(let [mute (fetch-mute mute-id)
active (nor (:expired mute) (:cancelled mute))
@@ -152,6 +150,9 @@ AND cancelled = false
;; Debug Page
+(defn error-header [& text]
+ (html [:h2 {"color" "red"} text]))
+
(defn exception-to-string [e]
(let [sw (java.io.StringWriter.)
pw (java.io.PrintWriter. sw)]
@@ -171,6 +172,7 @@ AND cancelled = false
st (fetch-template "debug" session)]
(.setAttribute st "flash" (:msg flash))
(.setAttribute st "mailtemps" (lookup-templates "template/mail" "welcome"))
+ (.setAttribute st "resettemps" (lookup-templates "template/mail" "reset"))
(.toString st))))
(defn debug-reg-email [session params]
@@ -179,14 +181,22 @@ AND cancelled = false
(params :template))
(str "Sent registration mail to " (params :to)))
-(defn debug-reset-email [session params]
- )
+(defn debug-reset-email [session {nick :nick to :to template :template}]
+ (if-let [info (fetch-nick nick)]
+ (let [nick (info :nick)
+ hash (info :hash)
+ ts (System/currentTimeMillis)
+ token (reset-token nick hash ts)
+ link (reset-link nick token ts)]
+ (do (send-reset-email nick to link template)
+ (str "Send reset email to " to)))
+ (error-header "Unknown user " nick)))
(def *debug-action-map* {"regemail" debug-reg-email
"resetemail" debug-reset-email})
(defn format-unknown-action [action]
- (html [:h2 {"color" "red"} ["Unknown action " action]]))
+ (error-header "Unknown action " action))
(defn format-debug-exception [action e]
(html
diff --git a/src/email.clj b/src/email.clj
index 1124f48..e2a5f1f 100644
--- a/src/email.clj
+++ b/src/email.clj
@@ -82,9 +82,11 @@
(let [[s b] (parse-mail-template temp {"nick" nick})]
(dump-mail [email] s b))))
-(defn send-reset-email [nick email key]
- (let [[s b] (parse-mail-template "reset" {"nick" nick "key" key})]
- (dump-mail [email] s b)))
+(defn send-reset-email
+ ([nick email link temp]
+ (let [[s b] (parse-mail-template temp {"nick" nick "link" link})]
+ (dump-mail [email] s b)))
+ ([nick email link] (send-reset-email nick email link "reset")))
(defn send-mute-email [user-nick admin-nick reason duration]
(let [subject (format "%s was muted by %s for %s"
diff --git a/src/image_utils.clj b/src/image_utils.clj
deleted file mode 100644
index 638cd05..0000000
--- a/src/image_utils.clj
+++ /dev/null
@@ -1,3 +0,0 @@
-(ns image-utils
- (:import javax.imageio.ImageIO))
-
diff --git a/src/site.clj b/src/site.clj
index f48f560..7848dcb 100644
--- a/src/site.clj
+++ b/src/site.clj
@@ -20,7 +20,8 @@
session-sweeper
feed
tags
- scheduled-agent))
+ scheduled-agent
+ user))
(def *run-flusher* true)
(def *flusher-sleep* (seconds 4))
@@ -240,12 +241,13 @@
(defn session-assoc-from-db
[user-info]
- (session-assoc :user_id (user-info :user_id)
- :nick (user-info :nick)
- :email (user-info :email)
- :is_admin (user-info :is_admin)
- :avatar (user-info :avatar)
- :password_login true))
+ (session-assoc
+ :user_id (user-info :user_id)
+ :nick (user-info :nick)
+ :email (user-info :email)
+ :is_admin (user-info :is_admin)
+ :avatar (user-info :avatar)
+ :password_login true))
;; login-token functions
@@ -348,9 +350,9 @@
(> (count (do-select [query (lower-case nick)])) 0))))
(defn register [session params]
- (let [nick (params :nick)
- email (params :email)
- hash (params :hash)
+ (let [nick (params :nick)
+ email (params :email)
+ hash (params :hash)
invalid-nick-reason (is-invalid-nick? nick)]
(cond invalid-nick-reason (resp-error invalid-nick-reason)
(nick-reserved? nick) (resp-error "NICK_TAKEN")
@@ -507,43 +509,42 @@ ORDER BY msg_count DESC")
"maker" (topic :maker)}))
(defn validate-topic-list [session]
- (if (is-vip? session)
- (let [st (fetch-template "topic_list" session)]
- (.setAttribute st "rooms"
- (map topic-map-from-room (vals @rooms)))
- (.toString st))
- [404 "UNKNOWN_ROOM"]))
+ (if-vip
+ (let [st (fetch-template "topic_list" session)]
+ (.setAttribute st "rooms"
+ (map topic-map-from-room (vals @rooms)))
+ (.toString st))))
(defn set-topic! [room topic deadline maker]
(dosync (ref-set (room :topic)
- {:topic topic
+ {:topic topic
:deadline deadline
- :maker maker})))
+ :maker maker})))
(defn end-topic! [room]
(dosync (ref-set (room :topic) nil)))
(defn validate-set-topic [session params]
- (let [room (lookup-room (params :room))
- topic (params :topic)
- deadline (params :deadline)
- maker (params :maker)]
- (cond (not (is-vip? session)) (resp-error "NOT_VIP")
- (not (valid-topic? topic)) (resp-error "INVALID_TOPIC")
- (not (valid-deadline? deadline)) (resp-error "INVALID_DEADLINE")
- (not room) (resp-error "INVALID_ROOM")
- (not maker) (resp-error "NOT_MAKER")
- :else (do
- (set-topic! room topic deadline maker)
- (resp-success "OK")))))
+ (if-vip
+ (let [room (lookup-room (params :room))
+ topic (params :topic)
+ deadline (params :deadline)
+ maker (params :maker)]
+ (cond (not (valid-topic? topic)) (resp-error "INVALID_TOPIC")
+ (not (valid-deadline? deadline)) (resp-error "INVALID_DEADLINE")
+ (not room) (resp-error "INVALID_ROOM")
+ (not maker) (resp-error "NOT_MAKER")
+ :else (do
+ (set-topic! room topic deadline maker)
+ (resp-success "OK"))))))
(defn validate-end-topic [session params]
- (let [room (lookup-room (params :room))]
- (cond (not (is-vip? :is_admin)) (resp-error "NOT_VIP")
- (not room) (resp-error "INVALID_ROOM")
- :else (do
- (end-topic! room)
- (resp-success "OK")))))
+ (if-vip
+ (if-let [room (lookup-room (params :room))]
+ (do
+ (end-topic! room)
+ (resp-success "OK"))
+ (resp-error "INVALID_ROOM"))))
;; Chat
@@ -678,13 +679,12 @@ ORDER BY msg_count DESC")
(.setAttribute st "json_user_nick" (if nick (json-str nick) "null"))
(.setAttribute st "roomname" (room :name))
(.setAttribute st "timestamp" now)
+ (.setAttribute st "version" *chat-version-number*)
(.toString st)))
;; Chat Log
-; TODO: Optimize dump counts
-; timb: ^^ done... i changed it to fetch one more than is shown per page to determine if next page is needed
(defn log [session room offset params]
(let [st (fetch-template "log" session)
logger (make-time-logger)
@@ -816,22 +816,45 @@ ORDER BY msg_count DESC")
;; Account resets
-(defn reset-request-page [session]
- (.toString (fetch-template "reset_request.st" session)))
+(defn reset-request! [session {nick :nick}]
+ (if-let [info (fetch-nick nick)]
+ (let [email (info :email)
+ hash (info :hash)
+ ts (System/currentTimeMillis)
+ token (reset-token nick hash ts)
+ link (reset-link nick token ts)]
+ (do (send-reset-email nick email link)
+ (resp-success "OK")))
+ (resp-error "NO_NICK")))
-(defn reset-account-request! [session params]
- )
+(defn reset-page [session params]
+ (let [st (fetch-template "reset" session)
+ nick (params :nick)
+ ts (maybe-parse-long (params :ts) 0)
+ token (params :token)
+ valid (valid-reset-link? nick token ts)]
+ (.setAttribute st "valid_request" valid)
+ (.setAttribute st "nick" nick)
+ (when valid
+ (.setAttribute st "link" (reset-link nick token ts)))
+ (.toString st)))
-(defn reset-account! [session key]
- )
+(defn reset-account! [session params]
+ (let [nick (params :nick)
+ ts (maybe-parse-long (params :ts) 0)
+ token (params :token)
+ hash (params :hash)]
+ (if (and (valid-reset-link? nick token ts) hash)
+ (let [info (fetch-nick nick)]
+ (update-nick-hash nick hash)
+ [(session-assoc-from-db info)
+ (redirect-to "/")])
+ [200 "BAD_REQUEST"])))
;; Upload
-
-(def *max-image-height* 2000)
-(def *max-image-width* 2000)
-(def *max-avatar-height* 2000)
-(def *max-avatar-width* 2000)
+(def *max-image-dimensions* [2000 2000])
+(def *max-avatar-dimensions* [2000 2000])
(def *vip-max-file-size* (mbytes 5)) ; don't be nuts guys
(def *max-file-size* (kbytes 750))
(def *ignore-size-limit-for-vip* true)
@@ -847,15 +870,15 @@ ORDER BY msg_count DESC")
(if (> (.length f) limit)
(str "FILE_TOO_BIG " limit))))
-(defn is-image-invalid? [f]
+(defn invalid-image-dimensions? [f [max-width max-height]]
(try
(let [i (ImageIO/read f)
height (.getHeight i)
width (.getWidth i)]
- (if (or (> width *max-image-width*)
- (> height *max-image-height*))
- (str "INVALID_RESOLUTION " *max-image-width* " " *max-image-height*)))
- (catch Exception _ "FILE_NOT_IMAGE")))
+ (if (or (> width max-width)
+ (> height max-height))
+ (str "INVALID_RESOLUTION " max-width " " max-height)))
+ (catch Exception _ "INVALID_IMAGE")))
(defn format-filename [s nick]
(let [spaceless (.replace s \space \-)
@@ -868,7 +891,7 @@ ORDER BY msg_count DESC")
(defn validate-upload-file [f vip]
(or (is-file-too-big? f vip)
- (is-image-invalid? f)))
+ (invalid-image-dimensions? f *max-image-dimensions*)))
; Upload notes:
@@ -920,19 +943,15 @@ ORDER BY msg_count DESC")
(defn upload-avatar [session params]
(let [image (params :image)]
- (cond (not image) [200 "INVALID_REQUEST"]
+ (cond (not image) [200 "INVALID_REQUEST"]
(not (session :nick)) [200 "NOT_LOGGED_IN"]
:else (do-upload-avatar session image))))
;; Compojure Routes
-(defn no-cache [resp]
- [{:headers {"Cache-Control" "no-cache, no-store, max-age=0, must-revalidate"}}
- resp])
-
(defn serve-static [dir path]
- ; TODO: cache policy for other static files (js, css, etc.)
+ ; TODO: cache other static files (js, css, etc.)
(let [cache-header (if (re-find pic-regex path)
{:headers {"Cache-Control" "max-age=604800,public"}}
{})]
@@ -940,12 +959,12 @@ ORDER BY msg_count DESC")
(serve-file dir path)]))
(defroutes static
- (GET "/static/*" (serve-static "static" (params :*)))
- (GET "/images/*" (serve-static *image-directory* (params :*)))
+ (GET "/static/*" (serve-static "static" (params :*)))
+ (GET "/images/*" (serve-static *image-directory* (params :*)))
(GET "/avatars/*" (serve-static *avatar-directory* (params :*))))
(defroutes pichat
- (GET "/" (no-cache (landing session)))
+ (GET "/" (landing session))
(GET "/favicon.ico" (serve-static "static" "favicon.ico"))
(GET "/u/:nick" (profile session (params :nick) "0"))
(GET "/u/:nick/" (profile session (params :nick) "0"))
@@ -954,16 +973,17 @@ ORDER BY msg_count DESC")
(GET "/u/:nick/favorites" (favorites session params))
(GET "/u/:nick/favorites/:offset" (favorites session params))
(GET "/json/:nick/favorites" (json-favorites session params))
- (GET "/u/:nick/:offset" (profile session
- (params :nick)
- (params :offset))) ; have to put this route after favs
+
+ ; have to put this route after favs
+ (GET "/u/:nick/:offset" (profile session (params :nick) (params :offset)))
+
(GET "/p/:nick/:postid" (single-message session (params :nick) (params :postid)))
(GET "/login" (login session params cookies))
(GET "/logout" (logout session))
(GET "/register" (serve-static "static" "register.html"))
- (GET "/:room/chat" (no-cache (validated-chat session (-> request :route-params :room) "chat")))
- (GET "/chat" (no-cache (validated-chat session "dumpfm" "chat")))
- (GET "/chat/:t" (no-cache (validated-chat session "dumpfm" (-> request :route-params :t))))
+ (GET "/:room/chat" (validated-chat session (params :room) "chat"))
+ (GET "/chat" (validated-chat session "dumpfm" "chat"))
+ (GET "/chat/:t" (validated-chat session "dumpfm" (params :t)))
(GET "/browser" (browser session))
(GET "/refresh" (validated-refresh session params))
(GET "/tag/:tag" (tagged-dumps session params (request-url request)))
@@ -971,12 +991,8 @@ ORDER BY msg_count DESC")
(POST "/cmd/tag/add" (validated-add-tag session params))
(POST "/cmd/tag/rm" (validated-remove-tag session params))
(GET "/log" (validated-log session "dumpfm" "0" params))
- (GET "/:room/log" (validated-log session
- (-> request :route-params :room)
- "0" params))
- (GET "/:room/log/:offset" (validated-log session
- (-> request :route-params :room)
- (-> request :route-params :offset)
+ (GET "/:room/log" (validated-log session (params :room) "0" params))
+ (GET "/:room/log/:offset" (validated-log session (params :room) (params :offset)
params))
;; TODO: add form tokens for all destructive actions
(POST "/msg" (validated-msg session params))
@@ -987,10 +1003,10 @@ ORDER BY msg_count DESC")
(POST "/end-topic" (validate-end-topic session params))
(GET "/directory" (directory session 0))
(GET "/directory/:offset"
- (directory session (maybe-parse-int (-> request :route-params :offset) 0)))
- (GET "/reset" (reset-request-page session))
- (POST "/reset-request" (reset-account-request! session params))
- (POST "/reset/:key" (reset-account! session (-> request :route-params :key)))
+ (directory session (maybe-parse-int (params :offset) 0)))
+ (POST "/reset-request" (reset-request! session params))
+ (GET "/reset" (reset-page session params))
+ (POST "/reset" (reset-account! session params))
;; Admin stuff (should be own route?)
(GET "/debug" (debug-page session flash))
diff --git a/src/user.clj b/src/user.clj
new file mode 100644
index 0000000..9d3060f
--- /dev/null
+++ b/src/user.clj
@@ -0,0 +1,42 @@
+(ns user
+ (:use compojure
+ utils))
+
+(def *nick-regex* #"^[A-Za-z0-9\-_∆˚†]*$")
+
+(defn is-invalid-nick? [n]
+ (cond
+ (< (count n) 3) "NICK_TOO_SHORT"
+ (not (re-matches *nick-regex* n)) "NICK_INVALID_CHARS"))
+
+(defn fetch-nick [nick]
+ (let [q1 "SELECT * FROM users WHERE nick = ? LIMIT 1"
+ ; ORDER BY ensures consistent retrieval of ambiguious names
+ q2 "SELECT * FROM users WHERE lower(nick) = ? ORDER BY nick LIMIT 1"]
+ (or (first-or-nil (do-select [q1 nick]))
+ (first-or-nil (do-select [q2 (lower-case nick)])))))
+
+(defn authorize-nick-hash [nick hash]
+ (let [db-user (fetch-nick nick)]
+ (and db-user (= (db-user :hash) hash) db-user)))
+
+(defn update-nick-hash [nick hash]
+ (if (not (assert-update
+ (do-update :users ["nick=?" nick]
+ {:hash hash})))
+ ; TODO: logging
+ (println (format "Error updating hash for %s" nick))))
+
+
+(defn reset-token [nick hash ts]
+ (sha1-hash nick hash ts))
+
+(defn reset-link [nick token ts]
+ (url-params "http://dump.fm/reset" {"nick" nick
+ "ts" ts
+ "token" token}))
+
+(defn valid-reset-link? [nick token ts]
+ (if-let [info (fetch-nick nick)]
+ (and (= token (reset-token (info :nick) (info :hash) ts))
+ (>= ts (ms-ago (days 2)))))) \ No newline at end of file
diff --git a/src/utils.clj b/src/utils.clj
index f42746c..628e4b8 100755
--- a/src/utils.clj
+++ b/src/utils.clj
@@ -56,7 +56,12 @@
:else s))
(defn seconds [t] (* t 1000))
-(defn minutes [t] (* t 60 1000))
+(defn minutes [t] (* 60 (seconds t)))
+(defn hours [t] (* 60 (minutes t)))
+(defn days [t] (* 24 (hours t)))
+
+(defn ms-ago [ms]
+ (- (System/currentTimeMillis) ms))
(defn kbytes [b] (* b 1024))
(defn mbytes [b] (* b 1024 1024))
@@ -72,12 +77,12 @@
(defn sha1-hash [& more]
(DigestUtils/shaHex (apply str more)))
-(defn gmt-string
- ([] (gmt-string (new Date)))
- ([dt]
- (let [df (new SimpleDateFormat "EEE, dd MMM yyyy kk:mm:ss z")]
- (.setTimeZone df (TimeZone/getTimeZone "GMT"))
- (.format df dt))))
+(defmacro with-timing [e]
+ `(let [s# (System/nanoTime)
+ r# ~e
+ f# (System/nanoTime)]
+ [(int (/ (- f# s#) 1000000.0)) r#]))
+
;; Formatters
@@ -123,6 +128,13 @@
[k (f v)]
[k v]))))
+(defn gmt-string
+ ([] (gmt-string (new Date)))
+ ([dt]
+ (let [df (new SimpleDateFormat "EEE, dd MMM yyyy kk:mm:ss z")]
+ (.setTimeZone df (TimeZone/getTimeZone "GMT"))
+ (.format df dt))))
+
;; JSON responses
(defmethod print-json Date
@@ -168,6 +180,10 @@
(with-connection *db*
(insert-values table cols values)))
+(defn assert-update
+ ([res ok err] (if (not (= (first res) 1)) err ok))
+ ([res] (assert-update res true false)))
+
;; Parsing
(defn maybe-parse-int
@@ -197,43 +213,24 @@
(def template-group (new StringTemplateGroup "dumpfm" "template"))
(.setRefreshInterval template-group 3)
-;; TODO: handle exception
+; TODO: handle exception, clean-up template setting
(defn fetch-template [template session]
(let [st (.getInstanceOf template-group template)]
(if (session :nick)
(do (.setAttribute st "user_email" (session :email))
(.setAttribute st "user_nick" (session :nick))
- (if (non-empty-string? (session :avatar)) (.setAttribute st "user_avatar" (session :avatar)))
- (.setAttribute st "isadmin" (session :is_admin)))) ;; TODO: consolidate session/user code
+ (if (non-empty-string? (session :avatar))
+ (.setAttribute st "user_avatar" (session :avatar)))
+ (.setAttribute st "isadmin" (session :is_admin))))
st))
(defn serve-template [template session]
(.toString (fetch-template template session)))
-
-;; User authentication
-; TODO: create user module
-
(defn first-or-nil [l]
(if (empty? l) nil (first l)))
-(def nick-regex #"^[A-Za-z0-9\-_∆˚†]*$")
-
-(defn is-invalid-nick? [n]
- (cond
- (< (count n) 3) "NICK_TOO_SHORT"
- (not (re-matches nick-regex n)) "NICK_INVALID_CHARS"))
-
-(defn fetch-nick [nick]
- (let [q1 "SELECT * FROM users WHERE nick = ? LIMIT 1"
- ; ORDER BY ensures consistent retrieval of ambiguious names
- q2 "SELECT * FROM users WHERE lower(nick) = ? ORDER BY nick LIMIT 1"]
- (or (first-or-nil (do-select [q1 nick]))
- (first-or-nil (do-select [q2 (lower-case nick)])))))
-
-(defn authorize-nick-hash [nick hash]
- (let [db-user (fetch-nick nick)]
- (and db-user (= (db-user :hash) hash) db-user)))
+;; VIP
(defn is-vip? [session]
(session :is_admin))