Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Profile pages #103

Merged
merged 21 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions jams/decorators.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from flask_security import current_user
from flask import abort, redirect, request, current_app, session, url_for, jsonify
from flask import abort, redirect, request, current_app, url_for, jsonify
from flask_login.config import EXEMPT_METHODS
from functools import wraps
from jams.util import helper, attendee_auth
from jams.models import db, Page, Attendee, Endpoint, APIKeyEndpoint
from jams.models import db, Page, Attendee, Endpoint, EndpointRule, APIKeyEndpoint
from jams.configuration import ConfigType, get_config_value


Expand All @@ -15,6 +15,8 @@ def wrapper(*args, **kwargs):
user_role_ids = current_user.role_ids
endpoint = helper.extract_endpoint()
page = Page.query.filter_by(endpoint=endpoint).first_or_404()
if page.default:
return func(*args, **kwargs)
page_role_ids = [page_role.role_id for page_role in page.role_pages]

for role_id in page_role_ids:
Expand Down Expand Up @@ -43,6 +45,11 @@ def enforce_login(func, *args, **kwargs):
def rbac_api(func, *args, **kwargs):
Jdplays marked this conversation as resolved.
Show resolved Hide resolved
endpoint = helper.extract_endpoint()
endpoint_obj = Endpoint.query.filter_by(endpoint=endpoint).first()
# Are there any endpoint rules where this endpoint is default
default_endpoint_rule = EndpointRule.query.filter_by(endpoint_id=Endpoint.id, default=True).first()
if default_endpoint_rule and current_user.is_authenticated:
return func(*args, **kwargs)

user_role_ids = current_user.role_ids if current_user.is_authenticated else None
endpoint_rules = helper.get_endpoint_rules_for_roles(endpoint_obj.id, user_role_ids, not current_user.is_authenticated)
if not endpoint_rules:
Expand Down
4 changes: 3 additions & 1 deletion jams/default_config/endpoints.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,20 +85,22 @@ groups:
users:
description: Manages user information
read:
get_user: routes.api_v1.private.admin.get_user
get_users: routes.api_v1.private.admin.get_users
get_users_field: routes.api_v1.private.admin.get_users_field
get_user_field: routes.api_v1.private.admin.get_user_field
get_users_public_info: routes.api_v1.private.admin.get_users_public_info
get_user_streak: routes.api_v1.private.admin.get_user_streak
write:
edit_user: routes.api_v1.private.admin.edit_user
upload_user_avatar: routes.api_v1.private.admin.upload_user_avatar
activate_user: routes.api_v1.private.admin.activate_user
archive_user: routes.api_v1.private.admin.archive_user

volunteer:
description: Manages voluntter information
read:
get_user_attendance: routes.api_v1.private.volunteer.get_user_attendance
get_user_streak: routes.api_v1.private.volunteer.get_streak
write:
add_user_attendance: routes.api_v1.private.volunteer.add_user_attendance
edit_user_attendance: routes.api_v1.private.volunteer.edit_user_attendance
Expand Down
45 changes: 39 additions & 6 deletions jams/default_config/pages.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
pages:

# Child Pages
# -------------------------------- WIDGETS -------------------------------- #

jolt:
api_endpoints:
get_jolt_status:
send_jolt_test_print:

streak_widget:
api_endpoints:
get_event:
get_next_event_id:
get_user_streak:

# -------------------------------- CHILD PAGES -------------------------------- #
# Workshops
# Events
add_workshop:
Expand Down Expand Up @@ -113,6 +126,8 @@ pages:
api_endpoints:
get_private_access_logs:
archive_user:
get_roles_field:
allowed_field: public

task_scheduler_log:
endpoint: routes.frontend.private.monitoring.task_scheduler_log
Expand Down Expand Up @@ -149,7 +164,7 @@ pages:
api_endpoints:
get_users:
get_user_field:
allowed_fields: id, role_ids
allowed_fields: id, role_ids, badge_text, badge_icon
edit_user:
archive_user:
activate_user:
Expand Down Expand Up @@ -286,14 +301,14 @@ pages:
# Event
attendee_list:
endpoint: routes.frontend.private.event.attendee_list
child_pages:
jolt:
api_endpoints:
get_next_event_id:
get_events_field:
allowed_fields: id, name, date
get_event_field:
allowed_fields: id, name, date
get_jolt_status:
send_jolt_test_print:
get_event_attendees:
add_attendee:
edit_attendee:
Expand All @@ -302,19 +317,37 @@ pages:

fire_list:
endpoint: routes.frontend.private.event.fire_list
child_pages:
jolt:
api_endpoints:
get_next_event_id:
get_events_field:
allowed_fields: id, name, date
get_event_field:
allowed_fields: id, name, date
get_jolt_status:
send_jolt_test_print:
get_event_fire_list:
check_in_event_fire_list_item:
check_out_event_fire_list_item:


