Compare commits

..

1 commit

Author SHA1 Message Date
Cara Salter
87adc200b3
Initial work on local passwords.
- Create password change modal on user dashboard
- Split login into two flows -- WPI and local

Need to define password strength requirements and create local login
page, as well as allow for setting an alternative contact email.
2024-05-04 11:21:32 +10:00
13 changed files with 99 additions and 158 deletions

View file

@ -8,7 +8,7 @@ from io import BytesIO
from PIL import Image from PIL import Image
import base64 import base64
from acmsite.models import EventCheckin, Link, Officer, User, Event from acmsite.models import Link, Officer, User, Event
from acmsite import models from acmsite import models
from .forms import EventForm, LinkForm, OfficerForm from .forms import EventForm, LinkForm, OfficerForm
@ -155,28 +155,6 @@ def update_create_event(id):
return redirect(url_for("admin.events")) return redirect(url_for("admin.events"))
@bp.route("/event/<string:id>/checkins")
@login_required
def event_checkins(id):
if not current_user.is_admin:
flash("Unauthorized")
return redirect(url_for("dashboard.home"))
event = db.session.execute(db.select(Event).where(Event.id == id)).scalar_one_or_none()
if event is None:
flash("Invalid event")
return redirect(url_for("admin.events"))
checkins = db.session.execute(db.select(EventCheckin).where(EventCheckin.event ==
id).join(User)).scalars()
processed_checkins = []
for c in checkins:
user = db.session.execute(db.select(User).where(User.id == c.user)).scalar_one_or_none()
processed_checkins.append({"name": f"{user.first_name} {user.last_name}", "email": user.email})
return render_template("admin/checkins.html", checkins=processed_checkins,e=event)
@bp.route("/links") @bp.route("/links")
@login_required @login_required
def links(): def links():

View file

@ -17,8 +17,7 @@ class LinkForm(FlaskForm):
class OfficerForm(FlaskForm): class OfficerForm(FlaskForm):
position = SelectField("Position", choices=["President", "Vice President", position = SelectField("Position", choices=["President", "Vice President",
"Treasurer", "Secretary", "PR Chair", "Treasurer", "Secretary", "PR Chair", "Hackathon Manager 1",
"Event Coordinator", "Hackathon Manager 1",
"Hackathon Manager 2", "System Administrator"], "Hackathon Manager 2", "System Administrator"],
validators=[DataRequired()]) validators=[DataRequired()])
term_start = DateField("Term Start", validators=[DataRequired()]) term_start = DateField("Term Start", validators=[DataRequired()])

View file

@ -10,9 +10,12 @@ bp = Blueprint('auth', __name__, url_prefix='/auth')
from acmsite import oauth from acmsite import oauth
@bp.route("/login")
@bp.route('/login')
def login(): def login():
return render_template('login.html')
@bp.route('/oauth')
def oauth_redirect():
return oauth.azure.authorize_redirect(url_for('auth.oauth2_callback', return oauth.azure.authorize_redirect(url_for('auth.oauth2_callback',
_external=True)) _external=True))

View file

