Skip to content

Commit 2e72f5b

Browse files
committed
first commit
0 parents  commit 2e72f5b

24 files changed

+796
-0
lines changed

.env.example

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Security Settings
2+
SECRET_KEY=change_me_dev_secret_key
3+
SESSION_SALT=xYzDeV@0000
4+
PBKDF2_ITERATIONS=600_000
5+
PBKDF2_ALGORITHM=sha256
6+
MAX_FINGERPRINT_VALUE=100000000
7+
BCRYPT_GENSALT=12
8+
9+
# CORS (comma-separated list)
10+
ALLOWED_CORS_ORIGINS=*
11+
12+
# CSRF Settings
13+
CSRF_COOKIE_NAME=XSRF-TOKEN
14+
CSRF_COOKIE_SECURE=false
15+
16+
# Database Settings
17+
DATABASE_DSN=postgresql://postgres:password@localhost:5432/app_db
18+
DATABASE_MIN_SIZE=4
19+
DATABASE_MAX_SIZE=16
20+
DATABASE_MAX_QUERIES=50000
21+
DATABASE_MAX_INACTIVE_CONNECTION_LIFETIME=300.0
22+
23+
# JWT Settings
24+
JWT_ALGORITHM=HS256

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
__pycache__
2+
.idea
3+
.env
4+
.venv
5+
venv
6+
.DS_Store
7+
.ruff_cache
8+
.vscode
9+
poetry.lock

