Skip to content

Commit

Permalink
➕ Add soft delete / disabling subscribers via panel
Browse files Browse the repository at this point in the history
  • Loading branch information
devmount committed May 31, 2024
1 parent e30cef6 commit 5b71e4a
Show file tree
Hide file tree
Showing 11 changed files with 90 additions and 19 deletions.
2 changes: 2 additions & 0 deletions backend/src/appointment/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ class Subscriber(Base):
# Encrypted (here) and hashed (by the associated hashing functions in routes/auth)
password = Column(StringEncryptedType(String, secret, AesEngine, "pkcs5", length=255), index=False)

active: bool = Column(Boolean, index=True, default=True)

# Use subscriber.preferred_email for any email, or other user-facing presence.
email = Column(StringEncryptedType(String, secret, AesEngine, "pkcs5", length=255), unique=True, index=True)
secondary_email = Column(StringEncryptedType(String, secret, AesEngine, "pkcs5", length=255), nullable=True, index=True)
Expand Down
18 changes: 17 additions & 1 deletion backend/src/appointment/database/repo/subscriber.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,24 @@ def update(db: Session, data: schemas.SubscriberIn, subscriber_id: int):
return db_subscriber


def disable(db: Session, subscriber: models.Subscriber):
"""Disable a given subscriber"""
subscriber.active = False
db.commit()
db.refresh(subscriber)
return subscriber


def enable(db: Session, subscriber: models.Subscriber):
"""Enable a given subscriber"""
subscriber.active = True
db.commit()
db.refresh(subscriber)
return subscriber


def delete(db: Session, subscriber: models.Subscriber):
"""Delete a subscriber by subscriber id"""
"""Delete a given subscriber"""
db.delete(subscriber)
db.commit()
return True
Expand Down
1 change: 1 addition & 0 deletions backend/src/appointment/database/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ class Config:


class SubscriberAdminOut(Subscriber):
active: bool | None = True
invite: Invite | None = None
time_created: datetime

Expand Down
1 change: 1 addition & 0 deletions backend/src/appointment/l10n/de/main.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ subscriber-already-exists = Eine Person mit dieser E-Mail-Adresse existiert bere

email-mismatch = E-Mail-Adressen stimmen nicht überein.
invalid-credentials = Die angegebenen Anmeldedaten sind nicht gültig.
disabled-account = Das Benutzerkonto wurde deaktiviert.
oauth-error = Es ist ein Fehler bei der Authentifizierung aufgetreten. Bitte erneut versuchen.
## Zoom Exceptions
Expand Down
1 change: 1 addition & 0 deletions backend/src/appointment/l10n/en/main.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ subscriber-already-exists = A subscriber with this email address already exists.

email-mismatch = Email mismatch.
invalid-credentials = The provided credentials are not valid.
disabled-account = Your account has been disabled.
oauth-error = There was an error with the authentication flow. Please try again.
## Zoom Exceptions
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""add subscriber active flag
Revision ID: d5de8f10ab87
Revises: 9fe08ba6f2ed
Create Date: 2024-05-31 14:54:23.772015
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'd5de8f10ab87'
down_revision = '9fe08ba6f2ed'
branch_labels = None
depends_on = None


def upgrade() -> None:
op.add_column('subscribers', sa.Column('active', sa.Boolean, index=True, default=True))


