Compare commits

..

5 commits

31 changed files with 1811 additions and 179 deletions

View file

@ -22,3 +22,13 @@ GEMINI_API_KEY=your-gemini-api-key-here
STORAGE_ACCESS_KEY=langlearn STORAGE_ACCESS_KEY=langlearn
STORAGE_SECRET_KEY=changeme-use-a-long-random-string STORAGE_SECRET_KEY=changeme-use-a-long-random-string
STORAGE_BUCKET=langlearn STORAGE_BUCKET=langlearn
# Transactional email — set to "scaleway" in production, "stub" logs instead of sending
TRANSACTIONAL_EMAIL_PROVIDER=stub
# Scaleway Transactional Email (https://console.scaleway.com/transactional-email)
# Required when TRANSACTIONAL_EMAIL_PROVIDER=scaleway
SCALEWAY_TEM_SECRET_KEY=your-scaleway-secret-key-here
SCALEWAY_TEM_PROJECT_ID=your-scaleway-project-id-here
SCALEWAY_TEM_FROM_ADDRESS=noreply@yourdomain.com
# SCALEWAY_TEM_REGION=fr-par # default; change to nl-ams if needed

View file

@ -0,0 +1,41 @@
"""add email verification tokens table
Revision ID: 0011
Revises: 0010
Create Date: 2026-04-10
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
revision: str = "0011"
down_revision: Union[str, None] = "0010"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"email_verification_tokens",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column(
"user_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("token", sa.Text(), nullable=False, unique=True),
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("used_at", sa.DateTime(timezone=True), nullable=True),
)
op.create_index("ix_email_verification_tokens_token", "email_verification_tokens", ["token"])
op.create_index("ix_email_verification_tokens_user_id", "email_verification_tokens", ["user_id"])
def downgrade() -> None:
op.drop_index("ix_email_verification_tokens_user_id", table_name="email_verification_tokens")
op.drop_index("ix_email_verification_tokens_token", table_name="email_verification_tokens")
op.drop_table("email_verification_tokens")

View file

@ -0,0 +1,24 @@
"""add human_name to users
Revision ID: 0012
Revises: 0011
Create Date: 2026-04-11
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
revision: str = "0012"
down_revision: Union[str, None] = "0011"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column("users", sa.Column("human_name", sa.Text(), nullable=True))
def downgrade() -> None:
op.drop_column("users", "human_name")

View file

@ -10,6 +10,11 @@ class Settings(BaseSettings):
gemini_api_key: str gemini_api_key: str
admin_user_emails: str = "" # comma-separated list of admin email addresses admin_user_emails: str = "" # comma-separated list of admin email addresses
api_base_url: str = "http://localhost:8000" api_base_url: str = "http://localhost:8000"
transactional_email_provider: str = "stub" # "stub" | "scaleway"
scaleway_tem_secret_key: str = ""
scaleway_tem_project_id: str = ""
scaleway_tem_from_address: str = ""
scaleway_tem_region: str = "fr-par"
storage_endpoint_url: str storage_endpoint_url: str
storage_access_key: str storage_access_key: str
storage_secret_key: str storage_secret_key: str

View file

@ -11,4 +11,5 @@ class Account:
is_active: bool is_active: bool
is_email_verified: bool is_email_verified: bool
created_at: datetime created_at: datetime
human_name: str | None = None
learnable_languages: list[LearnableLanguage] = field(default_factory=list) learnable_languages: list[LearnableLanguage] = field(default_factory=list)

View file

@ -1,4 +1,4 @@
from dataclasses import dataclass from dataclasses import dataclass, field
@dataclass @dataclass
@ -9,6 +9,15 @@ class Wordform:
tags: list[str] tags: list[str]
@dataclass
class SenseLink:
id: str
sense_id: str
link_text: str
link_target: str
target_lemma_id: str | None
@dataclass @dataclass
class Sense: class Sense:
id: str id: str
@ -17,6 +26,7 @@ class Sense:
gloss: str gloss: str
topics: list[str] topics: list[str]
tags: list[str] tags: list[str]
links: list[SenseLink] = field(default_factory=list)
@dataclass @dataclass

View file

