feat: migrate UID system from usernames to email addresses

- Database migration: Updated publicstream.uid from usernames to email addresses
  - devuser → oib@bubuit.net
  - oibchello → oib@chello.at
- Updated related tables (UploadLog, UserQuota) to use email-based UIDs
- Fixed backend audio route to map email UIDs to username-based directories
- Updated SSE event payloads to use email for UID and username for display
- Removed redundant display_name field from SSE events
- Fixed frontend rendering conflicts between nav.js and streams-ui.js
- Updated stream player template to display usernames instead of email addresses
- Added cache-busting parameters to force browser refresh
- Created migration script for future reference

Benefits:
- Eliminates UID duplicates and inconsistency
- Provides stable, unique email-based identifiers
- Maintains user-friendly username display
- Follows proper data normalization practices

Fixes: Stream UI now displays usernames (devuser, oibchello) instead of email addresses
This commit is contained in:
oib
2025-07-27 09:47:38 +02:00
parent 1171510683
commit 88e468b716
6 changed files with 213 additions and 11 deletions

View File

@ -85,8 +85,7 @@ async def list_streams_sse(db):
'uid': stream.uid or '',
'size': stream.storage_bytes or 0,
'mtime': int(stream.mtime) if stream.mtime is not None else 0,
'username': stream.username or stream.uid or '',
'display_name': stream.display_name or stream.username or stream.uid or '',
'username': stream.username or '',
'created_at': stream.created_at.isoformat() if stream.created_at else None,
'updated_at': stream.updated_at.isoformat() if stream.updated_at else None
}

15
main.py
View File

@ -92,7 +92,20 @@ from range_response import range_response
@app.get("/audio/{uid}/{filename}")
def get_audio(uid: str, filename: str, request: Request, db: Session = Depends(get_db)):
# Allow public access ONLY to stream.opus
user_dir = os.path.join("data", uid)
# Map email-based UID to username for file system access
# If UID contains @, it's an email - look up the corresponding username
if '@' in uid:
from models import User
user = db.exec(select(User).where(User.email == uid)).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
filesystem_uid = user.username
else:
# Legacy support for username-based UIDs
filesystem_uid = uid
user_dir = os.path.join("data", filesystem_uid)
file_path = os.path.join(user_dir, filename)
real_user_dir = os.path.realpath(user_dir)
real_file_path = os.path.realpath(file_path)

174
migrate_uid_to_email.py Normal file
View File