# -------------------------------- DEFAULT -------------------------------- #
dashboard:
endpoint: routes.frontend.private.general.dashboard
default: true
child_pages:
- streak_widget

profile:
endpoint: routes.frontend.private.general.user_profile
default: true
child_pages:
- streak_widget
api_endpoints:
get_roles_field:
allowed_fields: public
get_user:
upload_user_avatar:


# -------------------------------- PUBLIC -------------------------------- #
public_home:
Expand Down
20 changes: 17 additions & 3 deletions jams/endpoint_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,22 @@ def generate_page_endpoints_structure():
# Get the first key-value pair from the pages dictionary
for page_name, page_data in pages.items():
page_endpoint = page_data.get('endpoint', [])
page_is_default = page_data.get('default', [])
page_is_public = page_data.get('public', [])
if page_endpoint == []:
page_endpoint = None
if not page_is_default:
page_is_default = False
if not page_is_public:
page_is_public = False
page = Page.query.filter_by(name=page_name).first()
if not page:
page = Page(name=page_name, endpoint=page_endpoint, public=page_is_public)
page = Page(name=page_name, endpoint=page_endpoint, default=page_is_default, public=page_is_public)
db.session.add(page)
else:
page.endpoint = page_endpoint
page.default = page_is_default
page.public = page_is_public

db.session.commit()

Expand All @@ -101,6 +108,13 @@ def generate_page_endpoints_structure():
child_page = Page.query.filter_by(name=page_name).first()
if child_page:
child_page.parent_id = page.id

# Set all the child pages endpoint rules to be the same default as the parent page
page_endpoint_rules = PageEndpointRule.query.filter_by(page_id=child_page.id).all()
endpoint_rule_ids = [p_er.endpoint_rule_id for p_er in page_endpoint_rules]
endpoint_rules = EndpointRule.query.filter(EndpointRule.id.in_(endpoint_rule_ids)).all()
for er in endpoint_rules:
er.default = page_is_default
db.session.commit()


Expand All @@ -115,11 +129,11 @@ def generate_page_endpoints_structure():
print(f'Endpoint "{endpoint_name}" does not exist. Skipping...')
continue

endpoint_rule = EndpointRule.query.filter_by(endpoint_id=endpoint.id, allowed_fields=allowed_fields, public=page_is_public).first()
endpoint_rule = EndpointRule.query.filter_by(endpoint_id=endpoint.id, allowed_fields=allowed_fields, default=page_is_default, public=page_is_public).first()
if not endpoint_rule:
endpoint_rule = helper.get_endpoint_rule_for_page(endpoint_id=endpoint.id, page_id=page.id, public=page_is_public)
if not endpoint_rule:
endpoint_rule = EndpointRule(endpoint_id=endpoint.id, allowed_fields=allowed_fields, public=page_is_public)
endpoint_rule = EndpointRule(endpoint_id=endpoint.id, allowed_fields=allowed_fields, default=page_is_default, public=page_is_public)
db.session.add(endpoint_rule)
else:
endpoint_rule.allowed_fields = allowed_fields
Expand Down
1 change: 1 addition & 0 deletions jams/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def create_bucket(name, versioning=True):
return None
# Create required buckets
workshop_bucket = create_bucket('jams-workshops', True)
user_data_bucket = create_bucket('user-data', True)

def clear_table(model):
if hasattr(db, 'engine'):
Expand Down
51 changes: 39 additions & 12 deletions jams/models/auth.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from . import db
from sqlalchemy import Column, String, Integer, Boolean, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.orm import relationship, Mapped, mapped_column
from flask_security import UserMixin, RoleMixin
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
import uuid

# Define the UserRoles association table
Expand All @@ -24,6 +25,15 @@ class Role(db.Model, RoleMixin):
@property
def page_ids(self):
return [role_page.page_id for role_page in self.role_pages]

@property
def public(self):
return {
'id': self.id,
'name': self.name,
'description': self.description,
'display_colour': self.display_colour
}

# Requires name to be passed
def __init__(self, name, description=None, display_colour='#828181', priority=10, hidden=False, default=False):
Expand Down Expand Up @@ -56,12 +66,16 @@ class User(UserMixin, db.Model):
first_name = Column(String(50), nullable=True)
last_name = Column(String(50), nullable=True)
dob = Column(DateTime(), nullable=True)
bio = Column(String(255), nullable=True)
bio = Column(String(1000), nullable=True)
roles = relationship('Role', secondary='user_roles', backref='users')
fs_uniquifier = Column(String(255), unique=True, nullable=False, default=lambda: str(uuid.uuid4()))
open_id_sub = Column(String(255), unique=True, nullable=True) # OpenID 'sub' claim
user_induction = Column(Boolean(), nullable=False, default=False, server_default='false')
avatar_url = Column(String(255), nullable=True)
avatar_file_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey('file.id'), nullable=True)

