forked from jamesp/sasa-membership
stuff changed:
- ui has been made 'kinda better' (after making it worse for a while lol - ESP rfid readers are now supported [ill upload the code for them in another repo later] - admin system has been secured a bit better and seems to be working well
This commit is contained in:
@@ -44,6 +44,29 @@ from .schemas import (
|
||||
ProfileQuestionForUser,
|
||||
ProfileAnswerUpdate,
|
||||
ProfileAnswersUpdateRequest,
|
||||
EspReaderCreate,
|
||||
EspReaderUpdate,
|
||||
EspReaderResponse,
|
||||
EspReaderCreateResponse,
|
||||
EspReaderRegistrationRequest,
|
||||
EspReaderRegistrationResponse,
|
||||
EspReaderProvisioningResponse,
|
||||
RfidCardCreate,
|
||||
RfidCardUpdate,
|
||||
RfidCardResponse,
|
||||
RfidTapRequest,
|
||||
RfidTapResponse,
|
||||
RfidWriteJobCreate,
|
||||
RfidWriteJobCompleteRequest,
|
||||
RfidWriteJobResponse,
|
||||
EspTimeResponse,
|
||||
EspHeartbeatRequest,
|
||||
EspHeartbeatResponse,
|
||||
EspDashboardLoginResponse,
|
||||
RfidTapAdminResponse,
|
||||
AttendanceSessionResponse,
|
||||
StaleSessionCloseRequest,
|
||||
StaleSessionCloseResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
@@ -92,4 +115,27 @@ __all__ = [
|
||||
"ProfileQuestionForUser",
|
||||
"ProfileAnswerUpdate",
|
||||
"ProfileAnswersUpdateRequest",
|
||||
"EspReaderCreate",
|
||||
"EspReaderUpdate",
|
||||
"EspReaderResponse",
|
||||
"EspReaderCreateResponse",
|
||||
"EspReaderRegistrationRequest",
|
||||
"EspReaderRegistrationResponse",
|
||||
"EspReaderProvisioningResponse",
|
||||
"RfidCardCreate",
|
||||
"RfidCardUpdate",
|
||||
"RfidCardResponse",
|
||||
"RfidTapRequest",
|
||||
"RfidTapResponse",
|
||||
"RfidWriteJobCreate",
|
||||
"RfidWriteJobCompleteRequest",
|
||||
"RfidWriteJobResponse",
|
||||
"EspTimeResponse",
|
||||
"EspHeartbeatRequest",
|
||||
"EspHeartbeatResponse",
|
||||
"EspDashboardLoginResponse",
|
||||
"RfidTapAdminResponse",
|
||||
"AttendanceSessionResponse",
|
||||
"StaleSessionCloseRequest",
|
||||
"StaleSessionCloseResponse",
|
||||
]
|
||||
|
||||
+283
-33
@@ -1,11 +1,43 @@
|
||||
from pydantic import BaseModel, EmailStr, Field, ConfigDict
|
||||
from pydantic import BaseModel, EmailStr, Field, ConfigDict, field_serializer, field_validator
|
||||
from typing import Optional, Literal, Any
|
||||
from datetime import datetime, date
|
||||
from ..models.models import UserRole, MembershipStatus, PaymentStatus, PaymentMethod
|
||||
from ..core.datetime import to_utc_naive, to_zulu_iso
|
||||
from ..models.models import (
|
||||
UserRole,
|
||||
MembershipStatus,
|
||||
PaymentStatus,
|
||||
PaymentMethod,
|
||||
EspReaderProvisioningStatus,
|
||||
EspReaderType,
|
||||
EspTapAction,
|
||||
RfidWriteJobStatus,
|
||||
)
|
||||
|
||||
|
||||
class UTCBaseModel(BaseModel):
|
||||
@field_validator("*", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def normalize_datetime_inputs(cls, value: Any) -> Any:
|
||||
if isinstance(value, datetime):
|
||||
return to_utc_naive(value)
|
||||
return value
|
||||
|
||||
@field_validator("*", mode="after", check_fields=False)
|
||||
@classmethod
|
||||
def normalize_parsed_datetimes(cls, value: Any) -> Any:
|
||||
if isinstance(value, datetime):
|
||||
return to_utc_naive(value)
|
||||
return value
|
||||
|
||||
@field_serializer("*", when_used="json", check_fields=False)
|
||||
def serialize_datetime_outputs(self, value: Any) -> Any:
|
||||
if isinstance(value, datetime):
|
||||
return to_zulu_iso(value)
|
||||
return value
|
||||
|
||||
|
||||
# User Schemas
|
||||
class UserBase(BaseModel):
|
||||
class UserBase(UTCBaseModel):
|
||||
email: EmailStr
|
||||
first_name: str = Field(..., min_length=1, max_length=100)
|
||||
last_name: str = Field(..., min_length=1, max_length=100)
|
||||
@@ -17,7 +49,7 @@ class UserCreate(UserBase):
|
||||
password: str = Field(..., min_length=8)
|
||||
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
class UserUpdate(UTCBaseModel):
|
||||
email: Optional[EmailStr] = None
|
||||
first_name: Optional[str] = Field(None, min_length=1, max_length=100)
|
||||
last_name: Optional[str] = Field(None, min_length=1, max_length=100)
|
||||
@@ -43,37 +75,37 @@ class UserInDB(UserResponse):
|
||||
|
||||
|
||||
# Authentication Schemas
|
||||
class Token(BaseModel):
|
||||
class Token(UTCBaseModel):
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
|
||||
|
||||
class TokenData(BaseModel):
|
||||
class TokenData(UTCBaseModel):
|
||||
user_id: Optional[int] = None
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
class LoginRequest(UTCBaseModel):
|
||||
email: EmailStr
|
||||
password: str
|
||||
|
||||
|
||||
# Password Reset Schemas
|
||||
class ForgotPasswordRequest(BaseModel):
|
||||
class ForgotPasswordRequest(UTCBaseModel):
|
||||
email: EmailStr
|
||||
|
||||
|
||||
class ResetPasswordRequest(BaseModel):
|
||||
class ResetPasswordRequest(UTCBaseModel):
|
||||
token: str = Field(..., min_length=1)
|
||||
new_password: str = Field(..., min_length=8)
|
||||
|
||||
|
||||
class ChangePasswordRequest(BaseModel):
|
||||
class ChangePasswordRequest(UTCBaseModel):
|
||||
current_password: str = Field(..., min_length=1)
|
||||
new_password: str = Field(..., min_length=8)
|
||||
|
||||
|
||||
# Membership Tier Schemas
|
||||
class MembershipTierBase(BaseModel):
|
||||
class MembershipTierBase(UTCBaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=100)
|
||||
description: Optional[str] = None
|
||||
annual_fee: float = Field(..., ge=0)
|
||||
@@ -84,7 +116,7 @@ class MembershipTierCreate(MembershipTierBase):
|
||||
pass
|
||||
|
||||
|
||||
class MembershipTierUpdate(BaseModel):
|
||||
class MembershipTierUpdate(UTCBaseModel):
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
||||
description: Optional[str] = None
|
||||
annual_fee: Optional[float] = Field(None, ge=0)
|
||||
@@ -101,7 +133,7 @@ class MembershipTierResponse(MembershipTierBase):
|
||||
|
||||
|
||||
# Membership Schemas
|
||||
class MembershipBase(BaseModel):
|
||||
class MembershipBase(UTCBaseModel):
|
||||
tier_id: int
|
||||
auto_renew: bool = False
|
||||
|
||||
@@ -111,14 +143,14 @@ class MembershipCreate(MembershipBase):
|
||||
end_date: date
|
||||
|
||||
|
||||
class MembershipUpdate(BaseModel):
|
||||
class MembershipUpdate(UTCBaseModel):
|
||||
tier_id: Optional[int] = None
|
||||
status: Optional[MembershipStatus] = None
|
||||
end_date: Optional[date] = None
|
||||
auto_renew: Optional[bool] = None
|
||||
|
||||
|
||||
class MembershipResponse(BaseModel):
|
||||
class MembershipResponse(UTCBaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
@@ -133,7 +165,7 @@ class MembershipResponse(BaseModel):
|
||||
|
||||
|
||||
# Payment Schemas
|
||||
class PaymentBase(BaseModel):
|
||||
class PaymentBase(UTCBaseModel):
|
||||
amount: float = Field(..., gt=0)
|
||||
payment_method: PaymentMethod
|
||||
notes: Optional[str] = None
|
||||
@@ -143,14 +175,14 @@ class PaymentCreate(PaymentBase):
|
||||
membership_id: Optional[int] = None
|
||||
|
||||
|
||||
class PaymentUpdate(BaseModel):
|
||||
class PaymentUpdate(UTCBaseModel):
|
||||
status: Optional[PaymentStatus] = None
|
||||
transaction_id: Optional[str] = None
|
||||
payment_date: Optional[datetime] = None
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
class PaymentResponse(BaseModel):
|
||||
class PaymentResponse(UTCBaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
@@ -166,7 +198,7 @@ class PaymentResponse(BaseModel):
|
||||
|
||||
|
||||
# Square Payment Schemas
|
||||
class SquarePaymentRequest(BaseModel):
|
||||
class SquarePaymentRequest(UTCBaseModel):
|
||||
"""Request schema for Square payment processing"""
|
||||
source_id: str = Field(..., description="Payment source ID from Square Web Payments SDK")
|
||||
tier_id: int = Field(..., description="Membership tier ID to create membership for")
|
||||
@@ -176,7 +208,7 @@ class SquarePaymentRequest(BaseModel):
|
||||
billing_details: Optional[dict] = Field(None, description="Billing address and cardholder name for AVS")
|
||||
|
||||
|
||||
class SquarePaymentResponse(BaseModel):
|
||||
class SquarePaymentResponse(UTCBaseModel):
|
||||
"""Response schema for Square payment"""
|
||||
success: bool
|
||||
payment_id: Optional[str] = None
|
||||
@@ -189,7 +221,7 @@ class SquarePaymentResponse(BaseModel):
|
||||
membership_id: Optional[int] = Field(None, description="Created membership ID")
|
||||
|
||||
|
||||
class SquareRefundRequest(BaseModel):
|
||||
class SquareRefundRequest(UTCBaseModel):
|
||||
"""Request schema for Square payment refund"""
|
||||
payment_id: int = Field(..., description="Database payment ID")
|
||||
amount: Optional[float] = Field(None, gt=0, description="Amount to refund (None for full refund)")
|
||||
@@ -197,13 +229,13 @@ class SquareRefundRequest(BaseModel):
|
||||
|
||||
|
||||
# Message Response
|
||||
class MessageResponse(BaseModel):
|
||||
class MessageResponse(UTCBaseModel):
|
||||
message: str
|
||||
detail: Optional[str] = None
|
||||
|
||||
|
||||
# Email Template Schemas
|
||||
class EmailTemplateBase(BaseModel):
|
||||
class EmailTemplateBase(UTCBaseModel):
|
||||
template_key: str
|
||||
name: str
|
||||
subject: str
|
||||
@@ -216,7 +248,7 @@ class EmailTemplateCreate(EmailTemplateBase):
|
||||
pass
|
||||
|
||||
|
||||
class EmailTemplateUpdate(BaseModel):
|
||||
class EmailTemplateUpdate(UTCBaseModel):
|
||||
name: Optional[str] = None
|
||||
subject: Optional[str] = None
|
||||
html_body: Optional[str] = None
|
||||
@@ -235,7 +267,7 @@ class EmailTemplateResponse(EmailTemplateBase):
|
||||
|
||||
|
||||
# Event Schemas
|
||||
class EventBase(BaseModel):
|
||||
class EventBase(UTCBaseModel):
|
||||
title: str = Field(..., min_length=1, max_length=255)
|
||||
description: Optional[str] = None
|
||||
event_date: datetime
|
||||
@@ -248,7 +280,7 @@ class EventCreate(EventBase):
|
||||
pass
|
||||
|
||||
|
||||
class EventUpdate(BaseModel):
|
||||
class EventUpdate(UTCBaseModel):
|
||||
title: Optional[str] = Field(None, min_length=1, max_length=255)
|
||||
description: Optional[str] = None
|
||||
event_date: Optional[datetime] = None
|
||||
@@ -269,7 +301,7 @@ class EventResponse(EventBase):
|
||||
|
||||
|
||||
# Event RSVP Schemas
|
||||
class EventRSVPBase(BaseModel):
|
||||
class EventRSVPBase(UTCBaseModel):
|
||||
status: str = Field(..., pattern="^(pending|attending|not_attending|maybe)$")
|
||||
notes: Optional[str] = None
|
||||
|
||||
@@ -293,12 +325,12 @@ class EventRSVPResponse(EventRSVPBase):
|
||||
ProfileQuestionInputType = Literal["text", "number", "boolean", "date", "select"]
|
||||
|
||||
|
||||
class QuestionOption(BaseModel):
|
||||
class QuestionOption(UTCBaseModel):
|
||||
label: str = Field(..., min_length=1, max_length=100)
|
||||
value: str = Field(..., min_length=1, max_length=100)
|
||||
|
||||
|
||||
class ProfileQuestionBase(BaseModel):
|
||||
class ProfileQuestionBase(UTCBaseModel):
|
||||
key: str = Field(..., min_length=2, max_length=100, pattern=r"^[a-z0-9_]+$")
|
||||
label: str = Field(..., min_length=2, max_length=255)
|
||||
help_text: Optional[str] = None
|
||||
@@ -317,7 +349,7 @@ class ProfileQuestionCreate(ProfileQuestionBase):
|
||||
pass
|
||||
|
||||
|
||||
class ProfileQuestionUpdate(BaseModel):
|
||||
class ProfileQuestionUpdate(UTCBaseModel):
|
||||
key: Optional[str] = Field(None, min_length=2, max_length=100, pattern=r"^[a-z0-9_]+$")
|
||||
label: Optional[str] = Field(None, min_length=2, max_length=255)
|
||||
help_text: Optional[str] = None
|
||||
@@ -332,7 +364,7 @@ class ProfileQuestionUpdate(BaseModel):
|
||||
depends_on_value: Optional[str] = Field(None, max_length=255)
|
||||
|
||||
|
||||
class ProfileQuestionResponse(BaseModel):
|
||||
class ProfileQuestionResponse(UTCBaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
@@ -357,10 +389,228 @@ class ProfileQuestionForUser(ProfileQuestionResponse):
|
||||
can_edit: bool = True
|
||||
|
||||
|
||||
class ProfileAnswerUpdate(BaseModel):
|
||||
class ProfileAnswerUpdate(UTCBaseModel):
|
||||
question_id: int
|
||||
value: Optional[Any] = None
|
||||
|
||||
|
||||
class ProfileAnswersUpdateRequest(BaseModel):
|
||||
class ProfileAnswersUpdateRequest(UTCBaseModel):
|
||||
answers: list[ProfileAnswerUpdate]
|
||||
|
||||
|
||||
# ESP RFID Reader Schemas
|
||||
class EspReaderBase(UTCBaseModel):
|
||||
device_id: str = Field(..., min_length=2, max_length=100, pattern=r"^[A-Za-z0-9_.:-]+$")
|
||||
name: str = Field(..., min_length=1, max_length=255)
|
||||
location: Optional[str] = Field(None, max_length=255)
|
||||
reader_type: EspReaderType = EspReaderType.CHECKIN_CHECKOUT
|
||||
notes: Optional[str] = None
|
||||
is_active: bool = True
|
||||
can_write_cards: bool = False
|
||||
firmware_version: Optional[str] = Field(None, max_length=100)
|
||||
|
||||
|
||||
class EspReaderCreate(EspReaderBase):
|
||||
api_key: Optional[str] = Field(None, min_length=16, max_length=255)
|
||||
|
||||
|
||||
class EspReaderUpdate(UTCBaseModel):
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=255)
|
||||
location: Optional[str] = Field(None, max_length=255)
|
||||
reader_type: Optional[EspReaderType] = None
|
||||
notes: Optional[str] = None
|
||||
is_active: Optional[bool] = None
|
||||
can_write_cards: Optional[bool] = None
|
||||
rotate_api_key: bool = False
|
||||
|
||||
|
||||
class EspReaderResponse(EspReaderBase):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
provisioning_status: EspReaderProvisioningStatus
|
||||
last_seen_at: Optional[datetime] = None
|
||||
approved_at: Optional[datetime] = None
|
||||
provisioned_at: Optional[datetime] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class EspReaderCreateResponse(EspReaderResponse):
|
||||
api_key: str
|
||||
|
||||
|
||||
class EspReaderRegistrationRequest(UTCBaseModel):
|
||||
device_id: str = Field(..., min_length=2, max_length=100, pattern=r"^[A-Za-z0-9_.:-]+$")
|
||||
name: str = Field(..., min_length=1, max_length=255)
|
||||
location: Optional[str] = Field(None, max_length=255)
|
||||
reader_type: EspReaderType = EspReaderType.CHECKIN_CHECKOUT
|
||||
can_write_cards: bool = False
|
||||
firmware_version: Optional[str] = Field(None, max_length=100)
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
class EspReaderRegistrationResponse(UTCBaseModel):
|
||||
device_id: str
|
||||
provisioning_status: EspReaderProvisioningStatus
|
||||
registration_token: str
|
||||
message: str
|
||||
poll_interval_seconds: int = 5
|
||||
|
||||
|
||||
class EspReaderProvisioningResponse(UTCBaseModel):
|
||||
device_id: str
|
||||
provisioning_status: EspReaderProvisioningStatus
|
||||
message: str
|
||||
api_key: Optional[str] = None
|
||||
apiKey: Optional[str] = None
|
||||
poll_interval_seconds: int = 5
|
||||
|
||||
|
||||
class RfidCardBase(UTCBaseModel):
|
||||
uid: str = Field(..., min_length=2, max_length=100)
|
||||
user_id: Optional[int] = None
|
||||
label: Optional[str] = Field(None, max_length=255)
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class RfidCardCreate(RfidCardBase):
|
||||
pass
|
||||
|
||||
|
||||
class RfidCardUpdate(UTCBaseModel):
|
||||
user_id: Optional[int] = None
|
||||
label: Optional[str] = Field(None, max_length=255)
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
class RfidCardResponse(RfidCardBase):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class RfidTapRequest(UTCBaseModel):
|
||||
card_uid: str = Field(..., min_length=2, max_length=100)
|
||||
tapped_at: Optional[datetime] = None
|
||||
reader_type: Optional[EspReaderType] = None
|
||||
|
||||
|
||||
class RfidTapResponse(UTCBaseModel):
|
||||
accepted: bool
|
||||
action: EspTapAction
|
||||
message: str
|
||||
server_time_utc: datetime
|
||||
tap_id: int
|
||||
session_id: Optional[int] = None
|
||||
user_id: Optional[int] = None
|
||||
user_name: Optional[str] = None
|
||||
checked_in_at: Optional[datetime] = None
|
||||
checked_out_at: Optional[datetime] = None
|
||||
duration_seconds: Optional[int] = None
|
||||
|
||||
|
||||
class RfidWriteJobCreate(UTCBaseModel):
|
||||
reader_id: int
|
||||
user_id: int
|
||||
label: str = Field(..., min_length=1, max_length=255)
|
||||
|
||||
|
||||
class RfidWriteJobCompleteRequest(UTCBaseModel):
|
||||
card_uid: Optional[str] = Field(None, min_length=2, max_length=100)
|
||||
success: bool
|
||||
error_message: Optional[str] = Field(None, max_length=500)
|
||||
|
||||
|
||||
class RfidWriteJobResponse(UTCBaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
reader_id: int
|
||||
user_id: int
|
||||
card_id: Optional[int] = None
|
||||
label: str
|
||||
status: RfidWriteJobStatus
|
||||
requested_by_user_id: int
|
||||
card_uid: Optional[str] = None
|
||||
write_payload: Optional[str] = None
|
||||
claimed_at: Optional[datetime] = None
|
||||
completed_at: Optional[datetime] = None
|
||||
error_message: Optional[str] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class EspTimeResponse(UTCBaseModel):
|
||||
server_time_utc: datetime
|
||||
unix_ms: int
|
||||
poll_interval_seconds: int = 3
|
||||
|
||||
|
||||
class EspHeartbeatRequest(UTCBaseModel):
|
||||
mode: str = Field(..., max_length=50)
|
||||
message: Optional[str] = Field(None, max_length=255)
|
||||
wifi_rssi: Optional[int] = None
|
||||
free_heap: Optional[int] = None
|
||||
firmware_version: Optional[str] = Field(None, max_length=100)
|
||||
active_write_job_id: Optional[int] = None
|
||||
|
||||
|
||||
class EspHeartbeatResponse(UTCBaseModel):
|
||||
ok: bool
|
||||
server_time_utc: datetime
|
||||
unix_ms: int
|
||||
heartbeat_interval_seconds: int = 10
|
||||
time_poll_interval_seconds: int = 3
|
||||
write_job_poll_interval_seconds: int = 3
|
||||
|
||||
|
||||
class EspDashboardLoginResponse(UTCBaseModel):
|
||||
valid: bool
|
||||
user_id: Optional[int] = None
|
||||
role: Optional[UserRole] = None
|
||||
user_name: Optional[str] = None
|
||||
|
||||
|
||||
class RfidTapAdminResponse(UTCBaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
reader_id: int
|
||||
card_id: Optional[int] = None
|
||||
user_id: Optional[int] = None
|
||||
card_uid: str
|
||||
action: EspTapAction
|
||||
accepted: bool
|
||||
message: Optional[str] = None
|
||||
tapped_at: datetime
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class AttendanceSessionResponse(UTCBaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
user_id: int
|
||||
reader_id: int
|
||||
check_in_tap_id: int
|
||||
check_out_tap_id: Optional[int] = None
|
||||
checked_in_at: datetime
|
||||
checked_out_at: Optional[datetime] = None
|
||||
checkout_source: Optional[str] = None
|
||||
system_flag_reason: Optional[str] = None
|
||||
duration_seconds: Optional[int] = None
|
||||
is_open: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class StaleSessionCloseRequest(UTCBaseModel):
|
||||
cutoff_date: Optional[date] = None
|
||||
checkout_hour: int = Field(17, ge=0, le=23)
|
||||
|
||||
|
||||
class StaleSessionCloseResponse(UTCBaseModel):
|
||||
closed_count: int
|
||||
|
||||
Reference in New Issue
Block a user