skrift 0.1.0a7__tar.gz → 0.1.0a8__tar.gz
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.
- {skrift-0.1.0a7 → skrift-0.1.0a8}/PKG-INFO +1 -1
- {skrift-0.1.0a7 → skrift-0.1.0a8}/pyproject.toml +1 -1
- skrift-0.1.0a8/skrift/alembic/versions/20260129_add_provider_metadata.py +29 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/controllers/auth.py +14 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/db/models/oauth_account.py +15 -2
- skrift-0.1.0a8/skrift/db/services/oauth_service.py +195 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/.gitignore +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/README.md +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/__init__.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/__main__.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/admin/__init__.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/admin/controller.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/admin/navigation.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/alembic/env.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/alembic/script.py.mako +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/alembic/versions/20260120_210154_09b0364dbb7b_initial_schema.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/alembic/versions/20260122_152744_0b7c927d2591_add_roles_and_permissions.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/alembic/versions/20260122_172836_cdf734a5b847_add_sa_orm_sentinel_column.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/alembic/versions/20260122_175637_a9c55348eae7_remove_page_type_column.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/alembic/versions/20260122_200000_add_settings_table.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/alembic/versions/20260129_add_oauth_accounts.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/alembic.ini +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/asgi.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/auth/__init__.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/auth/guards.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/auth/roles.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/auth/services.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/cli.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/config.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/controllers/__init__.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/controllers/web.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/db/__init__.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/db/base.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/db/models/__init__.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/db/models/page.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/db/models/role.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/db/models/setting.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/db/models/user.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/db/services/__init__.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/db/services/page_service.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/db/services/setting_service.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/lib/__init__.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/lib/exceptions.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/lib/template.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/setup/__init__.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/setup/config_writer.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/setup/controller.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/setup/middleware.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/setup/providers.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/setup/state.py +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/static/css/style.css +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/templates/admin/admin.html +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/templates/admin/base.html +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/templates/admin/pages/edit.html +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/templates/admin/pages/list.html +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/templates/admin/settings/site.html +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/templates/admin/users/list.html +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/templates/admin/users/roles.html +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/templates/auth/dummy_login.html +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/templates/auth/login.html +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/templates/base.html +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/templates/error-404.html +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/templates/error-500.html +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/templates/error.html +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/templates/index.html +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/templates/page.html +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/templates/setup/admin.html +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/templates/setup/auth.html +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/templates/setup/base.html +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/templates/setup/complete.html +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/templates/setup/configuring.html +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/templates/setup/database.html +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/templates/setup/restart.html +0 -0
- {skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/templates/setup/site.html +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""add provider_metadata column to oauth_accounts
|
|
2
|
+
|
|
3
|
+
Revision ID: 2b3c4d5e6f7g
|
|
4
|
+
Revises: 1a2b3c4d5e6f
|
|
5
|
+
Create Date: 2026-01-29 12:00:00.000000
|
|
6
|
+
|
|
7
|
+
This migration adds a JSON column to store the full raw OAuth provider response.
|
|
8
|
+
"""
|
|
9
|
+
from typing import Sequence, Union
|
|
10
|
+
|
|
11
|
+
from alembic import op
|
|
12
|
+
import sqlalchemy as sa
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# revision identifiers, used by Alembic.
|
|
16
|
+
revision: str = '2b3c4d5e6f7g'
|
|
17
|
+
down_revision: Union[str, None] = '1a2b3c4d5e6f'
|
|
18
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
19
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def upgrade() -> None:
|
|
23
|
+
with op.batch_alter_table('oauth_accounts') as batch_op:
|
|
24
|
+
batch_op.add_column(sa.Column('provider_metadata', sa.JSON(), nullable=True))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def downgrade() -> None:
|
|
28
|
+
with op.batch_alter_table('oauth_accounts') as batch_op:
|
|
29
|
+
batch_op.drop_column('provider_metadata')
|
|
@@ -337,6 +337,8 @@ class AuthController(Controller):
|
|
|
337
337
|
# Update provider email if changed
|
|
338
338
|
if email:
|
|
339
339
|
oauth_account.provider_email = email
|
|
340
|
+
# Update provider metadata
|
|
341
|
+
oauth_account.provider_metadata = user_info
|
|
340
342
|
else:
|
|
341
343
|
# Step 2: Check if a user with this email already exists
|
|
342
344
|
user = None
|
|
@@ -352,6 +354,7 @@ class AuthController(Controller):
|
|
|
352
354
|
provider=provider,
|
|
353
355
|
provider_account_id=oauth_id,
|
|
354
356
|
provider_email=email,
|
|
357
|
+
provider_metadata=user_info,
|
|
355
358
|
user_id=user.id,
|
|
356
359
|
)
|
|
357
360
|
db_session.add(oauth_account)
|
|
@@ -375,6 +378,7 @@ class AuthController(Controller):
|
|
|
375
378
|
provider=provider,
|
|
376
379
|
provider_account_id=oauth_id,
|
|
377
380
|
provider_email=email,
|
|
381
|
+
provider_metadata=user_info,
|
|
378
382
|
user_id=user.id,
|
|
379
383
|
)
|
|
380
384
|
db_session.add(oauth_account)
|
|
@@ -444,6 +448,13 @@ class AuthController(Controller):
|
|
|
444
448
|
# Generate deterministic oauth_id from email
|
|
445
449
|
oauth_id = f"dummy_{hashlib.sha256(email.encode()).hexdigest()[:16]}"
|
|
446
450
|
|
|
451
|
+
# Create synthetic metadata for dummy provider
|
|
452
|
+
dummy_metadata = {
|
|
453
|
+
"id": oauth_id,
|
|
454
|
+
"email": email,
|
|
455
|
+
"name": name,
|
|
456
|
+
}
|
|
457
|
+
|
|
447
458
|
# Step 1: Check if OAuth account already exists
|
|
448
459
|
result = await db_session.execute(
|
|
449
460
|
select(OAuthAccount)
|
|
@@ -462,6 +473,7 @@ class AuthController(Controller):
|
|
|
462
473
|
user.email = email
|
|
463
474
|
user.last_login_at = datetime.now(UTC)
|
|
464
475
|
oauth_account.provider_email = email
|
|
476
|
+
oauth_account.provider_metadata = dummy_metadata
|
|
465
477
|
else:
|
|
466
478
|
# Step 2: Check if a user with this email already exists
|
|
467
479
|
result = await db_session.execute(
|
|
@@ -475,6 +487,7 @@ class AuthController(Controller):
|
|
|
475
487
|
provider=DUMMY_PROVIDER_KEY,
|
|
476
488
|
provider_account_id=oauth_id,
|
|
477
489
|
provider_email=email,
|
|
490
|
+
provider_metadata=dummy_metadata,
|
|
478
491
|
user_id=user.id,
|
|
479
492
|
)
|
|
480
493
|
db_session.add(oauth_account)
|
|
@@ -495,6 +508,7 @@ class AuthController(Controller):
|
|
|
495
508
|
provider=DUMMY_PROVIDER_KEY,
|
|
496
509
|
provider_account_id=oauth_id,
|
|
497
510
|
provider_email=email,
|
|
511
|
+
provider_metadata=dummy_metadata,
|
|
498
512
|
user_id=user.id,
|
|
499
513
|
)
|
|
500
514
|
db_session.add(oauth_account)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""OAuth account model for storing multiple OAuth identities per user."""
|
|
2
2
|
|
|
3
|
-
from typing import TYPE_CHECKING
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
4
|
from uuid import UUID
|
|
5
5
|
|
|
6
|
-
from sqlalchemy import ForeignKey, String, UniqueConstraint
|
|
6
|
+
from sqlalchemy import ForeignKey, JSON, String, UniqueConstraint
|
|
7
7
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
8
8
|
|
|
9
9
|
from skrift.db.base import Base
|
|
@@ -17,6 +17,16 @@ class OAuthAccount(Base):
|
|
|
17
17
|
|
|
18
18
|
This allows a single user to have multiple OAuth provider accounts
|
|
19
19
|
linked to their profile, enabling login via different providers.
|
|
20
|
+
|
|
21
|
+
The provider_metadata column stores the full raw OAuth provider response,
|
|
22
|
+
which varies by provider:
|
|
23
|
+
|
|
24
|
+
- Discord: id, username, global_name, discriminator, avatar, email, verified, locale
|
|
25
|
+
- GitHub: id, login, name, email, avatar_url, bio, company, location, public_repos
|
|
26
|
+
- Google: id, email, name, picture, verified_email, locale, hd
|
|
27
|
+
- Twitter: id, username, name
|
|
28
|
+
- Microsoft: id, displayName, mail, userPrincipalName
|
|
29
|
+
- Facebook: id, name, email, picture.data.url
|
|
20
30
|
"""
|
|
21
31
|
|
|
22
32
|
__tablename__ = "oauth_accounts"
|
|
@@ -24,6 +34,9 @@ class OAuthAccount(Base):
|
|
|
24
34
|
provider: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
25
35
|
provider_account_id: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
26
36
|
provider_email: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
37
|
+
provider_metadata: Mapped[dict[str, Any] | None] = mapped_column(
|
|
38
|
+
JSON, nullable=True, default=None
|
|
39
|
+
)
|
|
27
40
|
user_id: Mapped[UUID] = mapped_column(
|
|
28
41
|
ForeignKey("users.id", ondelete="CASCADE"), nullable=False
|
|
29
42
|
)
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""OAuth service for accessing OAuth account data and provider metadata."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
from sqlalchemy import select
|
|
7
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
8
|
+
|
|
9
|
+
from skrift.db.models.oauth_account import OAuthAccount
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def get_oauth_account_by_user_and_provider(
|
|
13
|
+
db_session: AsyncSession,
|
|
14
|
+
user_id: UUID,
|
|
15
|
+
provider: str,
|
|
16
|
+
) -> OAuthAccount | None:
|
|
17
|
+
"""Get a specific OAuth account for a user and provider.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
db_session: Database session
|
|
21
|
+
user_id: User UUID
|
|
22
|
+
provider: OAuth provider name (e.g., 'discord', 'github')
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
OAuthAccount or None if not found
|
|
26
|
+
"""
|
|
27
|
+
result = await db_session.execute(
|
|
28
|
+
select(OAuthAccount).where(
|
|
29
|
+
OAuthAccount.user_id == user_id,
|
|
30
|
+
OAuthAccount.provider == provider,
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
return result.scalar_one_or_none()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def get_oauth_accounts_by_user(
|
|
37
|
+
db_session: AsyncSession,
|
|
38
|
+
user_id: UUID,
|
|
39
|
+
) -> list[OAuthAccount]:
|
|
40
|
+
"""Get all OAuth accounts linked to a user.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
db_session: Database session
|
|
44
|
+
user_id: User UUID
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
List of OAuthAccount objects
|
|
48
|
+
"""
|
|
49
|
+
result = await db_session.execute(
|
|
50
|
+
select(OAuthAccount).where(OAuthAccount.user_id == user_id)
|
|
51
|
+
)
|
|
52
|
+
return list(result.scalars().all())
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def get_provider_metadata(
|
|
56
|
+
db_session: AsyncSession,
|
|
57
|
+
user_id: UUID,
|
|
58
|
+
provider: str,
|
|
59
|
+
) -> dict[str, Any] | None:
|
|
60
|
+
"""Get the raw provider metadata for a user's OAuth account.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
db_session: Database session
|
|
64
|
+
user_id: User UUID
|
|
65
|
+
provider: OAuth provider name
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Provider metadata dict or None if not found
|
|
69
|
+
"""
|
|
70
|
+
oauth_account = await get_oauth_account_by_user_and_provider(
|
|
71
|
+
db_session, user_id, provider
|
|
72
|
+
)
|
|
73
|
+
if oauth_account:
|
|
74
|
+
return oauth_account.provider_metadata
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def extract_metadata_field(
|
|
79
|
+
metadata: dict[str, Any] | None,
|
|
80
|
+
*keys: str,
|
|
81
|
+
default: Any = None,
|
|
82
|
+
) -> Any:
|
|
83
|
+
"""Safely extract a nested field from metadata.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
metadata: Provider metadata dict
|
|
87
|
+
*keys: Sequence of keys for nested access (e.g., 'picture', 'data', 'url')
|
|
88
|
+
default: Default value if field not found
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Field value or default
|
|
92
|
+
"""
|
|
93
|
+
if metadata is None:
|
|
94
|
+
return default
|
|
95
|
+
|
|
96
|
+
current = metadata
|
|
97
|
+
for key in keys:
|
|
98
|
+
if isinstance(current, dict) and key in current:
|
|
99
|
+
current = current[key]
|
|
100
|
+
else:
|
|
101
|
+
return default
|
|
102
|
+
return current
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def get_provider_username(
|
|
106
|
+
db_session: AsyncSession,
|
|
107
|
+
user_id: UUID,
|
|
108
|
+
provider: str,
|
|
109
|
+
) -> str | None:
|
|
110
|
+
"""Get the username from a provider's metadata.
|
|
111
|
+
|
|
112
|
+
Provider-specific username fields:
|
|
113
|
+
- Discord: username
|
|
114
|
+
- GitHub: login
|
|
115
|
+
- Twitter: username
|
|
116
|
+
- Google: email (no username concept)
|
|
117
|
+
- Microsoft: userPrincipalName
|
|
118
|
+
- Facebook: name (no username concept)
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
db_session: Database session
|
|
122
|
+
user_id: User UUID
|
|
123
|
+
provider: OAuth provider name
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Username string or None
|
|
127
|
+
"""
|
|
128
|
+
metadata = await get_provider_metadata(db_session, user_id, provider)
|
|
129
|
+
if metadata is None:
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
# Provider-specific username extraction
|
|
133
|
+
username_fields = {
|
|
134
|
+
"discord": "username",
|
|
135
|
+
"github": "login",
|
|
136
|
+
"twitter": "username",
|
|
137
|
+
"microsoft": "userPrincipalName",
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
field = username_fields.get(provider)
|
|
141
|
+
if field:
|
|
142
|
+
return extract_metadata_field(metadata, field)
|
|
143
|
+
|
|
144
|
+
# Fallback for providers without usernames
|
|
145
|
+
return extract_metadata_field(metadata, "email") or extract_metadata_field(
|
|
146
|
+
metadata, "name"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
async def get_provider_avatar_url(
|
|
151
|
+
db_session: AsyncSession,
|
|
152
|
+
user_id: UUID,
|
|
153
|
+
provider: str,
|
|
154
|
+
) -> str | None:
|
|
155
|
+
"""Get the avatar URL from a provider's metadata.
|
|
156
|
+
|
|
157
|
+
Provider-specific avatar URL construction:
|
|
158
|
+
- Discord: Constructed from id + avatar hash
|
|
159
|
+
- GitHub: avatar_url
|
|
160
|
+
- Google: picture
|
|
161
|
+
- Microsoft: No direct URL (requires Graph API call)
|
|
162
|
+
- Facebook: picture.data.url
|
|
163
|
+
- Twitter: No avatar in basic userinfo
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
db_session: Database session
|
|
167
|
+
user_id: User UUID
|
|
168
|
+
provider: OAuth provider name
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Avatar URL string or None
|
|
172
|
+
"""
|
|
173
|
+
metadata = await get_provider_metadata(db_session, user_id, provider)
|
|
174
|
+
if metadata is None:
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
if provider == "discord":
|
|
178
|
+
# Discord avatar URL must be constructed
|
|
179
|
+
user_id_discord = extract_metadata_field(metadata, "id")
|
|
180
|
+
avatar_hash = extract_metadata_field(metadata, "avatar")
|
|
181
|
+
if user_id_discord and avatar_hash:
|
|
182
|
+
return f"https://cdn.discordapp.com/avatars/{user_id_discord}/{avatar_hash}.png"
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
if provider == "github":
|
|
186
|
+
return extract_metadata_field(metadata, "avatar_url")
|
|
187
|
+
|
|
188
|
+
if provider == "google":
|
|
189
|
+
return extract_metadata_field(metadata, "picture")
|
|
190
|
+
|
|
191
|
+
if provider == "facebook":
|
|
192
|
+
return extract_metadata_field(metadata, "picture", "data", "url")
|
|
193
|
+
|
|
194
|
+
# Microsoft and Twitter don't provide direct avatar URLs
|
|
195
|
+
return None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{skrift-0.1.0a7 → skrift-0.1.0a8}/skrift/alembic/versions/20260122_200000_add_settings_table.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|