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:
@ -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
15
main.py
@ -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
174
migrate_uid_to_email.py
Normal 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()
|
@ -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");
|
||||
|
@ -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>";
|
||||
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user