diff --git a/extra/http/server/actions/actions.factor b/extra/http/server/actions/actions.factor
index 72c2d2df8e..7bee96edce 100755
--- a/extra/http/server/actions/actions.factor
+++ b/extra/http/server/actions/actions.factor
@@ -17,11 +17,6 @@ TUPLE: action init display submit get-params post-params ;
         [ <400> ] >>display
         [ <400> ] >>submit ;
 
-: with-validator ( string quot -- result error? )
-    '[ , @ f ] [
-        dup validation-error? [ t ] [ rethrow ] if
-    ] recover ; inline
-
 : validate-param ( name validator assoc -- error? )
     swap pick
     >r >r at r> with-validator swap r> set ;
diff --git a/extra/http/server/auth/login/edit-profile.fhtml b/extra/http/server/auth/login/edit-profile.fhtml
new file mode 100755
index 0000000000..7d94ca1791
--- /dev/null
+++ b/extra/http/server/auth/login/edit-profile.fhtml
@@ -0,0 +1,77 @@
+<% USING: http.server.components http.server.auth.login
+http.server namespaces kernel combinators ; %>
+<html>
+<body>
+<h1>Edit profile</h1>
+
+<form method="POST" action="edit-profile">
+<% hidden-form-field %>
+
+<table>
+
+<tr>
+<td>User name:</td>
+<td><% "username" component render-view %></td>
+</tr>
+
+<tr>
+<td>Real name:</td>
+<td><% "realname" component render-edit %></td>
+</tr>
+
+<tr>
+<td></td>
+<td>Specifying a real name is optional.</td>
+</tr>
+
+<tr>
+<td>Current password:</td>
+<td><% "password" component render-edit %></td>
+</tr>
+
+<tr>
+<td></td>
+<td>If you don't want to change your current password, leave this field blank.</td>
+</tr>
+
+<tr>
+<td>New password:</td>
+<td><% "new-password" component render-edit %></td>
+</tr>
+
+<tr>
+<td>Verify:</td>
+<td><% "verify-password" component render-edit %></td>
+</tr>
+
+<tr>
+<td></td>
+<td>If you are changing your password, enter it twice to ensure it is correct.</td>
+</tr>
+
+<tr>
+<td>E-mail:</td>
+<td><% "email" component render-edit %></td>
+</tr>
+
+<tr>
+<td></td>
+<td>Specifying an e-mail address is optional. It enables the "recover password" feature.</td>
+</tr>
+
+</table>
+
+<p><input type="submit" value="Update" />
+
+<% {
+    { [ login-failed? get ] [ "invalid password" render-error ] }
+    { [ password-mismatch? get ] [ "passwords do not match" render-error ] }
+    { [ t ] [ ] }
+} cond %>
+
+</p>
+
+</form>
+
+</body>
+</html>
diff --git a/extra/http/server/auth/login/login.factor b/extra/http/server/auth/login/login.factor
index 9b2648158d..8842e1639e 100755
--- a/extra/http/server/auth/login/login.factor
+++ b/extra/http/server/auth/login/login.factor
@@ -13,6 +13,8 @@ QUALIFIED: smtp
 
 TUPLE: login users ;
 
+: users login get users>> ;
+
 SYMBOL: post-login-url
 SYMBOL: login-failed?
 
@@ -49,7 +51,7 @@ SYMBOL: login-failed?
                 form validate-form
 
                 "password" value "username" value
