Skip to content

Final admin dashboard changes #70

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

Merged
merged 32 commits into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ef9eb82
take in new officer terms in a single transaction
EarthenSky Aug 20, 2024
73d8c4e
add daily cron file for updating officer permissions
EarthenSky Aug 22, 2024
29083cb
update daily
EarthenSky Aug 22, 2024
5741b66
fix legal name bug
EarthenSky Aug 22, 2024
407993c
don't touch autoflush
EarthenSky Aug 22, 2024
0d0d867
Merge branch 'main' into dev-final-admin-dashboard-changes
EarthenSky Aug 22, 2024
5e645eb
add google module
EarthenSky Aug 23, 2024
e70a981
small reformatting of github module
EarthenSky Aug 23, 2024
5ec3b94
update github module & add email function
EarthenSky Aug 23, 2024
f61167f
add /auth/info endpoint
EarthenSky Aug 23, 2024
3d29dca
treat error correctly
EarthenSky Aug 23, 2024
19e3a9f
add temp file for holding server secrets; todo: look into better meth…
EarthenSky Aug 23, 2024
12beaee
Merge branch 'main' into dev-final-admin-dashboard-changes
EarthenSky Aug 24, 2024
1c6ae82
add /officers/my_info endpoint
EarthenSky Aug 24, 2024
428d8bc
update database.py
EarthenSky Aug 24, 2024
a588327
fix bug adding serializable dict function
EarthenSky Aug 24, 2024
71950a1
reorder columns
EarthenSky Aug 24, 2024
3f59f60
remove is_valid from database & upload subset of data through /office…
EarthenSky Aug 25, 2024
eb031c4
add utils, clean discord module, and start update_info endpoint valid…
EarthenSky Aug 25, 2024
ed02b60
clean discord api & add checking for it in /update_info
EarthenSky Aug 30, 2024
6823b73
add function for getting current github permissions
EarthenSky Aug 30, 2024
cb0a229
add function to update github permissions
EarthenSky Aug 30, 2024
5537b19
add invite endpoint & simplify implementation
EarthenSky Aug 30, 2024
b9e47de
Add Google Drive API (#81)
EarthenSky Sep 1, 2024
711b90e
fix bugs, add support for using discord API in user login page
EarthenSky Sep 1, 2024
4831a60
Merge remote-tracking branch 'origin/dev-permission-update' into dev-…
EarthenSky Sep 1, 2024
14ea691
get more discord users, in case of nickname collisions
EarthenSky Sep 1, 2024
916d0d4
tests, formatting, use github API to validate username
EarthenSky Sep 1, 2024
f719526
implement & refactor officer term
EarthenSky Sep 2, 2024
78a1229
get info about any user by computing_id
EarthenSky Sep 2, 2024
35a9e1a
finalize officer term api endpoints
EarthenSky Sep 2, 2024
453ba38
appease linter
EarthenSky Sep 2, 2024
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# main
src/run/
logs/

# google drive api
google_key.json

# Python - Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
6 changes: 6 additions & 0 deletions config/cron.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# This script adds commands to the current crontab

# run the daily script at 1am every morning
# TODO: make sure timezone is PST
crontab -l | { cat; echo "0 1 * * * /home/csss-site/csss-site-backend/src/cron/daily.py"; } | crontab -

10 changes: 10 additions & 0 deletions config/export_secrets.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# TODO: only fill out this file in production
export GMAIL_USERNAME = "todo"
export GMAIL_PASSWORD = "todo"
export GOOGLE_DRIVE_TOKEN = "todo"
export GITHUB_TOKEN = "todo"
export DISCORD_TOKEN = "todo"

export CSSS_GUILD_ID = "todo"
export SFU_API_TOKEN = "todo"

1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ uvicorn[standard]==0.27.1
sqlalchemy==2.0.27
asyncpg==0.29.0
alembic==1.13.1
google-api-python-client==2.143.0

# minor
pyOpenSSL==24.0.0 # for generating cryptographically secure random numbers
Expand Down
25 changes: 25 additions & 0 deletions src/admin/email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import os
import smtplib

# TODO: set this up
GMAIL_PASSWORD = os.environ.get("GMAIL_PASSWORD")
GMAIL_ADDRESS = "[email protected]"
GMAIL_USERNAME = ""

# TODO: look into sending emails from an sfu maillist (this might be painful)
def send_email(
recipient_address: str,
subject: str,
content: str,
):
mail = smtplib.SMTP("smtp.gmail.com", 587)
mail.ehlo()
mail.starttls()
mail.login(GMAIL_ADDRESS, GMAIL_PASSWORD)

header = f"To: {recipient_address}\nFrom: {GMAIL_USERNAME}\nSubject: {subject}"
content = header + content

mail.sendmail(GMAIL_ADDRESS, recipient_address, content)
mail.quit()

2 changes: 0 additions & 2 deletions src/alembic/versions/166f3772fce7_auth_officer_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ def upgrade() -> None:
"officer_term",
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("computing_id", sa.String(length=32), sa.ForeignKey("site_user.computing_id"), nullable=False),
sa.Column("is_filled_in", sa.Boolean(), nullable=False),
sa.Column("position", sa.String(length=128), nullable=False),
sa.Column("start_date", sa.DateTime(), nullable=False),
sa.Column("end_date", sa.DateTime(), nullable=True),
Expand All @@ -53,7 +52,6 @@ def upgrade() -> None:
)
op.create_table(
"officer_info",
sa.Column("is_filled_in", sa.Boolean(), nullable=False),
sa.Column("legal_name", sa.String(length=128), nullable=False),
sa.Column("discord_id", sa.String(length=18), nullable=True),
sa.Column("discord_name", sa.String(length=32), nullable=True),
Expand Down
30 changes: 28 additions & 2 deletions src/auth/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from sqlalchemy.ext.asyncio import AsyncSession


async def create_user_session(db_session: AsyncSession, session_id: str, computing_id: str) -> None:
async def create_user_session(db_session: AsyncSession, session_id: str, computing_id: str):
"""
Updates the past user session if one exists, so no duplicate sessions can ever occur.

Expand Down Expand Up @@ -84,9 +84,35 @@ async def get_computing_id(db_session: AsyncSession, session_id: str) -> str | N


# remove all out of date user sessions
async def task_clean_expired_user_sessions(db_session: AsyncSession) -> None:
async def task_clean_expired_user_sessions(db_session: AsyncSession):
one_day_ago = datetime.now() - timedelta(days=0.5)

query = sqlalchemy.delete(UserSession).where(UserSession.issue_time < one_day_ago)
await db_session.execute(query)
await db_session.commit()


async def user_info(db_session: AsyncSession, session_id: str) -> None | dict:
query = (
sqlalchemy
.select(UserSession)
.where(UserSession.session_id == session_id)
)
user_session = await db_session.scalar(query)
if user_session is None:
return None

query = (
sqlalchemy
.select(SiteUser)
.where(SiteUser.computing_id == user_session.computing_id)
)
user = await db_session.scalar(query)
if user is None:
return None

return {
"computing_id": user_session.computing_id,
"first_logged_in": user.first_logged_in.isoformat(),
"last_logged_in": user.last_logged_in.isoformat()
}
23 changes: 23 additions & 0 deletions src/auth/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ async def login_user(
return response


# TODO: deprecate this when possible
@router.get(
"/check",
description="Check if the current user is logged in based on session_id from cookies",
Expand All @@ -93,6 +94,28 @@ async def check_authentication(
return JSONResponse(response_dict)


@router.get(
"/info",
description="Get info about the current user. Only accessible by that user",
)
async def get_info(
request: Request,
db_session: database.DBSession,
):
"""
Currently this endpoint only returns the info stored in tables in the auth module.
"""
session_id = request.cookies.get("session_id", None)
if session_id is None:
raise HTTPException(status_code=401, detail="User must be authenticated to get their info")

user_info = await crud.user_info(db_session, session_id)
if user_info is None:
raise HTTPException(status_code=401, detail="Could not find user with session_id, please log in")

return JSONResponse(user_info)


@router.post(
"/logout",
description="Logs out the current user by invalidating the session_id cookie",
Expand Down
7 changes: 5 additions & 2 deletions src/constants.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import os

root_ip_address = "http://localhost:8080" if os.environ.get("LOCAL") == "true" else "https://api.sfucsss.org"
guild_id = "1260652618875797504" if os.environ.get("LOCAL") == "true" else "228761314644852736"
github_org_name = "CSSS-Test-Organization" if os.environ.get("LOCAL") == "true" else "CSSS"
GITHUB_ORG_NAME = "CSSS-Test-Organization" if os.environ.get("LOCAL") == "true" else "CSSS"

W3_GUILD_ID = "1260652618875797504"
CSSS_GUILD_ID = "228761314644852736"
ACTIVE_GUILD_ID = W3_GUILD_ID if os.environ.get("LOCAL") == "true" else CSSS_GUILD_ID

SESSION_ID_LEN = 512
# technically a max of 8 digits https://www.sfu.ca/computing/about/support/tips/sfu-userid.html
Expand Down
69 changes: 69 additions & 0 deletions src/cron/daily.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""This module gets called by cron every day"""

import asyncio
import logging

import github
import google_api
import utils
from database import _db_session
from officers.crud import all_officer_terms, get_user_by_username, officer_terms

_logger = logging.getLogger(__name__)

async def update_google_permissions(db_session):
# TODO: implement this function
# google_permissions = google_api.all_permissions()
# one_year_ago = datetime.today() - timedelta(days=365)

# TODO: for performance, only include officers with recent end-date (1 yr)
# but measure performance first
for term in await all_officer_terms(db_session):
if utils.is_active(term):
# TODO: if google drive permission is not active, update them
pass
else:
# TODO: if google drive permissions are active, remove them
pass

_logger.info("updated google permissions")

async def update_github_permissions(db_session):
github_permissions, team_id_map = github.all_permissions()

for term in await all_officer_terms(db_session):
new_teams = (
# move all active officers to their respective teams
github.officer_teams(term.position)
if utils.is_active(term)
# move all inactive officers to the past_officers github organization
else ["past_officers"]
)
if term.username not in github_permissions:
user = get_user_by_username(term.username)
github.invite_user(
user.id,
[team_id_map[team] for team in new_teams],
)
else:
github.set_user_teams(
term.username,
github_permissions[term.username].teams,
new_teams
)

_logger.info("updated github permissions")

async def update_permissions():
db_session = _db_session()

update_google_permissions(db_session)
db_session.commit()
update_github_permissions(db_session)
db_session.commit()

_logger.info("all permissions updated")

if __name__ == "__main__":
asyncio.run(update_permissions())

10 changes: 6 additions & 4 deletions src/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,23 +84,25 @@ async def session(self) -> AsyncIterator[AsyncSession]:


if os.environ.get("DB_PORT") is not None:
# using a remote (or docker) database
db_port = os.environ.get("DB_PORT")
SQLALCHEMY_DATABASE_URL = f"postgresql+asyncpg://localhost:{db_port}/main"
SQLALCHEMY_TEST_DATABASE_URL = f"postgresql+asyncpg://localhost:{db_port}/test"
else:
SQLALCHEMY_DATABASE_URL = "postgresql+asyncpg:///main"
SQLALCHEMY_TEST_DATABASE_URL = "postgresql+asyncpg:///test"


# also TODO: make this nicer, using a class to hold state...
# and use this in load_test_db for the test db as well?
def setup_database():
global sessionmanager

# TODO: where is sys.stdout piped to? I want all these to go to a specific logs folder
if os.environ.get("LOCAL"):
sessionmanager = DatabaseSessionManager(SQLALCHEMY_TEST_DATABASE_URL, {"echo": True})
else:
sessionmanager = DatabaseSessionManager(SQLALCHEMY_DATABASE_URL, {"echo": True})
sessionmanager = DatabaseSessionManager(
SQLALCHEMY_TEST_DATABASE_URL if os.environ.get("LOCAL") else SQLALCHEMY_DATABASE_URL,
{ "echo": True },
)

@contextlib.asynccontextmanager
async def lifespan(app: FastAPI):
Expand Down
Loading
Loading