feat(auth): support trusted device (#52)
New API to maintain sessions and devices:
- GET /api/private/admin/sessions
- DELETE /api/private/admin/sessions/{session_id}
- GET /api/private/admin/trusted-devices
- DELETE /api/private/admin/trusted-devices/{device_id}
Auth:
web clients request `/oauth/token` and `/api/v2/session/verify` with `X-UUID` header to save the client as trusted device.
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
"""session: support multi-session
|
||||
|
||||
Revision ID: 72a9b8f3f863
|
||||
Revises: b1ac2154bd0d
|
||||
Create Date: 2025-10-02 07:17:19.297498
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "72a9b8f3f863"
|
||||
down_revision: str | Sequence[str] | None = "b1ac2154bd0d"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"trusted_devices",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("user_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("ip_address", sa.VARCHAR(length=45), nullable=False),
|
||||
sa.Column("user_agent", sa.Text(), nullable=False),
|
||||
sa.Column("client_type", sa.VARCHAR(length=10), nullable=False),
|
||||
sa.Column("web_uuid", sa.VARCHAR(length=36), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("last_used_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("expires_at", sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"],
|
||||
["lazer_users.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.alter_column(
|
||||
"login_sessions",
|
||||
"is_new_location",
|
||||
new_column_name="is_new_device",
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
)
|
||||
op.create_index(op.f("ix_trusted_devices_user_id"), "trusted_devices", ["user_id"], unique=False)
|
||||
op.add_column("login_sessions", sa.Column("web_uuid", sa.VARCHAR(length=36), nullable=True))
|
||||
op.alter_column(
|
||||
"login_sessions",
|
||||
"ip_address",
|
||||
existing_type=mysql.VARCHAR(length=255),
|
||||
type_=sa.VARCHAR(length=45),
|
||||
existing_nullable=False,
|
||||
)
|
||||
op.alter_column(
|
||||
"login_sessions", "user_agent", existing_type=mysql.VARCHAR(length=250), type_=sa.Text(), existing_nullable=True
|
||||
)
|
||||
op.drop_index(op.f("ix_login_sessions_session_token"), table_name="login_sessions")
|
||||
op.create_foreign_key(None, "login_sessions", "lazer_users", ["user_id"], ["id"])
|
||||
op.drop_column("login_sessions", "country_code")
|
||||
op.drop_column("login_sessions", "session_token")
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column("login_sessions", sa.Column("session_token", sa.VARCHAR(length=64), nullable=True))
|
||||
op.add_column("login_sessions", sa.Column("country_code", sa.VARCHAR(length=255), nullable=True))
|
||||
op.create_index(op.f("ix_login_sessions_session_token"), "login_sessions", ["session_token"], unique=False)
|
||||
|
||||
op.alter_column(
|
||||
"login_sessions",
|
||||
"user_agent",
|
||||
existing_type=sa.Text(),
|
||||
type_=mysql.VARCHAR(length=250),
|
||||
existing_nullable=True,
|
||||
)
|
||||
op.alter_column(
|
||||
"login_sessions",
|
||||
"ip_address",
|
||||
existing_type=sa.String(length=45),
|
||||
type_=mysql.VARCHAR(length=255),
|
||||
existing_nullable=False,
|
||||
)
|
||||
|
||||
op.drop_column("login_sessions", "web_uuid")
|
||||
op.alter_column(
|
||||
"login_sessions",
|
||||
"is_new_device",
|
||||
new_column_name="is_new_location",
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
)
|
||||
op.drop_constraint(op.f("fk_login_sessions_user_id_lazer_users"), "login_sessions", type_="foreignkey")
|
||||
|
||||
op.drop_index(op.f("ix_trusted_devices_user_id"), table_name="trusted_devices")
|
||||
op.drop_table("trusted_devices")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,40 @@
|
||||
"""auth: add refresh_token_expires_at
|
||||
|
||||
Revision ID: 7fe1319250c5
|
||||
Revises: 72a9b8f3f863
|
||||
Create Date: 2025-10-02 10:50:21.169065
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "7fe1319250c5"
|
||||
down_revision: str | Sequence[str] | None = "72a9b8f3f863"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column("oauth_tokens", sa.Column("refresh_token_expires_at", sa.DateTime(), nullable=True))
|
||||
op.create_index(op.f("ix_oauth_tokens_expires_at"), "oauth_tokens", ["expires_at"], unique=False)
|
||||
op.create_index(
|
||||
op.f("ix_oauth_tokens_refresh_token_expires_at"), "oauth_tokens", ["refresh_token_expires_at"], unique=False
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f("ix_oauth_tokens_refresh_token_expires_at"), table_name="oauth_tokens")
|
||||
op.drop_index(op.f("ix_oauth_tokens_expires_at"), table_name="oauth_tokens")
|
||||
op.drop_column("oauth_tokens", "refresh_token_expires_at")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,35 @@
|
||||
"""session: add device_id to LoginSession
|
||||
|
||||
Revision ID: 9556cd2ec11f
|
||||
Revises: 7fe1319250c5
|
||||
Create Date: 2025-10-02 11:03:09.803140
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "9556cd2ec11f"
|
||||
down_revision: str | Sequence[str] | None = "7fe1319250c5"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column("login_sessions", sa.Column("device_id", sa.BigInteger(), nullable=True))
|
||||
op.create_index(op.f("ix_login_sessions_device_id"), "login_sessions", ["device_id"], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("login_sessions", "device_id")
|
||||
# ### end Alembic commands ###
|
||||
Reference in New Issue
Block a user