Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
75ca5f6
feat: Implement user registration with username, email, and password
MathisVerstrepen Dec 30, 2025
862d501
feat: Implement email verification process with token generation and …
MathisVerstrepen Dec 30, 2025
22315a2
feat: Add layout definition and enhance UI for authentication pages
MathisVerstrepen Dec 30, 2025
71f252d
feat: Enhance verification email design with improved HTML and styling
MathisVerstrepen Dec 30, 2025
a058499
feat: Create reusable input components for username, password, and em…
MathisVerstrepen Dec 30, 2025
952fa3f
feat: Implement cooldown mechanism for resending verification code
MathisVerstrepen Dec 30, 2025
09fcc18
Merge pull request #247 from MathisVerstrepen/main
MathisVerstrepen Dec 30, 2025
516896d
feat: Update username validation to allow hyphens and improve error m…
MathisVerstrepen Dec 30, 2025
1abb826
feat: Add input mode and pattern for numeric code input in verificati…
MathisVerstrepen Dec 30, 2025
d1f878e
feat: Handle IntegrityError during user creation to prevent duplicate…
MathisVerstrepen Dec 30, 2025
42ab2a9
feat: Implement email update functionality for unverified users and e…
MathisVerstrepen Dec 30, 2025
f3c841d
feat: Add footer with GitHub and contact links to the authentication …
MathisVerstrepen Dec 30, 2025
d9692dd
feat: Enhance email verification process by using background tasks fo…
MathisVerstrepen Dec 30, 2025
f758d18
feat: Implement email verification template using Jinja2 and update e…
MathisVerstrepen Dec 30, 2025
2293b03
feat: Update verification code generation to use a 6-digit format for…
MathisVerstrepen Dec 30, 2025
8f3022b
feat: Refactor user verification process to use a dedicated function …
MathisVerstrepen Dec 30, 2025
863a207
feat: Update email service to use aiosmtplib for asynchronous email s…
MathisVerstrepen Dec 30, 2025
18595e0
fix: linter
MathisVerstrepen Dec 30, 2025
cd5bf74
Merge pull request #248 from MathisVerstrepen/dev
MathisVerstrepen Dec 30, 2025
db3c622
feat: Add admin user management features
MathisVerstrepen Dec 30, 2025
2742c33
fix: Update deleteUser function to use apiFetch and improve error han…
MathisVerstrepen Dec 30, 2025
f87b665
Merge pull request #249 from MathisVerstrepen/dev
MathisVerstrepen Dec 30, 2025
c516eb3
feat: Add has_seen_welcome field and welcome modal functionality for …
MathisVerstrepen Dec 30, 2025
c584099
Merge pull request #250 from MathisVerstrepen/dev
MathisVerstrepen Dec 30, 2025
a1c8164
feat: Implement account deletion functionality with confirmation modal
MathisVerstrepen Dec 30, 2025
bc04b9d
Merge pull request #251 from MathisVerstrepen/dev
MathisVerstrepen Dec 30, 2025
b06a844
feat: Implement usage limits for free tier users and enhance error ha…
MathisVerstrepen Dec 31, 2025
8192373
feat: Enhance usage limits for free tier users and implement GitHub n…
MathisVerstrepen Dec 31, 2025
f38eea0
fix: Correct usage percentage and depletion logic for better accuracy
MathisVerstrepen Dec 31, 2025
f816967
feat: Implement user storage management with usage tracking and limits
MathisVerstrepen Dec 31, 2025
c32e734
fix: Ensure temporary files are deleted on upload failure to prevent …
MathisVerstrepen Dec 31, 2025
0285092
Merge pull request #252 from MathisVerstrepen/dev
MathisVerstrepen Dec 31, 2025
3bcd748
refactor: Rework UI of handle fast action wheel
MathisVerstrepen Dec 31, 2025
1815e64
Merge pull request #253 from MathisVerstrepen/dev
MathisVerstrepen Dec 31, 2025
8b669eb
fix: Remove useless /library/combined api calls
MathisVerstrepen Dec 31, 2025
d3a1dd8
fix: linter
MathisVerstrepen Dec 31, 2025
06f5663
fix: Github file selection is no longer reset on branch change
MathisVerstrepen Dec 31, 2025
d24dc69
Merge pull request #255 from MathisVerstrepen/dev
MathisVerstrepen Dec 31, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/app/const/plans.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
"free": {
"web_search": 0,
"link_extraction": 0,
"storage": 50 * 1024 * 1024, # 50 MB
},
"premium": {
"web_search": 200,
"link_extraction": 1000,
"storage": 5 * 1024 * 1024 * 1024, # 5 GB
},
}
66 changes: 66 additions & 0 deletions api/app/database/pg/auth_ops/verification_crud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import datetime
import uuid
from datetime import timezone