# Extra
badge_text = Column(String(100), nullable=True)
badge_icon = Column(String(100), nullable=True)

# Tracking
last_login_at = Column(DateTime(), nullable=True)
Expand All @@ -70,6 +84,8 @@ class User(UserMixin, db.Model):
current_login_ip = Column(String(50), nullable=True)
login_count = Column(Integer, nullable=True)

avatar_file = relationship('File', foreign_keys=[avatar_file_id])

@property
def full_name(self):
if self.first_name is None:
Expand All @@ -90,11 +106,13 @@ def display_name(self):

@property
def main_role(self):
if self.badge_text:
return self.badge_text
role = Role.query.filter(Role.id.in_(self.role_ids)).order_by(Role.priority.asc()).first()
return role.name if role else 'No Role'

# Requires email, username, password to be passed
def __init__(self, email, username, password, active=False, first_name=None, last_name=None, dob=None, bio=None, roles=None, role_ids:list[int]=None, fs_uniquifier=None, last_login_at=None, current_login_at=None, last_login_ip=None, current_login_ip=None, login_count=0, open_id_sub=None, user_induction=False, avatar_url=None):
def __init__(self, email, username, password, active=False, first_name=None, last_name=None, dob=None, bio=None, roles=None, role_ids:list[int]=None, fs_uniquifier=None, last_login_at=None, current_login_at=None, last_login_ip=None, current_login_ip=None, login_count=0, open_id_sub=None, user_induction=False, avatar_file_id=None):
self.email = email
self.username = username
self.first_name = first_name
Expand All @@ -115,7 +133,7 @@ def __init__(self, email, username, password, active=False, first_name=None, las
self.login_count = login_count
self.open_id_sub = open_id_sub
self.user_induction = user_induction
self.avatar_url = avatar_url
self.avatar_file_id = avatar_file_id

def activate(self):
self.active = True
Expand Down Expand Up @@ -196,20 +214,23 @@ def to_dict(self):
'bio': self.bio,
'active': self.active,
'user_induction': self.user_induction,
'avatar_url': self.avatar_url
'avatar_file_id': self.avatar_file_id,
'badge_text': self.badge_text,
'badge_icon': self.badge_icon
}

def public_info_dict(self):
return {
'id': self.id,
'username': self.username,
'email': self.email,
'first_name': self.first_name,
'last_name': self.last_name,
'full_name': self.full_name,
'display_name': self.display_name,
'role_ids': self.role_ids,
'bio': self.bio,
'avatar_url': self.avatar_url
'avatar_file_id': self.avatar_file_id,
'badge_text': self.badge_text,
'badge_icon': self.badge_icon
}

## Role based Auth to pages
Expand All @@ -218,16 +239,18 @@ class Page(db.Model):

id = Column(Integer, primary_key=True)
name = Column(String(100), unique=True, nullable=False)
endpoint = Column(String(255), unique=True, nullable=False)
endpoint = Column(String(255), unique=True, nullable=True)
parent_id = Column(Integer, ForeignKey('page.id'), nullable=True)
default = Column(Boolean, nullable=False, default=False, server_default='false')
public = Column(Boolean, nullable=False, default=False)

parent = relationship('Page', remote_side=[id], backref='children')

def __init__(self, name, endpoint, parent_id=None, public=False):
def __init__(self, name, endpoint=None, parent_id=None, default=False, public=False):
self.name = name
self.endpoint = endpoint
self.parent_id = parent_id
self.default = default
self.public = public

def to_dict(self):
Expand All @@ -236,6 +259,7 @@ def to_dict(self):
'name': self.name,
'endpoint': self.endpoint,
'parent_id': self.parent,
'default': self.default,
'public': self.public
}

Expand All @@ -247,20 +271,23 @@ class EndpointRule(db.Model):
id = Column(Integer, primary_key=True)
endpoint_id = Column(Integer, ForeignKey('endpoint.id'), nullable=False)
allowed_fields = Column(String(255), nullable=True) # This is a comma seperated list of allowed fields for the endpoint
default = Column(Boolean, nullable=False, default=False, server_default='false')
public = Column(Boolean, nullable=False, default=False)

endpoint = relationship('Endpoint', backref='endpoint_rules')

def __init__(self, endpoint_id, allowed_fields=None, public=False):
def __init__(self, endpoint_id, allowed_fields=None, default=False, public=False):
self.endpoint_id = endpoint_id
self.allowed_fields = allowed_fields
self.default = default
self.public = public

def to_dict(self):
return {
'id': self.id,
'endpoint_id': self.endpoint_id,
'allowed_fields': self.allowed_fields if self.allowed_fields is not None else '',
'default': self.default,
'public': self.public
}

Expand Down
Loading