Skip to content
This repository was archived by the owner on Dec 8, 2022. It is now read-only.

Commit 93d0fd8

Browse files
authored
Merge pull request #36 from codewizardshq/forgot-password
Forgot Password + Reset Password
2 parents 1cb2944 + 8ee46d6 commit 93d0fd8

File tree

9 files changed

+218
-41
lines changed

9 files changed

+218
-41
lines changed

CodeChallenge/api/users.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ def forgot_password():
172172
"reset request has been issued for your account. If you "
173173
"did not make this request, you can ignore this email. "
174174
"To reset your password, use this link within 24 hours. "
175-
f"\n\n{current_app.config['EXTERNAL_URL']}/reset-password?token={token}"
175+
f"\n\n{current_app.config['EXTERNAL_URL']}/reset-password/{token}"
176176
f"\n\nAccount Username: {user.username}",
177177
recipients=[user.parent_email])
178178

@@ -191,7 +191,7 @@ def forgot_password():
191191

192192

193193
@bp.route("/reset-password", methods=["POST"])
194-
@limiter.limit("3 per hour", key_func=get_remote_address)
194+
@limiter.limit("10 per hour", key_func=get_remote_address)
195195
def reset_password():
196196
data = request.get_json()
197197
password = data.get("password")

CodeChallenge/config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class DefaultConfig:
2020
CODE_CHALLENGE_START = ""
2121
RATELIMIT_HEADERS_ENABLED = True
2222

23-
MAIL_SERVER = "localhost"
23+
MAIL_SERVER = "smtp.mailgun.org"
2424
MAIL_PORT = 587
2525
MAIL_USE_TLS = True
2626
MAIL_USERNAME = ""
@@ -72,6 +72,7 @@ class ProductionConfig(DefaultConfig):
7272

7373

7474
class DevelopmentConfig(ProductionConfig):
75+
EXTERNAL_URL = "http://localhost:8080"
7576
SQLALCHEMY_DATABASE_URI = "mysql://cc-user:password@localhost" \
7677
"/code_challenge_local"
7778
JWT_COOKIE_SECURE = False

src/api/auth.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ function currentUser() {
7474
return { ...state };
7575
}
7676

77+
async function forgotPassword(email) {
78+
return await request(routes.userapi_forgot_password, { data: { email } }, false);
79+
}
80+
81+
async function resetPassword(token, password) {
82+
return await request(routes.userapi_reset_password, { data: { token, password } }, false)
83+
}
84+
7785
export default {
7886
logout,
7987
login,
@@ -82,5 +90,7 @@ export default {
8290
createAccount,
8391
currentUser,
8492
onAuthStateChange,
85-
offAuthStateChange
93+
offAuthStateChange,
94+
forgotPassword,
95+
resetPassword
8696
};

src/api/request.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export default async function request(route, options = {}, tryRefresh = true) {
2626
// return original error
2727
return Promise.reject({
2828
status: err.response.status,
29+
headers: err.response.headers,
2930
message:
3031
!!err.response.data && !!err.response.data.reason
3132
? err.response.data.reason

src/api/routes.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ export default {
1212
userapi_logout: route("/api/v1/users/token/remove", "POST"),
1313
userapi_hello: route("/api/v1/users/hello", "GET"),
1414
userapi_refresh: route("/api/v1/users/token/refresh", "POST"),
15+
userapi_forgot_password: route("/api/v1/users/forgot", "POST"),
16+
userapi_reset_password: route("/api/v1/users/reset-password", "POST"),
1517
questionsapi_rank_reset: route("/api/v1/questions/reset", "DELETE"),
1618
questionsapi_answer_next_question: route("/api/v1/questions/answer", "POST"),
1719
questionsapi_get_rank: route("/api/v1/questions/rank", "GET"),
18-
questions_api_next_question: route("/api/v1/questions/next", "GET")
20+
questions_api_next_question: route("/api/v1/questions/next", "GET"),
1921
};
2022

2123
// export default {

src/plugins/router.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Vue from "vue";
22
import VueRouter from "vue-router";
3-
import { auth } from "@/api";
3+
import {auth} from "@/api";
44
import store from "@/store";
55

66
Vue.use(VueRouter);
@@ -20,7 +20,13 @@ const routes = [
2020
}
2121
},
2222
{
23-
path: "/reset-password",
23+
path: "/forgot-password",
24+
name: "forgot-password",
25+
component: () => import("@/views/ForgotPassword"),
26+
meta: { anon: true }
27+
},
28+
{
29+
path: "/reset-password/:token",
2430
name: "reset-password",
2531
component: () => import("@/views/ResetPassword")
2632
},

src/views/ForgotPassword.vue

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<template>
2+
<v-row align="center" justify="center">
3+
<v-col cols="12" sm="8" md="4">
4+
<v-card flat class="mt-12">
5+
<v-toolbar color="secondary" dark flat>
6+
<v-toolbar-title>Forgot password form</v-toolbar-title>
7+
</v-toolbar>
8+
<v-form @submit.prevent="validate" ref="form">
9+
<v-card-text>
10+
<p>
11+
Did you forget your password? Enter your parent's e-mail address
12+
below. They entered this when they created your account.
13+
</p>
14+
<v-text-field
15+
v-bind="fields.username"
16+
v-model="fields.username.value"
17+
:disabled="isSubmitting"
18+
/>
19+
</v-card-text>
20+
21+
<v-card-actions>
22+
<v-spacer/>
23+
<v-btn color="secondary" type="submit" dark :disabled="isSubmitting"
24+
>Send Reset Password Request
25+
</v-btn
26+
>
27+
</v-card-actions>
28+
</v-form>
29+
</v-card>
30+
</v-col>
31+
32+
<v-dialog
33+
v-model="multiple"
34+
max-width="290"
35+
>
36+
<v-card>
37+
<v-card-title class="headline">Multiple Accounts</v-card-title>
38+
<v-card-text>
39+
That email address is associated with multiple accounts.
40+
Password reset emails have been sent for each account.
41+
Double check the email body for the username so that you reset the intended account's password!
42+
</v-card-text>
43+
44+
<v-card-actions>
45+
<v-spacer/>
46+
<v-btn text color="green" @click="multiple = false">OK</v-btn>
47+
</v-card-actions>
48+
</v-card>
49+
</v-dialog>
50+
51+
</v-row>
52+
</template>
53+
54+
<script>
55+
import {auth} from "@/api";
56+
57+
export default {
58+
name: "forgot-password",
59+
methods: {
60+
async submit() {
61+
try {
62+
let res = await auth.forgotPassword(this.fields.username.value);
63+
this.multiple = res.multiple;
64+
this.$store.dispatch(
65+
"Snackbar/showInfo",
66+
"A password reset link was sent to your email."
67+
);
68+
} catch (e) {
69+
if (e.status === 400) {
70+
this.$store.dispatch(
71+
"Snackbar/showError",
72+
"No accounts associated with that email address."
73+
);
74+
return;
75+
}
76+
if (e.status === 429) {
77+
let totalSeconds = parseInt(e.headers["retry-after"]);
78+
let hours = Math.floor(totalSeconds / 3600);
79+
totalSeconds %= 3600;
80+
let minutes = Math.floor(totalSeconds / 60);
81+
let seconds = totalSeconds % 60;
82+
83+
this.$store.dispatch(
84+
"Snackbar/showError",
85+
`Reset attempts exceeded. Try again in ${hours} hours ${minutes} minutes ${seconds} seconds.`
86+
);
87+
return;
88+
}
89+
this.$store.dispatch(
90+
"Snackbar/showError",
91+
"Request failed, try again."
92+
);
93+
94+
console.error(e);
95+
}
96+
},
97+
validate() {
98+
if (this.$refs.form.validate()) {
99+
this.submit();
100+
}
101+
}
102+
},
103+
data() {
104+
return {
105+
isSubmitting: false,
106+
multiple: false,
107+
fields: {
108+
username: {
109+
label: "Parents E-mail",
110+
type: "email",
111+
rules: [v => !!v || "Please provide a valid e-mail address"]
112+
}
113+
}
114+
};
115+
}
116+
};
117+
</script>

src/views/Login.vue

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
</v-card-text>
2222

2323
<v-card-text>
24-
<router-link :to="{ name: 'reset-password' }"
24+
<router-link :to="{ name: 'forgot-password' }"
2525
>Forgot your password?</router-link
2626
>
2727
</v-card-text>
@@ -43,8 +43,9 @@
4343
</template>
4444

4545
<script>
46-
import { auth } from "@/api";
47-
export default {
46+
import {auth} from "@/api";
47+
48+
export default {
4849
name: "login",
4950
methods: {
5051
async submit() {

src/views/ResetPassword.vue

Lines changed: 70 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,30 @@
33
<v-col cols="12" sm="8" md="4">
44
<v-card flat class="mt-12">
55
<v-toolbar color="secondary" dark flat>
6-
<v-toolbar-title>Reset password form</v-toolbar-title>
6+
<v-toolbar-title>Password reset form</v-toolbar-title>
77
</v-toolbar>
88
<v-form @submit.prevent="validate" ref="form">
99
<v-card-text>
1010
<p>
11-
Did you forget your password? Enter your parent's e-mail address
12-
below. They entered this when they created your account.
11+
Create a new password.
1312
</p>
13+
14+
<v-text-field
15+
v-bind="fields.password"
16+
v-model="fields.password.value"
17+
:disabled="isSubmitting"
18+
/>
1419
<v-text-field
15-
v-bind="fields.username"
16-
v-model="fields.username.value"
20+
v-bind="fields.passwordConfirm"
21+
v-model="fields.passwordConfirm.value"
1722
:disabled="isSubmitting"
1823
/>
1924
</v-card-text>
2025

2126
<v-card-actions>
2227
<v-spacer />
2328
<v-btn color="secondary" type="submit" dark :disabled="isSubmitting"
24-
>Send Reset Password Request</v-btn
29+
>Reset Password</v-btn
2530
>
2631
</v-card-actions>
2732
</v-form>
@@ -31,32 +36,66 @@
3136
</template>
3237

3338
<script>
34-
export default {
35-
name: "reset-password",
36-
methods: {
37-
async submit() {
38-
this.$store.dispatch(
39-
"Snackbar/showError",
40-
"This feature not yet implemented"
41-
);
42-
},
43-
validate() {
44-
if (this.$refs.form.validate()) {
45-
this.submit();
39+
import {auth} from "@/api";
40+
41+
export default {
42+
name: "reset-password",
43+
created() {
44+
if (! this.$route.params.token) {
45+
this.$store.dispatch(
46+
"Snackbar/showError",
47+
"Missing token from URL. Password cannot be reset."
48+
);
49+
this.$router.push({ name: "forgot-password" });
4650
}
47-
}
48-
},
49-
data() {
50-
return {
51-
isSubmitting: false,
52-
fields: {
53-
username: {
54-
label: "Parents E-mail",
55-
type: "email",
56-
rules: [v => !!v || "Please provide a valid e-mail address"]
51+
},
52+
methods: {
53+
async submit() {
54+
try {
55+
await auth.resetPassword(this.$route.params.token, this.fields.password.value);
56+
this.$store.dispatch(
57+
"Snackbar/showInfo",
58+
"Password reset successfully. You may now login."
59+
);
60+
this.$router.push({ name: "login" })
61+
} catch (e) {
62+
console.error(e);
63+
this.$store.dispatch(
64+
"Snackbar/showError",
65+
"Request failed"
66+
);
67+
}
68+
},
69+
validate() {
70+
if (this.$refs.form.validate()) {
71+
this.submit();
5772
}
5873
}
59-
};
60-
}
61-
};
74+
},
75+
data() {
76+
return {
77+
isSubmitting: false,
78+
fields: {
79+
password: {
80+
label: "Password",
81+
type: "password",
82+
autocomplete: "new-password",
83+
value: "",
84+
rules: [
85+
v => !!v || "Don't forget to give a password",
86+
v => v.length >= 8 || "Password must be at least 8 characters"
87+
]
88+
},
89+
passwordConfirm: {
90+
label: "Confirm Password",
91+
type: "password",
92+
value: "",
93+
rules: [
94+
v => v == this.fields.password.value || "Passwords do not match"
95+
]
96+
},
97+
}
98+
};
99+
}
100+
};
62101
</script>

0 commit comments

Comments
 (0)