def downgrade() -> None:
op.drop_column('subscribers', 'active')
12 changes: 10 additions & 2 deletions backend/src/appointment/routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ def fxa_callback(
elif not subscriber:
subscriber = fxa_subscriber

# Only proceed if user account is enabled (which is the default case for new users)
if not subscriber.active:
raise HTTPException(status_code=403, detail=l10n('disabled-account'))

fxa_connections = repo.external_connection.get_by_type(db, subscriber.id, ExternalConnectionType.fxa)

# If we have fxa_connections, ensure the incoming one matches our known one.
Expand Down Expand Up @@ -217,6 +221,10 @@ def token(
if not subscriber or subscriber.password is None:
raise HTTPException(status_code=403, detail=l10n('invalid-credentials'))

# Only proceed if user account is enabled
if not subscriber.active:
raise HTTPException(status_code=403, detail=l10n('disabled-account'))

# Verify the incoming password, and re-hash our password if needed
try:
utils.verify_password(form_data.password, subscriber.password)
Expand Down Expand Up @@ -269,9 +277,9 @@ def me(


@router.post("/permission-check")
def permission_check(_: Subscriber = Depends(get_admin_subscriber)):
def permission_check(subscriber: Subscriber = Depends(get_admin_subscriber)):
"""Checks if they have admin permissions"""
return True
return subscriber.active


# @router.get('/test-create-account')
Expand Down
18 changes: 13 additions & 5 deletions backend/src/appointment/routes/subscriber.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,20 @@ def get_all_subscriber(db: Session = Depends(get_db), _: Subscriber = Depends(ge
@router.put("/disable/{email}")
def disable_subscriber(email: str, db: Session = Depends(get_db), _: Subscriber = Depends(get_admin_subscriber)):
"""endpoint to disable a subscriber by email, needs admin permissions"""
# TODO: Add status to subscriber, and disable it instead.
raise NotImplementedError
subscriber = repo.subscriber.get_by_email(db, email)
if not subscriber:
raise validation.SubscriberNotFoundException()

# Set active flag to false on the subscribers model.
return repo.subscriber.disable(db, subscriber)


@router.put("/enable/{email}")
def disable_subscriber(email: str, db: Session = Depends(get_db), _: Subscriber = Depends(get_admin_subscriber)):
"""endpoint to disable a subscriber by email, needs admin permissions"""
subscriber = repo.subscriber.get_by_email(db, email)
if not subscriber:
raise validation.SubscriberNotFoundException()
# TODO: CAUTION! This actually deletes the subscriber. We might want to only disable them.
# This needs an active flag on the subscribers model.
return repo.subscriber.delete(db, subscriber)

# Set active flag to true on the subscribers model.
return repo.subscriber.enable(db, subscriber)
4 changes: 4 additions & 0 deletions frontend/src/components/DataTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
<code>{{ fieldData.value }}</code>
<text-button :copy="fieldData.value" />
</span>
<span v-else-if="fieldData.type === tableDataType.bool">
<span v-if="fieldData.value">Yes</span>
<span v-else>No</span>
</span>
<span v-else-if="fieldData.type === tableDataType.link">
<a :href="fieldData.link" target="_blank">{{ fieldData.value }}</a>
</span>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ export const tableDataType = {
link: 2,
button: 3,
code: 4,
bool: 5,
};

export const tableDataButtonType = {
Expand Down
27 changes: 16 additions & 11 deletions frontend/src/views/admin/SubscriberPanelView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
:columns="columns"
:filters="filters"
:loading="loading"
@field-click="(_key, field) => disableSubscriber(field.email.value)"
@field-click="(_key, field) => toggleSubscriberState(field.email.value, field.active.value)"
>
<template v-slot:footer>
<div class="flex w-1/3 flex-col gap-4 text-center md:w-full md:flex-row md:text-left">
Expand Down Expand Up @@ -93,6 +93,10 @@ const filteredSubscribers = computed(() => subscribers.value.map((subscriber) =>
type: tableDataType.text,
value: subscriber.email,
},
active: {
type: tableDataType.bool,
value: subscriber.active,
},
timeCreated: {
type: tableDataType.text,
value: dj(subscriber.time_created).format('ll LTS'),
Expand All @@ -102,16 +106,14 @@ const filteredSubscribers = computed(() => subscribers.value.map((subscriber) =>
value: subscriber.timezone ?? 'Unset',
},
wasInvited: {
type: tableDataType.text,
value: subscriber.invite ? 'Yes' : 'No',
type: tableDataType.bool,
value: subscriber.invite,
},
/*
disable: {
type: tableDataType.button,
buttonType: tableDataButtonType.caution,
value: 'Disable',
buttonType: subscriber.active ? tableDataButtonType.caution : tableDataButtonType.primary,
value: subscriber.active ? 'Disable' : 'Enable',
},
*/
})));
const columns = [
{
Expand All @@ -126,6 +128,10 @@ const columns = [
key: 'email',
name: 'Email',
},
{
key: 'active',
name: 'Active',
},
{
key: 'createdAt',
name: 'Time Created',
Expand All @@ -138,12 +144,10 @@ const columns = [
key: 'wasInvited',
name: 'Was Invited?',
},
/*
{
key: 'disable',
name: '',
},
*/
];
const filters = [
{
Expand Down Expand Up @@ -194,12 +198,13 @@ const refresh = async () => {
* @param email
* @returns {Promise<void>}
*/
const disableSubscriber = async (email) => {
const toggleSubscriberState = async (email, currentState) => {
if (!email) {
return;
}
const response = await call(`subscriber/disable/${email}`).put().json();
const action = currentState ? 'disable' : 'enable';
const response = await call(`subscriber/${action}/${email}`).put().json();
const { data } = response;
if (data.value) {
Expand Down

0 comments on commit 5b71e4a

Please sign in to comment.