-                login get users>> check-login [
+                users check-login [
                     successful-login
                 ] [
                     login-failed? on
@@ -67,7 +69,7 @@ SYMBOL: login-failed?
             t >>required
             add-field
         "realname" <string> add-field
-        "password" <password>
+        "new-password" <password>
             t >>required
             add-field
         "verify-password" <password>
@@ -80,7 +82,7 @@ SYMBOL: password-mismatch?
 SYMBOL: user-exists?
 
 : same-password-twice ( -- )
-    "password" value "verify-password" value = [ 
+    "new-password" value "verify-password" value = [ 
         password-mismatch? on
         validation-failed
     ] unless ;
@@ -102,14 +104,13 @@ SYMBOL: user-exists?
 
                 same-password-twice
 
-                <user> values get [
-                    "username" get >>username
-                    "realname" get >>realname
-                    "password" get >>password
-                    "email" get >>email
-                ] bind
+                <user>
+                    "username" value >>username
+                    "realname" value >>realname
+                    "new-password" value >>password
+                    "email" value >>email
 
-                login get users>> new-user [
+                users new-user [
                     user-exists? on
                     validation-failed
                 ] unless*
@@ -118,6 +119,64 @@ SYMBOL: user-exists?
             ] >>submit
     ] ;
 
+! ! ! Editing user profile
+
+: <edit-profile-form> ( -- form )
+    "edit-profile" <form>
+        "resource:extra/http/server/auth/login/edit-profile.fhtml" >>edit-template
+        "username" <username> add-field
+        "realname" <string> add-field
+        "password" <password> add-field
+        "new-password" <password> add-field
+        "verify-password" <password> add-field
+        "email" <email> add-field ;
+
+SYMBOL: previous-page
+
+:: <edit-profile-action> ( -- action )
+    [let | form [ <edit-profile-form> ] |
+        <action>
+            [
+                blank-values
+                logged-in-user sget
+                dup username>> "username" set-value
+                dup realname>> "realname" set-value
+                dup email>> "email" set-value
+            ] >>init
+
+            [
+                "text/html" <content>
+                [ form edit-form ] >>body
+            ] >>display
+
+            [
+                blank-values
+                uid "username" set-value
+
+                form validate-form
+
+                "password" value empty? [
+                    logged-in-user sget
+                ] [
+                    same-password-twice
+
+                    "password" value uid users check-login
+                    [ login-failed? on validation-failed ] unless
+
+                    "new-password" value uid users set-password
+                    [ "User deleted" throw ] unless*
+                ] if
+
+                "realname" value >>realname
+                "email" value >>email
+
+                dup users update-user
+                logged-in-user sset
+
+                previous-page sget dup [ f <permanent-redirect> ] when
+            ] >>submit
+    ] ;
+
 ! ! ! Password recovery
 
 SYMBOL: lost-password-from
@@ -186,7 +245,7 @@ SYMBOL: lost-password-from
                 form validate-form
 
                 "email" value "username" value
-                login get users>> issue-ticket [
+                users issue-ticket [
                     send-password-email
                 ] when*
 
@@ -200,7 +259,7 @@ SYMBOL: lost-password-from
         "username" <username> <hidden>
             t >>required
             add-field
-        "password" <password>
+        "new-password" <password>
             t >>required
             add-field
         "verify-password" <password>
@@ -239,9 +298,9 @@ SYMBOL: lost-password-from
 
                 "ticket" value
                 "username" value
-                login get users>> claim-ticket [
-                    "password" value >>password
-                    login get users>> update-user
+                users claim-ticket [
+                    "new-password" value >>password
+                    users update-user
 
                     "resource:extra/http/server/auth/login/recover-4.fhtml"
                     serve-template
@@ -265,13 +324,18 @@ TUPLE: protected responder ;
 
 C: <protected> protected
 
+: show-login-page ( -- response )
+    request get request-url post-login-url sset
+    "login" f <permanent-redirect> ;
+
 M: protected call-responder ( path responder -- response )
-    logged-in-user sget [ responder>> call-responder ] [
+    logged-in-user sget [
+        request get request-url previous-page sset
+        responder>> call-responder
+    ] [
         2drop
-        request get method>> { "GET" "HEAD" } member? [
-            request get request-url post-login-url sset
-            "login" f <permanent-redirect>
-        ] [ <400> ] if
+        request get method>> { "GET" "HEAD" } member?
+        [ show-login-page ] [ <400> ] if
     ] if ;
 
 M: login call-responder ( path responder -- response )
@@ -287,6 +351,9 @@ M: login call-responder ( path responder -- response )
 
 ! ! ! Configuration
 
+: allow-edit-profile ( login -- login )
+    <edit-profile-action> <protected> "edit-profile" add-responder ;
+
 : allow-registration ( login -- login )
     <register-action> "register" add-responder ;
 
@@ -294,6 +361,9 @@ M: login call-responder ( path responder -- response )
     <recover-action-1> "recover-password" add-responder
     <recover-action-3> "new-password" add-responder ;
 
+: allow-edit-profile? ( -- ? )
+    login get responders>> "edit-profile" swap key? ;
+
 : allow-registration? ( -- ? )
     login get responders>> "register" swap key? ;
 
diff --git a/extra/http/server/auth/login/recover-3.fhtml b/extra/http/server/auth/login/recover-3.fhtml
index edd32fffe8..ca4823baab 100755
--- a/extra/http/server/auth/login/recover-3.fhtml
+++ b/extra/http/server/auth/login/recover-3.fhtml
@@ -17,7 +17,7 @@ namespaces kernel combinators ; %>
 
 <tr>
 <td>Password:</td>
-<td><% "password" component render-edit %></td>
+<td><% "new-password" component render-edit %></td>
 </tr>
 
 <tr>
diff --git a/extra/http/server/auth/login/register.fhtml b/extra/http/server/auth/login/register.fhtml
index 99d1547d03..9106497def 100755
--- a/extra/http/server/auth/login/register.fhtml
+++ b/extra/http/server/auth/login/register.fhtml
@@ -26,7 +26,7 @@ http.server namespaces kernel combinators ; %>
 
 <tr>
 <td>Password:</td>
-<td><% "password" component render-edit %></td>
+<td><% "new-password" component render-edit %></td>
 </tr>
 
 <tr>
diff --git a/extra/http/server/auth/providers/providers.factor b/extra/http/server/auth/providers/providers.factor
index 0aa27f870d..74620a4f5d 100755
--- a/extra/http/server/auth/providers/providers.factor
+++ b/extra/http/server/auth/providers/providers.factor
@@ -17,12 +17,12 @@ GENERIC: new-user ( user provider -- user/f )
 : check-login ( password username provider -- user/f )
     get-user dup [ [ password>> = ] keep and ] [ 2drop f ] if ;
 
-:: set-password ( password username provider -- ? )
+:: set-password ( password username provider -- user/f )
     [let | user [ username provider get-user ] |
         user [
             user
                 password >>password
-            provider update-user t
+            provider dup update-user
         ] [ f ] if
     ] ;
 
diff --git a/extra/http/server/components/components-tests.factor b/extra/http/server/components/components-tests.factor
index 2a507e6416..83ae7b0118 100755
--- a/extra/http/server/components/components-tests.factor
+++ b/extra/http/server/components/components-tests.factor
@@ -86,3 +86,16 @@ TUPLE: test-tuple text number more-text ;
 
     [ t ] [ "number" value validation-error? ] unit-test
 ] with-scope
+
+[
+    [ ] [
+        "n" <number>
+            0 >>min-value
+            10 >>max-value
+        "n" set
+    ] unit-test
+
+    [ "123" ] [
+        "123" "n" get validate value>>
+    ] unit-test
+] with-scope
diff --git a/extra/http/server/components/components.factor b/extra/http/server/components/components.factor
index bb0fc4b3dd..df46259c14 100755
--- a/extra/http/server/components/components.factor
+++ b/extra/http/server/components/components.factor
@@ -7,8 +7,6 @@ http.server.actions splitting mirrors hashtables
 combinators.cleave fry continuations math ;
 IN: http.server.components
 
-SYMBOL: validation-failed?
-
 SYMBOL: components
 
 TUPLE: component id required default ;
@@ -30,16 +28,13 @@ SYMBOL: values
 
 : validate ( value component -- result )
     '[
-        , ,
+        ,
         over empty? [
             [ default>> [ v-default ] when* ]
             [ required>> [ v-required ] when ]
             bi
         ] [ validate* ] if
-    ] [
-        dup validation-error?
-        [ validation-failed? on ] [ rethrow ] if
-    ] recover ;
+    ] with-validator ;
 
 : render-view ( component -- )
     [ id>> value ] [ render-view* ] bi ;
@@ -215,7 +210,12 @@ M: number render-error*
 ! Text areas
 TUPLE: text ;
 
-: <text> ( id -- component ) <string> text construct-delegate ;
+: <text> ( id -- component ) text <component> ;
+
+M: text validate* 2drop ;
+
+M: text render-view*
+    drop write ;
 
 : render-textarea
     <textarea
diff --git a/extra/http/server/validators/validators-tests.factor b/extra/http/server/validators/validators-tests.factor
index 3ef2b6c863..d0785b0126 100755
--- a/extra/http/server/validators/validators-tests.factor
+++ b/extra/http/server/validators/validators-tests.factor
@@ -13,10 +13,10 @@ accessors ;
 ] unit-test
 
 [ "slava@factorcode.o" v-email ]
-[ reason>> "invalid e-mail" = ] must-fail-with
+[ "invalid e-mail" = ] must-fail-with
 
 [ "sla@@factorcode.o" v-email ]
-[ reason>> "invalid e-mail" = ] must-fail-with
+[ "invalid e-mail" = ] must-fail-with
 
 [ "slava@factorcodeorg" v-email ]
-[ reason>> "invalid e-mail" = ] must-fail-with
+[ "invalid e-mail" = ] must-fail-with
diff --git a/extra/http/server/validators/validators.factor b/extra/http/server/validators/validators.factor
index 7eb5163d33..84f22b01f4 100755
--- a/extra/http/server/validators/validators.factor
+++ b/extra/http/server/validators/validators.factor
@@ -5,21 +5,26 @@ math.parser assocs new-slots regexp fry unicode.categories
 combinators.cleave sequences ;
 IN: http.server.validators
 
+SYMBOL: validation-failed?
+
 TUPLE: validation-error value reason ;
 
-: validation-error ( value reason -- * )
-    \ validation-error construct-boa throw ;
+C: <validation-error> validation-error
+
+: with-validator ( value quot -- result )
+    [ validation-failed? on <validation-error> ] recover ;
+    inline
 
 : v-default ( str def -- str )
     over empty? spin ? ;
 
 : v-required ( str -- str )
-    dup empty? [ "required" validation-error ] when ;
+    dup empty? [ "required" throw ] when ;
 
 : v-min-length ( str n -- str )
     over length over < [
         [ "must be at least " % # " characters" % ] "" make
-        validation-error
+        throw
     ] [
         drop
     ] if ;
@@ -27,35 +32,31 @@ TUPLE: validation-error value reason ;
 : v-max-length ( str n -- str )
     over length over > [
         [ "must be no more than " % # " characters" % ] "" make
-        validation-error
+        throw
     ] [
         drop
     ] if ;
 
 : v-number ( str -- n )
-    dup string>number [ ] [
-        "must be a number" validation-error
-    ] ?if ;
+    dup string>number [ ] [ "must be a number" throw ] ?if ;
 
 : v-min-value ( x n -- x )
     2dup < [
-        [ "must be at least " % # ] "" make
-        validation-error
+        [ "must be at least " % # ] "" make throw
     ] [
         drop
     ] if ;
 
 : v-max-value ( x n -- x )
     2dup > [
-        [ "must be no more than " % # ] "" make
-        validation-error
+        [ "must be no more than " % # ] "" make throw
     ] [
         drop
     ] if ;
 
 : v-regexp ( str what regexp -- str )
     >r over r> matches?
-    [ drop ] [ "invalid " swap append validation-error ] if ;
+    [ drop ] [ "invalid " swap append throw ] if ;
 
 : v-email ( str -- str )
     #! From http://www.regular-expressions.info/email.html
@@ -64,12 +65,12 @@ TUPLE: validation-error value reason ;
     v-regexp ;
 
 : v-captcha ( str -- str )
-    dup empty? [ "must remain blank" validation-error ] unless ;
+    dup empty? [ "must remain blank" throw ] unless ;
 
 : v-one-line ( str -- str )
     dup "\r\n" seq-intersect empty?
-    [ "must be a single line" validation-error ] unless ;
+    [ "must be a single line" throw ] unless ;
 
 : v-one-word ( str -- str )
     dup [ alpha? ] all?
-    [ "must be a single word" validation-error ] unless ;
+    [ "must be a single word" throw ] unless ;