@ -1,10 +1,8 @@
from werkzeug.security import generate_password_hash, check_password_hash
from flask import Blueprint, render_template, request, flash, redirect, url_for
from flask_login import current_user, login_required
from datetime import datetime from acmsite.dashboard.forms import PasswordForm
from flask import Blueprint, current_app, flash, redirect, render_template, url_for
from flask_login import login_required, current_user
from ulid import ulid
from acmsite.models import Event, EventCheckin
from acmsite import db from acmsite import db
@ -13,35 +11,29 @@ bp = Blueprint('dashboard', __name__, url_prefix='/dashboard')
@bp.route("/") @bp.route("/")
@login_required @login_required
def home(): def home():
now = datetime.now() form = PasswordForm()
events = db.session.execute(db.select(Event).where(Event.start_time < now, return render_template('dashboard.html', form=form)
Event.end_time >
now)).scalars()
return render_template('dashboard.html', events=events)
@bp.route("/checkin/<string:event_id>") @bp.route("/change_password", methods=["POST"])
@login_required @login_required
def checkin(event_id): def change_password():
# actually first check if the event even exists form = PasswordForm(request.form)
event = db.get_or_404(Event, event_id)
# first check if this user has already checked in
checkins = db.session.execute(db.select(EventCheckin).where(EventCheckin.user ==
current_user.id,
EventCheckin.event
==event_id)).scalar_one_or_none()
current_app.logger.debug(checkins) if form.validate_on_submit():
if checkins is None: current_password = request.form.get("current_password")
# There's not a checkin already, let's create one! new_password = request.form.get("new_password")
check = EventCheckin( password_confirm = request.form.get("password_confirm")
id = ulid(),
user = current_user.id, if new_password == password_confirm:
event = event_id) if current_password == '' and current_user.password == '':
db.session.add(check) current_user.password = generate_password_hash(new_password)
flash("Password set successfully.")
elif check_password_hash(current_user.password, current_password):
current_user.password = generate_password_hash(new_password)
flash("Password updated successfully.")
else:
flash("Incorrect password.")
else:
flash("Passwords do not match!")
db.session.commit() db.session.commit()
flash(f"Checked in to {event.name} successfully")
return redirect(url_for("dashboard.home"))
else:
flash(f"You've already checked in to {event.name}")
return redirect(url_for("dashboard.home")) return redirect(url_for("dashboard.home"))

View file

@ -0,0 +1,9 @@
from flask_wtf import FlaskForm
from wtforms.fields import PasswordField
from wtforms.validators import DataRequired
class PasswordForm(FlaskForm):
current_password = PasswordField('Current Password')
new_password = PasswordField('New Password', validators=[DataRequired()])
password_confirm = PasswordField('Confirm New Password',
validators=[DataRequired()])

View file

@ -80,12 +80,6 @@ class Event(db.Model):
"end_time": self.end_time.isoformat(), "end_time": self.end_time.isoformat(),
} }
class EventCheckin(db.Model):
__tablename__ = "acm_checkins"
id = Column(String, primary_key=True)
user = Column(String, ForeignKey("acm_users.id"), nullable=False)
event = Column(String, ForeignKey("acm_events.id"), nullable=False)
class Link(db.Model): class Link(db.Model):
__tablename__ = "acm_links" __tablename__ = "acm_links"
id = Column(String, primary_key=True) id = Column(String, primary_key=True)

View file

@ -74,12 +74,12 @@ very friendly. You can find a list of our upcoming events and meetings <a href="
</div> </div>
<div class="col"> <div class="col">
<div class="card" id="events-coordinator"> <div class="card" id="events-coordinator">
<img class="card-img-top" id="Event Coordinator-img" src="{{ url_for('static', <img class="card-img-top" id="Events Coordinator-img" src="{{ url_for('static',
filename='img/officers/placeholder.png') filename='img/officers/placeholder.png')
}}" alt="Event Coordinator"> }}" alt="Events Coordinator">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Events Coordinator</h5> <h5 class="card-title">Events Coordinator</h5>
<p class="card-text" id="Event Coordinator-name">Unavailable</p> <p class="card-text" id="Events Coordinator-name">Unavailable</p>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,35 +0,0 @@
{% extends "admin/admin-layout.html" %}
{% import "bootstrap5/form.html" as wtf %}
{% block app_content %}
<h1>Checkins for `{{ e.name }}`</h1>
{% for c in checkins %}
{{ c.__dict__ }}
{% endfor %}
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
{% for c in checkins %}
<tr>
<td>{{ c.name }}</td>
<td>{{ c.email }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<h3>Copy-pastable list for CampusGroups</h3>
{% for c in checkins %}
{{ c.email }},
{% endfor %}
{% endblock %}

View file

@ -40,11 +40,6 @@
<li class="dropdown-item"> <li class="dropdown-item">
<a href="#deleteModal" <a href="#deleteModal"
data-bs-toggle="modal" data-id="{{ e.id }}">Delete Event</a> data-bs-toggle="modal" data-id="{{ e.id }}">Delete Event</a>
</li>
<li class="dropdown-item">
<a href="{{ url_for('admin.event_checkins',
id=e.id) }}">Event Checkins</a>
</li>
</ul> </ul>
</div> </div>
</td> </td>