@ -1,4 +1,5 @@
import uuid import uuid
from datetime import datetime, timezone
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
@ -6,54 +7,69 @@ from sqlalchemy.ext.asyncio import AsyncSession
from ..models.account import Account from ..models.account import Account
from ..models.learnable_language import LearnableLanguage from ..models.learnable_language import LearnableLanguage
from ...auth import hash_password from ...auth import hash_password, verify_password
from ...config import settings
from ...outbound.email.protocol import TransactionalEmailClient
from ...outbound.postgres.entities.user_entity import User as UserEntity from ...outbound.postgres.entities.user_entity import User as UserEntity
from ...outbound.postgres.repositories import learnable_language_repository, user_repository from ...outbound.postgres.repositories import (
email_verification_token_repository,
learnable_language_repository,
user_repository,
)
class AccountService: class AccountService:
"""Handles account-level operations: registration, profile retrieval, and managing """Handles account-level operations: registration, authentication, email
the set of languages a user is learning. verification, and managing the set of languages a user is learning.
All methods operate on behalf of a single authenticated user (or, for
``create_account``, the user being created).
Usage:: Usage::
service = AccountService(db) service = AccountService(db)
# Registration # Registration — returns the new account and the verification link
account = await service.create_account("alice@example.com", "s3cr3t") account, link = await service.register_new_account("alice@example.com", "s3cr3t")
# Email verification
await service.verify_email_address(token_from_link)
# Authentication — raises ValueError on bad credentials or disabled account
account = await service.authenticate_with_password("alice@example.com", "s3cr3t")
# Profile retrieval # Profile retrieval
account = await service.get_account(user_id) account = await service.get_account(user_id)
print(account.learnable_languages) # [LearnableLanguage(...), ...]
# Add French (B1) to the account # Language management
lang = await service.add_learnable_language( lang = await service.add_learnable_language(user_id, "en", "fr", ["B1"])
user_id, source_language="en", target_language="fr", proficiencies=["B1"]
)
# Remove it again
await service.remove_learnable_language(user_id, lang.id) await service.remove_learnable_language(user_id, lang.id)
""" """
def __init__(self, db: AsyncSession) -> None: def __init__(
self,
db: AsyncSession,
email_client: TransactionalEmailClient | None = None,
) -> None:
self.db = db self.db = db
# Defer import to avoid circular dependency at module load time.
if email_client is not None:
self._email_client = email_client
else:
from ...outbound.email.factory import get_email_client
self._email_client = get_email_client()
async def create_account(self, email: str, password: str) -> Account: # ------------------------------------------------------------------
"""Create a new user account, hashing the plain-text password before storage. # Registration & authentication
# ------------------------------------------------------------------
Raises ``ValueError`` if the email address is already registered, so the async def register_new_account(self, email: str, password: str) -> tuple[Account, str]:
caller does not need to catch SQLAlchemy exceptions directly. """Create a new account, generate an email verification token, and send
the verification email.
Returns ``(account, verification_link)`` on success.
Raises ``ValueError`` if the email address is already registered.
Usage:: Usage::
try: account, link = await service.register_new_account("alice@example.com", "s3cr3t")
account = await service.create_account("alice@example.com", "s3cr3t")
except ValueError:
# email already taken
...
""" """
try: try:
user = await user_repository.create( user = await user_repository.create(
@ -65,14 +81,99 @@ class AccountService:
await self.db.rollback() await self.db.rollback()
raise ValueError("Email already registered") raise ValueError("Email already registered")
return Account( account = Account(
id=str(user.id), id=str(user.id),
email=user.email, email=user.email,
human_name=user.human_name,
is_active=user.is_active, is_active=user.is_active,
is_email_verified=user.is_email_verified, is_email_verified=user.is_email_verified,
created_at=user.created_at, created_at=user.created_at,
) )
token_row = await email_verification_token_repository.create(
self.db, uuid.UUID(account.id)
)
link = f"{settings.api_base_url}/api/auth/verify-email?token={token_row.token}"
await self._email_client.send_email(
to=account.email,
subject="Verify your email address",
html_body=(
f"<p>Thanks for signing up! Please verify your email address by clicking the link below:</p>"
f'<p><a href="{link}">{link}</a></p>'
f"<p>This link expires in 24 hours.</p>"
),
)
return account, link
async def authenticate_with_password(self, email: str, password: str) -> Account:
"""Validate credentials and return the matching account.
Raises ``ValueError("invalid_credentials")`` for an unrecognised email
or wrong password, and ``ValueError("account_disabled")`` if the account
has been deactivated.
Usage::
try:
account = await service.authenticate_with_password(email, password)
except ValueError as exc:
if str(exc) == "account_disabled":
... # 403
... # 401
"""
user = await user_repository.get_by_email(self.db, email)
if user is None or not verify_password(password, user.hashed_password):
raise ValueError("invalid_credentials")
if not user.is_active:
raise ValueError("account_disabled")
# TODO(email-verification): uncomment once email verification is tested end-to-end
# if not user.is_email_verified:
# raise ValueError("email_not_verified")
return Account(
id=str(user.id),
email=user.email,
human_name=user.human_name,
is_active=user.is_active,
is_email_verified=user.is_email_verified,
created_at=user.created_at,
)
async def verify_email_address(self, token: str) -> None:
"""Consume a verification token and mark the account as email-verified.
Raises ``ValueError`` if the token is invalid, already used, or expired.
Usage::
await service.verify_email_address(token_from_query_string)
"""
token_row = await email_verification_token_repository.get_by_token(self.db, token)
if token_row is None or token_row.used_at is not None:
raise ValueError("Verification link is invalid or has already been used")
if token_row.expires_at < datetime.now(timezone.utc):
raise ValueError("Verification link has expired")
await email_verification_token_repository.mark_used(self.db, token_row.id)
result = await self.db.execute(
select(UserEntity).where(UserEntity.id == token_row.user_id)
)
user = result.scalar_one()
user.is_email_verified = True
await self.db.commit()
# ------------------------------------------------------------------
# Profile
# ------------------------------------------------------------------
async def get_account(self, user_id: uuid.UUID) -> Account: async def get_account(self, user_id: uuid.UUID) -> Account:
"""Retrieve a user's account profile including all their learnable languages. """Retrieve a user's account profile including all their learnable languages.
@ -84,7 +185,6 @@ class AccountService:
for lang in account.learnable_languages: for lang in account.learnable_languages:
print(lang.target_language, lang.proficiencies) print(lang.target_language, lang.proficiencies)
""" """
# user_repository only exposes get_by_email; query by id directly
result = await self.db.execute( result = await self.db.execute(
select(UserEntity).where(UserEntity.id == user_id) select(UserEntity).where(UserEntity.id == user_id)
) )
@ -97,12 +197,70 @@ class AccountService:
return Account( return Account(
id=str(user.id), id=str(user.id),
email=user.email, email=user.email,
human_name=user.human_name,
is_active=user.is_active, is_active=user.is_active,
is_email_verified=user.is_email_verified, is_email_verified=user.is_email_verified,
created_at=user.created_at, created_at=user.created_at,
learnable_languages=languages, learnable_languages=languages,
) )
# ------------------------------------------------------------------
# Onboarding
# ------------------------------------------------------------------
async def complete_onboarding(
self,
user_id: uuid.UUID,
human_name: str,
language_pair: str,
proficiencies: list[str],
) -> Account:
"""Record the user's name and first language pair, completing onboarding.
``language_pair`` is a comma-separated ``"source,target"`` string (e.g. ``"en,fr"``).
Usage::
account = await service.complete_onboarding(
user_id, human_name="Alice", language_pair="en,fr", proficiencies=["B1"]
)
"""
source, target = language_pair.split(",", 1)
await user_repository.set_human_name(self.db, user_id, human_name)
await self.add_learnable_language(user_id, source, target, proficiencies)
return await self.get_account(user_id)
async def get_account_status(self, user_id: uuid.UUID) -> tuple[list[str], list[str]]:
"""Return ``(problem_flags, error_messages)`` describing any blockers on the account.
Current flags:
- ``unvalidated_email`` the user has not verified their email address
- ``no_onboarding`` the user has not added any language pairs yet
Usage::
flags, messages = await service.get_account_status(user_id)
if flags:
... # surface to the user
"""
account = await self.get_account(user_id)
flags: list[str] = []
messages: list[str] = []
if not account.is_email_verified:
flags.append("unvalidated_email")
messages.append("Please validate your email address")
if not account.learnable_languages:
flags.append("no_onboarding")
messages.append("Please complete onboarding")
return flags, messages
# ------------------------------------------------------------------
# Language management
# ------------------------------------------------------------------
async def add_learnable_language( async def add_learnable_language(
self, self,
user_id: uuid.UUID, user_id: uuid.UUID,
@ -121,7 +279,6 @@ class AccountService:
target_language="fr", target_language="fr",
proficiencies=["B1", "B2"], proficiencies=["B1", "B2"],
) )
print(lang.id) # UUID of the learnable_language row
""" """
return await learnable_language_repository.upsert( return await learnable_language_repository.upsert(
self.db, self.db,
@ -136,8 +293,8 @@ class AccountService:
) -> None: ) -> None:
"""Remove a learnable language from the user's account by its row ID. """Remove a learnable language from the user's account by its row ID.
Raises ``ValueError`` if the language entry does not exist or does not belong Raises ``ValueError`` if the language entry does not exist or does not
to ``user_id``. belong to ``user_id``.
Usage:: Usage::

View file

@ -7,3 +7,15 @@ SUPPORTED_LANGUAGES: dict[str, str] = {
} }
SUPPORTED_LEVELS = {"A1", "A2", "B1", "B2", "C1", "C2"} SUPPORTED_LEVELS = {"A1", "A2", "B1", "B2", "C1", "C2"}
# Ordered for display (SUPPORTED_LEVELS is a set and has no guaranteed order).
LEVEL_ORDER: list[str] = ["A1", "A2", "B1", "B2", "C1", "C2"]
LEVEL_DESCRIPTIONS: dict[str, str] = {
"A1": "Little to no prior knowledge of the language",
"A2": "Basic knowledge; can understand simple phrases and expressions",
"B1": "Intermediate; can handle most everyday situations",
"B2": "Upper intermediate; can interact fluently with native speakers",
"C1": "Advanced; able to use language flexibly and effectively",
"C2": "Near-native mastery of the language",
}

View file

@ -5,7 +5,6 @@ from .routers.api import generation, pos
from fastapi import FastAPI from fastapi import FastAPI
from .routers.api import jobs from .routers.api import jobs
from .routers import auth as auth_router
from .routers import media as media_router from .routers import media as media_router
from .routers.api.main import api_router from .routers.api.main import api_router
from .routers.bff.main import bff_router from .routers.bff.main import bff_router
@ -29,7 +28,6 @@ app = FastAPI(title="Language Learning API", lifespan=lifespan)
app.include_router(api_router) app.include_router(api_router)
app.include_router(bff_router) app.include_router(bff_router)
app.include_router(auth_router.router)
app.include_router(media_router.router) app.include_router(media_router.router)

View file

View file

@ -0,0 +1,25 @@
from .protocol import TransactionalEmailClient
def get_email_client() -> TransactionalEmailClient:
from ...config import settings
if settings.transactional_email_provider == "scaleway":
from ..scaleway_tem.tem_client import ScalewayTEMClient
return ScalewayTEMClient(
secret_key=settings.scaleway_tem_secret_key,
from_address=settings.scaleway_tem_from_address,
project_id=settings.scaleway_tem_project_id,
region=settings.scaleway_tem_region,
)
if settings.transactional_email_provider == "stub":
from .stub_client import StubEmailClient
return StubEmailClient()
raise ValueError(
f"Unknown transactional_email_provider: {settings.transactional_email_provider!r}. "
"Valid options: 'scaleway', 'stub'."
)

View file

@ -0,0 +1,6 @@
from typing import Protocol
class TransactionalEmailClient(Protocol):
async def send_email(self, to: str, subject: str, html_body: str) -> None:
...

View file

@ -0,0 +1,15 @@
import logging
logger = logging.getLogger(__name__)
class StubEmailClient:
"""Logs emails instead of sending them. Use for local development."""
async def send_email(self, to: str, subject: str, html_body: str) -> None:
logger.info(
"STUB EMAIL — would have sent:\n To: %s\n Subject: %s\n Body:\n%s",
to,
subject,
html_body,
)

View file

@ -0,0 +1,33 @@
import secrets
import uuid
from datetime import datetime, timedelta, timezone
from sqlalchemy import DateTime, ForeignKey, Text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
from ..database import Base
TOKEN_TTL_HOURS = 24
class EmailVerificationToken(Base):
__tablename__ = "email_verification_tokens"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
)
token: Mapped[str] = mapped_column(Text, nullable=False, unique=True)
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
@staticmethod
def make(user_id: uuid.UUID) -> "EmailVerificationToken":
return EmailVerificationToken(
user_id=user_id,
token=secrets.token_urlsafe(32),
expires_at=datetime.now(timezone.utc) + timedelta(hours=TOKEN_TTL_HOURS),
)

View file

