Skip to content
Snippets Groups Projects
Commit b45b2cf9 authored by Allen Gilliland's avatar Allen Gilliland
Browse files

initial support for resetting user password.

parent aa644a08
No related branches found
No related tags found
No related merge requests found
......@@ -77,8 +77,45 @@ AuthControllers.controller('Logout', ['$scope', '$location', '$timeout', 'ipCook
}]);
AuthControllers.controller('PasswordReset', ['$scope', '$cookies', '$location', 'Session', function($scope, $cookies, $location, Session) {
AuthControllers.controller('ForgotPassword', ['$scope', '$cookies', '$location', 'Session', function($scope, $cookies, $location, Session) {
// TODO: fill this out
$scope.sentNotification = false;
$scope.error = false;
$scope.sendResetNotification = function (email) {
Session.forgot_password({
'email': email
}, function (result) {
console.log('notification sent');
$scope.sentNotification = true;
}, function (error) {
$scope.error = true;
});
}
}]);
AuthControllers.controller('PasswordReset', ['$scope', '$routeParams', '$location', 'Session', function($scope, $routeParams, $location, Session) {
$scope.resetSuccess = false;
$scope.error = false;
// TODO - check for password matching
// TODO - check for password strength
$scope.resetPassword = function (password) {
Session.reset_password({
'token': $routeParams.token,
'password': password
}, function (result) {
console.log('reset happened!');
$scope.resetSuccess = true;
}, function (error) {
console.log(error);
$scope.error = true;
});
}
}]);
......@@ -13,7 +13,12 @@ Auth.config(['$routeProvider', function($routeProvider) {
controller: 'Logout'
});
$routeProvider.when('/auth/password_reset', {
$routeProvider.when('/auth/forgot_password', {
templateUrl: '/app/auth/partials/forgot_password.html',
controller: 'ForgotPassword'
});
$routeProvider.when('/auth/reset_password/:token', {
templateUrl: '/app/auth/partials/password_reset.html',
controller: 'PasswordReset'
});
......
<div class="layout-centered col col-md-6" ng-if="!sentNotification">
<h1 class="text-brand">
Forgot password?
</h1>
<form novalidate>
<p class="text-grey-4">
Happens to the best of us. Enter your e-mail address below, and we'll send you an e-mail allowing you to reset it.
</p>
<alert class="Form-group alert mx2" type="error" ng-if="error">The specified user does not exist in the system.</alert>
<div class="my2">
<input class="input block full" id="id_email" name="email" size="30" type="email" placeholder="Email address" autofocus ng-model="email">
</div>
<button class="Button Button--primary my2" ng-click="sendResetNotification(email)">
Send password reset email
</button>
</form>
<p class="text-grey-4">
Please <a class="text-brand" href="mailto:datahelp@expa.com">contact us</a> if you have any trouble resetting your password.
</p>
</div>
<div class="layout-centered col col-md-6" ng-if="sentNotification" ng-cloak>
<h1 class="text-brand my2">
Password reset sent.
</h1>
<p class="text-grey-3">We have sent you an e-mail. Please <a class="text-brand" href="mailto:datahelp@expa.com">contact us</a> if you do not receive it within a few minutes.</p>
</div>
\ No newline at end of file
......@@ -33,7 +33,7 @@
<button class="Button Button--primary" ng-click="login(email, password, remember_me)">
Sign in
</button>
<a class="Button" href="/auth/password_reset">
<a class="Button" href="/auth/forgot_password">
Forgot Password
</a>
</div>
......
<div class="layout-centered col col-md-6">
<h1 class="text-brand">
Forgot password?
<div class="layout-centered col col-md-6" ng-if="!resetSuccess">
<h1 class="text-brand my2">
Change password
</h1>
<form novalidate>
<p class="text-grey-4">
Happens to the best of us. Enter your e-mail address below, and we'll send you an e-mail allowing you to reset it.
</p>
<div class="row">
<form novalidate>
<p class="text-grey-4 px2">Your new password must be <b>10 characters</b> or longer, and <b>include an uppercase letter</b> and <b>a punctuation mark</b>. We know it's a bit much, but security is what the cool kids care about.</p>
<div class="my2">
<input class="input block full" id="id_email" name="email" size="30" type="email" placeholder="Email address" autofocus>
</div>
<alert class="Form-group alert mx2" type="error" ng-if="error">Failed to set new password.</alert>
<button class="Button Button--primary my2" ng-click="">
Send password reset email
</button>
</form>
<div class="col col-md-8">
<div class="py2">
<input class="input block full" id="id_password1" name="password1" type="password" placeholder="New Password" autofocus ng-model="password">
</div>
<div class="py2">
<input class="input block full" id="id_password2" name="password2" placeholder="New password again" type="password" ng-model="password2">
</div>
<div class="py2">
<button class="Button Button--primary" ng-click="resetPassword(password)">
Change Password
</button>
</div>
</div>
</form>
</div>
</div>
<div class="layout-centered col col-md-6" ng-if="resetSuccess" ng-cloak>
<h1 class="text-brand my2">
Password change successful!
</h1>
<p class="text-grey-4">
Please <a class="text-brand" href="mailto:datahelp@expa.com">contact us</a> if you have any trouble resetting your password.
Your password has been changed.
<a class="link" href="/auth/login">Sign in with your new password.</a>
</p>
</div>
\ No newline at end of file
......@@ -38,7 +38,6 @@ CorvusServices.factory('AppState', ['$rootScope', '$routeParams', '$q', '$locati
deferred.resolve(result);
}, function(error) {
console.log('unable to get current user', error);
$location.path('/unauthorized/');
deferred.reject(error);
});
......@@ -77,8 +76,12 @@ console.log('routeChangedImpl-noUser');
service.model.currentOrgSlug = null;
service.model.currentOrg = null;
// NOTE: we are taking for granted that $location won't send us to /auth/login repeatedly
$location.path('/auth/login');
if ($location.path().indexOf('/auth/') !== 0) {
// if the user is asking for a url outside of /auth/* then send them to login page
// otherwise we will let the user continue on to their requested page
$location.path('/auth/login');
}
return;
}
console.log('routeChangedImpl-withUser');
......@@ -567,6 +570,24 @@ CoreServices.factory('Session', ['$resource', '$cookies', function($resource, $c
},
delete: {
method: 'DELETE',
},
forgot_password: {
url: '/api/session/forgot_password',
method: 'POST',
headers: {
'X-CSRFToken': function() {
return $cookies.csrftoken;
}
}
},
reset_password: {
url: '/api/session/reset_password',
method: 'POST',
headers: {
'X-CSRFToken': function() {
return $cookies.csrftoken;
}
}
}
});
}]);
......@@ -613,13 +634,6 @@ CoreServices.factory('User', ['$resource', '$cookies', function($resource, $cook
return $cookies.csrftoken;
}
}
},
send_password_reset_email: {
url: '/api/user/send_password_reset_email/:userId',
method: 'GET',
params: {
'userId': '@id'
}
}
});
}]);
......
{
"databaseChangeLog": [
{"include": {"file": "migrations/liquibase_0.1.0.json"}},
{"include": {"file": "migrations/liquibase_0.5.0.json"}}
{"include": {"file": "migrations/liquibase_0.5.0.json"}},
{"include": {"file": "migrations/liquibase_0.6.0.json"}}
]
}
\ No newline at end of file
{
"databaseChangeLog": [
{
"changeSet": {
"id": "3",
"author": "agilliland",
"changes": [
{
"addColumn": {
"tableName": "core_user",
"columns": [
{
"column": {
"name": "password_salt",
"type": "varchar(254)",
"defaultValue": "default",
"constraints": {"nullable": false}
}
},
{
"column": {
"name": "reset_token",
"type": "varchar(254)"
}
},
{
"column": {
"name": "reset_triggered",
"type": "BIGINT"
}
}
]
}
}
]
}
}
]
}
\ No newline at end of file
......@@ -4,15 +4,15 @@
[metabase.api.common :refer :all]
[compojure.core :refer [defroutes POST DELETE]]
[metabase.db :refer :all]
(metabase.models [user :refer [User]]
(metabase.models [user :refer [User set-user-password]]
[session :refer [Session]])))
;; login
(defendpoint POST "/" [:as {{:keys [email password] :as body} :body}]
(require-params email password)
(let-400 [user (sel :one [User :id :password] :email email)]
(check (creds/bcrypt-verify password (:password user)) [400 "password mismatch"])
(let-400 [user (sel :one [User :id :password_salt :password] :email email)]
(check (creds/bcrypt-verify (str (:password_salt user) password) (:password user)) [400 "password mismatch"])
(let [session-id (str (java.util.UUID/randomUUID))]
(ins Session
:id session-id
......@@ -27,4 +27,26 @@
(del Session :id session_id))
;; forgotten password reset email
(defendpoint POST "/forgot_password" [:as {{:keys [email] :as body} :body}]
(require-params email)
(let [user-id (sel :one :id User :email email)
reset-token (java.util.UUID/randomUUID)]
(check-404 user-id)
(upd User user-id :reset_token reset-token :reset_triggered (System/currentTimeMillis))
;; TODO - send email
(println (str "/auth/reset_password/" reset-token))))
;; set password from reset token
(defendpoint POST "/reset_password" [:as {{:keys [token password] :as body} :body}]
(require-params token password)
(let-404 [user (sel :one :fields [User :id :reset_triggered] :reset_token token)]
(println user)
;; check that the reset was triggered within the last 1 HOUR, after that the token is considered expired
(check-404 (> (* 60 60 1000) (- (System/currentTimeMillis) (get user :reset_triggered 0))))
;; TODO - check that password is of required strength
(set-user-password (:id user) password)
{:success true}))
(define-routes)
......@@ -11,14 +11,11 @@
(metabase.middleware [auth :as auth]
[log-api-call :refer :all]
[format :refer :all])
[metabase.routes :as routes]))
[metabase.routes :as routes]
[metabase.db :as db]))
(defn liquibase-sql []
(let [conn (jdbc/get-connection {:subprotocol "postgresql"
:subname "//localhost:15432/corvus_test"
:user "corvus"
:password "corvus"})]
(com.metabase.corvus.migrations.LiquibaseMigrations/genSqlDatabase conn)))
(db/migrate :up))
(defn -main
"I don't do a whole lot ... yet."
......
(ns metabase.models.user
(:require [korma.core :refer :all]
(:require [cemerick.friend.credentials :as creds]
[korma.core :refer :all]
[metabase.db :refer :all]
(metabase.models [hydrate :refer :all]
[org-perm :refer [OrgPerm]])
......@@ -50,6 +51,18 @@
(merge defaults user)))
(defn set-user-password
"Updates the stored password for a specified `User` by hashing the password with a random salt."
[user-id password]
{:pre [(nil? user-id)
(nil? password)
(string? password)]}
(println user-id password)
(let [salt (.toString (java.util.UUID/randomUUID))
password (creds/hash-bcrypt (str salt password))]
(upd User user-id :password_salt salt :password password)))
(defn users-for-org
"Selects the ID and NAME for all users available to the given org-id."
[org-id]
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment