Skip to content

Commit

Permalink
Create registration resend email function
Browse files Browse the repository at this point in the history
  • Loading branch information
jdabtieu committed May 24, 2024
1 parent e6c5196 commit 55682a7
Show file tree
Hide file tree
Showing 24 changed files with 219 additions and 72 deletions.
38 changes: 31 additions & 7 deletions src/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,10 +290,9 @@ def register():

username = request.form.get("username")
password = request.form.get("password")
confirmation = request.form.get("confirmation")
email = request.form.get("email")

code = register_chk(username, password, confirmation, email)
code = register_chk(username, password, email)
if code:
return render_template("auth/register.html",
site_key=app.config['HCAPTCHA_SITE']), code
Expand All @@ -314,9 +313,9 @@ def register():
username, generate_password_hash(password), email)
except ValueError:
if db.execute("SELECT COUNT(*) AS cnt FROM users WHERE username=?", username)[0]["cnt"] > 0:

Check failure on line 315 in src/application.py

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

E501 line too long (100 > 90 characters)

Check failure on line 315 in src/application.py

View workflow job for this annotation

GitHub Actions / build (windows-latest)

E501 line too long (100 > 90 characters)
flash('Username already exists', 'danger')
flash('Username already in use', 'danger')
elif db.execute("SELECT COUNT(*) AS cnt FROM users WHERE email=?", email)[0]["cnt"] > 0:

Check failure on line 317 in src/application.py

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

E501 line too long (96 > 90 characters)

Check failure on line 317 in src/application.py

View workflow job for this annotation

GitHub Actions / build (windows-latest)

E501 line too long (96 > 90 characters)
flash('Email already exists', 'danger')
flash('Email already in use', 'danger')
return render_template("auth/register.html",
site_key=app.config['HCAPTCHA_SITE']), 400

Expand All @@ -327,11 +326,36 @@ def register():
send_email('CTFOJ Account Confirmation',
app.config['MAIL_DEFAULT_SENDER'], [email], text)

flash(('An account creation confirmation email has been sent to the email address '
'you provided. Be sure to check your spam folder!'), 'success')
logger.info((f"User {username} ({email}) has initiated a registration request "
f"on IP {request.remote_addr}"), extra={"section": "auth"})
return render_template("auth/register.html", site_key=app.config['HCAPTCHA_SITE'])
token = create_jwt({'username': username}, app.config['SECRET_KEY'])
return render_template("auth/register_interstitial.html", token=token)


@app.route("/auth/resend_registration_confirmation", methods=["POST"])
def resend_registration_confirmation():
try:
token = jwt.decode(request.form.get("token"), app.config['SECRET_KEY'], algorithms=['HS256'])

Check failure on line 338 in src/application.py

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

E501 line too long (101 > 90 characters)

Check failure on line 338 in src/application.py

View workflow job for this annotation

GitHub Actions / build (windows-latest)

E501 line too long (101 > 90 characters)
except Exception as e:

Check warning on line 339 in src/application.py

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

F841 local variable 'e' is assigned to but never used

Check warning on line 339 in src/application.py

View workflow job for this annotation

GitHub Actions / build (windows-latest)

F841 local variable 'e' is assigned to but never used
return "Invalid token. Please contact an admin.", 400
if datetime.strptime(token["expiration"], "%Y-%m-%dT%H:%M:%S.%f") < datetime.utcnow():
return "Page expired. Please contact an admin.", 400
username = token["username"]
user = db.execute("SELECT * FROM users WHERE username=?", username)
if len(user) == 0:
return "User doesn't exist.", 400
if user[0]["verified"]:
return "/login", 302
if user[0]['registration_resend_attempts'] >= 2: # allow 2 tries

Check failure on line 349 in src/application.py

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

E261 at least two spaces before inline comment

Check failure on line 349 in src/application.py

View workflow job for this annotation

GitHub Actions / build (windows-latest)

E261 at least two spaces before inline comment
return "You have tried too many times. Please contact an admin.", 400
if not app.config['TESTING']:
token = create_jwt({'email': user[0]["email"]}, app.config['SECRET_KEY'])
text = render_template('email/confirm_account.html',
username=username, token=token)
send_email('CTFOJ Account Confirmation',
app.config['MAIL_DEFAULT_SENDER'], [user[0]["email"]], text)
db.execute("UPDATE users SET registration_resend_attempts=registration_resend_attempts+1 WHERE username=?", username)

Check failure on line 357 in src/application.py

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

E501 line too long (121 > 90 characters)

Check failure on line 357 in src/application.py

View workflow job for this annotation

GitHub Actions / build (windows-latest)