View file

@ -4,19 +4,6 @@
<h1>Welcome back, {{ current_user.first_name }}!</h1> <h1>Welcome back, {{ current_user.first_name }}!</h1>
{% if events %}
<p>The following events are available for check-in:</p>
{% for e in events %}
<h5>{{ e.name }} <a class="btn btn-primary" href="{{ url_for('dashboard.checkin', event_id=e.id)
}}">Check in</a></h5>
{% endfor %}
{% else %}
<p>There are no events available for check-in.</p>
{% endif %}
<hr/>
<p>For a list of upcoming events, take a look at our <a href="{{ <p>For a list of upcoming events, take a look at our <a href="{{
url_for('main.events') url_for('main.events')
}}">events }}">events
@ -26,4 +13,46 @@
unless you're an unless you're an
officer!</p> officer!</p>
<button type="button" class="btn btn-secondary" data-bs-toggle="modal"
data-bs-target="#passwordModal">Change or Set
Local Password</button>
<!-- Modals -->
<div class="modal" id="passwordModal" tabindex="-1" aria-labelledby="passwordModalLabel"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="passwordModalLabel">Change Password</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form class="form" id="edit-form" action="/dashboard/change_password" role="form" method="post">
<div class="modal-body">
{{ form.csrf_token }}
<div class="form-floating mb-3 required">
{{ form.current_password(class="form-control") }}
{{ form.current_password.label() }}
</div>
<div class="row">
<div class="col">
<div class="form-floating mb-3 required">
{{ form.new_password(class="form-control") }}
{{ form.new_password.label() }}
</div>
</div>
<div class="col">
<div class="form-floating mb-3 required">
{{ form.password_confirm(class="form-control") }}
{{ form.password_confirm.label() }}
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary" id="edit-save">Save changes</button>
</div>
</form>
</div>
</div>
</div>
{% endblock app_content %} {% endblock app_content %}

View file

@ -40,7 +40,7 @@
{% endif %} {% endif %}
{{ render_nav_item('auth.logout', 'Logout') }} {{ render_nav_item('auth.logout', 'Logout') }}
{% else %} {% else %}
{{ render_nav_item('auth.login', 'Login with WPI') }} {{ render_nav_item('auth.login', 'Login') }}
{% endif %} {% endif %}
</ul> </ul>
</div> </div>

View file

@ -0,0 +1,12 @@
{% extends "layout.html" %}
{% block app_content %}
<h1>Login Methods</h1>
<div>
<a class="btn btn-primary" href="{{ url_for('auth.oauth_redirect') }}">Login with WPI</a>
</div>
<div class="mt-1 mb-3">
<a class="btn btn-secondary" href="">Login with Local Account</a>
</div>
{% endblock app_content %}

View file

@ -1,35 +0,0 @@
"""add event checkins
Revision ID: 53a76e988b5a
Revises: 300f24071c14
Create Date: 2024-08-25 15:18:22.451548
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '53a76e988b5a'
down_revision = '300f24071c14'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('acm_checkins',
sa.Column('id', sa.String(), nullable=False),
sa.Column('user', sa.String(), nullable=False),
sa.Column('event', sa.String(), nullable=False),
sa.ForeignKeyConstraint(['event'], ['acm_events.id'], ),
sa.ForeignKeyConstraint(['user'], ['acm_users.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('acm_checkins')
# ### end Alembic commands ###