from database.pg.models import User, VerificationToken
from sqlalchemy import delete, select, update
from sqlalchemy.ext.asyncio import AsyncEngine as SQLAlchemyAsyncEngine
from sqlmodel import and_
from sqlmodel.ext.asyncio.session import AsyncSession


async def create_verification_token(
pg_engine: SQLAlchemyAsyncEngine, user_id: uuid.UUID, email: str, code: str
) -> VerificationToken:
"""
Creates a new verification token for a user.
"""
expires_at = datetime.datetime.now(timezone.utc) + datetime.timedelta(minutes=15)

async with AsyncSession(pg_engine) as session:
await delete_verification_tokens_for_user(pg_engine, user_id)

token = VerificationToken(user_id=user_id, email=email, code=code, expires_at=expires_at)
session.add(token)
await session.commit()
await session.refresh(token)
return token


async def get_verification_token(
pg_engine: SQLAlchemyAsyncEngine, email: str, code: str
) -> VerificationToken | None:
"""
Retrieves a verification token by email and code.
"""
async with AsyncSession(pg_engine) as session:
stmt = select(VerificationToken).where(
and_(VerificationToken.email == email, VerificationToken.code == code)
)
result = await session.exec(stmt) # type: ignore
return result.scalars().one_or_none() # type: ignore


async def delete_verification_tokens_for_user(
pg_engine: SQLAlchemyAsyncEngine, user_id: uuid.UUID
) -> None:
"""
Deletes all verification tokens for a specific user.
"""
async with AsyncSession(pg_engine) as session:
stmt = delete(VerificationToken).where(and_(VerificationToken.user_id == user_id))
await session.exec(stmt)
await session.commit()


async def mark_user_as_verified(pg_engine: SQLAlchemyAsyncEngine, user_id: uuid.UUID) -> User:
"""
Updates the user's status to verified.
"""
async with AsyncSession(pg_engine) as session:
stmt = update(User).where(and_(User.id == user_id)).values(is_verified=True).returning(User)
result = await session.exec(stmt)
user = result.scalar_one()
await session.commit()
await session.refresh(user)
return user # type: ignore
153 changes: 133 additions & 20 deletions api/app/database/pg/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,37 @@ class QueryTypeEnum(str, Enum):
LINK_EXTRACTION = "link_extraction"


class UserStorageUsage(SQLModel, table=True):
__tablename__ = "user_storage_usage"

id: Optional[uuid.UUID] = Field(
default=None,
sa_column=Column(
PG_UUID(as_uuid=True),
primary_key=True,
server_default=func.uuid_generate_v4(),
),
)
user_id: uuid.UUID = Field(
sa_column=Column(
PG_UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
unique=True,
)
)
total_bytes_used: int = Field(default=0, nullable=False)
updated_at: Optional[datetime.datetime] = Field(
default=None,
sa_column=Column(
TIMESTAMP(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
),
)


class Folder(SQLModel, table=True):
__tablename__ = "folders"

Expand Down Expand Up @@ -253,7 +284,7 @@ class Edge(SQLModel, table=True):
["nodes.graph_id", "nodes.id"],
ondelete="CASCADE",
),
ForeignKeyConstraint(["graph_id"], ["graphs.id"]),
ForeignKeyConstraint(["graph_id"], ["graphs.id"], ondelete="CASCADE"),
Index("idx_edges_graph_id", "graph_id"),
Index("idx_edges_source_node", "graph_id", "source_node_id"),
Index("idx_edges_target_node", "graph_id", "target_node_id"),
Expand All @@ -263,8 +294,20 @@ class Edge(SQLModel, table=True):
class TemplateBookmark(SQLModel, table=True):
__tablename__ = "template_bookmarks"

user_id: uuid.UUID = Field(foreign_key="users.id", primary_key=True)
template_id: uuid.UUID = Field(foreign_key="prompt_templates.id", primary_key=True)
user_id: uuid.UUID = Field(
sa_column=Column(
PG_UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
primary_key=True,
)
)
template_id: uuid.UUID = Field(
sa_column=Column(
PG_UUID(as_uuid=True),
ForeignKey("prompt_templates.id", ondelete="CASCADE"),
primary_key=True,
)
)
created_at: datetime.datetime = Field(
default_factory=datetime.datetime.now,
sa_column=Column(TIMESTAMP(timezone=True), server_default=func.now(), nullable=False),
Expand Down Expand Up @@ -293,6 +336,8 @@ class User(SQLModel, table=True):
default="free", sa_column=Column(TEXT, nullable=False)
) # Options: "premium", "free"
is_admin: bool = Field(default=False, nullable=False)
is_verified: bool = Field(default=False, nullable=False)
has_seen_welcome: bool = Field(default=False, nullable=False)

