Skip to content

Commit

Permalink
🔨 Move subscriber active flag to time_deleted
Browse files Browse the repository at this point in the history
  • Loading branch information
devmount committed Jun 1, 2024
1 parent 5b71e4a commit fe51d04
Show file tree
Hide file tree
Showing 12 changed files with 101 additions and 43 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

Invite others to grab times on your calendar. Choose a date. Make appointments as easy as it gets.


## Feedback and Support

If you'd like to give feedback or need support, please see our [Topicbox](https://thunderbird.topicbox.com/groups/services).
Expand Down
14 changes: 11 additions & 3 deletions backend/src/appointment/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,16 +98,24 @@ def get_columns(self) -> list:
time_updated = Column(DateTime, server_default=func.now(), default=func.now(), onupdate=func.now(), index=True)


class Subscriber(Base):
class HasSoftDelete:
"""Mixing in a column to support deletion without removing the record"""
time_deleted = Column(DateTime, nullable=True)

@property
def is_deleted(self):
"""A record is marked deleted if a delete time is set."""
return self.time_deleted is not None


class Subscriber(HasSoftDelete, Base):
__tablename__ = "subscribers"

id = Column(Integer, primary_key=True, index=True)
username = Column(StringEncryptedType(String, secret, AesEngine, "pkcs5", length=255), unique=True, index=True)
# 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
13 changes: 11 additions & 2 deletions backend/src/appointment/database/repo/subscriber.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""

import re
import datetime

from sqlalchemy.orm import Session
from .. import models, schemas
Expand Down Expand Up @@ -73,15 +74,23 @@ def update(db: Session, data: schemas.SubscriberIn, subscriber_id: int):

def disable(db: Session, subscriber: models.Subscriber):
"""Disable a given subscriber"""
subscriber.active = False
# mark subscriber deleted = disable them
subscriber.time_deleted = datetime.datetime.now(datetime.UTC)
# invalidate session
# subscriber.minimum_valid_iat_time = datetime.datetime.now(datetime.UTC)
db.add(subscriber)
db.commit()
db.refresh(subscriber)
return subscriber


def enable(db: Session, subscriber: models.Subscriber):
"""Enable a given subscriber"""
subscriber.active = True
# mark subscriber deleted = disable them
subscriber.time_deleted = None
# refresh session
# subscriber.minimum_valid_iat_time = datetime.datetime.now(datetime.UTC)
db.add(subscriber)
db.commit()
db.refresh(subscriber)
return subscriber
Expand Down
2 changes: 1 addition & 1 deletion backend/src/appointment/database/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,9 @@ class Config:


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

class Config:
from_attributes = True
Expand Down
1 change: 1 addition & 0 deletions backend/src/appointment/dependencies/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def get_user_from_token(db, token: str):
# Token has been expired by us - temp measure to avoid spinning a refresh system, or a deny list for this issue
if any([
subscriber is None,
subscriber.is_deleted,
subscriber and subscriber.minimum_valid_iat_time and not iat,
subscriber and subscriber.minimum_valid_iat_time and subscriber.minimum_valid_iat_time.timestamp() > int(iat)
]):
Expand Down
27 changes: 27 additions & 0 deletions backend/src/appointment/exceptions/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,30 @@ class CreateSubscriberAlreadyExistsException(APIException):

def get_msg(self):
return l10n('subscriber-already-exists')


class SubscriberAlreadyDeletedException(APIException):
"""Raise when a subscriber failed to be marked deleted because they already are"""
id_code = 'SUBSCRIBER_ALREADY_DELETED'
status_code = 400

def get_msg(self):
return l10n('subscriber-already-deleted')


class SubscriberAlreadyEnabledException(APIException):
"""Raise when a subscriber failed to be marked undeleted because they already are"""
id_code = 'SUBSCRIBER_ALREADY_ENABLED'
status_code = 400

def get_msg(self):
return l10n('subscriber-already-enabled')


class SubscriberSelfDeleteException(APIException):
"""Raise when a subscriber tries to delete themselves where not allowed"""
id_code = 'SUBSCRIBER_SELF_DELETE'
status_code = 403

def get_msg(self):
return l10n('subscriber-self-delete')
3 changes: 3 additions & 0 deletions backend/src/appointment/l10n/de/main.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ event-could-not-be-accepted = Es ist ein Fehler bei der Annahme der Buchungsdate
failed-to-create-subscriber = Es gab einen Fehler beim Anlegen der Person. Bitte später erneut versuchen.
subscriber-already-exists = Eine Person mit dieser E-Mail-Adresse existiert bereits.
subscriber-already-deleted = Die Person ist bereits gelöscht.
subscriber-already-enabled = Die Person ist bereits aktiv.
subscriber-self-delete = Die Löschung des eigenen Benutzerkontos ist hier nicht möglich.
## Authentication Exceptions