@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""
Migration script to update PublicStream UIDs from usernames to email addresses.
This script:
1. Maps current username-based UIDs to their corresponding email addresses
2. Updates the publicstream table to use email addresses as UIDs
3. Updates any other tables that reference the old UID format
4. Provides rollback capability
"""
import sys
from sqlmodel import Session, select
from database import engine
from models import User, PublicStream, UploadLog, UserQuota
def get_username_to_email_mapping():
"""Get mapping of username -> email from user table"""
with Session(engine) as session:
users = session.exec(select(User)).all()
mapping = {}
for user in users:
mapping[user.username] = user.email
return mapping
def migrate_publicstream_uids():
"""Migrate PublicStream UIDs from usernames to emails"""
mapping = get_username_to_email_mapping()
with Session(engine) as session:
# Get all public streams with username-based UIDs
streams = session.exec(select(PublicStream)).all()
updates = []
for stream in streams:
if stream.uid in mapping:
old_uid = stream.uid
new_uid = mapping[stream.uid]
updates.append((old_uid, new_uid, stream))
print(f"Will update: {old_uid} -> {new_uid}")
else:
print(f"WARNING: No email found for username: {stream.uid}")
if not updates:
print("No updates needed - all UIDs are already in correct format")
return
# Confirm before proceeding
response = input(f"\nProceed with updating {len(updates)} records? (y/N): ")
if response.lower() != 'y':
print("Migration cancelled")
return
# Perform the updates
for old_uid, new_uid, stream in updates:
# Delete the old record
session.delete(stream)
session.flush() # Ensure deletion is committed before insert
# Create new record with email-based UID
new_stream = PublicStream(
uid=new_uid,
username=stream.username,
display_name=stream.display_name,
storage_bytes=stream.storage_bytes,
mtime=stream.mtime,
last_updated=stream.last_updated,
created_at=stream.created_at,
updated_at=stream.updated_at
)
session.add(new_stream)
print(f"Updated: {old_uid} -> {new_uid}")
session.commit()
print(f"\nSuccessfully migrated {len(updates)} PublicStream records")
def migrate_related_tables():
"""Update other tables that reference UIDs"""
mapping = get_username_to_email_mapping()
with Session(engine) as session:
# Update UploadLog table
upload_logs = session.exec(select(UploadLog)).all()
upload_updates = 0
for log in upload_logs:
if log.uid in mapping:
old_uid = log.uid
new_uid = mapping[log.uid]
log.uid = new_uid
upload_updates += 1
print(f"Updated UploadLog: {old_uid} -> {new_uid}")
# Update UserQuota table
quotas = session.exec(select(UserQuota)).all()
quota_updates = 0
for quota in quotas:
if quota.uid in mapping:
old_uid = quota.uid
new_uid = mapping[quota.uid]
quota.uid = new_uid
quota_updates += 1
print(f"Updated UserQuota: {old_uid} -> {new_uid}")
if upload_updates > 0 or quota_updates > 0:
session.commit()
print(f"\nUpdated {upload_updates} UploadLog and {quota_updates} UserQuota records")
else:
print("No related table updates needed")
def verify_migration():
"""Verify the migration was successful"""
print("\n=== Migration Verification ===")
with Session(engine) as session:
# Check PublicStream UIDs
streams = session.exec(select(PublicStream)).all()
print(f"PublicStream records: {len(streams)}")
for stream in streams:
if '@' in stream.uid:
print(f"{stream.uid} (email format)")
else:
print(f"{stream.uid} (still username format)")
# Check if all UIDs correspond to actual user emails
users = session.exec(select(User)).all()
user_emails = {user.email for user in users}
orphaned_streams = []
for stream in streams:
if stream.uid not in user_emails:
orphaned_streams.append(stream.uid)
if orphaned_streams:
print(f"\nWARNING: Found {len(orphaned_streams)} streams with UIDs not matching any user email:")
for uid in orphaned_streams:
print(f" - {uid}")
else:
print("\n✓ All stream UIDs correspond to valid user emails")
def main():
print("=== UID Migration: Username -> Email ===")
print("This script will update PublicStream UIDs from usernames to email addresses")
# Show current mapping
mapping = get_username_to_email_mapping()
print(f"\nFound {len(mapping)} users:")
for username, email in mapping.items():
print(f" {username} -> {email}")
if len(sys.argv) > 1 and sys.argv[1] == '--verify-only':
verify_migration()
return
# Perform migration
print("\n1. Migrating PublicStream table...")
migrate_publicstream_uids()
print("\n2. Migrating related tables...")
migrate_related_tables()
print("\n3. Verifying migration...")
verify_migration()
print("\n=== Migration Complete ===")
print("Remember to:")
print("1. Restart the application service")
print("2. Test the streams functionality")
print("3. Check for any frontend issues with the new UID format")
if __name__ == "__main__":
main()

View File

@ -21,7 +21,7 @@
}
</style>
<link rel="modulepreload" href="/static/sound.js" />
<script src="/static/streams-ui.js" type="module"></script>
<script src="/static/streams-ui.js?v=3" type="module"></script>
<script src="/static/app.js" type="module"></script>
</head>
<body>
@ -196,11 +196,11 @@
<script type="module" src="/static/dashboard.js"></script>
<script type="module" src="/static/app.js"></script>
<!-- Load public streams UI logic -->
<script type="module" src="/static/streams-ui.js"></script>
<script type="module" src="/static/streams-ui.js?v=3"></script>
<!-- Load upload functionality -->
<script type="module" src="/static/upload.js"></script>
<script type="module">
import "/static/nav.js";
import "/static/nav.js?v=2";
window.addEventListener("pageshow", () => {
const dz = document.querySelector("#user-upload-area");
if (dz) dz.classList.remove("uploading");

View File

@ -343,9 +343,23 @@ document.addEventListener("DOMContentLoaded", () => {
const ul = document.getElementById("stream-list");
if (!ul) return;
if (streams.length) {
streams.sort();
ul.innerHTML = streams.map(uid => `
<li><a href="/?profile=${encodeURIComponent(uid)}" class="profile-link">▶ ${uid}</a></li>
// Handle both array of UIDs (legacy) and array of stream objects (new)
const streamItems = streams.map(item => {
if (typeof item === 'string') {
// Legacy: array of UIDs
return { uid: item, username: item };
} else {
// New: array of stream objects
return {
uid: item.uid || '',
username: item.username || 'Unknown User'
};
}
});
streamItems.sort((a, b) => (a.username || '').localeCompare(b.username || ''));
ul.innerHTML = streamItems.map(stream => `
<li><a href="/?profile=${encodeURIComponent(stream.uid)}" class="profile-link">▶ ${stream.username}</a></li>
`).join("");
} else {
ul.innerHTML = "<li>No active streams.</li>";

View File

@ -314,6 +314,7 @@ function loadAndRenderStreams() {
// Render each stream in sorted order
streams.forEach((stream, index) => {
const uid = stream.uid || `stream-${index}`;
const username = stream.username || 'Unknown User';
const sizeMb = stream.size ? (stream.size / (1024 * 1024)).toFixed(1) : '?';
const mtime = stream.mtime ? new Date(stream.mtime * 1000).toISOString().split('T')[0].replace(/-/g, '/') : '';
@ -323,7 +324,7 @@ function loadAndRenderStreams() {
try {
li.innerHTML = `
<article class="stream-player" data-uid="${escapeHtml(uid)}">
<h3>${escapeHtml(uid)}</h3>
<h3>${escapeHtml(username)}</h3>
<div class="audio-controls">
<button class="play-pause-btn" data-uid="${escapeHtml(uid)}" aria-label="Play">▶️</button>
</div>
@ -397,9 +398,10 @@ export function renderStreamList(streams) {
ul.innerHTML = streams
.map(stream => {
const uid = stream.uid || '';
const username = stream.username || 'Unknown User';
const sizeKb = stream.size ? (stream.size / 1024).toFixed(1) : '?';
const mtime = stream.mtime ? new Date(stream.mtime * 1000).toLocaleString() : '';
return `<li><a href="/?profile=${encodeURIComponent(uid)}" class="profile-link">▶ ${uid}</a> <span style='color:var(--text-muted);font-size:90%'>[${sizeKb} KB, ${mtime}]</span></li>`;
return `<li><a href="/?profile=${encodeURIComponent(uid)}" class="profile-link">▶ ${escapeHtml(username)}</a> <span style='color:var(--text-muted);font-size:90%'>[${sizeKb} KB, ${mtime}]</span></li>`;
})
.join('');
} else {