Skip to content

Commit

Permalink
Merge branch 'main' into features/662-freebusy
Browse files Browse the repository at this point in the history
  • Loading branch information
devmount authored Jan 10, 2025
2 parents 7282df2 + 80dfb37 commit 8825de0
Showing 123 changed files with 9,560 additions and 1,863 deletions.
36 changes: 36 additions & 0 deletions .github/workflows/deploy-production.yml
Original file line number Diff line number Diff line change
@@ -233,3 +233,39 @@ jobs:
terragrunt validate
terragrunt plan -var "image=${{ steps.get_ecr_tag.outputs.ecr_tag }}" -out tfplan
terragrunt apply tfplan
prod-sanity-browserstack:
name: prod-sanity-browserstack
needs: [deploy-prod-backend, deploy-prod-frontend]
runs-on: ubuntu-latest
environment: production
env:
APPT_PROD_LOGIN_EMAIL: ${{ secrets.E2E_APPT_PROD_LOGIN_EMAIL }}
APPT_PROD_LOGIN_PWORD: ${{ secrets.E2E_APPT_PROD_LOGIN_PASSWORD }}
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache-dependency-path: 'test/e2e/package-lock.json'

- name: Install dependencies
run: |
cd ./test/e2e
npm install
- name: BrowserStack Env Setup
uses: browserstack/github-actions/setup-env@master
with:
username: ${{ secrets.BROWSERSTACK_USERNAME }}
access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
project-name: 'Thunderbird Appointment'
build-name: 'Production Deployment Tests: BUILD_INFO'

- name: Run Playwright Tests on Browserstack
run: |
cd ./test/e2e
cp .env.example .env
npm run prod-sanity-test-browserstack-gha
51 changes: 51 additions & 0 deletions .github/workflows/nightly-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: nightly-tests

concurrency:
group: nightly-tests
cancel-in-progress: true

on:
schedule:
# run every day at 1am UTC (8PM EST)
- cron: '0 1 * * *'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

permissions:
contents: read # This is required for actions/checkout

jobs:
prod-sanity-browserstack:
name: prod-sanity-browserstack
runs-on: ubuntu-latest
environment: production
env:
APPT_PROD_LOGIN_EMAIL: ${{ secrets.E2E_APPT_PROD_LOGIN_EMAIL }}
APPT_PROD_LOGIN_PWORD: ${{ secrets.E2E_APPT_PROD_LOGIN_PASSWORD }}
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache-dependency-path: 'test/e2e/package-lock.json'

- name: Install dependencies
run: |
cd ./test/e2e
npm install
- name: BrowserStack Env Setup
uses: browserstack/github-actions/setup-env@master
with:
username: ${{ secrets.BROWSERSTACK_USERNAME }}
access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
project-name: 'Thunderbird Appointment'
build-name: 'Nightly Tests: BUILD_INFO'

- name: Run Playwright Tests on Browserstack
run: |
cd ./test/e2e
cp .env.example .env
npm run prod-sanity-test-browserstack-gha
2 changes: 2 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
@@ -9,6 +9,8 @@ on:
branches:
- '**'
- '!main'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

permissions:
id-token: write # This is required for requesting the JWT
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Thunderbird Appointment

**Note: Thunderbird Appointment is in a beta state, so be prepared to encounter bugs**
> [!IMPORTANT]
> Thunderbird Appointment is in a beta state, so be prepared to encounter bugs
Invite others to grab times on your calendar. Choose a date. Make appointments as easy as it gets.

@@ -40,6 +41,7 @@ Check out the project's respective readmes:

* [Backend Readme](backend/README.md)
* [Frontend Readme](frontend/README.md)
* [E2E Tests Readme](test/e2e/README.md)

### Localization

6 changes: 2 additions & 4 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -86,8 +86,6 @@ SIGNED_SECRET=
SENTRY_DSN=

# -- TESTING --
AUTH0_TEST_USER=
AUTH0_TEST_PASS=
CALDAV_TEST_PRINCIPAL_URL=
CALDAV_TEST_CALENDAR_URL=
CALDAV_TEST_USER=
@@ -108,8 +106,8 @@ REDIS_USE_CLUSTER
# In minutes, the time a cached remote event will expire at.
REDIS_EVENT_EXPIRE_TIME=15

TBA_PRIVACY_POLICY_URL=
TBA_TERMS_OF_USE_URL=
TBA_PRIVACY_POLICY_LOCATION=../legal/services-privacy-policy.md
TBA_TERMS_OF_USE_LOCATION=https://raw.githubusercontent.com/mozilla/legal-docs/main/{locale}/websites_tou.md