Expand Down
3 changes: 3 additions & 0 deletions backend/src/appointment/l10n/en/main.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ event-could-not-be-accepted = There was an error accepting the booking details.
failed-to-create-subscriber = There was an error creating the subscriber. Please try again later.
subscriber-already-exists = A subscriber with this email address already exists.
subscriber-already-deleted = The subscriber is already deleted.
subscriber-already-enabled = The subscriber is already enabled.
subscriber-self-delete = You are not allowed to delete yourself here.
## Authentication Exceptions

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""add subscriber active flag
"""add subscriber soft delete
Revision ID: d5de8f10ab87
Revises: 9fe08ba6f2ed
Expand All @@ -17,8 +17,8 @@


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


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

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

fxa_connections = repo.external_connection.get_by_type(db, subscriber.id, ExternalConnectionType.fxa)
Expand Down Expand Up @@ -222,7 +222,7 @@ def token(
raise HTTPException(status_code=403, detail=l10n('invalid-credentials'))

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

# Verify the incoming password, and re-hash our password if needed
Expand Down Expand Up @@ -279,7 +279,7 @@ def me(
@router.post("/permission-check")
def permission_check(subscriber: Subscriber = Depends(get_admin_subscriber)):
"""Checks if they have admin permissions"""
return subscriber.active
return subscriber.is_deleted


# @router.get('/test-create-account')
Expand Down
28 changes: 17 additions & 11 deletions backend/src/appointment/routes/subscriber.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,28 @@ 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"""
subscriber = repo.subscriber.get_by_email(db, email)
if not subscriber:
def disable_subscriber(email: str, db: Session = Depends(get_db), subscriber: Subscriber = Depends(get_admin_subscriber)):
"""endpoint to mark a subscriber deleted by email, needs admin permissions"""
subscriber_to_delete = repo.subscriber.get_by_email(db, email)
if not subscriber_to_delete:
raise validation.SubscriberNotFoundException()

if subscriber_to_delete.is_deleted:
raise validation.SubscriberAlreadyDeletedException()
if subscriber.email == subscriber_to_delete.email:
raise validation.SubscriberSelfDeleteException()

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


@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:
"""endpoint to enable a subscriber by email, needs admin permissions"""
subscriber_to_enable = repo.subscriber.get_by_email(db, email)
if not subscriber_to_enable:
raise validation.SubscriberNotFoundException()

if not subscriber_to_enable.is_deleted:
raise validation.SubscriberAlreadyEnabledException()

# Set active flag to true on the subscribers model.
return repo.subscriber.enable(db, subscriber)
return repo.subscriber.enable(db, subscriber_to_enable)
40 changes: 21 additions & 19 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) => toggleSubscriberState(field.email.value, field.active.value)"
@field-click="(_key, field) => toggleSubscriberState(field.email.value, field.timeDeleted.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 @@ -54,18 +54,19 @@
</template>

<script setup>
import {
computed, inject, onMounted, ref,
} from 'vue';
import { useI18n } from 'vue-i18n';
import { alertSchemes, tableDataButtonType, tableDataType } from '@/definitions';
import DataTable from '@/components/DataTable.vue';
import { computed, inject, onMounted, ref } from 'vue';
import { IconSend } from '@tabler/icons-vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useUserStore } from '@/stores/user-store';
import AdminNav from '@/elements/admin/AdminNav.vue';
import AlertBox from '@/elements/AlertBox.vue';
import DataTable from '@/components/DataTable.vue';
import LoadingSpinner from '@/elements/LoadingSpinner.vue';
import PrimaryButton from '@/elements/PrimaryButton.vue';
import { IconSend } from '@tabler/icons-vue';
import AlertBox from '@/elements/AlertBox.vue';
import AdminNav from '@/elements/admin/AdminNav.vue';
const user = useUserStore();
const router = useRouter();
const { t } = useI18n();
Expand Down Expand Up @@ -93,14 +94,14 @@ 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'),
},
timeDeleted: {
type: tableDataType.text,
value: subscriber.time_deleted ? dj(subscriber.time_deleted).format('ll LTS') : '',
},
timezone: {
type: tableDataType.text,
value: subscriber.timezone ?? 'Unset',
Expand All @@ -111,8 +112,9 @@ const filteredSubscribers = computed(() => subscribers.value.map((subscriber) =>
},
disable: {
type: tableDataType.button,
buttonType: subscriber.active ? tableDataButtonType.caution : tableDataButtonType.primary,
value: subscriber.active ? 'Disable' : 'Enable',
buttonType: subscriber.time_deleted ? tableDataButtonType.primary : tableDataButtonType.caution,
value: subscriber.time_deleted ? 'Enable' : 'Disable',
disabled: !subscriber.time_deleted && subscriber.email === user.data.email,
},
})));
const columns = [
Expand All @@ -128,14 +130,14 @@ const columns = [
key: 'email',
name: 'Email',
},
{
key: 'active',
name: 'Active',
},
{
key: 'createdAt',
name: 'Time Created',
},
{
key: 'deletedAt',
name: 'Time Deleted',
},
{
key: 'timezone',
name: 'Timezone',
Expand Down

0 comments on commit fe51d04

Please sign in to comment.