diff --git a/scripts/db_migrations/008_foreignkey_refactoring.py b/scripts/db_migrations/008_foreignkey_refactoring.py
index 0727757..3f09f99 100644
--- a/scripts/db_migrations/008_foreignkey_refactoring.py
+++ b/scripts/db_migrations/008_foreignkey_refactoring.py
@@ -306,8 +306,9 @@ def migrate_sqlite():
print("\n=== Phase 4: Update Foreign Key references ===")
- # 4a. Update UserOrganization.organization to reference new User IDs
- cursor.execute("SELECT id, organization FROM userorganization")
+ # 4a. Update UserOrganization.organization_id to reference new User IDs
+ # NOTE: Peewee ForeignKeyField creates columns with _id suffix
+ cursor.execute("SELECT id, organization_id FROM userorganization")
memberships = cursor.fetchall()
for membership_id, old_org_id in memberships:
@@ -319,7 +320,7 @@ def migrate_sqlite():
if result:
new_user_id = result[0]
cursor.execute(
- "UPDATE userorganization SET organization = ? WHERE id = ?",
+ "UPDATE userorganization SET organization_id = ? WHERE id = ?",
(new_user_id, membership_id),
)
@@ -609,10 +610,11 @@ def migrate_postgres():
print("\n=== Phase 4: Update Foreign Key references ===")
- # 4a. Update UserOrganization.organization to reference new User IDs
+ # 4a. Update UserOrganization.organization_id to reference new User IDs
+ # NOTE: Peewee ForeignKeyField creates columns with _id suffix
cursor.execute(
- "UPDATE userorganization SET organization = m.new_user_id "
- "FROM _org_id_mapping m WHERE userorganization.organization = m.old_org_id"
+ "UPDATE userorganization SET organization_id = m.new_user_id "
+ "FROM _org_id_mapping m WHERE userorganization.organization_id = m.old_org_id"
)
affected = cursor.rowcount
db.commit()
diff --git a/src/kohaku-hub-admin/src/components.d.ts b/src/kohaku-hub-admin/src/components.d.ts
index 8632f92..899fbac 100644
--- a/src/kohaku-hub-admin/src/components.d.ts
+++ b/src/kohaku-hub-admin/src/components.d.ts
@@ -29,9 +29,12 @@ declare module 'vue' {
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElProgress: typeof import('element-plus/es')['ElProgress']
+ ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElSelect: typeof import('element-plus/es')['ElSelect']
+ ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
+ ElStatistic: typeof import('element-plus/es')['ElStatistic']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
diff --git a/src/kohaku-hub-ui/src/pages/organizations/new.vue b/src/kohaku-hub-ui/src/pages/organizations/new.vue
index cfa6f29..a43a7ea 100644
--- a/src/kohaku-hub-ui/src/pages/organizations/new.vue
+++ b/src/kohaku-hub-ui/src/pages/organizations/new.vue
@@ -30,7 +30,7 @@
/>
- Only lowercase letters, numbers, and hyphens are allowed
+ Letters (case-insensitive), numbers, hyphens and underscores allowed
@@ -120,8 +120,8 @@ const rules = {
trigger: "blur",
},
{
- pattern: /^[a-z0-9-]+$/,
- message: "Only lowercase letters, numbers, and hyphens allowed",
+ pattern: /^[a-zA-Z0-9_-]+$/,
+ message: "Only letters, numbers, hyphens and underscores allowed",
trigger: "blur",
},
{
@@ -133,7 +133,8 @@ const rules = {
};
function validateOrgName() {
- form.value.name = form.value.name.toLowerCase().replace(/[^a-z0-9-]/g, "");
+ // Remove invalid characters but preserve case (case-insensitive matching done server-side)
+ form.value.name = form.value.name.replace(/[^a-zA-Z0-9_-]/g, "");
}
function handleCancel() {
diff --git a/src/kohakuhub/api/admin.py b/src/kohakuhub/api/admin.py
index dc7295c..e8a0f73 100644
--- a/src/kohakuhub/api/admin.py
+++ b/src/kohakuhub/api/admin.py
@@ -2,6 +2,7 @@
import asyncio
import hashlib
+import json
import secrets
from datetime import datetime, timedelta, timezone
@@ -13,12 +14,13 @@ from pydantic import BaseModel
from kohakuhub.async_utils import run_in_s3_executor
from kohakuhub.config import cfg
from kohakuhub.db import (
- db,
Commit,
File,
+ Invitation,
LFSObjectHistory,
Repository,
User,
+ db,
)
from kohakuhub.db_operations import (
check_invitation_available,
@@ -395,8 +397,6 @@ async def create_register_invitation_admin(
Returns:
Created invitation token and link
"""
- import json
-
# Validate role if org_id provided
if request.org_id:
if request.role not in ["visitor", "member", "admin"]:
@@ -475,8 +475,6 @@ async def list_invitations_admin(
Returns:
List of invitations with details
"""
- from kohakuhub.db import Invitation
-
query = Invitation.select()
if action:
diff --git a/src/kohakuhub/api/avatar.py b/src/kohakuhub/api/avatar.py
index dc6d56b..6df39a5 100644
--- a/src/kohakuhub/api/avatar.py
+++ b/src/kohakuhub/api/avatar.py
@@ -11,8 +11,9 @@ from kohakuhub.db import User
from kohakuhub.db_operations import (
get_organization,
get_user_by_username,
- update_user,
+ get_user_organization,
update_organization,
+ update_user,
)
from kohakuhub.logger import get_logger
from kohakuhub.auth.dependencies import get_current_user, get_optional_user
@@ -249,8 +250,6 @@ async def upload_org_avatar(
Raises:
HTTPException: If not authorized or invalid image
"""
- from kohakuhub.db_operations import get_user_organization
-
org = get_organization(org_name)
if not org:
raise HTTPException(404, detail="Organization not found")
@@ -354,8 +353,6 @@ async def delete_org_avatar(
Returns:
Success message
"""
- from kohakuhub.db_operations import get_user_organization
-
org = get_organization(org_name)
if not org:
raise HTTPException(404, detail="Organization not found")
diff --git a/src/kohakuhub/api/commit/routers/history.py b/src/kohakuhub/api/commit/routers/history.py
index 0a0d2a0..66e4f5b 100644
--- a/src/kohakuhub/api/commit/routers/history.py
+++ b/src/kohakuhub/api/commit/routers/history.py
@@ -1,15 +1,14 @@
"""Commit history API endpoints."""
-from typing import Optional
import asyncio
import difflib
+from typing import Optional
from fastapi import APIRouter, Depends, Query
-import httpx
from kohakuhub.config import cfg
-from kohakuhub.db import Repository, User
-from kohakuhub.db_operations import get_commit, get_repository, list_commits_by_repo
+from kohakuhub.db import User
+from kohakuhub.db_operations import get_commit, get_repository
from kohakuhub.lakefs_rest_client import get_lakefs_rest_client
from kohakuhub.logger import get_logger
from kohakuhub.auth.dependencies import get_optional_user
@@ -282,8 +281,6 @@ async def get_commit_diff(
# Fetch File records for LFS status using repository FK and backref
if file_paths:
- from kohakuhub.db import File
-
file_records = {
f.path_in_repo: f
for f in repo_row.files.select().where(
diff --git a/src/kohakuhub/api/commit/routers/operations.py b/src/kohakuhub/api/commit/routers/operations.py
index cac71ab..d0b1362 100644
--- a/src/kohakuhub/api/commit/routers/operations.py
+++ b/src/kohakuhub/api/commit/routers/operations.py
@@ -24,7 +24,7 @@ from kohakuhub.auth.dependencies import get_current_user
from kohakuhub.auth.permissions import check_repo_write_permission
from kohakuhub.utils.lakefs import get_lakefs_client, lakefs_repo_name
from kohakuhub.utils.s3 import get_object_metadata, object_exists
-from kohakuhub.api.quota.util import update_namespace_storage
+from kohakuhub.api.quota.util import update_namespace_storage, update_repository_storage
from kohakuhub.api.repo.utils.gc import run_gc_for_file, track_lfs_object
logger = get_logger("FILE")
@@ -744,13 +744,19 @@ async def commit(
f"GC: Cleaned up {deleted_count} old version(s) of {lfs_info['path']}"
)
- # Update storage usage for namespace after successful commit
+ # Update storage usage for namespace and repository after successful commit
try:
+ # Recalculate repository storage (keeps repo.used_bytes accurate)
+ await update_repository_storage(repo_row)
+ logger.debug(
+ f"Updated repository storage for {repo_id}: {repo_row.used_bytes:,} bytes"
+ )
+
# Check if namespace is organization (User with is_org=True)
org = get_organization(namespace)
is_org = org is not None
- # Recalculate storage usage
+ # Recalculate namespace storage usage
await update_namespace_storage(namespace, is_org)
logger.debug(
f"Updated storage usage for {'org' if is_org else 'user'} {namespace}"
diff --git a/src/kohakuhub/api/settings.py b/src/kohakuhub/api/settings.py
index fb8cd68..4054e38 100644
--- a/src/kohakuhub/api/settings.py
+++ b/src/kohakuhub/api/settings.py
@@ -1,17 +1,19 @@
"""User, organization, and repository settings API endpoints."""
+import json
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, EmailStr
-from kohakuhub.db import Repository, User, UserOrganization
+from kohakuhub.db import User
from kohakuhub.db_operations import (
get_organization,
get_repository,
get_user_by_email_excluding_id,
get_user_by_username,
get_user_organization,
+ list_organization_members,
update_organization,
update_repository,
update_user,
@@ -19,8 +21,8 @@ from kohakuhub.db_operations import (
from kohakuhub.logger import get_logger
from kohakuhub.auth.dependencies import get_current_user
from kohakuhub.auth.permissions import check_repo_delete_permission
-from kohakuhub.api.repo.utils.hf import hf_repo_not_found
from kohakuhub.api.quota.util import calculate_repository_storage, check_quota
+from kohakuhub.api.repo.utils.hf import hf_repo_not_found
logger = get_logger("SETTINGS")
@@ -83,8 +85,6 @@ async def update_user_settings(
update_fields["website"] = req.website
if req.social_media is not None:
- import json
-
# Validate social_media structure
if not isinstance(req.social_media, dict):
raise HTTPException(400, detail="social_media must be a dictionary")
@@ -113,8 +113,6 @@ async def get_user_profile(username: str):
if not user:
raise HTTPException(404, detail="User not found")
- import json
-
# Parse social_media JSON if exists
social_media = None
if user.social_media:
@@ -185,8 +183,6 @@ async def update_organization_settings(
update_fields["website"] = req.website
if req.social_media is not None:
- import json
-
# Validate social_media structure
if not isinstance(req.social_media, dict):
raise HTTPException(400, detail="social_media must be a dictionary")
@@ -215,8 +211,6 @@ async def get_organization_profile(org_name: str):
if not org:
raise HTTPException(404, detail="Organization not found")
- import json
-
# Parse social_media JSON if exists
social_media = None
if org.social_media:
@@ -226,8 +220,6 @@ async def get_organization_profile(org_name: str):
social_media = None
# Count members
- from kohakuhub.db_operations import list_organization_members
-
members = list_organization_members(org)
member_count = len(members)
diff --git a/src/kohakuhub/api/validation.py b/src/kohakuhub/api/validation.py
index bb23341..ff4c684 100644
--- a/src/kohakuhub/api/validation.py
+++ b/src/kohakuhub/api/validation.py
@@ -73,7 +73,7 @@ async def check_name_availability(req: CheckNameRequest) -> CheckNameResponse:
available=False,
normalized_name=normalized,
conflict_with=f"{req.namespace}/{repo.name}",
- message=f"Repository name conflicts with existing repository: {repo.name}",
+ message=f"Repository name conflicts with existing repository: {repo.name} (case-insensitive)",
)
return CheckNameResponse(
@@ -100,7 +100,7 @@ async def check_name_availability(req: CheckNameRequest) -> CheckNameResponse:
available=False,
normalized_name=normalized,
conflict_with=user.username,
- message=f"Username conflicts with existing user: {user.username}",
+ message=f"Username conflicts with existing user: {user.username} (case-insensitive)",
)
# Check organization name (now unified with User model using normalized_name for efficiency)
@@ -113,7 +113,7 @@ async def check_name_availability(req: CheckNameRequest) -> CheckNameResponse:
available=False,
normalized_name=normalized,
conflict_with=existing_org.username,
- message=f"Name conflicts with existing organization: {existing_org.username}",
+ message=f"Name conflicts with existing organization: {existing_org.username} (case-insensitive)",
)
return CheckNameResponse(
diff --git a/src/kohakuhub/auth/routes.py b/src/kohakuhub/auth/routes.py
index 177ea76..d472274 100644
--- a/src/kohakuhub/auth/routes.py
+++ b/src/kohakuhub/auth/routes.py
@@ -1,6 +1,7 @@
"""Authentication API routes."""
import asyncio
+import json
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, Response
@@ -8,26 +9,27 @@ from fastapi.responses import RedirectResponse
from pydantic import BaseModel, EmailStr
from kohakuhub.config import cfg
-from kohakuhub.db import EmailVerification, Session, Token, User, db
+from kohakuhub.db import Session, Token, User, db
from kohakuhub.db_operations import (
+ check_invitation_available,
create_email_verification,
create_session,
create_token,
create_user,
+ create_user_organization,
delete_email_verification,
- delete_session,
delete_token,
get_email_verification,
- get_session,
- get_token_by_hash,
+ get_invitation,
get_user_by_email,
get_user_by_id,
get_user_by_username,
list_user_tokens,
+ mark_invitation_used,
update_user,
)
from kohakuhub.logger import get_logger
-from kohakuhub.auth.dependencies import get_current_user, get_optional_user
+from kohakuhub.auth.dependencies import get_current_user
from kohakuhub.auth.email import send_verification_email
from kohakuhub.auth.utils import (
generate_session_secret,
@@ -80,8 +82,6 @@ async def register(req: RegisterRequest, invitation_token: str | None = None):
)
# Validate invitation token
- from kohakuhub.db_operations import check_invitation_available, get_invitation
-
invitation = get_invitation(invitation_token)
if not invitation:
raise HTTPException(400, detail="Invalid invitation token")
@@ -134,9 +134,6 @@ async def register(req: RegisterRequest, invitation_token: str | None = None):
# If registration used an invitation, mark it and add to org if specified
if cfg.auth.invitation_only and invitation_token:
- from kohakuhub.db_operations import get_invitation, mark_invitation_used
- import json
-
invitation = get_invitation(invitation_token)
if invitation:
try:
@@ -148,11 +145,6 @@ async def register(req: RegisterRequest, invitation_token: str | None = None):
# Add user to organization if specified
if org_id:
- from kohakuhub.db_operations import (
- create_user_organization,
- get_user_by_id,
- )
-
org = get_user_by_id(org_id)
if org:
role = params.get("role", "member")