Changed module name from my_auth to myauth
Changed encryption algorithm to argon2 Added unit tests
This commit is contained in:
6
src/myauth/persistence/__init__.py
Normal file
6
src/myauth/persistence/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .sqlite import UserRepository, TokenRepository
|
||||
|
||||
__all__ = [
|
||||
"UserRepository",
|
||||
"TokenRepository",
|
||||
]
|
||||
226
src/myauth/persistence/base.py
Normal file
226
src/myauth/persistence/base.py
Normal file
@@ -0,0 +1,226 @@
|
||||
"""
|
||||
Abstract base classes for persistence layer.
|
||||
|
||||
This module defines the interfaces that all database implementations must
|
||||
follow. It provides abstract base classes for user and token repositories,
|
||||
ensuring consistency across different database backends (MongoDB, SQLite,
|
||||
PostgreSQL, custom engines, etc.).
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from ..models.token import TokenData
|
||||
from ..models.user import UserCreate, UserInDB, UserUpdate
|
||||
|
||||
|
||||
class UserRepository(ABC):
|
||||
"""
|
||||
Abstract base class for user persistence operations.
|
||||
|
||||
This interface defines all methods required for user management in the
|
||||
database. Concrete implementations must provide all these methods for
|
||||
their specific database backend.
|
||||
|
||||
All methods should raise appropriate exceptions (UserNotFoundError,
|
||||
UserAlreadyExistsError, etc.) when operations fail.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def create_user(self, user_data: UserCreate, hashed_password: str) -> UserInDB:
|
||||
"""
|
||||
Create a new user in the database.
|
||||
|
||||
Args:
|
||||
user_data: User information from registration.
|
||||
hashed_password: Pre-hashed password (hashing is done by the service layer).
|
||||
|
||||
Returns:
|
||||
The created user with database-assigned ID and timestamps.
|
||||
|
||||
Raises:
|
||||
UserAlreadyExistsError: If email already exists in database.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_user_by_email(self, email: str) -> UserInDB | None:
|
||||
"""
|
||||
Retrieve a user by their email address.
|
||||
|
||||
Args:
|
||||
email: The email address to search for.
|
||||
|
||||
Returns:
|
||||
The user if found, None otherwise.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_user_by_id(self, user_id: str) -> UserInDB | None:
|
||||
"""
|
||||
Retrieve a user by their unique identifier.
|
||||
|
||||
Args:
|
||||
user_id: The unique user identifier.
|
||||
|
||||
Returns:
|
||||
The user if found, None otherwise.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update_user(self, user_id: str, updates: UserUpdate) -> UserInDB:
|
||||
"""
|
||||
Update an existing user's information.
|
||||
|
||||
Only fields present in the updates object (non-None) should be updated.
|
||||
The updated_at timestamp should be automatically set to the current time.
|
||||
|
||||
Args:
|
||||
user_id: The unique user identifier.
|
||||
updates: Pydantic model containing fields to update.
|
||||
|
||||
Returns:
|
||||
The updated user.
|
||||
|
||||
Raises:
|
||||
UserNotFoundError: If user does not exist.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete_user(self, user_id: str) -> bool:
|
||||
"""
|
||||
Delete a user from the database.
|
||||
|
||||
Implementation can choose between soft delete (setting is_active=False)
|
||||
or hard delete (removing from database). The choice depends on the
|
||||
application's requirements for data retention.
|
||||
|
||||
Args:
|
||||
user_id: The unique user identifier.
|
||||
|
||||
Returns:
|
||||
True if user was deleted, False if user was not found.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def email_exists(self, email: str) -> bool:
|
||||
"""
|
||||
Check if an email address is already registered.
|
||||
|
||||
This is useful for validating registration requests without
|
||||
retrieving the full user object.
|
||||
|
||||
Args:
|
||||
email: The email address to check.
|
||||
|
||||
Returns:
|
||||
True if email exists, False otherwise.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class TokenRepository(ABC):
|
||||
"""
|
||||
Abstract base class for token persistence operations.
|
||||
|
||||
This interface defines all methods required for token management in the
|
||||
database. It handles both refresh tokens and password reset tokens using
|
||||
a discriminator field (token_type).
|
||||
|
||||
All methods should raise appropriate exceptions (InvalidTokenError,
|
||||
ExpiredTokenError, etc.) when operations fail.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def save_token(self, token_data: TokenData) -> None:
|
||||
"""
|
||||
Save a token to the database.
|
||||
|
||||
This is used for both refresh tokens and password reset tokens.
|
||||
The token_type field in TokenData distinguishes between them.
|
||||
|
||||
Args:
|
||||
token_data: Complete token information including type, user_id, expiration.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_token(self, token: str, token_type: str) -> TokenData | None:
|
||||
"""
|
||||
Retrieve a token from the database.
|
||||
|
||||
Args:
|
||||
token: The token string to search for.
|
||||
token_type: Type of token ("refresh" or "password_reset").
|
||||
|
||||
Returns:
|
||||
The token data if found, None otherwise.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def revoke_token(self, token: str) -> bool:
|
||||
"""
|
||||
Revoke a specific token.
|
||||
|
||||
This sets the is_revoked flag to True, preventing the token from
|
||||
being used again without physically deleting it (useful for audit trails).
|
||||
|
||||
Args:
|
||||
token: The token string to revoke.
|
||||
|
||||
Returns:
|
||||
True if token was revoked, False if token was not found.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def revoke_all_user_tokens(self, user_id: str, token_type: str) -> int:
|
||||
"""
|
||||
Revoke all tokens of a specific type for a user.
|
||||
|
||||
This is useful for logout-all-devices functionality or when a user's
|
||||
password is changed (invalidating all refresh tokens).
|
||||
|
||||
Args:
|
||||
user_id: The user whose tokens should be revoked.
|
||||
token_type: Type of tokens to revoke ("refresh" or "password_reset").
|
||||
|
||||
Returns:
|
||||
Number of tokens revoked.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete_expired_tokens(self) -> int:
|
||||
"""
|
||||
Delete all expired tokens from the database.
|
||||
|
||||
This maintenance operation should be called periodically to keep
|
||||
the tokens collection clean. It permanently removes tokens that
|
||||
have passed their expiration time.
|
||||
|
||||
Returns:
|
||||
Number of tokens deleted.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def is_token_valid(self, token: str, token_type: str) -> bool:
|
||||
"""
|
||||
Check if a token is valid (exists, not revoked, not expired).
|
||||
|
||||
This is a convenience method that combines multiple checks into
|
||||
a single boolean result.
|
||||
|
||||
Args:
|
||||
token: The token string to validate.
|
||||
token_type: Type of token ("refresh" or "password_reset").
|
||||
|
||||
Returns:
|
||||
True if token is valid and can be used, False otherwise.
|
||||
"""
|
||||
pass
|
||||
611
src/myauth/persistence/sqlite.py
Normal file
611
src/myauth/persistence/sqlite.py
Normal file
@@ -0,0 +1,611 @@
|
||||
"""
|
||||
SQLite implementation of persistence layer.
|
||||
|
||||
This module provides SQLite database implementations for user and token
|
||||
repositories. It uses the standard library sqlite3 module and handles
|
||||
JSON serialization for complex fields.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from .base import UserRepository, TokenRepository
|
||||
from ..models.user import UserCreate, UserInDB, UserUpdate
|
||||
from ..models.token import TokenData
|
||||
from ..exceptions import UserAlreadyExistsError, UserNotFoundError
|
||||
|
||||
|
||||
class SQLiteUserRepository(UserRepository):
|
||||
"""
|
||||
SQLite implementation of UserRepository.
|
||||
|
||||
This implementation uses sqlite3 to manage user data. JSON fields
|
||||
(roles, user_settings) are serialized/deserialized automatically.
|
||||
The database schema is created automatically on initialization.
|
||||
|
||||
Attributes:
|
||||
db_path: Path to the SQLite database file.
|
||||
"""
|
||||
|
||||
def __init__(self, db_path: str):
|
||||
"""
|
||||
Initialize SQLite user repository.
|
||||
|
||||
Creates the users table and indexes if they don't exist.
|
||||
|
||||
Args:
|
||||
db_path: Path to the SQLite database file.
|
||||
"""
|
||||
self.db_path = db_path
|
||||
self._create_tables()
|
||||
|
||||
def _create_tables(self) -> None:
|
||||
"""
|
||||
Create users table and indexes if they don't exist.
|
||||
|
||||
The table schema includes:
|
||||
- id: Primary key (UUID string)
|
||||
- email: Unique, indexed for fast lookups
|
||||
- username: Non-unique display name
|
||||
- hashed_password: Bcrypt hash
|
||||
- roles: JSON array of role strings
|
||||
- user_settings: JSON object for custom settings
|
||||
- is_verified: Boolean (stored as INTEGER)
|
||||
- is_active: Boolean (stored as INTEGER)
|
||||
- created_at: ISO format timestamp
|
||||
- updated_at: ISO format timestamp
|
||||
"""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Create users table
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS users
|
||||
(
|
||||
id
|
||||
TEXT
|
||||
PRIMARY
|
||||
KEY,
|
||||
email
|
||||
TEXT
|
||||
UNIQUE
|
||||
NOT
|
||||
NULL,
|
||||
username
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
hashed_password
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
roles
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
user_settings
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
is_verified
|
||||
INTEGER
|
||||
NOT
|
||||
NULL
|
||||
DEFAULT
|
||||
0,
|
||||
is_active
|
||||
INTEGER
|
||||
NOT
|
||||
NULL
|
||||
DEFAULT
|
||||
1,
|
||||
created_at
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
updated_at
|
||||
TEXT
|
||||
NOT
|
||||
NULL
|
||||
)
|
||||
""")
|
||||
|
||||
# Create index on email for fast lookups
|
||||
cursor.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email
|
||||
ON users(email)
|
||||
""")
|
||||
|
||||
conn.commit()
|
||||
|
||||
def _row_to_user(self, row: tuple) -> UserInDB:
|
||||
"""
|
||||
Convert a database row to a UserInDB model.
|
||||
|
||||
Args:
|
||||
row: Database row tuple.
|
||||
|
||||
Returns:
|
||||
UserInDB model instance.
|
||||
"""
|
||||
return UserInDB(
|
||||
id=row[0],
|
||||
email=row[1],
|
||||
username=row[2],
|
||||
hashed_password=row[3],
|
||||
roles=json.loads(row[4]),
|
||||
user_settings=json.loads(row[5]),
|
||||
is_verified=bool(row[6]),
|
||||
is_active=bool(row[7]),
|
||||
created_at=datetime.fromisoformat(row[8]),
|
||||
updated_at=datetime.fromisoformat(row[9])
|
||||
)
|
||||
|
||||
def create_user(self, user_data: UserCreate, hashed_password: str) -> UserInDB:
|
||||
"""
|
||||
Create a new user in the database.
|
||||
|
||||
Args:
|
||||
user_data: User information from registration.
|
||||
hashed_password: Pre-hashed password.
|
||||
|
||||
Returns:
|
||||
The created user with generated ID and timestamps.
|
||||
|
||||
Raises:
|
||||
UserAlreadyExistsError: If email already exists.
|
||||
"""
|
||||
if self.email_exists(user_data.email):
|
||||
raise UserAlreadyExistsError(f"User with email {user_data.email} already exists")
|
||||
|
||||
user_id = str(uuid4())
|
||||
now = datetime.now()
|
||||
|
||||
user = UserInDB(
|
||||
id=user_id,
|
||||
email=user_data.email,
|
||||
username=user_data.username,
|
||||
hashed_password=hashed_password,
|
||||
roles=user_data.roles,
|
||||
user_settings=user_data.user_settings,
|
||||
is_verified=False,
|
||||
is_active=True,
|
||||
created_at=now,
|
||||
updated_at=now
|
||||
)
|
||||
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
INSERT INTO users (id, email, username, hashed_password, roles,
|
||||
user_settings, is_verified, is_active, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
user.id,
|
||||
user.email,
|
||||
user.username,
|
||||
user.hashed_password,
|
||||
json.dumps(user.roles),
|
||||
json.dumps(user.user_settings),
|
||||
int(user.is_verified),
|
||||
int(user.is_active),
|
||||
user.created_at.isoformat(),
|
||||
user.updated_at.isoformat()
|
||||
))
|
||||
conn.commit()
|
||||
|
||||
return user
|
||||
|
||||
def get_user_by_email(self, email: str) -> Optional[UserInDB]:
|
||||
"""
|
||||
Retrieve a user by their email address.
|
||||
|
||||
Args:
|
||||
email: The email address to search for.
|
||||
|
||||
Returns:
|
||||
The user if found, None otherwise.
|
||||
"""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT id,
|
||||
email,
|
||||
username,
|
||||
hashed_password,
|
||||
roles,
|
||||
user_settings,
|
||||
is_verified,
|
||||
is_active,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM users
|
||||
WHERE email = ?
|
||||
""", (email,))
|
||||
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return self._row_to_user(row)
|
||||
return None
|
||||
|
||||
def get_user_by_id(self, user_id: str) -> Optional[UserInDB]:
|
||||
"""
|
||||
Retrieve a user by their unique identifier.
|
||||
|
||||
Args:
|
||||
user_id: The unique user identifier.
|
||||
|
||||
Returns:
|
||||
The user if found, None otherwise.
|
||||
"""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT id,
|
||||
email,
|
||||
username,
|
||||
hashed_password,
|
||||
roles,
|
||||
user_settings,
|
||||
is_verified,
|
||||
is_active,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM users
|
||||
WHERE id = ?
|
||||
""", (user_id,))
|
||||
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return self._row_to_user(row)
|
||||
return None
|
||||
|
||||
def update_user(self, user_id: str, updates: UserUpdate) -> UserInDB:
|
||||
"""
|
||||
Update an existing user's information.
|
||||
|
||||
Only non-None fields in the updates object are applied.
|
||||
The updated_at timestamp is automatically set to current time.
|
||||
|
||||
Args:
|
||||
user_id: The unique user identifier.
|
||||
updates: Pydantic model containing fields to update.
|
||||
|
||||
Returns:
|
||||
The updated user.
|
||||
|
||||
Raises:
|
||||
UserNotFoundError: If user does not exist.
|
||||
"""
|
||||
user = self.get_user_by_id(user_id)
|
||||
if not user:
|
||||
raise UserNotFoundError(f"User with id {user_id} not found")
|
||||
|
||||
# Build update query dynamically based on non-None fields
|
||||
update_fields = []
|
||||
update_values = []
|
||||
|
||||
update_data = updates.model_dump(exclude_unset=True)
|
||||
|
||||
for field, value in update_data.items():
|
||||
if value is not None:
|
||||
if field == "roles":
|
||||
update_fields.append("roles = ?")
|
||||
update_values.append(json.dumps(value))
|
||||
elif field == "user_settings":
|
||||
update_fields.append("user_settings = ?")
|
||||
update_values.append(json.dumps(value))
|
||||
elif field == "password":
|
||||
# Password should be hashed before calling this method
|
||||
# This field should contain hashed_password value
|
||||
update_fields.append("hashed_password = ?")
|
||||
update_values.append(value)
|
||||
elif field in ["is_verified", "is_active"]:
|
||||
update_fields.append(f"{field} = ?")
|
||||
update_values.append(int(value))
|
||||
else:
|
||||
update_fields.append(f"{field} = ?")
|
||||
update_values.append(value)
|
||||
|
||||
if not update_fields:
|
||||
return user
|
||||
|
||||
# Always update the updated_at timestamp
|
||||
update_fields.append("updated_at = ?")
|
||||
update_values.append(datetime.now().isoformat())
|
||||
|
||||
# Add user_id for WHERE clause
|
||||
update_values.append(user_id)
|
||||
|
||||
query = f"UPDATE users SET {', '.join(update_fields)} WHERE id = ?"
|
||||
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(query, update_values)
|
||||
conn.commit()
|
||||
|
||||
# Return updated user
|
||||
updated_user = self.get_user_by_id(user_id)
|
||||
if not updated_user:
|
||||
raise UserNotFoundError(f"User with id {user_id} not found after update")
|
||||
|
||||
return updated_user
|
||||
|
||||
def delete_user(self, user_id: str) -> bool:
|
||||
"""
|
||||
Delete a user from the database (hard delete).
|
||||
|
||||
Args:
|
||||
user_id: The unique user identifier.
|
||||
|
||||
Returns:
|
||||
True if user was deleted, False if user was not found.
|
||||
"""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM users WHERE id = ?", (user_id,))
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def email_exists(self, email: str) -> bool:
|
||||
"""
|
||||
Check if an email address is already registered.
|
||||
|
||||
Args:
|
||||
email: The email address to check.
|
||||
|
||||
Returns:
|
||||
True if email exists, False otherwise.
|
||||
"""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT 1 FROM users WHERE email = ? LIMIT 1", (email,))
|
||||
return cursor.fetchone() is not None
|
||||
|
||||
|
||||
class SQLiteTokenRepository(TokenRepository):
|
||||
"""
|
||||
SQLite implementation of TokenRepository.
|
||||
|
||||
This implementation manages both refresh tokens and password reset tokens
|
||||
in a single table with a discriminator field (token_type).
|
||||
|
||||
Attributes:
|
||||
db_path: Path to the SQLite database file.
|
||||
"""
|
||||
|
||||
def __init__(self, db_path: str):
|
||||
"""
|
||||
Initialize SQLite token repository.
|
||||
|
||||
Creates the tokens table and indexes if they don't exist.
|
||||
|
||||
Args:
|
||||
db_path: Path to the SQLite database file.
|
||||
"""
|
||||
self.db_path = db_path
|
||||
self._create_tables()
|
||||
|
||||
def _create_tables(self) -> None:
|
||||
"""
|
||||
Create tokens table and indexes if they don't exist.
|
||||
|
||||
The table schema includes:
|
||||
- token: Primary key (random secure string)
|
||||
- token_type: Discriminator ("refresh" or "password_reset")
|
||||
- user_id: Foreign key to users
|
||||
- expires_at: ISO format timestamp
|
||||
- created_at: ISO format timestamp
|
||||
- is_revoked: Boolean (stored as INTEGER)
|
||||
|
||||
Indexes are created for efficient queries.
|
||||
"""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Create tokens table
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS tokens
|
||||
(
|
||||
token
|
||||
TEXT
|
||||
PRIMARY
|
||||
KEY,
|
||||
token_type
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
user_id
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
expires_at
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
created_at
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
is_revoked
|
||||
INTEGER
|
||||
NOT
|
||||
NULL
|
||||
DEFAULT
|
||||
0
|
||||
)
|
||||
""")
|
||||
|
||||
# Create index on user_id for revoking all user tokens
|
||||
cursor.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_tokens_user_id
|
||||
ON tokens(user_id)
|
||||
""")
|
||||
|
||||
# Create composite index on token_type and user_id
|
||||
cursor.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_tokens_type_user
|
||||
ON tokens(token_type, user_id)
|
||||
""")
|
||||
|
||||
# Create index on expires_at for cleanup operations
|
||||
cursor.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_tokens_expires_at
|
||||
ON tokens(expires_at)
|
||||
""")
|
||||
|
||||
conn.commit()
|
||||
|
||||
def _row_to_token(self, row: tuple) -> TokenData:
|
||||
"""
|
||||
Convert a database row to a TokenData model.
|
||||
|
||||
Args:
|
||||
row: Database row tuple.
|
||||
|
||||
Returns:
|
||||
TokenData model instance.
|
||||
"""
|
||||
return TokenData(
|
||||
token=row[0],
|
||||
token_type=row[1],
|
||||
user_id=row[2],
|
||||
expires_at=datetime.fromisoformat(row[3]),
|
||||
created_at=datetime.fromisoformat(row[4]),
|
||||
is_revoked=bool(row[5])
|
||||
)
|
||||
|
||||
def save_token(self, token_data: TokenData) -> None:
|
||||
"""
|
||||
Save a token to the database.
|
||||
|
||||
Args:
|
||||
token_data: Complete token information.
|
||||
"""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
INSERT INTO tokens (token, token_type, user_id, expires_at, created_at, is_revoked)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
token_data.token,
|
||||
token_data.token_type,
|
||||
token_data.user_id,
|
||||
token_data.expires_at.isoformat(),
|
||||
token_data.created_at.isoformat(),
|
||||
int(token_data.is_revoked)
|
||||
))
|
||||
conn.commit()
|
||||
|
||||
def get_token(self, token: str, token_type: str) -> Optional[TokenData]:
|
||||
"""
|
||||
Retrieve a token from the database.
|
||||
|
||||
Args:
|
||||
token: The token string to search for.
|
||||
token_type: Type of token ("refresh" or "password_reset").
|
||||
|
||||
Returns:
|
||||
The token data if found, None otherwise.
|
||||
"""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT token, token_type, user_id, expires_at, created_at, is_revoked
|
||||
FROM tokens
|
||||
WHERE token = ?
|
||||
AND token_type = ?
|
||||
""", (token, token_type))
|
||||
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return self._row_to_token(row)
|
||||
return None
|
||||
|
||||
def revoke_token(self, token: str) -> bool:
|
||||
"""
|
||||
Revoke a specific token.
|
||||
|
||||
Args:
|
||||
token: The token string to revoke.
|
||||
|
||||
Returns:
|
||||
True if token was revoked, False if token was not found.
|
||||
"""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
UPDATE tokens
|
||||
SET is_revoked = 1
|
||||
WHERE token = ?
|
||||
""", (token,))
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def revoke_all_user_tokens(self, user_id: str, token_type: str) -> int:
|
||||
"""
|
||||
Revoke all tokens of a specific type for a user.
|
||||
|
||||
Args:
|
||||
user_id: The user whose tokens should be revoked.
|
||||
token_type: Type of tokens to revoke.
|
||||
|
||||
Returns:
|
||||
Number of tokens revoked.
|
||||
"""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
UPDATE tokens
|
||||
SET is_revoked = 1
|
||||
WHERE user_id = ?
|
||||
AND token_type = ?
|
||||
""", (user_id, token_type))
|
||||
conn.commit()
|
||||
return cursor.rowcount
|
||||
|
||||
def delete_expired_tokens(self) -> int:
|
||||
"""
|
||||
Delete all expired tokens from the database.
|
||||
|
||||
Returns:
|
||||
Number of tokens deleted.
|
||||
"""
|
||||
now = datetime.now().isoformat()
|
||||
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
DELETE
|
||||
FROM tokens
|
||||
WHERE expires_at < ?
|
||||
""", (now,))
|
||||
conn.commit()
|
||||
return cursor.rowcount
|
||||
|
||||
def is_token_valid(self, token: str, token_type: str) -> bool:
|
||||
"""
|
||||
Check if a token is valid (exists, not revoked, not expired).
|
||||
|
||||
Args:
|
||||
token: The token string to validate.
|
||||
token_type: Type of token.
|
||||
|
||||
Returns:
|
||||
True if token is valid, False otherwise.
|
||||
"""
|
||||
token_data = self.get_token(token, token_type)
|
||||
|
||||
if not token_data:
|
||||
return False
|
||||
|
||||
if token_data.is_revoked:
|
||||
return False
|
||||
|
||||
if token_data.expires_at < datetime.now():
|
||||
return False
|
||||
|
||||
return True
|
||||
Reference in New Issue
Block a user