@ -16,9 +16,9 @@ class User(Base):
) )
email: Mapped[str] = mapped_column(String(255), nullable=False, unique=True, index=True) email: Mapped[str] = mapped_column(String(255), nullable=False, unique=True, index=True)
hashed_password: Mapped[str] = mapped_column(String(255), nullable=False) hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
human_name: Mapped[str | None] = mapped_column(Text, nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
# TODO(email-verification): set to False and require verification once transactional email is implemented is_email_verified: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
is_email_verified: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
created_at: Mapped[datetime] = mapped_column( created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc), default=lambda: datetime.now(timezone.utc),

View file

@ -0,0 +1,44 @@
import uuid
from datetime import datetime, timezone
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from ..entities.email_verification_token_entity import EmailVerificationToken
async def create(db: AsyncSession, user_id: uuid.UUID) -> EmailVerificationToken:
row = EmailVerificationToken.make(user_id)
db.add(row)
await db.commit()
await db.refresh(row)
return row
async def get_by_token(db: AsyncSession, token: str) -> EmailVerificationToken | None:
result = await db.execute(
select(EmailVerificationToken).where(EmailVerificationToken.token == token)
)
return result.scalar_one_or_none()
async def get_active_for_user(db: AsyncSession, user_id: uuid.UUID) -> EmailVerificationToken | None:
"""Return the most recently created unused, unexpired token for this user."""
result = await db.execute(
select(EmailVerificationToken)
.where(EmailVerificationToken.user_id == user_id)
.where(EmailVerificationToken.used_at.is_(None))
.where(EmailVerificationToken.expires_at > datetime.now(timezone.utc))
.order_by(EmailVerificationToken.expires_at.desc())
.limit(1)
)
return result.scalar_one_or_none()
async def mark_used(db: AsyncSession, token_id: uuid.UUID) -> None:
result = await db.execute(
select(EmailVerificationToken).where(EmailVerificationToken.id == token_id)
)
row = result.scalar_one()
row.used_at = datetime.now(timezone.utc)
await db.commit()

View file

@ -1,3 +1,5 @@
import uuid
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@ -15,3 +17,10 @@ async def create(db: AsyncSession, email: str, hashed_password: str) -> User:
async def get_by_email(db: AsyncSession, email: str) -> User | None: async def get_by_email(db: AsyncSession, email: str) -> User | None:
result = await db.execute(select(User).where(User.email == email)) result = await db.execute(select(User).where(User.email == email))
return result.scalar_one_or_none() return result.scalar_one_or_none()
async def set_human_name(db: AsyncSession, user_id: uuid.UUID, human_name: str) -> None:
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one()
user.human_name = human_name
await db.commit()

View file

@ -0,0 +1,29 @@
import httpx
_TEM_API_URL = "https://api.scaleway.com/transactional-email/v1alpha1/regions/{region}/emails"
class ScalewayTEMClient:
def __init__(self, secret_key: str, from_address: str, project_id: str, region: str = "fr-par") -> None:
self._secret_key = secret_key
self._from_address = from_address
self._project_id = project_id
self._url = _TEM_API_URL.format(region=region)
async def send_email(self, to: str, subject: str, html_body: str) -> None:
async with httpx.AsyncClient() as client:
response = await client.post(
self._url,
headers={
"X-Auth-Token": self._secret_key,
"Content-Type": "application/json",
},
json={
"project_id": self._project_id,
"from": {"email": self._from_address},
"to": [{"email": to}],
"subject": subject,
"html": html_body,
},
)
response.raise_for_status()

View file

@ -1,7 +1,7 @@
import uuid import uuid
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, field_validator from pydantic import BaseModel, field_validator, model_validator
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from ...auth import verify_token from ...auth import verify_token
@ -76,6 +76,71 @@ async def add_learnable_language(
) )
class OnboardingRequest(BaseModel):
human_name: str
language_pairs: list[str]
proficiencies: list[list[str]]
@model_validator(mode="after")
def validate_shape(self) -> "OnboardingRequest":
if len(self.language_pairs) != 1:
raise ValueError("language_pairs must contain exactly one entry")
if len(self.proficiencies) != 1:
raise ValueError("proficiencies must contain exactly one entry")
pair = self.language_pairs[0]
parts = pair.split(",")
if len(parts) != 2:
raise ValueError(f"language_pairs entry must be 'source,target', got {pair!r}")
source, target = parts
if source not in SUPPORTED_LANGUAGES:
raise ValueError(f"Unsupported source language: {source!r}")
if target not in SUPPORTED_LANGUAGES:
raise ValueError(f"Unsupported target language: {target!r}")
if source == target:
raise ValueError("Source and target language must differ")
levels = self.proficiencies[0]
if not (1 <= len(levels) <= 2):
raise ValueError("proficiencies entry must contain 1 or 2 levels")
invalid = [l for l in levels if l not in SUPPORTED_LEVELS]
if invalid:
raise ValueError(f"Invalid proficiency levels: {invalid}")
return self
class AccountStatusResponse(BaseModel):
problem_flags: list[str]
error_messages: list[str]
@router.post("/onboarding", status_code=status.HTTP_200_OK)
async def complete_onboarding(
body: OnboardingRequest,
db: AsyncSession = Depends(get_db),
token_data: dict = Depends(verify_token),
) -> dict:
user_id = uuid.UUID(token_data["sub"])
await AccountService(db).complete_onboarding(
user_id=user_id,
human_name=body.human_name,
language_pair=body.language_pairs[0],
proficiencies=body.proficiencies[0],
)
return {"success": True}
@router.get("/status", response_model=AccountStatusResponse)
async def get_account_status(
db: AsyncSession = Depends(get_db),
token_data: dict = Depends(verify_token),
) -> AccountStatusResponse:
user_id = uuid.UUID(token_data["sub"])
flags, messages = await AccountService(db).get_account_status(user_id)
return AccountStatusResponse(problem_flags=flags, error_messages=messages)
@router.delete( @router.delete(
"/learnable-languages/{language_id}", "/learnable-languages/{language_id}",
status_code=status.HTTP_204_NO_CONTENT, status_code=status.HTTP_204_NO_CONTENT,

View file

@ -0,0 +1,88 @@
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, EmailStr, field_validator
from sqlalchemy.ext.asyncio import AsyncSession
from ...auth import create_access_token
from ...domain.services.account_service import AccountService
from ...outbound.postgres.database import get_db
router = APIRouter(prefix="/auth", tags=["auth"])
class RegisterRequest(BaseModel):
email: EmailStr
password: str
@field_validator("password")
@classmethod
def password_min_length(cls, v: str) -> str:
if len(v) < 8:
raise ValueError("Password must be at least 8 characters long")
return v
class RegisterResponse(BaseModel):
success: bool
error_message: str | None = None
_todo_remove_me_validate_account_link: str | None = None
model_config = {"populate_by_name": True}
class LoginRequest(BaseModel):
email: EmailStr
password: str
class TokenResponse(BaseModel):
access_token: str
token_type: str = "bearer"
@router.post("/register", response_model=RegisterResponse, status_code=status.HTTP_201_CREATED)
async def register(
body: RegisterRequest,
db: AsyncSession = Depends(get_db),
) -> RegisterResponse:
try:
account, link = await AccountService(db).register_new_account(body.email, body.password)
except ValueError:
return RegisterResponse(
success=False,
error_message="Email address not valid or already in use",
)
return RegisterResponse(
success=True,
**{"_todo_remove_me_validate_account_link": link},
)
@router.post("/login", response_model=TokenResponse)
async def login(
body: LoginRequest,
db: AsyncSession = Depends(get_db),
) -> TokenResponse:
try:
account = await AccountService(db).authenticate_with_password(body.email, body.password)
except ValueError as exc:
if str(exc) == "account_disabled":
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Account disabled")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid email or password"
)
return TokenResponse(access_token=create_access_token(account.id, account.email))
@router.get("/verify-email")
async def verify_email(
token: str,
db: AsyncSession = Depends(get_db),
) -> dict:
try:
await AccountService(db).verify_email_address(token)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
return {"success": True}

View file

@ -1,4 +1,5 @@
from .account import router as account_router from .account import router as account_router
from .auth import router as auth_router
from .flashcards import router as flashcards_router from .flashcards import router as flashcards_router
from .pos import router as pos_router from .pos import router as pos_router
from .translate import router as translate_router from .translate import router as translate_router
@ -11,6 +12,7 @@ from fastapi import APIRouter
api_router = APIRouter(prefix="/api", tags=["api"]) api_router = APIRouter(prefix="/api", tags=["api"])
api_router.include_router(auth_router)
api_router.include_router(account_router) api_router.include_router(account_router)
api_router.include_router(flashcards_router) api_router.include_router(flashcards_router)
api_router.include_router(pos_router) api_router.include_router(pos_router)

View file

@ -1,65 +0,0 @@
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, EmailStr
from sqlalchemy.ext.asyncio import AsyncSession
from ..auth import create_access_token, verify_password
from ..domain.services.account_service import AccountService
from ..outbound.postgres.database import get_db
from ..outbound.postgres.repositories import user_repository
router = APIRouter(prefix="/auth", tags=["auth"])
class RegisterRequest(BaseModel):
email: EmailStr
password: str
class LoginRequest(BaseModel):
email: EmailStr
password: str
class TokenResponse(BaseModel):
access_token: str
token_type: str = "bearer"
@router.post("/register", status_code=status.HTTP_201_CREATED)
async def register(body: RegisterRequest, db: AsyncSession = Depends(get_db)):
# TODO(email-verification): send verification email here once transactional
# email is implemented. Set is_email_verified=False on the User model and
# require verification before allowing login.
try:
account = await AccountService(db).create_account(body.email, body.password)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc))
return {"id": account.id, "email": account.email}
@router.post("/login", response_model=TokenResponse)
async def login(body: LoginRequest, db: AsyncSession = Depends(get_db)):
user = await user_repository.get_by_email(db, body.email)
if user is None or not verify_password(body.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid email or password",
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Account disabled",
)
# TODO(email-verification): uncomment once email verification is in place
# if not user.is_email_verified:
# raise HTTPException(
# status_code=status.HTTP_403_FORBIDDEN,
# detail="Email address not verified",
# )
token = create_access_token(str(user.id), user.email)
return TokenResponse(access_token=token)

