belgie-core 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- belgie_core/__init__.py +65 -0
- belgie_core/core/__init__.py +0 -0
- belgie_core/core/belgie.py +407 -0
- belgie_core/core/client.py +250 -0
- belgie_core/core/exceptions.py +26 -0
- belgie_core/core/hooks.py +87 -0
- belgie_core/core/protocols.py +19 -0
- belgie_core/core/settings.py +100 -0
- belgie_core/providers/__init__.py +10 -0
- belgie_core/providers/google.py +284 -0
- belgie_core/providers/protocols.py +43 -0
- belgie_core/py.typed +0 -0
- belgie_core/session/__init__.py +3 -0
- belgie_core/session/manager.py +168 -0
- belgie_core/utils/__init__.py +9 -0
- belgie_core/utils/crypto.py +10 -0
- belgie_core/utils/scopes.py +49 -0
- belgie_core-0.1.0.dist-info/METADATA +13 -0
- belgie_core-0.1.0.dist-info/RECORD +20 -0
- belgie_core-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from uuid import UUID
|
|
3
|
+
|
|
4
|
+
from belgie_proto import (
|
|
5
|
+
AccountProtocol,
|
|
6
|
+
AdapterProtocol,
|
|
7
|
+
DBConnection,
|
|
8
|
+
OAuthStateProtocol,
|
|
9
|
+
SessionProtocol,
|
|
10
|
+
UserProtocol,
|
|
11
|
+
)
|
|
12
|
+
from fastapi import HTTPException, Request, Response, status
|
|
13
|
+
from fastapi.security import SecurityScopes
|
|
14
|
+
|
|
15
|
+
from belgie_core.core.hooks import HookContext, HookRunner, Hooks
|
|
16
|
+
from belgie_core.core.settings import CookieSettings
|
|
17
|
+
from belgie_core.session.manager import SessionManager
|
|
18
|
+
from belgie_core.utils.scopes import validate_scopes
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
22
|
+
class BelgieClient[
|
|
23
|
+
UserT: UserProtocol,
|
|
24
|
+
AccountT: AccountProtocol,
|
|
25
|
+
SessionT: SessionProtocol,
|
|
26
|
+
OAuthStateT: OAuthStateProtocol,
|
|
27
|
+
]:
|
|
28
|
+
"""Client for authentication operations with injected database session.
|
|
29
|
+
|
|
30
|
+
This class provides authentication methods with a captured database session,
|
|
31
|
+
allowing for convenient auth operations without explicitly passing db to each method.
|
|
32
|
+
|
|
33
|
+
Typically obtained via Belgie.__call__() as a FastAPI dependency:
|
|
34
|
+
client: BelgieClient = Depends(belgie)
|
|
35
|
+
|
|
36
|
+
Type Parameters:
|
|
37
|
+
UserT: User model type implementing UserProtocol
|
|
38
|
+
AccountT: Account model type implementing AccountProtocol
|
|
39
|
+
SessionT: Session model type implementing SessionProtocol
|
|
40
|
+
OAuthStateT: OAuth state model type implementing OAuthStateProtocol
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
db: Captured database connection
|
|
44
|
+
adapter: Database adapter for persistence operations
|
|
45
|
+
session_manager: Session manager for session lifecycle operations
|
|
46
|
+
cookie_settings: Settings for the session cookie
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
>>> @app.delete("/account")
|
|
50
|
+
>>> async def delete_account(
|
|
51
|
+
... client: BelgieClient = Depends(belgie),
|
|
52
|
+
... request: Request,
|
|
53
|
+
... ):
|
|
54
|
+
... user = await client.get_user(SecurityScopes(), request)
|
|
55
|
+
... await client.delete_user(user)
|
|
56
|
+
... return {"message": "Account deleted"}
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
db: DBConnection
|
|
60
|
+
adapter: AdapterProtocol[UserT, AccountT, SessionT, OAuthStateT]
|
|
61
|
+
session_manager: SessionManager[UserT, AccountT, SessionT, OAuthStateT]
|
|
62
|
+
cookie_settings: CookieSettings = field(default_factory=CookieSettings)
|
|
63
|
+
hook_runner: HookRunner = field(default_factory=lambda: HookRunner(Hooks()))
|
|
64
|
+
|
|
65
|
+
async def _get_session_from_cookie(self, request: Request) -> SessionT | None:
|
|
66
|
+
"""Extract and validate session from request cookies.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
request: FastAPI Request object containing cookies
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Valid session object or None if cookie missing/invalid/expired
|
|
73
|
+
"""
|
|
74
|
+
if not (session_id_str := request.cookies.get(self.cookie_settings.name)):
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
session_id = UUID(session_id_str)
|
|
79
|
+
except ValueError:
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
return await self.session_manager.get_session(self.db, session_id)
|
|
83
|
+
|
|
84
|
+
async def get_user(self, security_scopes: SecurityScopes, request: Request) -> UserT:
|
|
85
|
+
"""Get the authenticated user from the request session.
|
|
86
|
+
|
|
87
|
+
Extracts the session from cookies, validates it, and returns the authenticated user.
|
|
88
|
+
Optionally validates user-level scopes if specified.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
security_scopes: FastAPI SecurityScopes for scope validation
|
|
92
|
+
request: FastAPI Request object containing cookies
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Authenticated user object
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
HTTPException: 401 if not authenticated or session invalid
|
|
99
|
+
HTTPException: 403 if required scopes are not granted
|
|
100
|
+
|
|
101
|
+
Example:
|
|
102
|
+
>>> user = await client.get_user(SecurityScopes(scopes=["read"]), request)
|
|
103
|
+
>>> print(user.email)
|
|
104
|
+
"""
|
|
105
|
+
if not (session := await self._get_session_from_cookie(request)):
|
|
106
|
+
raise HTTPException(
|
|
107
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
108
|
+
detail="not authenticated",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if not (user := await self.adapter.get_user_by_id(self.db, session.user_id)):
|
|
112
|
+
raise HTTPException(
|
|
113
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
114
|
+
detail="user not found",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Validate user-level scopes if required
|
|
118
|
+
if security_scopes.scopes and not validate_scopes(user.scopes, security_scopes.scopes):
|
|
119
|
+
raise HTTPException(
|
|
120
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
121
|
+
detail="Insufficient permissions",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return user
|
|
125
|
+
|
|
126
|
+
async def get_session(self, request: Request) -> SessionT:
|
|
127
|
+
"""Get the current session from the request.
|
|
128
|
+
|
|
129
|
+
Extracts and validates the session from cookies.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
request: FastAPI Request object containing cookies
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Active session object
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
HTTPException: 401 if not authenticated or session invalid/expired
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
>>> session = await client.get_session(request)
|
|
142
|
+
>>> print(session.expires_at)
|
|
143
|
+
"""
|
|
144
|
+
if not (session := await self._get_session_from_cookie(request)):
|
|
145
|
+
raise HTTPException(
|
|
146
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
147
|
+
detail="not authenticated",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return session
|
|
151
|
+
|
|
152
|
+
async def delete_user(self, user: UserT) -> bool:
|
|
153
|
+
"""Delete a user and all associated data."""
|
|
154
|
+
async with self.hook_runner.dispatch("on_delete", HookContext(user=user, db=self.db)):
|
|
155
|
+
return await self.adapter.delete_user(self.db, user.id)
|
|
156
|
+
|
|
157
|
+
async def get_user_from_session(self, session_id: UUID) -> UserT | None:
|
|
158
|
+
"""Retrieve user from a session ID.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
session_id: UUID of the session
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
User object if session is valid and user exists, None otherwise
|
|
165
|
+
|
|
166
|
+
Example:
|
|
167
|
+
>>> from uuid import UUID
|
|
168
|
+
>>> session_id = UUID("...")
|
|
169
|
+
>>> user = await client.get_user_from_session(session_id)
|
|
170
|
+
>>> if user:
|
|
171
|
+
... print(f"Found user: {user.email}")
|
|
172
|
+
"""
|
|
173
|
+
if not (session := await self.session_manager.get_session(self.db, session_id)):
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
return await self.adapter.get_user_by_id(self.db, session.user_id)
|
|
177
|
+
|
|
178
|
+
async def sign_up( # noqa: PLR0913
|
|
179
|
+
self,
|
|
180
|
+
email: str,
|
|
181
|
+
*,
|
|
182
|
+
request: Request | None = None,
|
|
183
|
+
name: str | None = None,
|
|
184
|
+
image: str | None = None,
|
|
185
|
+
email_verified: bool = False,
|
|
186
|
+
ip_address: str | None = None,
|
|
187
|
+
user_agent: str | None = None,
|
|
188
|
+
) -> tuple[UserT, SessionT]:
|
|
189
|
+
if not (user := await self.adapter.get_user_by_email(self.db, email)):
|
|
190
|
+
user = await self.adapter.create_user(
|
|
191
|
+
self.db,
|
|
192
|
+
email=email,
|
|
193
|
+
name=name,
|
|
194
|
+
image=image,
|
|
195
|
+
email_verified=email_verified,
|
|
196
|
+
)
|
|
197
|
+
async with self.hook_runner.dispatch("on_signup", HookContext(user=user, db=self.db)):
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
if request:
|
|
201
|
+
if ip_address is None and request.client:
|
|
202
|
+
ip_address = request.client.host
|
|
203
|
+
if user_agent is None:
|
|
204
|
+
user_agent = request.headers.get("user-agent")
|
|
205
|
+
|
|
206
|
+
session = await self.session_manager.create_session(
|
|
207
|
+
self.db,
|
|
208
|
+
user_id=user.id,
|
|
209
|
+
ip_address=ip_address,
|
|
210
|
+
user_agent=user_agent,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
async with self.hook_runner.dispatch("on_signin", HookContext(user=user, db=self.db)):
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
return user, session
|
|
217
|
+
|
|
218
|
+
def create_session_cookie[R: Response](self, session: SessionT, response: R) -> R:
|
|
219
|
+
response.set_cookie(
|
|
220
|
+
key=self.cookie_settings.name,
|
|
221
|
+
value=str(session.id),
|
|
222
|
+
max_age=self.session_manager.max_age,
|
|
223
|
+
httponly=self.cookie_settings.http_only,
|
|
224
|
+
secure=self.cookie_settings.secure,
|
|
225
|
+
samesite=self.cookie_settings.same_site,
|
|
226
|
+
domain=self.cookie_settings.domain,
|
|
227
|
+
)
|
|
228
|
+
return response
|
|
229
|
+
|
|
230
|
+
async def sign_out(self, session_id: UUID) -> bool:
|
|
231
|
+
"""Sign out a user by deleting their session.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
session_id: UUID of the session to delete
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
True if session was deleted, False if session didn't exist
|
|
238
|
+
|
|
239
|
+
Example:
|
|
240
|
+
>>> session = await client.get_session(request)
|
|
241
|
+
>>> await client.sign_out(session.id)
|
|
242
|
+
"""
|
|
243
|
+
if not (session := await self.session_manager.get_session(self.db, session_id)):
|
|
244
|
+
return False
|
|
245
|
+
|
|
246
|
+
if not (user := await self.adapter.get_user_by_id(self.db, session.user_id)):
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
async with self.hook_runner.dispatch("on_signout", HookContext(user=user, db=self.db)):
|
|
250
|
+
return await self.session_manager.delete_session(self.db, session_id)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class BelgieError(Exception):
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AuthenticationError(BelgieError):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AuthorizationError(BelgieError):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SessionExpiredError(AuthenticationError):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class InvalidStateError(BelgieError):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class OAuthError(BelgieError):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ConfigurationError(BelgieError):
|
|
26
|
+
pass
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Sequence
|
|
5
|
+
from contextlib import AbstractAsyncContextManager, AbstractContextManager, AsyncExitStack, asynccontextmanager
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TYPE_CHECKING, Literal, cast
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from belgie_proto import DBConnection
|
|
11
|
+
else: # pragma: no cover
|
|
12
|
+
DBConnection = object
|
|
13
|
+
|
|
14
|
+
from belgie_proto import UserProtocol
|
|
15
|
+
|
|
16
|
+
HookEvent = Literal["on_signup", "on_signin", "on_signout", "on_delete"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
20
|
+
class HookContext[UserT: UserProtocol]:
|
|
21
|
+
user: UserT
|
|
22
|
+
db: DBConnection
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
type HookFunc = Callable[[HookContext], None | Awaitable[None]]
|
|
26
|
+
type HookCtxMgr = Callable[[HookContext], AbstractContextManager[None] | AbstractAsyncContextManager[None]]
|
|
27
|
+
type HookHandler = HookFunc | HookCtxMgr
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
31
|
+
class Hooks:
|
|
32
|
+
on_signup: HookHandler | Sequence[HookHandler] | None = None
|
|
33
|
+
on_signin: HookHandler | Sequence[HookHandler] | None = None
|
|
34
|
+
on_signout: HookHandler | Sequence[HookHandler] | None = None
|
|
35
|
+
on_delete: HookHandler | Sequence[HookHandler] | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class HookRunner:
|
|
39
|
+
def __init__(self, hooks: Hooks) -> None:
|
|
40
|
+
self._hooks = hooks
|
|
41
|
+
|
|
42
|
+
@asynccontextmanager
|
|
43
|
+
async def dispatch(self, event: HookEvent | str, context: HookContext) -> AsyncIterator[None]:
|
|
44
|
+
handlers = self._normalize(self._handlers_for(event))
|
|
45
|
+
|
|
46
|
+
if not handlers:
|
|
47
|
+
yield
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
async with AsyncExitStack() as stack:
|
|
51
|
+
for handler in handlers:
|
|
52
|
+
result = handler(context)
|
|
53
|
+
|
|
54
|
+
if hasattr(result, "__aenter__") and hasattr(result, "__aexit__"):
|
|
55
|
+
await stack.enter_async_context(result) # type: ignore[arg-type]
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
if hasattr(result, "__enter__") and hasattr(result, "__exit__"):
|
|
59
|
+
stack.enter_context(result) # type: ignore[arg-type]
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
if inspect.isawaitable(result):
|
|
63
|
+
await result
|
|
64
|
+
|
|
65
|
+
yield
|
|
66
|
+
|
|
67
|
+
def _handlers_for(self, event: HookEvent | str) -> HookHandler | Sequence[HookHandler] | None:
|
|
68
|
+
match event:
|
|
69
|
+
case "on_signup":
|
|
70
|
+
return self._hooks.on_signup
|
|
71
|
+
case "on_signin":
|
|
72
|
+
return self._hooks.on_signin
|
|
73
|
+
case "on_signout":
|
|
74
|
+
return self._hooks.on_signout
|
|
75
|
+
case "on_delete":
|
|
76
|
+
return self._hooks.on_delete
|
|
77
|
+
case _:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
def _normalize(self, handlers: HookHandler | Sequence[HookHandler] | None) -> list[HookHandler]:
|
|
81
|
+
if handlers is None:
|
|
82
|
+
return []
|
|
83
|
+
|
|
84
|
+
if isinstance(handlers, Sequence) and not isinstance(handlers, (str, bytes)):
|
|
85
|
+
return list(cast("Sequence[HookHandler]", handlers))
|
|
86
|
+
|
|
87
|
+
return [cast("HookHandler", handlers)]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from belgie_core.core.belgie import Belgie
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@runtime_checkable
|
|
10
|
+
class Plugin(Protocol):
|
|
11
|
+
"""Protocol for Belgie plugins."""
|
|
12
|
+
|
|
13
|
+
def router(self, belgie: "Belgie") -> APIRouter:
|
|
14
|
+
"""Return the FastAPI router for this plugin."""
|
|
15
|
+
...
|
|
16
|
+
|
|
17
|
+
def public_router(self, belgie: "Belgie") -> APIRouter:
|
|
18
|
+
"""Return the FastAPI router for public root-level routes."""
|
|
19
|
+
...
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from typing import TYPE_CHECKING, Literal
|
|
3
|
+
|
|
4
|
+
from pydantic import Field, SecretStr, field_validator
|
|
5
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from belgie_core.providers.protocols import OAuthProviderProtocol
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ProviderSettings(BaseSettings):
|
|
12
|
+
"""Base settings class for OAuth providers.
|
|
13
|
+
|
|
14
|
+
All provider-specific settings should inherit from this class
|
|
15
|
+
to ensure consistent configuration structure.
|
|
16
|
+
|
|
17
|
+
Subclasses must implement __call__ to construct their provider instance.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
client_id: str
|
|
21
|
+
client_secret: SecretStr
|
|
22
|
+
redirect_uri: str
|
|
23
|
+
|
|
24
|
+
@field_validator("client_id", "redirect_uri")
|
|
25
|
+
@classmethod
|
|
26
|
+
def validate_non_empty(cls, v: str, info) -> str: # noqa: ANN001
|
|
27
|
+
"""Ensure required OAuth fields are non-empty."""
|
|
28
|
+
if not v or not v.strip():
|
|
29
|
+
msg = f"{info.field_name} must be a non-empty string"
|
|
30
|
+
raise ValueError(msg)
|
|
31
|
+
return v.strip()
|
|
32
|
+
|
|
33
|
+
@field_validator("client_secret")
|
|
34
|
+
@classmethod
|
|
35
|
+
def validate_client_secret(cls, v: SecretStr) -> SecretStr:
|
|
36
|
+
"""Ensure client_secret is non-empty and trim whitespace."""
|
|
37
|
+
secret_value = v.get_secret_value()
|
|
38
|
+
if not secret_value or not secret_value.strip():
|
|
39
|
+
msg = "client_secret must be a non-empty string"
|
|
40
|
+
raise ValueError(msg)
|
|
41
|
+
# Return a new SecretStr with trimmed value
|
|
42
|
+
return SecretStr(secret_value.strip())
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def __call__(self) -> "OAuthProviderProtocol":
|
|
46
|
+
"""Create and return the OAuth provider instance.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
OAuth provider configured with these settings
|
|
50
|
+
"""
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SessionSettings(BaseSettings):
|
|
55
|
+
model_config = SettingsConfigDict(env_prefix="BELGIE_SESSION_")
|
|
56
|
+
|
|
57
|
+
max_age: int = Field(default=604800)
|
|
58
|
+
update_age: int = Field(default=86400)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class CookieSettings(BaseSettings):
|
|
62
|
+
model_config = SettingsConfigDict(env_prefix="BELGIE_COOKIE_")
|
|
63
|
+
|
|
64
|
+
name: str = Field(default="belgie_session")
|
|
65
|
+
secure: bool = Field(default=True)
|
|
66
|
+
http_only: bool = Field(default=True)
|
|
67
|
+
same_site: Literal["lax", "strict", "none"] = Field(default="lax")
|
|
68
|
+
domain: str | None = Field(default=None)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class URLSettings(BaseSettings):
|
|
72
|
+
model_config = SettingsConfigDict(env_prefix="BELGIE_URLS_")
|
|
73
|
+
|
|
74
|
+
signin_redirect: str = Field(default="/dashboard")
|
|
75
|
+
signout_redirect: str = Field(default="/")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class BelgieSettings(BaseSettings):
|
|
79
|
+
model_config = SettingsConfigDict(
|
|
80
|
+
env_prefix="BELGIE_",
|
|
81
|
+
env_file=".env",
|
|
82
|
+
env_file_encoding="utf-8",
|
|
83
|
+
case_sensitive=False,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
secret: str
|
|
87
|
+
base_url: str
|
|
88
|
+
|
|
89
|
+
session: SessionSettings = Field(default_factory=SessionSettings)
|
|
90
|
+
cookie: CookieSettings = Field(default_factory=CookieSettings)
|
|
91
|
+
urls: URLSettings = Field(default_factory=URLSettings)
|
|
92
|
+
|
|
93
|
+
@field_validator("secret", "base_url")
|
|
94
|
+
@classmethod
|
|
95
|
+
def validate_non_empty(cls, value: str, info) -> str: # noqa: ANN001
|
|
96
|
+
"""Ensure required Belgie settings are non-empty."""
|
|
97
|
+
if not value or not value.strip():
|
|
98
|
+
msg = f"{info.field_name} must be a non-empty string"
|
|
99
|
+
raise ValueError(msg)
|
|
100
|
+
return value.strip()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from belgie_core.providers.google import GoogleOAuthProvider, GoogleProviderSettings, GoogleUserInfo
|
|
2
|
+
from belgie_core.providers.protocols import OAuthProviderProtocol, Providers
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"GoogleOAuthProvider",
|
|
6
|
+
"GoogleProviderSettings",
|
|
7
|
+
"GoogleUserInfo",
|
|
8
|
+
"OAuthProviderProtocol",
|
|
9
|
+
"Providers",
|
|
10
|
+
]
|