POSTHOG_HOST=https://us.i.posthog.com
POSTHOG_PROJECT_KEY=
2 changes: 0 additions & 2 deletions backend/.env.test
Original file line number Diff line number Diff line change
@@ -78,8 +78,6 @@ JWT_ALGO=HS256
JWT_EXPIRE_IN_MINS=10000

# -- TESTING --
AUTH0_TEST_USER=
AUTH0_TEST_PASS=
CALDAV_TEST_PRINCIPAL_URL=https://example.org
CALDAV_TEST_CALENDAR_URL=https://example.org
CALDAV_TEST_USER=hello-world
2 changes: 1 addition & 1 deletion backend/README.md
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ More information will be provided in the future. There is currently a docker fil

In order to create a user with password authentication mode, you will need to set `APP_ALLOW_FIRST_TIME_REGISTER=True` in your `.env`.

After the first login you'll want to fill the `APP_ADMIN_ALLOW_LIST` env variable with your account's email to access the basic admin panel located at `/admin/subscribers`.
After the first login you'll want to fill the `APP_ADMIN_ALLOW_LIST` env variable with your account's email to access the basic admin panel located at `/admin/subscribers`.

### Configuration

5 changes: 2 additions & 3 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "appointment"
version = "0.4.0"
version = "0.5.0"
description = "Backend component to Thunderbird Appointment"
requires-python = ">3.12"
dynamic = ["dependencies"]
@@ -17,8 +17,7 @@ cli = [
"ruff",
]
db = [
"mysqlclient==2.1.1",
"mysql-connector-python==8.0.32",
"mysqlclient==2.2.5",
]
test = [
"Faker==26.0.0",
16 changes: 8 additions & 8 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
alembic==1.13.2
alembic==1.13.3
argon2-cffi==23.1.0
argon2-cffi-bindings==21.2.0
Babel==2.15.0
caldav==1.3.9
cryptography==43.0.1
fastapi==0.111.0
fastapi[standard]==0.115.4
fluent.runtime==0.4.0
fluent.syntax==0.19.0
google-api-python-client==2.134.0
google-auth-httplib2==0.2.0
google-auth-oauthlib==1.2.0
jinja2==3.1.4
jinja2==3.1.5
icalendar==5.0.13
itsdangerous==2.2.0
markdown==3.6
MarkupSafe==2.1.2
nh3==0.2.17
nh3==0.2.18
python-dotenv==1.0.1
python-multipart==0.0.9
python-multipart==0.0.18
PyJWT==2.6.0
pydantic[email]==2.7.4
pydantic[email]==2.9.2
sentry-sdk==2.10.0
starlette-context==0.3.6
sqlalchemy-utils==0.41.2
sqlalchemy==2.0.31
sqlalchemy==2.0.36
typer==0.12.3
tzdata==2024.1
uvicorn==0.30.1
uvicorn==0.32.0
validators==0.28.3
oauthlib==3.2.2
requests-oauthlib==2.0.0
21 changes: 16 additions & 5 deletions backend/src/appointment/commands/download_legal.py
Original file line number Diff line number Diff line change
@@ -5,33 +5,44 @@
import markdown


def open_or_get(path: str):
if path.startswith('http'):
return requests.get(path).text

# Otherwise it's a path
with open(path, 'r') as fh:
return fh.read()


def run():
"""Helper function to update privacy and terms.
Please check to ensure you're not getting a 404 before committing lol.
"""
print('Downloading the latest legal documents...')

extensions = ['markdown.extensions.attr_list']
# Attr_List: In-case remote markdown has attributes
# TOC: For ids on headers
extensions = ['markdown.extensions.attr_list', 'markdown.extensions.toc']
# Only english for now. There's no german TB privacy policy?
locales = ['en']

for locale in locales:
privacy_policy = os.getenv('TBA_PRIVACY_POLICY_URL').format(locale=locale)
terms_of_use = os.getenv('TBA_TERMS_OF_USE_URL').format(locale=locale)
privacy_policy = os.getenv('TBA_PRIVACY_POLICY_LOCATION').format(locale=locale)
terms_of_use = os.getenv('TBA_TERMS_OF_USE_LOCATION').format(locale=locale)

os.makedirs(f'{os.path.dirname(__file__)}/../tmp/legal/{locale}', exist_ok=True)

if privacy_policy:
print('Privacy policy url found.')
contents = requests.get(privacy_policy).text
contents = open_or_get(privacy_policy)
html = markupsafe.Markup(markdown.markdown(contents, extensions=extensions))

with open(f'{os.path.dirname(__file__)}/../tmp/legal/{locale}/privacy.html', 'w') as fh:
fh.write(html)

if terms_of_use:
print('Terms of use url found.')
contents = requests.get(terms_of_use).text
contents = open_or_get(terms_of_use)
html = markupsafe.Markup(markdown.markdown(contents, extensions=extensions))

with open(f'{os.path.dirname(__file__)}/../tmp/legal/{locale}/terms.html', 'w') as fh:
41 changes: 41 additions & 0 deletions backend/src/appointment/commands/generate_documentation_pages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import os

import markupsafe
import requests
import markdown


def open_or_get(path: str):
if path.startswith('http'):
return requests.get(path).text

# Otherwise it's a path
with open(path, 'r') as fh:
return fh.read()


def run():
"""Helper function to generate documentation/help pages into plain html
"""
print('Fetching documentation...')

# Attr_List: In-case remote markdown has attributes
# TOC: For ids on headers
extensions = ['markdown.extensions.attr_list', 'markdown.extensions.toc']
# Only english for now. There's no german TB privacy policy?
locales = ['en']

for locale in locales:
using_zoom_doc = f'../docs/zoom/{locale}/using-zoom.md'

os.makedirs(f'{os.path.dirname(__file__)}/../tmp/docs/{locale}', exist_ok=True)

if using_zoom_doc:
print('Using zoom doc found.')
contents = open_or_get(using_zoom_doc)
html = markupsafe.Markup(markdown.markdown(contents, extensions=extensions))

with open(f'{os.path.dirname(__file__)}/../tmp/docs/{locale}/using-zoom.html', 'w') as fh:
fh.write(html)

print('Done! Copy them over to the frontend/src/assets/docs!')
17 changes: 11 additions & 6 deletions backend/src/appointment/commands/update_db.py
Original file line number Diff line number Diff line change
@@ -23,18 +23,23 @@ def run():

engine, _ = get_engine_and_session()

fresh_db = False

with engine.begin() as connection:
context = migration.MigrationContext.configure(connection)
# Returns a tuple, empty if there's no revisions saved
revisions = context.get_current_heads()

# If we have no revisions, then fully create the database from the model metadata,
# and set our revision number to the latest revision. Otherwise run any new migrations
if len(revisions) == 0:
print('Initializing database, and setting it to the latest revision')
models.Base.metadata.create_all(bind=engine)
command.stamp(alembic_cfg, 'head')
else:
print('Database already initialized, running migrations')
command.upgrade(alembic_cfg, 'head')
print('Finished checking database')
fresh_db = True

# If it's a fresh db set our revision number to the latest revision. Otherwise run any new migrations
if fresh_db:
command.stamp(alembic_cfg, 'head')
else:
print('Database already initialized, running migrations')
command.upgrade(alembic_cfg, 'head')
print('Finished checking database')
18 changes: 14 additions & 4 deletions backend/src/appointment/controller/apis/google_client.py
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
from ...database.models import CalendarProvider
from ...database.schemas import CalendarConnection
from ...defines import DATETIMEFMT
from ...exceptions.calendar import EventNotCreatedException, FreeBusyTimeException
from ...exceptions.calendar import EventNotCreatedException, EventNotDeletedException, FreeBusyTimeException
from ...exceptions.google_api import GoogleScopeChanged, GoogleInvalidCredentials


@@ -163,6 +163,8 @@ def list_events(self, calendar_id, time_min, time_max, token):
# Limit the fields we request
fields = ','.join(
(
'items/id',
'items/iCalUID',
'items/status',
'items/summary',
'items/description',
@@ -201,19 +203,27 @@ def list_events(self, calendar_id, time_min, time_max, token):

return items

def create_event(self, calendar_id, body, token):
def save_event(self, calendar_id, body, token):
response = None
with build('calendar', 'v3', credentials=token, cache_discovery=False) as service:
try:
response = service.events().import_(calendarId=calendar_id, body=body).execute()
except HttpError as e:
logging.warning(f'[google_client.create_event] Request Error: {e.status_code}/{e.error_details}')
logging.warning(f'[google_client.save_event] Request Error: {e.status_code}/{e.error_details}')
raise EventNotCreatedException()

return response

def delete_event(self, calendar_id, event_id, token):
pass
response = None
with build('calendar', 'v3', credentials=token, cache_discovery=False) as service:
try:
response = service.events().delete(calendarId=calendar_id, eventId=event_id).execute()
except HttpError as e:
logging.warning(f'[google_client.delete_event] Request Error: {e.status_code}/{e.error_details}')
raise EventNotDeletedException()

return response

def sync_calendars(self, db, subscriber_id: int, token):
# Grab all the Google calendars
2 changes: 1 addition & 1 deletion backend/src/appointment/controller/auth.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Module: auth
Handle authentification with Auth0 and get subscription data.
Handle authentification with FxA and get subscription data.
"""

import os
Loading

0 comments on commit 8825de0

Please sign in to comment.