View file

@ -0,0 +1,118 @@
import uuid
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from ...auth import verify_token
from ...config import settings
from ...domain.services.account_service import AccountService
from ...languages import SUPPORTED_LANGUAGES, SUPPORTED_LEVELS, LEVEL_ORDER, LEVEL_DESCRIPTIONS
from ...outbound.postgres.database import get_db
from ...outbound.postgres.repositories import email_verification_token_repository
router = APIRouter(prefix="/account", tags=["bff"])
class LanguagePairOption(BaseModel):
value: str
label: str
description: str
class ProficiencyOption(BaseModel):
value: str
label: str
description: str
class OnboardingResponse(BaseModel):
_todo_remove_me_validate_account_link: str | None = None
language_pairs: list[LanguagePairOption]
proficiencies: list[ProficiencyOption]
model_config = {"populate_by_name": True}
def _build_language_pairs() -> list[LanguagePairOption]:
pairs = []
for source_code, source_name in SUPPORTED_LANGUAGES.items():
for target_code, target_name in SUPPORTED_LANGUAGES.items():
if source_code == target_code:
continue
pairs.append(
LanguagePairOption(
value=f"{source_code},{target_code}",
label=f"{source_name} to {target_name}",
description=f"You are a {source_name} speaker, learning {target_name}",
)
)
return pairs
def _build_proficiencies() -> list[ProficiencyOption]:
return [
ProficiencyOption(
value=level,
label=level,
description=LEVEL_DESCRIPTIONS[level],
)
for level in LEVEL_ORDER
if level in SUPPORTED_LEVELS
]
class AccountLanguagePair(BaseModel):
id: str
source_language: str
target_language: str
proficiencies: list[str]
class AccountResponse(BaseModel):
email: str
human_name: str | None
language_pairs: list[AccountLanguagePair]
@router.get("", response_model=AccountResponse)
async def get_account(
db: AsyncSession = Depends(get_db),
token_data: dict = Depends(verify_token),
) -> AccountResponse:
user_id = uuid.UUID(token_data["sub"])
account = await AccountService(db).get_account(user_id)
return AccountResponse(
email=account.email,
human_name=account.human_name,
language_pairs=[
AccountLanguagePair(
id=lang.id,
source_language=lang.source_language,
target_language=lang.target_language,
proficiencies=lang.proficiencies,
)
for lang in account.learnable_languages
],
)
@router.get("/onboarding", response_model=OnboardingResponse)
async def get_onboarding(
db: AsyncSession = Depends(get_db),
token_data: dict = Depends(verify_token),
) -> OnboardingResponse:
user_id = uuid.UUID(token_data["sub"])
active_token = await email_verification_token_repository.get_active_for_user(db, user_id)
link = (
f"{settings.api_base_url}/api/auth/verify-email?token={active_token.token}"
if active_token
else None
)
return OnboardingResponse(
**{"_todo_remove_me_validate_account_link": link},
language_pairs=_build_language_pairs(),
proficiencies=_build_proficiencies(),
)

View file

@ -1,3 +1,4 @@
from .account import router as account_router
from .articles import router as article_router from .articles import router as article_router
from .user_profile import router as user_profile_router from .user_profile import router as user_profile_router
@ -5,5 +6,6 @@ from fastapi import APIRouter
bff_router = APIRouter(prefix="/bff", tags=["bff"]) bff_router = APIRouter(prefix="/bff", tags=["bff"])
bff_router.include_router(account_router)
bff_router.include_router(article_router) bff_router.include_router(article_router)
bff_router.include_router(user_profile_router) bff_router.include_router(user_profile_router)

View file

@ -2,7 +2,29 @@
This is a HTTP API, written in Python, using the Fastapi framework. This is a HTTP API, written in Python, using the Fastapi framework.
The code should be organised using both Domain Driven Design and Hexagonal Architecture principles. The code should be organised using both Domain Driven Design and Hexagonal Architecture principles. Domain driven design is present when method names are human readable, and translate to what's actually happening or the process it's trying to model.
For example, consider the following:
```py
flashcard_service.create('bonjour', 'hello', 'en', 'fr', user_id)
flashcard_service.record_event(flashcard_id, 'seen', user_id)
```
vs.
```py
flascard_service.create_flashcard_in_language_pair_for_user(
['bonjour', 'hello'],
['fr', 'en'],
user_id
)
flashcard_service.record_card_seen_by_user(flashcard_id, user_id, now)
```
The latter is closer to how the code should read, as a set of more human-readable statements.
Although the structure is outlined below, pragmatism wins over zealous adherence.
## Domain ## Domain
@ -14,6 +36,8 @@ This is where all of the logic around the actual language learning lives.
In `app/domain/models` contains the core Domain Entities, i.e. classes that represent objects for core domain processes. In `app/domain/models` contains the core Domain Entities, i.e. classes that represent objects for core domain processes.
Where possible, the codebase adopts an object-oriented approach. The models therefore present tangible entities. in the system.
### Services ### Services
The `app/domain/services` directory contains modules that encapsulate the "orchestration" or "choreography" of other components of the system to achieve complex, domain actions. The `app/domain/services` directory contains modules that encapsulate the "orchestration" or "choreography" of other components of the system to achieve complex, domain actions.
@ -22,6 +46,32 @@ For example:
- `TextGenerationService` details the step-by-step process of how a series of text is synthesised, audio is generated, parts of speech tagged, timed transcripts generated, etc. which is then used by the learner for language learning. - `TextGenerationService` details the step-by-step process of how a series of text is synthesised, audio is generated, parts of speech tagged, timed transcripts generated, etc. which is then used by the learner for language learning.
### Routers
This is where definitions of the HTTP endpoints lives.
Logic that lives here is called 'controller logic' (after the Rails convention), and it should be responsible for processing an incoming request and formatting an outgoing response.
Business logic should be delegated, as much as possible, to the relevant services and utilities.
There are two kinds of endpoints: `/api` and `/bff`
#### API Routers
Routes which start with `/api` often signify _actions_ the user wants to take, e.g. registering or logging in; creating a Flashcard Event; retrieving the next batch of cards.
These are resource-oriented typical RESTful endpoints. GET for retrieving, POST for creating, PUT for updating, DELETE for deletion.
#### BFF Routers
BFF stands for "backend for frontend". BFF routes are those used on specific screens in the Web UI an allow the loading of varies types of data from various sources. Because they are not "pure" RESTful endpoints, which are resource-driven, they acknowledge that they are tied to a specific screen in the UI.
BFF endpoints are only ever GET, never POST.
Even if two screens _seem_ identical, they will get separate BFFs, to allow separate evolution.
The BFF route will, as much as possible, be the route on the UI prefaced with `/bff`. E.g. the `/account` screen on the UI would be powered by the `/bff/account` endpoint.
## Outbound ## Outbound
The `app/outbound` directory contains modules that allow this project to communicate with external systems. The `app/outbound` directory contains modules that allow this project to communicate with external systems.

View file

@ -49,6 +49,7 @@ services:
STORAGE_ACCESS_KEY: ${STORAGE_ACCESS_KEY:-langlearn} STORAGE_ACCESS_KEY: ${STORAGE_ACCESS_KEY:-langlearn}
STORAGE_SECRET_KEY: ${STORAGE_SECRET_KEY} STORAGE_SECRET_KEY: ${STORAGE_SECRET_KEY}
STORAGE_BUCKET: ${STORAGE_BUCKET:-langlearn} STORAGE_BUCKET: ${STORAGE_BUCKET:-langlearn}
TRANSACTIONAL_EMAIL_PROVIDER: ${TRANSACTIONAL_EMAIL_PROVIDER:-stub}
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy

File diff suppressed because one or more lines are too long

View file

