skrift 0.1.0a12__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.
- skrift/__init__.py +1 -0
- skrift/__main__.py +12 -0
- skrift/admin/__init__.py +11 -0
- skrift/admin/controller.py +452 -0
- skrift/admin/navigation.py +105 -0
- skrift/alembic/env.py +92 -0
- skrift/alembic/script.py.mako +26 -0
- skrift/alembic/versions/20260120_210154_09b0364dbb7b_initial_schema.py +70 -0
- skrift/alembic/versions/20260122_152744_0b7c927d2591_add_roles_and_permissions.py +57 -0
- skrift/alembic/versions/20260122_172836_cdf734a5b847_add_sa_orm_sentinel_column.py +31 -0
- skrift/alembic/versions/20260122_175637_a9c55348eae7_remove_page_type_column.py +43 -0
- skrift/alembic/versions/20260122_200000_add_settings_table.py +38 -0
- skrift/alembic/versions/20260129_add_oauth_accounts.py +141 -0
- skrift/alembic/versions/20260129_add_provider_metadata.py +29 -0
- skrift/alembic.ini +77 -0
- skrift/asgi.py +670 -0
- skrift/auth/__init__.py +58 -0
- skrift/auth/guards.py +130 -0
- skrift/auth/roles.py +129 -0
- skrift/auth/services.py +184 -0
- skrift/cli.py +143 -0
- skrift/config.py +259 -0
- skrift/controllers/__init__.py +4 -0
- skrift/controllers/auth.py +595 -0
- skrift/controllers/web.py +67 -0
- skrift/db/__init__.py +3 -0
- skrift/db/base.py +7 -0
- skrift/db/models/__init__.py +7 -0
- skrift/db/models/oauth_account.py +50 -0
- skrift/db/models/page.py +26 -0
- skrift/db/models/role.py +56 -0
- skrift/db/models/setting.py +13 -0
- skrift/db/models/user.py +36 -0
- skrift/db/services/__init__.py +1 -0
- skrift/db/services/oauth_service.py +195 -0
- skrift/db/services/page_service.py +217 -0
- skrift/db/services/setting_service.py +206 -0
- skrift/lib/__init__.py +3 -0
- skrift/lib/exceptions.py +168 -0
- skrift/lib/template.py +108 -0
- skrift/setup/__init__.py +14 -0
- skrift/setup/config_writer.py +213 -0
- skrift/setup/controller.py +888 -0
- skrift/setup/middleware.py +89 -0
- skrift/setup/providers.py +214 -0
- skrift/setup/state.py +315 -0
- skrift/static/css/style.css +1003 -0
- skrift/templates/admin/admin.html +19 -0
- skrift/templates/admin/base.html +24 -0
- skrift/templates/admin/pages/edit.html +32 -0
- skrift/templates/admin/pages/list.html +62 -0
- skrift/templates/admin/settings/site.html +32 -0
- skrift/templates/admin/users/list.html +58 -0
- skrift/templates/admin/users/roles.html +42 -0
- skrift/templates/auth/dummy_login.html +102 -0
- skrift/templates/auth/login.html +139 -0
- skrift/templates/base.html +52 -0
- skrift/templates/error-404.html +19 -0
- skrift/templates/error-500.html +19 -0
- skrift/templates/error.html +19 -0
- skrift/templates/index.html +9 -0
- skrift/templates/page.html +26 -0
- skrift/templates/setup/admin.html +24 -0
- skrift/templates/setup/auth.html +110 -0
- skrift/templates/setup/base.html +407 -0
- skrift/templates/setup/complete.html +17 -0
- skrift/templates/setup/configuring.html +158 -0
- skrift/templates/setup/database.html +125 -0
- skrift/templates/setup/restart.html +28 -0
- skrift/templates/setup/site.html +39 -0
- skrift-0.1.0a12.dist-info/METADATA +235 -0
- skrift-0.1.0a12.dist-info/RECORD +74 -0
- skrift-0.1.0a12.dist-info/WHEEL +4 -0
- skrift-0.1.0a12.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""${message}
|
|
2
|
+
|
|
3
|
+
Revision ID: ${up_revision}
|
|
4
|
+
Revises: ${down_revision | comma,n}
|
|
5
|
+
Create Date: ${create_date}
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from typing import Sequence, Union
|
|
9
|
+
|
|
10
|
+
from alembic import op
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
${imports if imports else ""}
|
|
13
|
+
|
|
14
|
+
# revision identifiers, used by Alembic.
|
|
15
|
+
revision: str = ${repr(up_revision)}
|
|
16
|
+
down_revision: Union[str, None] = ${repr(down_revision)}
|
|
17
|
+
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
|
18
|
+
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def upgrade() -> None:
|
|
22
|
+
${upgrades if upgrades else "pass"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def downgrade() -> None:
|
|
26
|
+
${downgrades if downgrades else "pass"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""initial schema
|
|
2
|
+
|
|
3
|
+
Revision ID: 09b0364dbb7b
|
|
4
|
+
Revises:
|
|
5
|
+
Create Date: 2026-01-20 21:01:54.470260
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from typing import Sequence, Union
|
|
9
|
+
|
|
10
|
+
from alembic import op
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
from advanced_alchemy.types import GUID, DateTimeUTC
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# revision identifiers, used by Alembic.
|
|
16
|
+
revision: str = '09b0364dbb7b'
|
|
17
|
+
down_revision: Union[str, None] = None
|
|
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
|
+
# Create users table
|
|
24
|
+
op.create_table(
|
|
25
|
+
'users',
|
|
26
|
+
sa.Column('id', GUID(length=16), nullable=False),
|
|
27
|
+
sa.Column('created_at', DateTimeUTC(timezone=True), nullable=False),
|
|
28
|
+
sa.Column('updated_at', DateTimeUTC(timezone=True), nullable=False),
|
|
29
|
+
sa.Column('oauth_provider', sa.String(length=50), nullable=False),
|
|
30
|
+
sa.Column('oauth_id', sa.String(length=255), nullable=False),
|
|
31
|
+
sa.Column('email', sa.String(length=255), nullable=False),
|
|
32
|
+
sa.Column('name', sa.String(length=255), nullable=True),
|
|
33
|
+
sa.Column('picture_url', sa.String(length=512), nullable=True),
|
|
34
|
+
sa.Column('is_active', sa.Boolean(), nullable=False, default=True),
|
|
35
|
+
sa.Column('last_login_at', sa.DateTime(timezone=True), nullable=True),
|
|
36
|
+
sa.PrimaryKeyConstraint('id'),
|
|
37
|
+
sa.UniqueConstraint('oauth_id'),
|
|
38
|
+
sa.UniqueConstraint('email'),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Create pages table
|
|
42
|
+
op.create_table(
|
|
43
|
+
'pages',
|
|
44
|
+
sa.Column('id', GUID(length=16), nullable=False),
|
|
45
|
+
sa.Column('created_at', DateTimeUTC(timezone=True), nullable=False),
|
|
46
|
+
sa.Column('updated_at', DateTimeUTC(timezone=True), nullable=False),
|
|
47
|
+
sa.Column('user_id', GUID(length=16), nullable=True),
|
|
48
|
+
sa.Column('type', sa.String(length=50), nullable=False, default='page'),
|
|
49
|
+
sa.Column('slug', sa.String(length=255), nullable=False),
|
|
50
|
+
sa.Column('title', sa.String(length=500), nullable=False),
|
|
51
|
+
sa.Column('content', sa.Text(), nullable=False, default=''),
|
|
52
|
+
sa.Column('is_published', sa.Boolean(), nullable=False, default=False),
|
|
53
|
+
sa.Column('published_at', sa.DateTime(timezone=True), nullable=True),
|
|
54
|
+
sa.PrimaryKeyConstraint('id'),
|
|
55
|
+
sa.ForeignKeyConstraint(['user_id'], ['users.id']),
|
|
56
|
+
sa.UniqueConstraint('slug'),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Create indexes
|
|
60
|
+
op.create_index('ix_pages_slug', 'pages', ['slug'])
|
|
61
|
+
op.create_index('ix_pages_user_id', 'pages', ['user_id'])
|
|
62
|
+
op.create_index('ix_pages_type_published', 'pages', ['type', 'is_published'])
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def downgrade() -> None:
|
|
66
|
+
op.drop_index('ix_pages_type_published', 'pages')
|
|
67
|
+
op.drop_index('ix_pages_user_id', 'pages')
|
|
68
|
+
op.drop_index('ix_pages_slug', 'pages')
|
|
69
|
+
op.drop_table('pages')
|
|
70
|
+
op.drop_table('users')
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""add_roles_and_permissions
|
|
2
|
+
|
|
3
|
+
Revision ID: 0b7c927d2591
|
|
4
|
+
Revises: 09b0364dbb7b
|
|
5
|
+
Create Date: 2026-01-22 15:27:44.922770
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from typing import Sequence, Union
|
|
9
|
+
|
|
10
|
+
from alembic import op
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
import advanced_alchemy.types
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# revision identifiers, used by Alembic.
|
|
16
|
+
revision: str = '0b7c927d2591'
|
|
17
|
+
down_revision: Union[str, None] = '09b0364dbb7b'
|
|
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
|
+
op.create_table('roles',
|
|
24
|
+
sa.Column('id', advanced_alchemy.types.guid.GUID(length=16), nullable=False),
|
|
25
|
+
sa.Column('name', sa.String(length=50), nullable=False),
|
|
26
|
+
sa.Column('display_name', sa.String(length=100), nullable=True),
|
|
27
|
+
sa.Column('description', sa.String(length=500), nullable=True),
|
|
28
|
+
sa.Column('sa_orm_sentinel', sa.Integer(), nullable=True),
|
|
29
|
+
sa.Column('created_at', advanced_alchemy.types.datetime.DateTimeUTC(timezone=True), nullable=False),
|
|
30
|
+
sa.Column('updated_at', advanced_alchemy.types.datetime.DateTimeUTC(timezone=True), nullable=False),
|
|
31
|
+
sa.PrimaryKeyConstraint('id', name=op.f('pk_roles')),
|
|
32
|
+
sa.UniqueConstraint('name', name=op.f('uq_roles_name'))
|
|
33
|
+
)
|
|
34
|
+
op.create_table('role_permissions',
|
|
35
|
+
sa.Column('id', advanced_alchemy.types.guid.GUID(length=16), nullable=False),
|
|
36
|
+
sa.Column('role_id', advanced_alchemy.types.guid.GUID(length=16), nullable=False),
|
|
37
|
+
sa.Column('permission', sa.String(length=100), nullable=False),
|
|
38
|
+
sa.Column('sa_orm_sentinel', sa.Integer(), nullable=True),
|
|
39
|
+
sa.Column('created_at', advanced_alchemy.types.datetime.DateTimeUTC(timezone=True), nullable=False),
|
|
40
|
+
sa.Column('updated_at', advanced_alchemy.types.datetime.DateTimeUTC(timezone=True), nullable=False),
|
|
41
|
+
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], name=op.f('fk_role_permissions_role_id_roles'), ondelete='CASCADE'),
|
|
42
|
+
sa.PrimaryKeyConstraint('id', name=op.f('pk_role_permissions')),
|
|
43
|
+
sa.UniqueConstraint('role_id', 'permission', name='uq_role_permission')
|
|
44
|
+
)
|
|
45
|
+
op.create_table('user_roles',
|
|
46
|
+
sa.Column('user_id', advanced_alchemy.types.guid.GUID(length=16), nullable=False),
|
|
47
|
+
sa.Column('role_id', advanced_alchemy.types.guid.GUID(length=16), nullable=False),
|
|
48
|
+
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], name=op.f('fk_user_roles_role_id_roles'), ondelete='CASCADE'),
|
|
49
|
+
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('fk_user_roles_user_id_users'), ondelete='CASCADE'),
|
|
50
|
+
sa.PrimaryKeyConstraint('user_id', 'role_id', name=op.f('pk_user_roles'))
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def downgrade() -> None:
|
|
55
|
+
op.drop_table('user_roles')
|
|
56
|
+
op.drop_table('role_permissions')
|
|
57
|
+
op.drop_table('roles')
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""add sa_orm_sentinel column
|
|
2
|
+
|
|
3
|
+
Revision ID: cdf734a5b847
|
|
4
|
+
Revises: 0b7c927d2591
|
|
5
|
+
Create Date: 2026-01-22 17:28:36.586660
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from typing import Sequence, Union
|
|
9
|
+
|
|
10
|
+
from alembic import op
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# revision identifiers, used by Alembic.
|
|
15
|
+
revision: str = 'cdf734a5b847'
|
|
16
|
+
down_revision: Union[str, None] = '0b7c927d2591'
|
|
17
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
18
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def upgrade() -> None:
|
|
22
|
+
# Add sa_orm_sentinel column to users table
|
|
23
|
+
op.add_column('users', sa.Column('sa_orm_sentinel', sa.Integer(), nullable=True))
|
|
24
|
+
|
|
25
|
+
# Add sa_orm_sentinel column to pages table
|
|
26
|
+
op.add_column('pages', sa.Column('sa_orm_sentinel', sa.Integer(), nullable=True))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def downgrade() -> None:
|
|
30
|
+
op.drop_column('pages', 'sa_orm_sentinel')
|
|
31
|
+
op.drop_column('users', 'sa_orm_sentinel')
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""remove page type column
|
|
2
|
+
|
|
3
|
+
Revision ID: a9c55348eae7
|
|
4
|
+
Revises: cdf734a5b847
|
|
5
|
+
Create Date: 2026-01-22 17:56:37.000000
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from typing import Sequence, Union
|
|
9
|
+
|
|
10
|
+
from alembic import op
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# revision identifiers, used by Alembic.
|
|
15
|
+
revision: str = 'a9c55348eae7'
|
|
16
|
+
down_revision: Union[str, None] = 'cdf734a5b847'
|
|
17
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
18
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def upgrade() -> None:
|
|
22
|
+
# Delete any posts (convert to pages not needed since we're removing posts entirely)
|
|
23
|
+
op.execute("DELETE FROM pages WHERE type = 'post'")
|
|
24
|
+
|
|
25
|
+
# Drop the composite index on type and is_published
|
|
26
|
+
op.drop_index('ix_pages_type_published', table_name='pages')
|
|
27
|
+
|
|
28
|
+
# Drop the type column
|
|
29
|
+
op.drop_column('pages', 'type')
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def downgrade() -> None:
|
|
33
|
+
# Add the type column back
|
|
34
|
+
op.add_column('pages', sa.Column('type', sa.String(50), nullable=True))
|
|
35
|
+
|
|
36
|
+
# Set all existing pages to type 'page'
|
|
37
|
+
op.execute("UPDATE pages SET type = 'page'")
|
|
38
|
+
|
|
39
|
+
# Make the column non-nullable
|
|
40
|
+
op.alter_column('pages', 'type', nullable=False)
|
|
41
|
+
|
|
42
|
+
# Recreate the composite index
|
|
43
|
+
op.create_index('ix_pages_type_published', 'pages', ['type', 'is_published'])
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""add settings table
|
|
2
|
+
|
|
3
|
+
Revision ID: 8f3a5c2d1e0b
|
|
4
|
+
Revises: a9c55348eae7
|
|
5
|
+
Create Date: 2026-01-22 20:00:00.000000
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from typing import Sequence, Union
|
|
9
|
+
|
|
10
|
+
from alembic import op
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
import advanced_alchemy.types
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# revision identifiers, used by Alembic.
|
|
16
|
+
revision: str = '8f3a5c2d1e0b'
|
|
17
|
+
down_revision: Union[str, None] = 'a9c55348eae7'
|
|
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
|
+
op.create_table('settings',
|
|
24
|
+
sa.Column('id', advanced_alchemy.types.guid.GUID(length=16), nullable=False),
|
|
25
|
+
sa.Column('key', sa.String(length=255), nullable=False),
|
|
26
|
+
sa.Column('value', sa.Text(), nullable=True),
|
|
27
|
+
sa.Column('sa_orm_sentinel', sa.Integer(), nullable=True),
|
|
28
|
+
sa.Column('created_at', advanced_alchemy.types.datetime.DateTimeUTC(timezone=True), nullable=False),
|
|
29
|
+
sa.Column('updated_at', advanced_alchemy.types.datetime.DateTimeUTC(timezone=True), nullable=False),
|
|
30
|
+
sa.PrimaryKeyConstraint('id', name=op.f('pk_settings')),
|
|
31
|
+
sa.UniqueConstraint('key', name=op.f('uq_settings_key'))
|
|
32
|
+
)
|
|
33
|
+
op.create_index(op.f('ix_settings_key'), 'settings', ['key'], unique=True)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def downgrade() -> None:
|
|
37
|
+
op.drop_index(op.f('ix_settings_key'), table_name='settings')
|
|
38
|
+
op.drop_table('settings')
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""add oauth_accounts table
|
|
2
|
+
|
|
3
|
+
Revision ID: 1a2b3c4d5e6f
|
|
4
|
+
Revises: 8f3a5c2d1e0b
|
|
5
|
+
Create Date: 2026-01-29 10:00:00.000000
|
|
6
|
+
|
|
7
|
+
This migration:
|
|
8
|
+
1. Creates the oauth_accounts table to store multiple OAuth identities per user
|
|
9
|
+
2. Migrates existing oauth data from users table to oauth_accounts
|
|
10
|
+
3. Makes users.email nullable (for providers like Twitter that don't provide email)
|
|
11
|
+
4. Removes oauth_provider and oauth_id columns from users table
|
|
12
|
+
"""
|
|
13
|
+
from typing import Sequence, Union
|
|
14
|
+
|
|
15
|
+
from alembic import op
|
|
16
|
+
import sqlalchemy as sa
|
|
17
|
+
from advanced_alchemy.types import GUID, DateTimeUTC
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# revision identifiers, used by Alembic.
|
|
21
|
+
revision: str = '1a2b3c4d5e6f'
|
|
22
|
+
down_revision: Union[str, None] = '8f3a5c2d1e0b'
|
|
23
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
24
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def upgrade() -> None:
|
|
28
|
+
# Step 1: Create oauth_accounts table
|
|
29
|
+
op.create_table(
|
|
30
|
+
'oauth_accounts',
|
|
31
|
+
sa.Column('id', GUID(length=16), nullable=False),
|
|
32
|
+
sa.Column('created_at', DateTimeUTC(timezone=True), nullable=False),
|
|
33
|
+
sa.Column('updated_at', DateTimeUTC(timezone=True), nullable=False),
|
|
34
|
+
sa.Column('sa_orm_sentinel', sa.Integer(), nullable=True),
|
|
35
|
+
sa.Column('provider', sa.String(length=50), nullable=False),
|
|
36
|
+
sa.Column('provider_account_id', sa.String(length=255), nullable=False),
|
|
37
|
+
sa.Column('provider_email', sa.String(length=255), nullable=True),
|
|
38
|
+
sa.Column('user_id', GUID(length=16), nullable=False),
|
|
39
|
+
sa.PrimaryKeyConstraint('id', name=op.f('pk_oauth_accounts')),
|
|
40
|
+
sa.ForeignKeyConstraint(
|
|
41
|
+
['user_id'], ['users.id'],
|
|
42
|
+
name=op.f('fk_oauth_accounts_user_id_users'),
|
|
43
|
+
ondelete='CASCADE'
|
|
44
|
+
),
|
|
45
|
+
sa.UniqueConstraint(
|
|
46
|
+
'provider', 'provider_account_id',
|
|
47
|
+
name='uq_oauth_provider_account'
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
op.create_index(
|
|
51
|
+
op.f('ix_oauth_accounts_user_id'),
|
|
52
|
+
'oauth_accounts',
|
|
53
|
+
['user_id'],
|
|
54
|
+
unique=False
|
|
55
|
+
)
|
|
56
|
+
op.create_index(
|
|
57
|
+
op.f('ix_oauth_accounts_provider_account'),
|
|
58
|
+
'oauth_accounts',
|
|
59
|
+
['provider', 'provider_account_id'],
|
|
60
|
+
unique=True
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Step 2: Migrate existing data from users to oauth_accounts
|
|
64
|
+
# Generate binary UUIDs (16 bytes) for new records and copy oauth data
|
|
65
|
+
conn = op.get_bind()
|
|
66
|
+
dialect = conn.dialect.name
|
|
67
|
+
|
|
68
|
+
if dialect == 'sqlite':
|
|
69
|
+
uuid_func = 'randomblob(16)'
|
|
70
|
+
else: # PostgreSQL and others
|
|
71
|
+
uuid_func = 'gen_random_uuid()'
|
|
72
|
+
|
|
73
|
+
conn.execute(sa.text(f"""
|
|
74
|
+
INSERT INTO oauth_accounts (id, created_at, updated_at, provider, provider_account_id, provider_email, user_id)
|
|
75
|
+
SELECT
|
|
76
|
+
{uuid_func},
|
|
77
|
+
created_at,
|
|
78
|
+
updated_at,
|
|
79
|
+
oauth_provider,
|
|
80
|
+
oauth_id,
|
|
81
|
+
email,
|
|
82
|
+
id
|
|
83
|
+
FROM users
|
|
84
|
+
WHERE oauth_provider IS NOT NULL AND oauth_id IS NOT NULL
|
|
85
|
+
"""))
|
|
86
|
+
|
|
87
|
+
# Step 3: Make email nullable on users table
|
|
88
|
+
# SQLite doesn't support ALTER COLUMN, so we need to recreate the table
|
|
89
|
+
# For SQLite, we'll use batch_alter_table
|
|
90
|
+
with op.batch_alter_table('users', schema=None) as batch_op:
|
|
91
|
+
# Drop the unique constraint on oauth_id
|
|
92
|
+
batch_op.drop_constraint('uq_users_oauth_id', type_='unique')
|
|
93
|
+
# Drop the oauth columns
|
|
94
|
+
batch_op.drop_column('oauth_provider')
|
|
95
|
+
batch_op.drop_column('oauth_id')
|
|
96
|
+
# Make email nullable - this requires recreating the column in SQLite
|
|
97
|
+
batch_op.alter_column('email',
|
|
98
|
+
existing_type=sa.String(length=255),
|
|
99
|
+
nullable=True)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def downgrade() -> None:
|
|
103
|
+
# Step 1: Add back oauth columns to users table
|
|
104
|
+
with op.batch_alter_table('users', schema=None) as batch_op:
|
|
105
|
+
batch_op.add_column(sa.Column('oauth_provider', sa.String(length=50), nullable=True))
|
|
106
|
+
batch_op.add_column(sa.Column('oauth_id', sa.String(length=255), nullable=True))
|
|
107
|
+
batch_op.alter_column('email',
|
|
108
|
+
existing_type=sa.String(length=255),
|
|
109
|
+
nullable=False)
|
|
110
|
+
|
|
111
|
+
# Step 2: Migrate data back from oauth_accounts to users
|
|
112
|
+
# Only migrate the first oauth account per user
|
|
113
|
+
conn = op.get_bind()
|
|
114
|
+
conn.execute(sa.text("""
|
|
115
|
+
UPDATE users
|
|
116
|
+
SET oauth_provider = (
|
|
117
|
+
SELECT provider FROM oauth_accounts
|
|
118
|
+
WHERE oauth_accounts.user_id = users.id
|
|
119
|
+
LIMIT 1
|
|
120
|
+
),
|
|
121
|
+
oauth_id = (
|
|
122
|
+
SELECT provider_account_id FROM oauth_accounts
|
|
123
|
+
WHERE oauth_accounts.user_id = users.id
|
|
124
|
+
LIMIT 1
|
|
125
|
+
)
|
|
126
|
+
"""))
|
|
127
|
+
|
|
128
|
+
# Step 3: Make oauth columns non-nullable and add unique constraint
|
|
129
|
+
with op.batch_alter_table('users', schema=None) as batch_op:
|
|
130
|
+
batch_op.alter_column('oauth_provider',
|
|
131
|
+
existing_type=sa.String(length=50),
|
|
132
|
+
nullable=False)
|
|
133
|
+
batch_op.alter_column('oauth_id',
|
|
134
|
+
existing_type=sa.String(length=255),
|
|
135
|
+
nullable=False)
|
|
136
|
+
batch_op.create_unique_constraint('uq_users_oauth_id', ['oauth_id'])
|
|
137
|
+
|
|
138
|
+
# Step 4: Drop oauth_accounts table
|
|
139
|
+
op.drop_index(op.f('ix_oauth_accounts_provider_account'), table_name='oauth_accounts')
|
|
140
|
+
op.drop_index(op.f('ix_oauth_accounts_user_id'), table_name='oauth_accounts')
|
|
141
|
+
op.drop_table('oauth_accounts')
|
|
@@ -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')
|
skrift/alembic.ini
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Alembic Configuration File
|
|
2
|
+
|
|
3
|
+
[alembic]
|
|
4
|
+
# Path to migration scripts (relative to this file)
|
|
5
|
+
script_location = %(here)s/alembic
|
|
6
|
+
|
|
7
|
+
# Template used to generate migration files
|
|
8
|
+
file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d%%(second).2d_%%(rev)s_%%(slug)s
|
|
9
|
+
|
|
10
|
+
# Prepend sys.path with this value before running migrations
|
|
11
|
+
prepend_sys_path = .
|
|
12
|
+
|
|
13
|
+
# Timezone for timestamps in migration files (uses system timezone if not set)
|
|
14
|
+
# timezone =
|
|
15
|
+
|
|
16
|
+
# Max length of characters to apply to the "slug" field
|
|
17
|
+
truncate_slug_length = 40
|
|
18
|
+
|
|
19
|
+
# Set to 'true' to run the environment during revision generation (for autogenerate support)
|
|
20
|
+
revision_environment = false
|
|
21
|
+
|
|
22
|
+
# Set to 'true' to allow running in "offline" mode
|
|
23
|
+
# sourceless = false
|
|
24
|
+
|
|
25
|
+
# Version path separator (os, space, :, ;)
|
|
26
|
+
# version_path_separator = os
|
|
27
|
+
|
|
28
|
+
# Output encoding used when revision files are written from script.py.mako
|
|
29
|
+
# output_encoding = utf-8
|
|
30
|
+
|
|
31
|
+
# Database URL - override with environment variable DATABASE_URL
|
|
32
|
+
# The env.py file reads this from the Settings class, so this is just a placeholder
|
|
33
|
+
sqlalchemy.url = sqlite+aiosqlite:///./app.db
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
[post_write_hooks]
|
|
37
|
+
# Format newly generated revision files using ruff
|
|
38
|
+
# hooks = ruff_format
|
|
39
|
+
# ruff_format.type = exec
|
|
40
|
+
# ruff_format.executable = ruff
|
|
41
|
+
# ruff_format.options = format REVISION_SCRIPT_FILENAME
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Logging configuration
|
|
45
|
+
[loggers]
|
|
46
|
+
keys = root,sqlalchemy,alembic
|
|
47
|
+
|
|
48
|
+
[handlers]
|
|
49
|
+
keys = console
|
|
50
|
+
|
|
51
|
+
[formatters]
|
|
52
|
+
keys = generic
|
|
53
|
+
|
|
54
|
+
[logger_root]
|
|
55
|
+
level = WARN
|
|
56
|
+
handlers = console
|
|
57
|
+
qualname =
|
|
58
|
+
|
|
59
|
+
[logger_sqlalchemy]
|
|
60
|
+
level = WARN
|
|
61
|
+
handlers =
|
|
62
|
+
qualname = sqlalchemy.engine
|
|
63
|
+
|
|
64
|
+
[logger_alembic]
|
|
65
|
+
level = INFO
|
|
66
|
+
handlers =
|
|
67
|
+
qualname = alembic
|
|
68
|
+
|
|
69
|
+
[handler_console]
|
|
70
|
+
class = StreamHandler
|
|
71
|
+
args = (sys.stderr,)
|
|
72
|
+
level = NOTSET
|
|
73
|
+
formatter = generic
|
|
74
|
+
|
|
75
|
+
[formatter_generic]
|
|
76
|
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
77
|
+
datefmt = %H:%M:%S
|