feat: add unified linting scripts and git hooks for code quality enforcement
- Introduced `lint` and `lint-fix` applications in `flake.nix` for unified linting of backend (Python) and frontend (TypeScript/Svelte) code. - Added `scripts/lint.sh` for manual linting execution. - Created `scripts/install-hooks.sh` to set up git hooks for automatic linting before commits and optional tests before pushes. - Updated `README.md` with instructions for using the new linting features and git hooks.
This commit is contained in:
@@ -1,2 +1 @@
|
||||
"""Authentication module."""
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""JWT token generation and validation."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from jose import JWTError, jwt
|
||||
@@ -8,15 +8,15 @@ from jose import JWTError, jwt
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
def create_access_token(user_id: UUID, email: str, expires_delta: Optional[timedelta] = None) -> str:
|
||||
def create_access_token(user_id: UUID, email: str, expires_delta: timedelta | None = None) -> str:
|
||||
"""
|
||||
Create a new JWT access token.
|
||||
|
||||
|
||||
Args:
|
||||
user_id: User's UUID
|
||||
email: User's email address
|
||||
expires_delta: Optional custom expiration time
|
||||
|
||||
|
||||
Returns:
|
||||
Encoded JWT token string
|
||||
"""
|
||||
@@ -24,26 +24,20 @@ def create_access_token(user_id: UUID, email: str, expires_delta: Optional[timed
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
|
||||
to_encode = {
|
||||
"sub": str(user_id),
|
||||
"email": email,
|
||||
"exp": expire,
|
||||
"iat": datetime.utcnow(),
|
||||
"type": "access"
|
||||
}
|
||||
|
||||
|
||||
to_encode = {"sub": str(user_id), "email": email, "exp": expire, "iat": datetime.utcnow(), "type": "access"}
|
||||
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
def decode_access_token(token: str) -> Optional[dict]:
|
||||
def decode_access_token(token: str) -> dict | None:
|
||||
"""
|
||||
Decode and validate a JWT access token.
|
||||
|
||||
|
||||
Args:
|
||||
token: JWT token string to decode
|
||||
|
||||
|
||||
Returns:
|
||||
Decoded token payload if valid, None otherwise
|
||||
"""
|
||||
@@ -52,4 +46,3 @@ def decode_access_token(token: str) -> Optional[dict]:
|
||||
return payload
|
||||
except JWTError:
|
||||
return None
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
"""User repository for database operations."""
|
||||
from typing import Optional
|
||||
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.auth.security import hash_password
|
||||
@@ -16,7 +14,7 @@ class UserRepository:
|
||||
def __init__(self, db: Session):
|
||||
"""
|
||||
Initialize repository.
|
||||
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
"""
|
||||
@@ -25,48 +23,45 @@ class UserRepository:
|
||||
def create_user(self, email: str, password: str) -> User:
|
||||
"""
|
||||
Create a new user.
|
||||
|
||||
|
||||
Args:
|
||||
email: User email (will be lowercased)
|
||||
password: Plain text password (will be hashed)
|
||||
|
||||
|
||||
Returns:
|
||||
Created user instance
|
||||
"""
|
||||
email = email.lower()
|
||||
password_hash = hash_password(password)
|
||||
|
||||
user = User(
|
||||
email=email,
|
||||
password_hash=password_hash
|
||||
)
|
||||
|
||||
|
||||
user = User(email=email, password_hash=password_hash)
|
||||
|
||||
self.db.add(user)
|
||||
self.db.commit()
|
||||
self.db.refresh(user)
|
||||
|
||||
|
||||
return user
|
||||
|
||||
def get_user_by_email(self, email: str) -> Optional[User]:
|
||||
def get_user_by_email(self, email: str) -> User | None:
|
||||
"""
|
||||
Get user by email address.
|
||||
|
||||
|
||||
Args:
|
||||
email: User email to search for
|
||||
|
||||
|
||||
Returns:
|
||||
User if found, None otherwise
|
||||
"""
|
||||
email = email.lower()
|
||||
return self.db.query(User).filter(User.email == email).first()
|
||||
|
||||
def get_user_by_id(self, user_id: UUID) -> Optional[User]:
|
||||
def get_user_by_id(self, user_id: UUID) -> User | None:
|
||||
"""
|
||||
Get user by ID.
|
||||
|
||||
|
||||
Args:
|
||||
user_id: User UUID
|
||||
|
||||
|
||||
Returns:
|
||||
User if found, None otherwise
|
||||
"""
|
||||
@@ -75,13 +70,12 @@ class UserRepository:
|
||||
def email_exists(self, email: str) -> bool:
|
||||
"""
|
||||
Check if email already exists.
|
||||
|
||||
|
||||
Args:
|
||||
email: Email to check
|
||||
|
||||
|
||||
Returns:
|
||||
True if email exists, False otherwise
|
||||
"""
|
||||
email = email.lower()
|
||||
return self.db.query(User).filter(User.email == email).first() is not None
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Authentication schemas for request/response validation."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
@@ -42,4 +42,3 @@ class TokenResponse(BaseModel):
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
user: UserResponse
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Password hashing utilities using passlib."""
|
||||
|
||||
import re
|
||||
|
||||
from passlib.context import CryptContext
|
||||
|
||||
# Create password context for hashing and verification
|
||||
@@ -9,10 +11,10 @@ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
def hash_password(password: str) -> str:
|
||||
"""
|
||||
Hash a password using bcrypt.
|
||||
|
||||
|
||||
Args:
|
||||
password: Plain text password
|
||||
|
||||
|
||||
Returns:
|
||||
Hashed password string
|
||||
"""
|
||||
@@ -22,11 +24,11 @@ def hash_password(password: str) -> str:
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""
|
||||
Verify a plain password against a hashed password.
|
||||
|
||||
|
||||
Args:
|
||||
plain_password: Plain text password to verify
|
||||
hashed_password: Hashed password from database
|
||||
|
||||
|
||||
Returns:
|
||||
True if password matches, False otherwise
|
||||
"""
|
||||
@@ -36,30 +38,29 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
def validate_password_strength(password: str) -> tuple[bool, str]:
|
||||
"""
|
||||
Validate password meets complexity requirements.
|
||||
|
||||
|
||||
Requirements:
|
||||
- At least 8 characters
|
||||
- At least 1 uppercase letter
|
||||
- At least 1 lowercase letter
|
||||
- At least 1 number
|
||||
|
||||
|
||||
Args:
|
||||
password: Plain text password to validate
|
||||
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, error_message)
|
||||
"""
|
||||
if len(password) < 8:
|
||||
return False, "Password must be at least 8 characters long"
|
||||
|
||||
|
||||
if not re.search(r"[A-Z]", password):
|
||||
return False, "Password must contain at least one uppercase letter"
|
||||
|
||||
|
||||
if not re.search(r"[a-z]", password):
|
||||
return False, "Password must contain at least one lowercase letter"
|
||||
|
||||
|
||||
if not re.search(r"\d", password):
|
||||
return False, "Password must contain at least one number"
|
||||
|
||||
return True, ""
|
||||
|
||||
return True, ""
|
||||
|
||||
Reference in New Issue
Block a user