E501 line too long (121 > 90 characters)
return "OK"


@app.route('/confirmregister/<token>')
Expand Down
1 change: 0 additions & 1 deletion src/assets/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,6 @@ td {

.pagination {
overflow-x: auto;
margin-bottom: 0;
}

.hidden {
Expand Down
6 changes: 6 additions & 0 deletions src/daily_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import datetime
import os
import shutil
from db import db

# Back up data
timestamp = datetime.date.strftime(datetime.datetime.now(), "%d-%m-%Y-%H-%M-%S")
Expand All @@ -15,3 +16,8 @@
timestamp = datetime.date.strftime(datetime.datetime.now(), "%d-%m-%Y")
shutil.copy2("logs/application.log", f"logs/{timestamp}-application.log")
open("logs/application.log", "w").close()

# Remove unverified users > 1 week
db.execute(
"DELETE FROM users WHERE verified=0 AND unixepoch(join_date) < unixepoch() - 604800"
)

Check warning on line 23 in src/daily_tasks.py

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

W292 no newline at end of file

Check warning on line 23 in src/daily_tasks.py

View workflow job for this annotation

GitHub Actions / build (windows-latest)

W292 no newline at end of file
6 changes: 1 addition & 5 deletions src/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ def login_chk(rows):
return 0


def register_chk(username, password, confirmation, email):
def register_chk(username, password, email):
"""
Determines if the user is allowed to register
Used by register() in application.py
Expand All @@ -348,10 +348,6 @@ def register_chk(username, password, confirmation, email):
flash('Password must be at least 8 characters', 'danger')
return 400

if not confirmation or password != confirmation:
flash('Passwords do not match', 'danger')
return 400

if "+" in email:
flash('Plus character not allowed in email', 'danger')
return 400
Expand Down
2 changes: 2 additions & 0 deletions src/migrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
db.execute("DROP TABLE contest_problems")
db.execute("ALTER TABLE contest_problems_migration RENAME TO contest_problems")

db.execute("ALTER TABLE users ADD COLUMN 'registration_resend_attempts' integer NOT NULL DEFAULT(0)")

db.execute("COMMIT")

with open('settings.py', 'a') as f:
Expand Down
3 changes: 2 additions & 1 deletion src/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ CREATE TABLE 'users' (
'api' varchar(36) UNIQUE,
'total_points' integer NOT NULL DEFAULT(0),
'contests_completed' integer NOT NULL DEFAULT(0),
'problems_solved' integer NOT NULL DEFAULT(0)
'problems_solved' integer NOT NULL DEFAULT(0),
'registration_resend_attempts' integer NOT NULL DEFAULT(0)
);
CREATE TABLE 'user_perms' (
'user_id' integer NOT NULL,
Expand Down
20 changes: 20 additions & 0 deletions src/templates/admin/users.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ <h1>Users</h1>
{{ 'user-ban' if row['banned'] }}">
{{ row["username"] }}
</a>
{% if not row['verified'] %}
<span class="user-ban">(unverified)</span>
{% endif %}
</td>
<td>{{ row["email"] }}</td>
<td class="dt">{{ row["join_date"] }}</td>
Expand Down Expand Up @@ -70,6 +73,15 @@ <h1>Users</h1>
alt="Reset password"
title="Reset password">
</a>
{% if not row['verified'] %}
<a href="#">
<img src="/assets/images/check.svg"
onerror="this.src='/assets/images/check.png'"
class="svg-green icon users-verify"
alt="Verify user"
title="Verify user">
</a>
{% endif %}
</td>
</tr>
{% endfor %}
Expand Down Expand Up @@ -126,5 +138,13 @@ <h3>Search Users</h3>
createForm(msg, "/admin/makeadmin", this.parentElement.getAttribute("data-id"));
});
}

for (let node of document.getElementsByClassName("users-verify")) {
node.parentElement.addEventListener("click", function() {
var username = this.parentElement.getAttribute("data-username");
var msg = `Are you sure you want to manually verify ${username}? Click here to confirm.`;
createForm(msg, "/admin/verify", this.parentElement.getAttribute("data-id"));
});
}
</script>
{% endblock %}
55 changes: 31 additions & 24 deletions src/templates/auth/register.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,24 @@ <h1 class="text-center">Register</h1>
autocomplete="new-password"
required>
<label for="password">Password</label>
<div class="invalid-feedback">
Password must have at least 8 characters.
</div>
</div>
<div class="form-floating">
<input class="form-control mb-3"
<div class="form-floating mb-3">
<input class="form-control"
name="confirmation"
id="confirmation"
placeholder="Confirmation"
placeholder="Password Confirmation"
type="password"
autocomplete="new-password"
required>
<label for="confirmation">Confirmation</label>
<label for="confirmation">Password Confirmation</label>
<div class="invalid-feedback">
Passwords must match.
</div>
</div>
<button class="btn btn-primary" type="submit">Register</button>
<button class="btn btn-primary" type="submit" id="reg-submit">Register</button>
{% if USE_CAPTCHA %}
<script src="https://hcaptcha.com/1/api.js" async defer></script>
<hr>
Expand All @@ -68,27 +74,28 @@ <h1 class="text-center">Register</h1>
<script>
const newPassword = document.getElementById("password");
const confirmPassword = document.getElementById("confirmation");
document.querySelector("form").addEventListener("submit", event => {
if (newPassword.value != confirmPassword.value) {
makeAlert("Passwords do not match");
event.preventDefault();
}
const submitBtn = document.getElementById("reg-submit");
function checkPasswordConfirmation() {
let disable = false;
if (newPassword.value.length < 8) {
makeAlert("Password must be at least 8 characters");
event.preventDefault();
newPassword.classList.add("is-invalid");
disable = true;
} else {
newPassword.classList.remove("is-invalid");
}
if (confirmPassword.value != newPassword.value) {
confirmPassword.classList.add("is-invalid");
disable = true;
} else {
confirmPassword.classList.remove("is-invalid");
}
if (disable) {
submitBtn.setAttribute("disabled", "");
} else {
submitBtn.removeAttribute("disabled");
}
});

function makeAlert(message) {
var tmp = document.createElement('div');
tmp.innerHTML = `<div class="alert alert-danger alert-dismissible fade show" role="alert">
${message}
<button type="button"
class="btn-close"
data-bs-dismiss="alert"
aria-label="Close"></button>
</div>`;
document.querySelector("main").prepend(tmp.firstChild);
}
document.getElementById("confirmation").addEventListener("change", checkPasswordConfirmation);
document.getElementById("password").addEventListener("change", checkPasswordConfirmation);
</script>
{% endblock %}
68 changes: 68 additions & 0 deletions src/templates/auth/register_interstitial.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{% extends "layout.html" %}

{% block title %}Register{% endblock %}
{% block active %}Register{% endblock %}

{% block main %}
<h1 class="text-center">Register</h1>
<div class="alert alert-success show text-center" role="alert">
An account creation confirmation email has been sent to the email address
you provided. Be sure to check your spam folder!
</div>
<div class="text-center">
<p>Didn't receive the email?</p>
<button id="resend" class="btn btn-primary" disabled>Resend in <span id="count">1:00</span></button>
</div>
{% endblock %}
{% block script %}
<script>
const token = "{{ token }}";
let now = new Date();
let resends = 0;
let int = setInterval(() => {
let diff = 60 - Math.round((new Date() - now) / 1000);
if (diff <= 0) {
document.querySelector("#resend").removeAttribute("disabled");
document.querySelector("#resend").innerText = "Resend";
clearInterval(int);
} else {
document.querySelector("#count").innerText = Math.floor(diff / 60) + ":";
document.querySelector("#count").innerText += (diff % 60).toString().padStart(2, '0');
}
}, 500);
document.querySelector("#resend").addEventListener("click", e => {
e.target.setAttribute("disabled", "");
fetch("/auth/resend_registration_confirmation", {
method: "POST",
body: "csrf_token={{ csrf_token() }}&token="+token,
headers: {"Content-Type": "application/x-www-form-urlencoded"}
}).then(b => {
if (b.status == 400) {
b.text().then(x => alert(x));
} else if (b.status == 302) {
alert("You are already verified. Please log in.");
b.text().then(x => window.location = "/login");
}
});
if (++resends == 2) {
document.querySelector("#resend").classList.add("btn-danger");
document.querySelector("#resend").classList.remove("btn-primary");
document.querySelector("#resend").innerText = 'Please contact an admin if you still do not receive the confirmation email.';
return;
}
now = new Date();
document.querySelector("#resend").innerHTML = 'Resend in <span id="count">5:00</span>';
int = setInterval(() => {
let diff = 300 - Math.round((new Date() - now) / 1000);
if (diff <= 0) {
document.querySelector("#resend").removeAttribute("disabled");
document.querySelector("#resend").innerText = "Resend";
clearInterval(int);
} else {
document.querySelector("#count").innerText = Math.floor(diff / 60) + ":";
document.querySelector("#count").innerText += (diff % 60).toString().padStart(2, '0');
}
}, 500);
});
</script>
{% endblock %}
2 changes: 1 addition & 1 deletion src/templates/problem/archived_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ <h1>Archived Problems</h1>
{% endfor %}
</select>
</div>
<div class="mb-1"><a href="/problems">Back to unarchived problems</a></div>
<div class="mb-1" style="margin-top: -1rem;"><a href="/problems">Back to unarchived problems</a></div>
<div style="overflow-x: auto;">
<table class="table table-hover table-full-width">
<thead class="table-dark">
Expand Down
2 changes: 1 addition & 1 deletion src/templates/problem/problems.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ <h1>Problems</h1>
{% endfor %}
</select>
</div>
<div class="mb-1"><a href="/problems/archived">View archived problems</a></div>
<div class="mb-1" style="margin-top: -1rem;"><a href="/problems/archived">View archived problems</a></div>
<div style="overflow-x: auto;">
<table class="table table-hover table-full-width">
<thead class="table-dark">
Expand Down
2 changes: 1 addition & 1 deletion src/tests/test_2fa.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ def test_2fa(client, database):
database.execute(
("INSERT INTO 'users' VALUES(1, 'user', 'pbkdf2:sha256:150000$XoLKRd3I$"
"2dbdacb6a37de2168298e419c6c54e768d242aee475aadf1fa9e6c30aa02997f', 'e@ex.com', "
"datetime('now'), 0, 1, 0, NULL, 0, 0, 0)"))
"datetime('now'), 0, 1, 0, NULL, 0, 0, 0, 0)"))

result = client.get('/confirmlogin/badtoken')
assert b'Invalid' in result.data
Expand Down
11 changes: 9 additions & 2 deletions src/tests/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def test_admin(client, database):
database.execute(
("INSERT INTO 'users' VALUES(1, 'admin', 'pbkdf2:sha256:150000$XoLKRd3I$"
"2dbdacb6a37de2168298e419c6c54e768d242aee475aadf1fa9e6c30aa02997f', 'e1', "
"datetime('now'), 0, 1, 0, NULL, 0, 0, 0)"))
"datetime('now'), 0, 1, 0, NULL, 0, 0, 0, 0)"))
database.execute("INSERT INTO user_perms VALUES(1, ?)", USER_PERM["ADMIN"])
client.post('/login', data={'username': 'admin', 'password': 'CTFOJadmin'})
result = client.get('/admin/users')
Expand Down Expand Up @@ -37,7 +37,7 @@ def test_admin(client, database):
database.execute(
("INSERT INTO 'users' VALUES(2, 'normal_user', 'pbkdf2:sha256:150000$XoLKRd3I$"
"2dbdacb6a37de2168298e419c6c54e768d242aee475aadf1fa9e6c30aa02997f', 'e2', "
"datetime('now'), 0, 1, 0, NULL, 0, 0, 0)"))
"datetime('now'), 0, 1, 0, NULL, 0, 0, 0, 0)"))
result_user = client.post('/login', data={
'username': 'normal_user',
'password': 'CTFOJadmin'
Expand Down Expand Up @@ -88,6 +88,13 @@ def test_admin(client, database):
assert result.status_code == 200
assert b'was reset' in result.data

# Test manual user verification
result = client.post('/admin/verify', data={'user_id': 2}, follow_redirects=True)
assert b'already verified' in result.data
database.execute("UPDATE users SET verified=0 WHERE id=2")
result = client.post('/admin/verify', data={'user_id': 2}, follow_redirects=True)
assert b'is now verified' in result.data

Check warning on line 97 in src/tests/test_admin.py

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

W293 blank line contains whitespace

Check warning on line 97 in src/tests/test_admin.py

View workflow job for this annotation

GitHub Actions / build (windows-latest)

W293 blank line contains whitespace
# Test make admin feature
result = client.post('/admin/updateperms?user_id=2',
data={'perms': [USER_PERM["ADMIN"]]},
Expand Down
2 changes: 1 addition & 1 deletion src/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def test_api(client, database):
database.execute(
("INSERT INTO 'users' VALUES(1, 'user', 'pbkdf2:sha256:150000$XoLKRd3I$"
"2dbdacb6a37de2168298e419c6c54e768d242aee475aadf1fa9e6c30aa02997f', 'e', "
"datetime('now'), 0, 1, 0, NULL, 0, 0, 0)"))
"datetime('now'), 0, 1, 0, NULL, 0, 0, 0, 0)"))
client.post('/login', data={'username': 'user', 'password': 'CTFOJadmin'})
result = client.post('/api/getkey')
assert (database.execute("SELECT * FROM users")[0]["api"]
Expand Down
Loading

0 comments on commit 55682a7

Please sign in to comment.