@ -2,7 +2,7 @@
import type { Client, Options as Options2, TDataShape } from './client'; import type { Client, Options as Options2, TDataShape } from './client';
import { client } from './client.gen'; import { client } from './client.gen';
import type { AnalyzePosApiPosPostData, AnalyzePosApiPosPostErrors, AnalyzePosApiPosPostResponses, CreateGenerationJobApiGeneratePostData, CreateGenerationJobApiGeneratePostErrors, CreateGenerationJobApiGeneratePostResponses, GetArticleBffArticlesArticleIdGetData, GetArticleBffArticlesArticleIdGetErrors, GetArticleBffArticlesArticleIdGetResponses, GetJobApiJobsJobIdGetData, GetJobApiJobsJobIdGetErrors, GetJobApiJobsJobIdGetResponses, GetJobsApiJobsGetData, GetJobsApiJobsGetResponses, GetMediaFileMediaFilenameGetData, GetMediaFileMediaFilenameGetErrors, GetMediaFileMediaFilenameGetResponses, GetUserProfileBffUserProfileGetData, GetUserProfileBffUserProfileGetResponses, HealthHealthGetData, HealthHealthGetResponses, ListArticlesBffArticlesGetData, ListArticlesBffArticlesGetErrors, ListArticlesBffArticlesGetResponses, LoginAuthLoginPostData, LoginAuthLoginPostErrors, LoginAuthLoginPostResponses, RegenerateAudioApiJobsJobIdRegenerateAudioPostData, RegenerateAudioApiJobsJobIdRegenerateAudioPostErrors, RegenerateAudioApiJobsJobIdRegenerateAudioPostResponses, RegisterAuthRegisterPostData, RegisterAuthRegisterPostErrors, RegisterAuthRegisterPostResponses, TranslateTextApiTranslateGetData, TranslateTextApiTranslateGetErrors, TranslateTextApiTranslateGetResponses, UpsertLearnableLanguageApiLearnableLanguagesPostData, UpsertLearnableLanguageApiLearnableLanguagesPostErrors, UpsertLearnableLanguageApiLearnableLanguagesPostResponses } from './types.gen'; import type { AddFromTokenApiVocabFromTokenPostData, AddFromTokenApiVocabFromTokenPostErrors, AddFromTokenApiVocabFromTokenPostResponses, AddLearnableLanguageApiAccountLearnableLanguagesPostData, AddLearnableLanguageApiAccountLearnableLanguagesPostErrors, AddLearnableLanguageApiAccountLearnableLanguagesPostResponses, AddWordApiVocabPostData, AddWordApiVocabPostErrors, AddWordApiVocabPostResponses, AnalyzePosApiPosPostData, AnalyzePosApiPosPostErrors, AnalyzePosApiPosPostResponses, CompleteOnboardingApiAccountOnboardingPostData, CompleteOnboardingApiAccountOnboardingPostErrors, CompleteOnboardingApiAccountOnboardingPostResponses, CreateGenerationJobApiGeneratePostData, CreateGenerationJobApiGeneratePostErrors, CreateGenerationJobApiGeneratePostResponses, GenerateFlashcardsApiVocabEntryIdFlashcardsPostData, GenerateFlashcardsApiVocabEntryIdFlashcardsPostErrors, GenerateFlashcardsApiVocabEntryIdFlashcardsPostResponses, GetAccountBffAccountGetData, GetAccountBffAccountGetResponses, GetAccountStatusApiAccountStatusGetData, GetAccountStatusApiAccountStatusGetResponses, GetArticleBffArticlesArticleIdGetData, GetArticleBffArticlesArticleIdGetErrors, GetArticleBffArticlesArticleIdGetResponses, GetJobApiJobsJobIdGetData, GetJobApiJobsJobIdGetErrors, GetJobApiJobsJobIdGetResponses, GetJobsApiJobsGetData, GetJobsApiJobsGetResponses, GetMediaFileMediaFilenameGetData, GetMediaFileMediaFilenameGetErrors, GetMediaFileMediaFilenameGetResponses, GetOnboardingBffAccountOnboardingGetData, GetOnboardingBffAccountOnboardingGetResponses, GetUserProfileBffUserProfileGetData, GetUserProfileBffUserProfileGetResponses, HealthHealthGetData, HealthHealthGetResponses, ListArticlesBffArticlesGetData, ListArticlesBffArticlesGetErrors, ListArticlesBffArticlesGetResponses, ListEntriesApiVocabGetData, ListEntriesApiVocabGetErrors, ListEntriesApiVocabGetResponses, ListFlashcardsApiFlashcardsGetData, ListFlashcardsApiFlashcardsGetResponses, LoginApiAuthLoginPostData, LoginApiAuthLoginPostErrors, LoginApiAuthLoginPostResponses, PendingDisambiguationApiVocabPendingDisambiguationGetData, PendingDisambiguationApiVocabPendingDisambiguationGetResponses, RecordEventApiFlashcardsFlashcardIdEventsPostData, RecordEventApiFlashcardsFlashcardIdEventsPostErrors, RecordEventApiFlashcardsFlashcardIdEventsPostResponses, RegenerateAudioApiJobsJobIdRegenerateAudioPostData, RegenerateAudioApiJobsJobIdRegenerateAudioPostErrors, RegenerateAudioApiJobsJobIdRegenerateAudioPostResponses, RegisterApiAuthRegisterPostData, RegisterApiAuthRegisterPostErrors, RegisterApiAuthRegisterPostResponses, RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteData, RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteErrors, RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteResponses, ResolveSenseApiVocabEntryIdSensePatchData, ResolveSenseApiVocabEntryIdSensePatchErrors, ResolveSenseApiVocabEntryIdSensePatchResponses, TranslateTextApiTranslateGetData, TranslateTextApiTranslateGetErrors, TranslateTextApiTranslateGetResponses, UpsertLearnableLanguageApiLearnableLanguagesPostData, UpsertLearnableLanguageApiLearnableLanguagesPostErrors, UpsertLearnableLanguageApiLearnableLanguagesPostResponses, VerifyEmailApiAuthVerifyEmailGetData, VerifyEmailApiAuthVerifyEmailGetErrors, VerifyEmailApiAuthVerifyEmailGetResponses } from './types.gen';
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & { export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
/** /**
@ -18,6 +18,114 @@ export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends
meta?: Record<string, unknown>; meta?: Record<string, unknown>;
}; };
/**
* Register
*/
export const registerApiAuthRegisterPost = <ThrowOnError extends boolean = false>(options: Options<RegisterApiAuthRegisterPostData, ThrowOnError>) => (options.client ?? client).post<RegisterApiAuthRegisterPostResponses, RegisterApiAuthRegisterPostErrors, ThrowOnError>({
url: '/api/auth/register',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Login
*/
export const loginApiAuthLoginPost = <ThrowOnError extends boolean = false>(options: Options<LoginApiAuthLoginPostData, ThrowOnError>) => (options.client ?? client).post<LoginApiAuthLoginPostResponses, LoginApiAuthLoginPostErrors, ThrowOnError>({
url: '/api/auth/login',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Verify Email
*/
export const verifyEmailApiAuthVerifyEmailGet = <ThrowOnError extends boolean = false>(options: Options<VerifyEmailApiAuthVerifyEmailGetData, ThrowOnError>) => (options.client ?? client).get<VerifyEmailApiAuthVerifyEmailGetResponses, VerifyEmailApiAuthVerifyEmailGetErrors, ThrowOnError>({ url: '/api/auth/verify-email', ...options });
/**
* Add Learnable Language
*/
export const addLearnableLanguageApiAccountLearnableLanguagesPost = <ThrowOnError extends boolean = false>(options: Options<AddLearnableLanguageApiAccountLearnableLanguagesPostData, ThrowOnError>) => (options.client ?? client).post<AddLearnableLanguageApiAccountLearnableLanguagesPostResponses, AddLearnableLanguageApiAccountLearnableLanguagesPostErrors, ThrowOnError>({
security: [{ scheme: 'bearer', type: 'http' }],
url: '/api/account/learnable-languages',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Complete Onboarding
*/
export const completeOnboardingApiAccountOnboardingPost = <ThrowOnError extends boolean = false>(options: Options<CompleteOnboardingApiAccountOnboardingPostData, ThrowOnError>) => (options.client ?? client).post<CompleteOnboardingApiAccountOnboardingPostResponses, CompleteOnboardingApiAccountOnboardingPostErrors, ThrowOnError>({
security: [{ scheme: 'bearer', type: 'http' }],
url: '/api/account/onboarding',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Get Account Status
*/
export const getAccountStatusApiAccountStatusGet = <ThrowOnError extends boolean = false>(options?: Options<GetAccountStatusApiAccountStatusGetData, ThrowOnError>) => (options?.client ?? client).get<GetAccountStatusApiAccountStatusGetResponses, unknown, ThrowOnError>({
security: [{ scheme: 'bearer', type: 'http' }],
url: '/api/account/status',
...options
});
/**
* Remove Learnable Language
*/
export const removeLearnableLanguageApiAccountLearnableLanguagesLanguageIdDelete = <ThrowOnError extends boolean = false>(options: Options<RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteData, ThrowOnError>) => (options.client ?? client).delete<RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteResponses, RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteErrors, ThrowOnError>({
security: [{ scheme: 'bearer', type: 'http' }],
url: '/api/account/learnable-languages/{language_id}',
...options
});
/**
* Generate Flashcards
*/
export const generateFlashcardsApiVocabEntryIdFlashcardsPost = <ThrowOnError extends boolean = false>(options: Options<GenerateFlashcardsApiVocabEntryIdFlashcardsPostData, ThrowOnError>) => (options.client ?? client).post<GenerateFlashcardsApiVocabEntryIdFlashcardsPostResponses, GenerateFlashcardsApiVocabEntryIdFlashcardsPostErrors, ThrowOnError>({
security: [{ scheme: 'bearer', type: 'http' }],
url: '/api/vocab/{entry_id}/flashcards',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* List Flashcards
*/
export const listFlashcardsApiFlashcardsGet = <ThrowOnError extends boolean = false>(options?: Options<ListFlashcardsApiFlashcardsGetData, ThrowOnError>) => (options?.client ?? client).get<ListFlashcardsApiFlashcardsGetResponses, unknown, ThrowOnError>({
security: [{ scheme: 'bearer', type: 'http' }],
url: '/api/flashcards',
...options
});
/**
* Record Event
*/
export const recordEventApiFlashcardsFlashcardIdEventsPost = <ThrowOnError extends boolean = false>(options: Options<RecordEventApiFlashcardsFlashcardIdEventsPostData, ThrowOnError>) => (options.client ?? client).post<RecordEventApiFlashcardsFlashcardIdEventsPostResponses, RecordEventApiFlashcardsFlashcardIdEventsPostErrors, ThrowOnError>({
security: [{ scheme: 'bearer', type: 'http' }],
url: '/api/flashcards/{flashcard_id}/events',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/** /**
* Analyze Pos * Analyze Pos
*/ */
@ -93,6 +201,81 @@ export const upsertLearnableLanguageApiLearnableLanguagesPost = <ThrowOnError ex
} }
}); });
/**
* List Entries
*/
export const listEntriesApiVocabGet = <ThrowOnError extends boolean = false>(options: Options<ListEntriesApiVocabGetData, ThrowOnError>) => (options.client ?? client).get<ListEntriesApiVocabGetResponses, ListEntriesApiVocabGetErrors, ThrowOnError>({
security: [{ scheme: 'bearer', type: 'http' }],
url: '/api/vocab',
...options
});
/**
* Add Word
*/
export const addWordApiVocabPost = <ThrowOnError extends boolean = false>(options: Options<AddWordApiVocabPostData, ThrowOnError>) => (options.client ?? client).post<AddWordApiVocabPostResponses, AddWordApiVocabPostErrors, ThrowOnError>({
security: [{ scheme: 'bearer', type: 'http' }],
url: '/api/vocab',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Add From Token
*/
export const addFromTokenApiVocabFromTokenPost = <ThrowOnError extends boolean = false>(options: Options<AddFromTokenApiVocabFromTokenPostData, ThrowOnError>) => (options.client ?? client).post<AddFromTokenApiVocabFromTokenPostResponses, AddFromTokenApiVocabFromTokenPostErrors, ThrowOnError>({
security: [{ scheme: 'bearer', type: 'http' }],
url: '/api/vocab/from-token',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Pending Disambiguation
*/
export const pendingDisambiguationApiVocabPendingDisambiguationGet = <ThrowOnError extends boolean = false>(options?: Options<PendingDisambiguationApiVocabPendingDisambiguationGetData, ThrowOnError>) => (options?.client ?? client).get<PendingDisambiguationApiVocabPendingDisambiguationGetResponses, unknown, ThrowOnError>({
security: [{ scheme: 'bearer', type: 'http' }],
url: '/api/vocab/pending-disambiguation',
...options
});
/**
* Resolve Sense
*/
export const resolveSenseApiVocabEntryIdSensePatch = <ThrowOnError extends boolean = false>(options: Options<ResolveSenseApiVocabEntryIdSensePatchData, ThrowOnError>) => (options.client ?? client).patch<ResolveSenseApiVocabEntryIdSensePatchResponses, ResolveSenseApiVocabEntryIdSensePatchErrors, ThrowOnError>({
security: [{ scheme: 'bearer', type: 'http' }],
url: '/api/vocab/{entry_id}/sense',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Get Account
*/
export const getAccountBffAccountGet = <ThrowOnError extends boolean = false>(options?: Options<GetAccountBffAccountGetData, ThrowOnError>) => (options?.client ?? client).get<GetAccountBffAccountGetResponses, unknown, ThrowOnError>({
security: [{ scheme: 'bearer', type: 'http' }],
url: '/bff/account',
...options
});
/**
* Get Onboarding
*/
export const getOnboardingBffAccountOnboardingGet = <ThrowOnError extends boolean = false>(options?: Options<GetOnboardingBffAccountOnboardingGetData, ThrowOnError>) => (options?.client ?? client).get<GetOnboardingBffAccountOnboardingGetResponses, unknown, ThrowOnError>({
security: [{ scheme: 'bearer', type: 'http' }],
url: '/bff/account/onboarding',
...options
});
/** /**
* List Articles * List Articles
*/ */
@ -120,30 +303,6 @@ export const getUserProfileBffUserProfileGet = <ThrowOnError extends boolean = f
...options ...options
}); });
/**
* Register
*/
export const registerAuthRegisterPost = <ThrowOnError extends boolean = false>(options: Options<RegisterAuthRegisterPostData, ThrowOnError>) => (options.client ?? client).post<RegisterAuthRegisterPostResponses, RegisterAuthRegisterPostErrors, ThrowOnError>({
url: '/auth/register',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Login
*/
export const loginAuthLoginPost = <ThrowOnError extends boolean = false>(options: Options<LoginAuthLoginPostData, ThrowOnError>) => (options.client ?? client).post<LoginAuthLoginPostResponses, LoginAuthLoginPostErrors, ThrowOnError>({
url: '/auth/login',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/** /**
* Get Media File * Get Media File
*/ */

View file

@ -4,6 +4,134 @@ export type ClientOptions = {
baseUrl: `${string}://src` | (string & {}); baseUrl: `${string}://src` | (string & {});
}; };
/**
* AccountLanguagePair
*/
export type AccountLanguagePair = {
/**
* Id
*/
id: string;
/**
* Source Language
*/
source_language: string;
/**
* Target Language
*/
target_language: string;
/**
* Proficiencies
*/
proficiencies: Array<string>;
};
/**
* AccountResponse
*/
export type AccountResponse = {
/**
* Email
*/
email: string;
/**
* Human Name
*/
human_name: string | null;
/**
* Language Pairs
*/
language_pairs: Array<AccountLanguagePair>;
};
/**
* AccountStatusResponse
*/
export type AccountStatusResponse = {
/**
* Problem Flags
*/
problem_flags: Array<string>;
/**
* Error Messages
*/
error_messages: Array<string>;
};
/**
* AddFromTokenRequest
*/
export type AddFromTokenRequest = {
/**
* Language Pair Id
*/
language_pair_id: string;
/**
* Surface
*/
surface: string;
/**
* Spacy Lemma
*/
spacy_lemma: string;
/**
* Pos Ud
*/
pos_ud: string;
/**
* Language
*/
language: string;
/**
* Source Article Id
*/
source_article_id?: string | null;
};
/**
* AddLearnableLanguageRequest
*/
export type AddLearnableLanguageRequest = {
/**
* Source Language
*/
source_language: string;
/**
* Target Language
*/
target_language: string;
/**
* Proficiencies
*/
proficiencies: Array<string>;
};
/**
* AddWordRequest
*/
export type AddWordRequest = {
/**
* Language Pair Id
*/
language_pair_id: string;
/**
* Surface Text
*/
surface_text: string;
/**
* Entry Pathway
*/
entry_pathway?: string;
/**
* Is Phrase
*/
is_phrase?: boolean;
/**
* Source Article Id
*/
source_article_id?: string | null;
};
/** /**
* ArticleDetail * ArticleDetail
*/ */
@ -112,6 +240,115 @@ export type ArticleListResponse = {
articles: Array<ArticleItem>; articles: Array<ArticleItem>;
}; };
/**
* FlashcardEventResponse
*/
export type FlashcardEventResponse = {
/**
* Id
*/
id: string;
/**
* Flashcard Id
*/
flashcard_id: string;
/**
* User Id
*/
user_id: string;
/**
* Event Type
*/
event_type: string;
/**
* User Response
*/
user_response: string | null;
/**
* Created At
*/
created_at: string;
};
/**
* FlashcardResponse
*/
export type FlashcardResponse = {
/**
* Id
*/
id: string;
/**
* User Id
*/
user_id: string;
/**
* Bank Entry Id
*/
bank_entry_id: string;
/**
* Source Lang
*/
source_lang: string;
/**
* Target Lang
*/
target_lang: string;
/**
* Prompt Text
*/
prompt_text: string;
/**
* Answer Text
*/
answer_text: string;
/**
* Prompt Context Text
*/
prompt_context_text: string | null;
/**
* Answer Context Text
*/
answer_context_text: string | null;
/**
* Card Direction
*/
card_direction: string;
/**
* Prompt Modality
*/
prompt_modality: string;
/**
* Created At
*/
created_at: string;
};
/**
* FromTokenResponse
*/
export type FromTokenResponse = {
entry: WordBankEntryResponse;
/**
* Sense Candidates
*/
sense_candidates: Array<SenseCandidateResponse>;
/**
* Matched Via
*/
matched_via: string;
};
/**
* GenerateFlashcardsRequest
*/
export type GenerateFlashcardsRequest = {
/**
* Direction
*/
direction?: string | null;
};
/** /**
* GenerationRequest * GenerationRequest
*/ */
@ -224,6 +461,24 @@ export type JobSummary = {
error_message?: string | null; error_message?: string | null;
}; };
/**
* LanguagePairOption
*/
export type LanguagePairOption = {
/**
* Value
*/
value: string;
/**
* Label
*/
label: string;
/**
* Description
*/
description: string;
};
/** /**
* LearnableLanguageItem * LearnableLanguageItem
*/ */
@ -300,6 +555,38 @@ export type LoginRequest = {
password: string; password: string;
}; };
/**
* OnboardingRequest
*/
export type OnboardingRequest = {
/**
* Human Name
*/
human_name: string;
/**
* Language Pairs
*/
language_pairs: Array<string>;
/**
* Proficiencies
*/
proficiencies: Array<Array<string>>;
};
/**
* OnboardingResponse
*/
export type OnboardingResponse = {
/**
* Language Pairs
*/
language_pairs: Array<LanguagePairOption>;
/**
* Proficiencies
*/
proficiencies: Array<ProficiencyOption>;
};
/** /**
* POSRequest * POSRequest
*/ */
@ -328,6 +615,38 @@ export type PosResponse = {
tokens: Array<TokenInfo>; tokens: Array<TokenInfo>;
}; };
/**
* ProficiencyOption
*/
export type ProficiencyOption = {
/**
* Value
*/
value: string;
/**
* Label
*/
label: string;
/**
* Description
*/
description: string;
};
/**
* RecordEventRequest
*/
export type RecordEventRequest = {
/**
* Event Type
*/
event_type: string;
/**
* User Response
*/
user_response?: string | null;
};
/** /**
* RegisterRequest * RegisterRequest
*/ */
@ -342,6 +661,52 @@ export type RegisterRequest = {
password: string; password: string;
}; };
/**
* RegisterResponse
*/
export type RegisterResponse = {
/**
* Success
*/
success: boolean;
/**
* Error Message
*/
error_message?: string | null;
};
/**
* SenseCandidateResponse
*/
export type SenseCandidateResponse = {
/**
* Id
*/
id: string;
/**
* Gloss
*/
gloss: string;
/**
* Topics
*/
topics: Array<string>;
/**
* Tags
*/
tags: Array<string>;
};
/**
* SetSenseRequest
*/
export type SetSenseRequest = {
/**
* Sense Id
*/
sense_id: string;
};
/** /**
* TokenInfo * TokenInfo
*/ */
@ -442,6 +807,320 @@ export type ValidationError = {
}; };
}; };
/**
* WordBankEntryResponse
*/
export type WordBankEntryResponse = {
/**
* Id
*/
id: string;
/**
* User Id
*/
user_id: string;
/**
* Language Pair Id
*/
language_pair_id: string;
/**
* Sense Id
*/
sense_id: string | null;
/**
* Wordform Id
*/
wordform_id: string | null;
/**
* Surface Text
*/
surface_text: string;
/**
* Is Phrase
*/
is_phrase: boolean;
/**
* Entry Pathway
*/
entry_pathway: string;
/**
* Source Article Id
*/
source_article_id: string | null;
/**
* Disambiguation Status
*/
disambiguation_status: string;
/**
* Created At
*/
created_at: string;
};
export type RegisterApiAuthRegisterPostData = {
body: RegisterRequest;
path?: never;
query?: never;
url: '/api/auth/register';
};
export type RegisterApiAuthRegisterPostErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type RegisterApiAuthRegisterPostError = RegisterApiAuthRegisterPostErrors[keyof RegisterApiAuthRegisterPostErrors];
export type RegisterApiAuthRegisterPostResponses = {
/**
* Successful Response
*/
201: RegisterResponse;
};
export type RegisterApiAuthRegisterPostResponse = RegisterApiAuthRegisterPostResponses[keyof RegisterApiAuthRegisterPostResponses];
export type LoginApiAuthLoginPostData = {
body: LoginRequest;
path?: never;
query?: never;
url: '/api/auth/login';
};
export type LoginApiAuthLoginPostErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type LoginApiAuthLoginPostError = LoginApiAuthLoginPostErrors[keyof LoginApiAuthLoginPostErrors];
export type LoginApiAuthLoginPostResponses = {
/**
* Successful Response
*/
200: TokenResponse;
};
export type LoginApiAuthLoginPostResponse = LoginApiAuthLoginPostResponses[keyof LoginApiAuthLoginPostResponses];
export type VerifyEmailApiAuthVerifyEmailGetData = {
body?: never;
path?: never;
query: {
/**
* Token
*/
token: string;
};
url: '/api/auth/verify-email';
};
export type VerifyEmailApiAuthVerifyEmailGetErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type VerifyEmailApiAuthVerifyEmailGetError = VerifyEmailApiAuthVerifyEmailGetErrors[keyof VerifyEmailApiAuthVerifyEmailGetErrors];
export type VerifyEmailApiAuthVerifyEmailGetResponses = {
/**
* Response Verify Email Api Auth Verify Email Get
*
* Successful Response
*/
200: {
[key: string]: unknown;
};
};
export type VerifyEmailApiAuthVerifyEmailGetResponse = VerifyEmailApiAuthVerifyEmailGetResponses[keyof VerifyEmailApiAuthVerifyEmailGetResponses];
export type AddLearnableLanguageApiAccountLearnableLanguagesPostData = {
body: AddLearnableLanguageRequest;
path?: never;
query?: never;
url: '/api/account/learnable-languages';
};
export type AddLearnableLanguageApiAccountLearnableLanguagesPostErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type AddLearnableLanguageApiAccountLearnableLanguagesPostError = AddLearnableLanguageApiAccountLearnableLanguagesPostErrors[keyof AddLearnableLanguageApiAccountLearnableLanguagesPostErrors];
export type AddLearnableLanguageApiAccountLearnableLanguagesPostResponses = {
/**
* Successful Response
*/
201: LearnableLanguageResponse;
};
export type AddLearnableLanguageApiAccountLearnableLanguagesPostResponse = AddLearnableLanguageApiAccountLearnableLanguagesPostResponses[keyof AddLearnableLanguageApiAccountLearnableLanguagesPostResponses];
export type CompleteOnboardingApiAccountOnboardingPostData = {
body: OnboardingRequest;
path?: never;
query?: never;
url: '/api/account/onboarding';
};
export type CompleteOnboardingApiAccountOnboardingPostErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type CompleteOnboardingApiAccountOnboardingPostError = CompleteOnboardingApiAccountOnboardingPostErrors[keyof CompleteOnboardingApiAccountOnboardingPostErrors];
export type CompleteOnboardingApiAccountOnboardingPostResponses = {
/**
* Response Complete Onboarding Api Account Onboarding Post
*
* Successful Response
*/
200: {
[key: string]: unknown;
};
};
export type CompleteOnboardingApiAccountOnboardingPostResponse = CompleteOnboardingApiAccountOnboardingPostResponses[keyof CompleteOnboardingApiAccountOnboardingPostResponses];
export type GetAccountStatusApiAccountStatusGetData = {
body?: never;
path?: never;
query?: never;
url: '/api/account/status';
};
export type GetAccountStatusApiAccountStatusGetResponses = {
/**
* Successful Response
*/
200: AccountStatusResponse;
};
export type GetAccountStatusApiAccountStatusGetResponse = GetAccountStatusApiAccountStatusGetResponses[keyof GetAccountStatusApiAccountStatusGetResponses];
export type RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteData = {
body?: never;
path: {
/**
* Language Id
*/
language_id: string;
};
query?: never;
url: '/api/account/learnable-languages/{language_id}';
};
export type RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteError = RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteErrors[keyof RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteErrors];
export type RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteResponses = {
/**
* Successful Response
*/
204: void;
};
export type RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteResponse = RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteResponses[keyof RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteResponses];
export type GenerateFlashcardsApiVocabEntryIdFlashcardsPostData = {
body: GenerateFlashcardsRequest;
path: {
/**
* Entry Id
*/
entry_id: string;
};
query?: never;
url: '/api/vocab/{entry_id}/flashcards';
};
export type GenerateFlashcardsApiVocabEntryIdFlashcardsPostErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type GenerateFlashcardsApiVocabEntryIdFlashcardsPostError = GenerateFlashcardsApiVocabEntryIdFlashcardsPostErrors[keyof GenerateFlashcardsApiVocabEntryIdFlashcardsPostErrors];
export type GenerateFlashcardsApiVocabEntryIdFlashcardsPostResponses = {
/**
* Response Generate Flashcards Api Vocab Entry Id Flashcards Post
*
* Successful Response
*/
201: Array<FlashcardResponse>;
};
export type GenerateFlashcardsApiVocabEntryIdFlashcardsPostResponse = GenerateFlashcardsApiVocabEntryIdFlashcardsPostResponses[keyof GenerateFlashcardsApiVocabEntryIdFlashcardsPostResponses];
export type ListFlashcardsApiFlashcardsGetData = {
body?: never;
path?: never;
query?: never;
url: '/api/flashcards';
};
export type ListFlashcardsApiFlashcardsGetResponses = {
/**
* Response List Flashcards Api Flashcards Get
*
* Successful Response
*/
200: Array<FlashcardResponse>;
};
export type ListFlashcardsApiFlashcardsGetResponse = ListFlashcardsApiFlashcardsGetResponses[keyof ListFlashcardsApiFlashcardsGetResponses];
export type RecordEventApiFlashcardsFlashcardIdEventsPostData = {
body: RecordEventRequest;
path: {
/**
* Flashcard Id
*/
flashcard_id: string;
};
query?: never;
url: '/api/flashcards/{flashcard_id}/events';
};
export type RecordEventApiFlashcardsFlashcardIdEventsPostErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type RecordEventApiFlashcardsFlashcardIdEventsPostError = RecordEventApiFlashcardsFlashcardIdEventsPostErrors[keyof RecordEventApiFlashcardsFlashcardIdEventsPostErrors];
export type RecordEventApiFlashcardsFlashcardIdEventsPostResponses = {
/**
* Successful Response
*/
201: FlashcardEventResponse;
};
export type RecordEventApiFlashcardsFlashcardIdEventsPostResponse = RecordEventApiFlashcardsFlashcardIdEventsPostResponses[keyof RecordEventApiFlashcardsFlashcardIdEventsPostResponses];
export type AnalyzePosApiPosPostData = { export type AnalyzePosApiPosPostData = {
body: PosRequest; body: PosRequest;
path?: never; path?: never;
@ -635,6 +1314,168 @@ export type UpsertLearnableLanguageApiLearnableLanguagesPostResponses = {
export type UpsertLearnableLanguageApiLearnableLanguagesPostResponse = UpsertLearnableLanguageApiLearnableLanguagesPostResponses[keyof UpsertLearnableLanguageApiLearnableLanguagesPostResponses]; export type UpsertLearnableLanguageApiLearnableLanguagesPostResponse = UpsertLearnableLanguageApiLearnableLanguagesPostResponses[keyof UpsertLearnableLanguageApiLearnableLanguagesPostResponses];
export type ListEntriesApiVocabGetData = {
body?: never;
path?: never;
query: {
/**
* Language Pair Id
*/
language_pair_id: string;
};
url: '/api/vocab';
};
export type ListEntriesApiVocabGetErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type ListEntriesApiVocabGetError = ListEntriesApiVocabGetErrors[keyof ListEntriesApiVocabGetErrors];
export type ListEntriesApiVocabGetResponses = {
/**
* Response List Entries Api Vocab Get
*
* Successful Response
*/
200: Array<WordBankEntryResponse>;
};
export type ListEntriesApiVocabGetResponse = ListEntriesApiVocabGetResponses[keyof ListEntriesApiVocabGetResponses];
export type AddWordApiVocabPostData = {
body: AddWordRequest;
path?: never;
query?: never;
url: '/api/vocab';
};
export type AddWordApiVocabPostErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type AddWordApiVocabPostError = AddWordApiVocabPostErrors[keyof AddWordApiVocabPostErrors];
export type AddWordApiVocabPostResponses = {
/**
* Successful Response
*/
201: WordBankEntryResponse;
};
export type AddWordApiVocabPostResponse = AddWordApiVocabPostResponses[keyof AddWordApiVocabPostResponses];
export type AddFromTokenApiVocabFromTokenPostData = {
body: AddFromTokenRequest;
path?: never;
query?: never;
url: '/api/vocab/from-token';
};
export type AddFromTokenApiVocabFromTokenPostErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type AddFromTokenApiVocabFromTokenPostError = AddFromTokenApiVocabFromTokenPostErrors[keyof AddFromTokenApiVocabFromTokenPostErrors];
export type AddFromTokenApiVocabFromTokenPostResponses = {
/**
* Successful Response
*/
201: FromTokenResponse;
};
export type AddFromTokenApiVocabFromTokenPostResponse = AddFromTokenApiVocabFromTokenPostResponses[keyof AddFromTokenApiVocabFromTokenPostResponses];
export type PendingDisambiguationApiVocabPendingDisambiguationGetData = {
body?: never;
path?: never;
query?: never;
url: '/api/vocab/pending-disambiguation';
};
export type PendingDisambiguationApiVocabPendingDisambiguationGetResponses = {
/**
* Response Pending Disambiguation Api Vocab Pending Disambiguation Get
*
* Successful Response
*/
200: Array<WordBankEntryResponse>;
};
export type PendingDisambiguationApiVocabPendingDisambiguationGetResponse = PendingDisambiguationApiVocabPendingDisambiguationGetResponses[keyof PendingDisambiguationApiVocabPendingDisambiguationGetResponses];
export type ResolveSenseApiVocabEntryIdSensePatchData = {
body: SetSenseRequest;
path: {
/**
* Entry Id
*/
entry_id: string;
};
query?: never;
url: '/api/vocab/{entry_id}/sense';
};
export type ResolveSenseApiVocabEntryIdSensePatchErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type ResolveSenseApiVocabEntryIdSensePatchError = ResolveSenseApiVocabEntryIdSensePatchErrors[keyof ResolveSenseApiVocabEntryIdSensePatchErrors];
export type ResolveSenseApiVocabEntryIdSensePatchResponses = {
/**
* Successful Response
*/
200: WordBankEntryResponse;
};
export type ResolveSenseApiVocabEntryIdSensePatchResponse = ResolveSenseApiVocabEntryIdSensePatchResponses[keyof ResolveSenseApiVocabEntryIdSensePatchResponses];
export type GetAccountBffAccountGetData = {
body?: never;
path?: never;
query?: never;
url: '/bff/account';
};
export type GetAccountBffAccountGetResponses = {
/**
* Successful Response
*/
200: AccountResponse;
};
export type GetAccountBffAccountGetResponse = GetAccountBffAccountGetResponses[keyof GetAccountBffAccountGetResponses];
export type GetOnboardingBffAccountOnboardingGetData = {
body?: never;
path?: never;
query?: never;
url: '/bff/account/onboarding';
};
export type GetOnboardingBffAccountOnboardingGetResponses = {
/**
* Successful Response
*/
200: OnboardingResponse;
};
export type GetOnboardingBffAccountOnboardingGetResponse = GetOnboardingBffAccountOnboardingGetResponses[keyof GetOnboardingBffAccountOnboardingGetResponses];
export type ListArticlesBffArticlesGetData = { export type ListArticlesBffArticlesGetData = {
body?: never; body?: never;
path?: never; path?: never;
@ -711,54 +1552,6 @@ export type GetUserProfileBffUserProfileGetResponses = {
export type GetUserProfileBffUserProfileGetResponse = GetUserProfileBffUserProfileGetResponses[keyof GetUserProfileBffUserProfileGetResponses]; export type GetUserProfileBffUserProfileGetResponse = GetUserProfileBffUserProfileGetResponses[keyof GetUserProfileBffUserProfileGetResponses];
export type RegisterAuthRegisterPostData = {
body: RegisterRequest;
path?: never;
query?: never;
url: '/auth/register';
};
export type RegisterAuthRegisterPostErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type RegisterAuthRegisterPostError = RegisterAuthRegisterPostErrors[keyof RegisterAuthRegisterPostErrors];
export type RegisterAuthRegisterPostResponses = {
/**
* Successful Response
*/
201: unknown;
};
export type LoginAuthLoginPostData = {
body: LoginRequest;
path?: never;
query?: never;
url: '/auth/login';
};
export type LoginAuthLoginPostErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type LoginAuthLoginPostError = LoginAuthLoginPostErrors[keyof LoginAuthLoginPostErrors];
export type LoginAuthLoginPostResponses = {
/**
* Successful Response
*/
200: TokenResponse;
};
export type LoginAuthLoginPostResponse = LoginAuthLoginPostResponses[keyof LoginAuthLoginPostResponses];
export type GetMediaFileMediaFilenameGetData = { export type GetMediaFileMediaFilenameGetData = {
body?: never; body?: never;
path: { path: {

File diff suppressed because one or more lines are too long