created_at: Optional[datetime.datetime] = Field(
default=None,
Expand Down Expand Up @@ -329,6 +374,45 @@ class User(SQLModel, table=True):
)


class VerificationToken(SQLModel, table=True):
__tablename__ = "verification_tokens"

id: uuid.UUID = Field(
default_factory=uuid.uuid4,
sa_column=Column(
PG_UUID(as_uuid=True),
primary_key=True,
server_default=func.uuid_generate_v4(),
nullable=False,
),
)
user_id: uuid.UUID = Field(
sa_column=Column(
PG_UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
),
)
email: str = Field(index=True, nullable=False)
code: str = Field(max_length=6, nullable=False)
expires_at: datetime.datetime = Field(
sa_column=Column(TIMESTAMP(timezone=True), nullable=False)
)
created_at: datetime.datetime = Field(
default_factory=datetime.datetime.now,
sa_column=Column(
TIMESTAMP(timezone=True),
server_default=func.now(),
nullable=False,
),
)

__table_args__ = (
Index("idx_verification_tokens_user_id", "user_id"),
Index("idx_verification_tokens_email", "email"),
)


class PromptTemplate(SQLModel, table=True):
__tablename__ = "prompt_templates"

Expand All @@ -342,9 +426,12 @@ class PromptTemplate(SQLModel, table=True):
),
)
user_id: uuid.UUID = Field(
foreign_key="users.id",
nullable=False,
index=True,
sa_column=Column(
PG_UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
)
name: str = Field(max_length=255, nullable=False)
description: Optional[str] = Field(default=None, sa_column=Column(TEXT))
Expand Down Expand Up @@ -399,10 +486,13 @@ class Settings(SQLModel, table=True):
),
)
user_id: uuid.UUID = Field(
foreign_key="users.id",
nullable=False,
index=True,
unique=True, # Ensures one settings entry per user
sa_column=Column(
PG_UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
index=True,
unique=True,
)
)

# Store all settings as a single JSONB object
Expand Down Expand Up @@ -502,8 +592,11 @@ class RefreshToken(SQLModel, table=True):
),
)
user_id: uuid.UUID = Field(
foreign_key="users.id",
nullable=False,
sa_column=Column(
PG_UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
)
)
token: str = Field(max_length=255, nullable=False)
expires_at: datetime.datetime = Field(
Expand All @@ -518,7 +611,13 @@ class UsedRefreshToken(SQLModel, table=True):

id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
token: str = Field(index=True, unique=True, nullable=False)
user_id: uuid.UUID = Field(foreign_key="users.id", nullable=False)
user_id: uuid.UUID = Field(
sa_column=Column(
PG_UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
)
)
expires_at: datetime.datetime = Field(
sa_column=Column(TIMESTAMP(timezone=True), nullable=False)
)
Expand All @@ -541,9 +640,12 @@ class ProviderToken(SQLModel, table=True):
),
)
user_id: uuid.UUID = Field(
foreign_key="users.id",
nullable=False,
index=True,
sa_column=Column(
PG_UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
)
provider: str = Field(max_length=50, nullable=False, index=True) # e.g., 'github'
access_token: str = Field(sa_column=Column(TEXT, nullable=False)) # Should always be encrypted
Expand Down Expand Up @@ -591,7 +693,14 @@ class Repository(SQLModel, table=True):
nullable=False,
),
)
user_id: uuid.UUID = Field(foreign_key="users.id", nullable=False, index=True)
user_id: uuid.UUID = Field(
sa_column=Column(
PG_UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
)
provider: str = Field(default="github", max_length=50, nullable=False)
repo_name: str = Field(max_length=255, nullable=False) # e.g., "my-org/my-awesome-project"
clone_url: str = Field(sa_column=Column(TEXT, nullable=False))
Expand Down Expand Up @@ -649,9 +758,12 @@ class UserQueryUsage(SQLModel, table=True):
),
)
user_id: uuid.UUID = Field(
foreign_key="users.id",
nullable=False,
index=True,
sa_column=Column(
PG_UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
)
query_type: str = Field(max_length=50, nullable=False, index=True)
used_queries: int = Field(default=0, nullable=False)
Expand Down Expand Up @@ -698,6 +810,7 @@ async def create_initial_users(
username=user.username,
password=user.password,
oauth_provider="userpass",
is_verified=False,
)
session.add(new_user)
users.append(new_user)
Expand Down
Loading
Loading