README.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
## Litestar Auth
2+
3+
### 1. Prerequisites
4+
5+
* Python 3.13+
6+
* [Poetry](https://python-poetry.org/) (dependency manager)
7+
8+
Poetry installation (if you don't have it):
9+
10+
```bash
11+
curl -sSL https://install.python-poetry.org | python3 -
12+
```
13+
14+
### 2. Environment variables (.env)
15+
16+
Copy the `.env.example` file to `.env` and change the values as needed:
17+
18+
```bash
19+
cp .env.example .env
20+
```
21+
22+
### 3. Install dependencies
23+
24+
```bash
25+
poetry install
26+
```
27+
28+
Configure pre-commit hooks:
29+
30+
```bash
31+
pre-commit install --config pre-commit.yaml
32+
```
33+
34+
Activate the virtual shell
35+
36+
```bash
37+
poetry shell
38+
```
39+
40+
### 4. Database
41+
42+
Create the database (adjust the name according to your DSN):
43+
44+
```bash
45+
createdb db
46+
```
47+
48+
Apply manual SQL migrations (e.g.: content in `src/db/migrations/`).
49+
50+
### 5. Run the server
51+
52+
Basic command:
53+
54+
```bash
55+
litestar run
56+
```
57+
58+
Examples with options:
59+
60+
```bash
61+
litestar run -r -P -d -p 9000 -H 0.0.0.0
62+
litestar run --wc 4
63+
litestar run -h
64+
```
65+
66+
### 6. OpenAPI / ReDoc
67+
68+
Access at:
69+
70+
```
71+
http://localhost:PORT/schema/redoc
72+
```
73+
74+
Replace `PORT` with the port used (default 8000 if not specified).
75+
76+
---
77+
78+
**More features:** [litestar-asyncpg](https://github.com/YuriFontella/litestar-asyncpg)

app.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from litestar import Litestar
2+
from src.server.core import ApplicationCore
3+
import logging
4+
5+
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(name)s %(message)s')
6+
7+
8+
def create_app() -> Litestar:
9+
return Litestar(plugins=[ApplicationCore()])
10+
11+
12+
app = create_app()

pre-commit.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
repos:
2+
# Black
3+
- repo: https://github.com/psf/black
4+
rev: 25.1.0
5+
hooks:
6+
- id: black
7+
8+
# Ruff
9+
- repo: https://github.com/astral-sh/ruff-pre-commit
10+
rev: v0.12.12
11+
hooks:
12+
- id: ruff
13+
args: [--fix]
14+
- id: ruff-format

pyproject.toml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[project]
2+
name = "litestar-auth-api"
3+
version = "0.1.0"
4+
description = ""
5+
authors = [
6+
{name = "Yuri Fontella", email = "yurifc4@gmail.com"}
7+
]
8+
requires-python = ">=3.13,<4.0"
9+
dependencies = [
10+
"litestar[standard] (>=2.17.0,<3.0.0)",
11+
"litestar-asyncpg (>=0.5.0,<1.0.0)",
12+
"pyjwt (>=2.10.1,<3.0.0)",
13+
"bcrypt (>=4.3.0,<5.0.0)",
14+
"python-dotenv (>=1.1.1,<2.0.0)",
15+
"dnspython (>=2.8.0,<3.0.0)",
16+
"email-validator (>=2.3.0,<3.0.0)",
17+
]
18+
19+
[tool.poetry]
20+
packages = [{ include = "src" }]
21+
22+
[tool.poetry.group.dev.dependencies]
23+
black = "^25.1.0"
24+
ruff = "^0.12.12"
25+
pre-commit = "^4.3.0"
26+
27+
[build-system]
28+
requires = ["poetry-core>=2.2.0,<3.0.0"]
29+
build-backend = "poetry.core.masonry.api"

src/config/app.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from typing import Literal, Tuple
2+
3+
from litestar.config.compression import CompressionConfig
4+
from litestar.config.cors import CORSConfig
5+
from litestar.config.csrf import CSRFConfig
6+
from litestar.middleware.rate_limit import RateLimitConfig
7+
from litestar_asyncpg import AsyncpgConfig, PoolConfig
8+
9+
from src.config.base import get_settings
10+
11+
settings = get_settings()
12+
13+
csrf = CSRFConfig(
14+
secret=settings.app.SECRET_KEY,
15+
cookie_name=settings.app.CSRF_COOKIE_NAME,
16+
cookie_secure=settings.app.CSRF_COOKIE_SECURE,
17+
safe_methods={
18+
"POST",
19+
"GET",
20+
"OPTIONS",
21+
},
22+
)
23+
24+
cors = CORSConfig(
25+
allow_origins=settings.app.ALLOWED_CORS_ORIGINS,
26+
allow_methods=[
27+
"GET",
28+
"POST",
29+
"DELETE",
30+
"PUT",
31+
"PATCH",
32+
"OPTIONS",
33+
],
34+
allow_headers=["Origin", "Content-Type", "X-CSRFToken", "X-Access-Token"],
35+
allow_credentials=True,
36+
)
37+
38+
compression = CompressionConfig(backend="gzip", gzip_compress_level=9)
39+
40+
rate_limit: Tuple[Literal["second"], int] = ("second", 10)
41+
rate_limit_config = RateLimitConfig(rate_limit=rate_limit, exclude=["/schema"])
42+
43+
asyncpg = AsyncpgConfig(
44+
pool_config=PoolConfig(
45+
dsn=settings.db.DSN,
46+
min_size=settings.db.MIN_SIZE,
47+
max_size=settings.db.MAX_SIZE,
48+
max_queries=settings.db.MAX_QUERIES,
49+
max_inactive_connection_lifetime=settings.db.MAX_INACTIVE_CONNECTION_LIFETIME,
50+
)
51+
)

src/config/base.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import os
2+
from typing import Optional, List
3+
4+
from dataclasses import dataclass, field
5+
from functools import lru_cache
6+
7+
8+
@dataclass
9+
class DatabaseSettings:
10+
DSN: Optional[str] = None
11+
MIN_SIZE: int = 4
12+
MAX_SIZE: int = 16
13+
MAX_QUERIES: int = 50000
14+
MAX_INACTIVE_CONNECTION_LIFETIME: float = 300.0
15+
16+
def __post_init__(self):
17+
self.DSN = self.DSN or os.getenv("DATABASE_DSN")
18+
self.MIN_SIZE = int(os.getenv("DATABASE_MIN_SIZE", self.MIN_SIZE))
19+
self.MAX_SIZE = int(os.getenv("DATABASE_MAX_SIZE", self.MAX_SIZE))
20+
self.MAX_QUERIES = int(os.getenv("DATABASE_MAX_QUERIES", self.MAX_QUERIES))
21+
self.MAX_INACTIVE_CONNECTION_LIFETIME = float(
22+
os.getenv(
23+
"DATABASE_MAX_INACTIVE_CONNECTION_LIFETIME",
24+
self.MAX_INACTIVE_CONNECTION_LIFETIME,
25+
)
26+
)
27+
28+
29+
@dataclass
30+
class AppSettings:
31+
SECRET_KEY: Optional[str] = None
32+
ALLOWED_CORS_ORIGINS: List[str] = field(default_factory=list)
33+
CSRF_COOKIE_NAME: str = "XSRF-TOKEN"
34+
CSRF_COOKIE_SECURE: bool = False
35+
JWT_ALGORITHM: str = "HS256"
36+
SESSION_SALT: Optional[str] = None
37+
PBKDF2_ITERATIONS: int = 600_000
38+
PBKDF2_ALGORITHM: str = "sha256"
39+
MAX_FINGERPRINT_VALUE: int = 100_000_000
40+
BCRYPT_GENSALT: int = 12
41+
42+
def __post_init__(self):
43+
self.SECRET_KEY = self.SECRET_KEY or os.getenv("SECRET_KEY")
44+
self.SESSION_SALT = self.SESSION_SALT or os.getenv("SESSION_SALT")
45+
self.PBKDF2_ITERATIONS = int(os.getenv("PBKDF2_ITERATIONS", self.PBKDF2_ITERATIONS))
46+
self.PBKDF2_ALGORITHM = os.getenv("PBKDF2_ALGORITHM", self.PBKDF2_ALGORITHM)
47+
self.MAX_FINGERPRINT_VALUE = int(os.getenv("MAX_FINGERPRINT_VALUE", self.MAX_FINGERPRINT_VALUE))
48+
self.BCRYPT_GENSALT = int(os.getenv("BCRYPT_GENSALT", self.BCRYPT_GENSALT))
49+
50+
if not self.ALLOWED_CORS_ORIGINS:
51+
cors_origins = os.getenv("ALLOWED_CORS_ORIGINS")
52+
if cors_origins:
53+
self.ALLOWED_CORS_ORIGINS = [
54+
origin.strip() for origin in cors_origins.split(",")
55+
]
56+
else:
57+
self.ALLOWED_CORS_ORIGINS = ["*"]
58+
59+
self.CSRF_COOKIE_NAME = os.getenv("CSRF_COOKIE_NAME", self.CSRF_COOKIE_NAME)
60+
self.CSRF_COOKIE_SECURE = os.getenv("CSRF_COOKIE_SECURE", "").lower() in (
61+
"true",
62+
"1",
63+
"yes",
64+
)
65+
self.JWT_ALGORITHM = os.getenv("JWT_ALGORITHM", self.JWT_ALGORITHM)
66+
67+
68+
@dataclass
69+
class Settings:
70+
app: AppSettings = field(default_factory=AppSettings)
71+
db: DatabaseSettings = field(default_factory=DatabaseSettings)
72+
73+
@classmethod
74+
def from_env(cls, dotenv_filename: str = ".env") -> "Settings":
75+
from pathlib import Path
76+
from dotenv import load_dotenv
77+
78+
env_file = Path(f"{os.curdir}/{dotenv_filename}")
79+
if env_file.is_file():
80+
load_dotenv(env_file, override=True)
81+
return Settings()
82+
83+
84+
@lru_cache(maxsize=1, typed=True)
85+
def get_settings() -> Settings:
86+
return Settings.from_env()

src/config/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from pathlib import Path
2+
3+
4+
MIGRATIONS_DIR = Path(__file__).resolve().parent.parent / "db" / "migrations"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
2+

0 commit comments

Comments
 (0)