karaoke-gen 0.86.7__py3-none-any.whl → 0.96.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- backend/.coveragerc +20 -0
- backend/.gitignore +37 -0
- backend/Dockerfile +43 -0
- backend/Dockerfile.base +74 -0
- backend/README.md +242 -0
- backend/__init__.py +0 -0
- backend/api/__init__.py +0 -0
- backend/api/dependencies.py +457 -0
- backend/api/routes/__init__.py +0 -0
- backend/api/routes/admin.py +742 -0
- backend/api/routes/audio_search.py +903 -0
- backend/api/routes/auth.py +348 -0
- backend/api/routes/file_upload.py +2076 -0
- backend/api/routes/health.py +344 -0
- backend/api/routes/internal.py +435 -0
- backend/api/routes/jobs.py +1610 -0
- backend/api/routes/review.py +652 -0
- backend/api/routes/themes.py +162 -0
- backend/api/routes/users.py +1014 -0
- backend/config.py +172 -0
- backend/main.py +133 -0
- backend/middleware/__init__.py +5 -0
- backend/middleware/audit_logging.py +124 -0
- backend/models/__init__.py +0 -0
- backend/models/job.py +519 -0
- backend/models/requests.py +123 -0
- backend/models/theme.py +153 -0
- backend/models/user.py +254 -0
- backend/models/worker_log.py +164 -0
- backend/pyproject.toml +29 -0
- backend/quick-check.sh +93 -0
- backend/requirements.txt +29 -0
- backend/run_tests.sh +60 -0
- backend/services/__init__.py +0 -0
- backend/services/audio_analysis_service.py +243 -0
- backend/services/audio_editing_service.py +278 -0
- backend/services/audio_search_service.py +702 -0
- backend/services/auth_service.py +630 -0
- backend/services/credential_manager.py +792 -0
- backend/services/discord_service.py +172 -0
- backend/services/dropbox_service.py +301 -0
- backend/services/email_service.py +1093 -0
- backend/services/encoding_interface.py +454 -0
- backend/services/encoding_service.py +405 -0
- backend/services/firestore_service.py +512 -0
- backend/services/flacfetch_client.py +573 -0
- backend/services/gce_encoding/README.md +72 -0
- backend/services/gce_encoding/__init__.py +22 -0
- backend/services/gce_encoding/main.py +589 -0
- backend/services/gce_encoding/requirements.txt +16 -0
- backend/services/gdrive_service.py +356 -0
- backend/services/job_logging.py +258 -0
- backend/services/job_manager.py +842 -0
- backend/services/job_notification_service.py +271 -0
- backend/services/local_encoding_service.py +590 -0
- backend/services/local_preview_encoding_service.py +407 -0
- backend/services/lyrics_cache_service.py +216 -0
- backend/services/metrics.py +413 -0
- backend/services/packaging_service.py +287 -0
- backend/services/rclone_service.py +106 -0
- backend/services/storage_service.py +209 -0
- backend/services/stripe_service.py +275 -0
- backend/services/structured_logging.py +254 -0
- backend/services/template_service.py +330 -0
- backend/services/theme_service.py +469 -0
- backend/services/tracing.py +543 -0
- backend/services/user_service.py +721 -0
- backend/services/worker_service.py +558 -0
- backend/services/youtube_service.py +112 -0
- backend/services/youtube_upload_service.py +445 -0
- backend/tests/__init__.py +4 -0
- backend/tests/conftest.py +224 -0
- backend/tests/emulator/__init__.py +7 -0
- backend/tests/emulator/conftest.py +88 -0
- backend/tests/emulator/test_e2e_cli_backend.py +1053 -0
- backend/tests/emulator/test_emulator_integration.py +356 -0
- backend/tests/emulator/test_style_loading_direct.py +436 -0
- backend/tests/emulator/test_worker_logs_direct.py +229 -0
- backend/tests/emulator/test_worker_logs_subcollection.py +443 -0
- backend/tests/requirements-test.txt +10 -0
- backend/tests/requirements.txt +6 -0
- backend/tests/test_admin_email_endpoints.py +411 -0
- backend/tests/test_api_integration.py +460 -0
- backend/tests/test_api_routes.py +93 -0
- backend/tests/test_audio_analysis_service.py +294 -0
- backend/tests/test_audio_editing_service.py +386 -0
- backend/tests/test_audio_search.py +1398 -0
- backend/tests/test_audio_services.py +378 -0
- backend/tests/test_auth_firestore.py +231 -0
- backend/tests/test_config_extended.py +68 -0
- backend/tests/test_credential_manager.py +377 -0
- backend/tests/test_dependencies.py +54 -0
- backend/tests/test_discord_service.py +244 -0
- backend/tests/test_distribution_services.py +820 -0
- backend/tests/test_dropbox_service.py +472 -0
- backend/tests/test_email_service.py +492 -0
- backend/tests/test_emulator_integration.py +322 -0
- backend/tests/test_encoding_interface.py +412 -0
- backend/tests/test_file_upload.py +1739 -0
- backend/tests/test_flacfetch_client.py +632 -0
- backend/tests/test_gdrive_service.py +524 -0
- backend/tests/test_instrumental_api.py +431 -0
- backend/tests/test_internal_api.py +343 -0
- backend/tests/test_job_creation_regression.py +583 -0
- backend/tests/test_job_manager.py +339 -0
- backend/tests/test_job_manager_notifications.py +329 -0
- backend/tests/test_job_notification_service.py +443 -0
- backend/tests/test_jobs_api.py +273 -0
- backend/tests/test_local_encoding_service.py +423 -0
- backend/tests/test_local_preview_encoding_service.py +567 -0
- backend/tests/test_main.py +87 -0
- backend/tests/test_models.py +918 -0
- backend/tests/test_packaging_service.py +382 -0
- backend/tests/test_requests.py +201 -0
- backend/tests/test_routes_jobs.py +282 -0
- backend/tests/test_routes_review.py +337 -0
- backend/tests/test_services.py +556 -0
- backend/tests/test_services_extended.py +112 -0
- backend/tests/test_storage_service.py +448 -0
- backend/tests/test_style_upload.py +261 -0
- backend/tests/test_template_service.py +295 -0
- backend/tests/test_theme_service.py +516 -0
- backend/tests/test_unicode_sanitization.py +522 -0
- backend/tests/test_upload_api.py +256 -0
- backend/tests/test_validate.py +156 -0
- backend/tests/test_video_worker_orchestrator.py +847 -0
- backend/tests/test_worker_log_subcollection.py +509 -0
- backend/tests/test_worker_logging.py +365 -0
- backend/tests/test_workers.py +1116 -0
- backend/tests/test_workers_extended.py +178 -0
- backend/tests/test_youtube_service.py +247 -0
- backend/tests/test_youtube_upload_service.py +568 -0
- backend/validate.py +173 -0
- backend/version.py +27 -0
- backend/workers/README.md +597 -0
- backend/workers/__init__.py +11 -0
- backend/workers/audio_worker.py +618 -0
- backend/workers/lyrics_worker.py +683 -0
- backend/workers/render_video_worker.py +483 -0
- backend/workers/screens_worker.py +525 -0
- backend/workers/style_helper.py +198 -0
- backend/workers/video_worker.py +1277 -0
- backend/workers/video_worker_orchestrator.py +701 -0
- backend/workers/worker_logging.py +278 -0
- karaoke_gen/instrumental_review/static/index.html +7 -4
- karaoke_gen/karaoke_finalise/karaoke_finalise.py +6 -1
- karaoke_gen/style_loader.py +3 -1
- karaoke_gen/utils/__init__.py +163 -8
- karaoke_gen/video_background_processor.py +9 -4
- {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/METADATA +2 -1
- {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/RECORD +187 -42
- lyrics_transcriber/correction/agentic/providers/config.py +9 -5
- lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +1 -51
- lyrics_transcriber/correction/corrector.py +192 -130
- lyrics_transcriber/correction/operations.py +24 -9
- lyrics_transcriber/frontend/package-lock.json +2 -2
- lyrics_transcriber/frontend/package.json +1 -1
- lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +1 -1
- lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +11 -7
- lyrics_transcriber/frontend/src/components/EditActionBar.tsx +31 -5
- lyrics_transcriber/frontend/src/components/EditModal.tsx +28 -10
- lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +123 -27
- lyrics_transcriber/frontend/src/components/EditWordList.tsx +112 -60
- lyrics_transcriber/frontend/src/components/Header.tsx +90 -76
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +53 -31
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +44 -13
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +66 -50
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +124 -30
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +1 -1
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +12 -5
- lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +3 -3
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +1 -1
- lyrics_transcriber/frontend/src/components/WordDivider.tsx +11 -7
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +4 -2
- lyrics_transcriber/frontend/src/hooks/useManualSync.ts +103 -1
- lyrics_transcriber/frontend/src/theme.ts +42 -15
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/frontend/vite.config.js +5 -0
- lyrics_transcriber/frontend/web_assets/assets/{index-BECn1o8Q.js → index-BSMgOq4Z.js} +6959 -5782
- lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js.map +1 -0
- lyrics_transcriber/frontend/web_assets/index.html +6 -2
- lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.svg +5 -0
- lyrics_transcriber/output/generator.py +17 -3
- lyrics_transcriber/output/video.py +60 -95
- lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js.map +0 -1
- {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/entry_points.txt +0 -0
- {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OAuth credential management API routes.
|
|
3
|
+
|
|
4
|
+
Provides endpoints for:
|
|
5
|
+
1. Checking credential status
|
|
6
|
+
2. Device authorization flow for re-authentication
|
|
7
|
+
3. Credential validation before job submission
|
|
8
|
+
"""
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Optional
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from fastapi import APIRouter, HTTPException, BackgroundTasks
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
|
|
15
|
+
from backend.services.credential_manager import (
|
|
16
|
+
get_credential_manager,
|
|
17
|
+
CredentialStatus,
|
|
18
|
+
CredentialCheckResult,
|
|
19
|
+
)
|
|
20
|
+
from backend.config import get_settings
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
router = APIRouter(prefix="/auth", tags=["auth"])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ===========================================================================
|
|
27
|
+
# Request/Response Models
|
|
28
|
+
# ===========================================================================
|
|
29
|
+
|
|
30
|
+
class CredentialStatusResponse(BaseModel):
|
|
31
|
+
"""Response model for credential status."""
|
|
32
|
+
service: str
|
|
33
|
+
status: str
|
|
34
|
+
message: str
|
|
35
|
+
last_checked: datetime
|
|
36
|
+
expires_at: Optional[datetime] = None
|
|
37
|
+
|
|
38
|
+
class Config:
|
|
39
|
+
from_attributes = True
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AllCredentialsStatusResponse(BaseModel):
|
|
43
|
+
"""Response model for all credentials status."""
|
|
44
|
+
youtube: CredentialStatusResponse
|
|
45
|
+
gdrive: CredentialStatusResponse
|
|
46
|
+
dropbox: CredentialStatusResponse
|
|
47
|
+
all_valid: bool
|
|
48
|
+
services_needing_auth: list[str]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class DeviceAuthStartRequest(BaseModel):
|
|
52
|
+
"""Request to start device authorization flow."""
|
|
53
|
+
client_id: Optional[str] = None # Optional - reads from Secret Manager if not provided
|
|
54
|
+
client_secret: Optional[str] = None # Optional - reads from Secret Manager if not provided
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class DeviceAuthStartResponse(BaseModel):
|
|
58
|
+
"""Response with device authorization info."""
|
|
59
|
+
device_code: str
|
|
60
|
+
user_code: str
|
|
61
|
+
verification_url: str
|
|
62
|
+
expires_in: int
|
|
63
|
+
interval: int
|
|
64
|
+
instructions: str
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class DeviceAuthPollResponse(BaseModel):
|
|
68
|
+
"""Response from polling device authorization."""
|
|
69
|
+
status: str # "pending", "complete", "expired", "error"
|
|
70
|
+
message: Optional[str] = None
|
|
71
|
+
credentials_saved: bool = False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class CredentialValidationRequest(BaseModel):
|
|
75
|
+
"""Request to validate credentials for specific services."""
|
|
76
|
+
youtube: bool = False
|
|
77
|
+
gdrive: bool = False
|
|
78
|
+
dropbox: bool = False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class CredentialValidationResponse(BaseModel):
|
|
82
|
+
"""Response from credential validation."""
|
|
83
|
+
valid: bool
|
|
84
|
+
invalid_services: list[str]
|
|
85
|
+
message: str
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ===========================================================================
|
|
89
|
+
# Endpoints
|
|
90
|
+
# ===========================================================================
|
|
91
|
+
|
|
92
|
+
@router.get("/status", response_model=AllCredentialsStatusResponse)
|
|
93
|
+
async def get_credentials_status():
|
|
94
|
+
"""
|
|
95
|
+
Get the status of all OAuth credentials.
|
|
96
|
+
|
|
97
|
+
Returns the validation status for YouTube, Google Drive, and Dropbox
|
|
98
|
+
credentials, including whether they are valid, expired, or need
|
|
99
|
+
re-authorization.
|
|
100
|
+
"""
|
|
101
|
+
manager = get_credential_manager()
|
|
102
|
+
results = manager.check_all_credentials()
|
|
103
|
+
|
|
104
|
+
def to_response(result: CredentialCheckResult) -> CredentialStatusResponse:
|
|
105
|
+
return CredentialStatusResponse(
|
|
106
|
+
service=result.service,
|
|
107
|
+
status=result.status.value,
|
|
108
|
+
message=result.message,
|
|
109
|
+
last_checked=result.last_checked,
|
|
110
|
+
expires_at=result.expires_at
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
services_needing_auth = [
|
|
114
|
+
name for name, result in results.items()
|
|
115
|
+
if result.status in (CredentialStatus.INVALID, CredentialStatus.EXPIRED, CredentialStatus.NOT_CONFIGURED)
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
return AllCredentialsStatusResponse(
|
|
119
|
+
youtube=to_response(results["youtube"]),
|
|
120
|
+
gdrive=to_response(results["gdrive"]),
|
|
121
|
+
dropbox=to_response(results["dropbox"]),
|
|
122
|
+
all_valid=len(services_needing_auth) == 0,
|
|
123
|
+
services_needing_auth=services_needing_auth
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@router.get("/status/{service}", response_model=CredentialStatusResponse)
|
|
128
|
+
async def get_service_credential_status(service: str):
|
|
129
|
+
"""
|
|
130
|
+
Get the status of a specific service's OAuth credentials.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
service: One of "youtube", "gdrive", "dropbox"
|
|
134
|
+
"""
|
|
135
|
+
manager = get_credential_manager()
|
|
136
|
+
|
|
137
|
+
if service == "youtube":
|
|
138
|
+
result = manager.check_youtube_credentials()
|
|
139
|
+
elif service == "gdrive":
|
|
140
|
+
result = manager.check_gdrive_credentials()
|
|
141
|
+
elif service == "dropbox":
|
|
142
|
+
result = manager.check_dropbox_credentials()
|
|
143
|
+
else:
|
|
144
|
+
raise HTTPException(status_code=400, detail=f"Unknown service: {service}")
|
|
145
|
+
|
|
146
|
+
return CredentialStatusResponse(
|
|
147
|
+
service=result.service,
|
|
148
|
+
status=result.status.value,
|
|
149
|
+
message=result.message,
|
|
150
|
+
last_checked=result.last_checked,
|
|
151
|
+
expires_at=result.expires_at
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@router.post("/validate", response_model=CredentialValidationResponse)
|
|
156
|
+
async def validate_credentials(request: CredentialValidationRequest):
|
|
157
|
+
"""
|
|
158
|
+
Validate that credentials are available for requested services.
|
|
159
|
+
|
|
160
|
+
Use this before submitting a job to ensure all required
|
|
161
|
+
credentials are valid.
|
|
162
|
+
"""
|
|
163
|
+
manager = get_credential_manager()
|
|
164
|
+
invalid_services = []
|
|
165
|
+
|
|
166
|
+
if request.youtube:
|
|
167
|
+
result = manager.check_youtube_credentials()
|
|
168
|
+
if result.status != CredentialStatus.VALID:
|
|
169
|
+
invalid_services.append("youtube")
|
|
170
|
+
|
|
171
|
+
if request.gdrive:
|
|
172
|
+
result = manager.check_gdrive_credentials()
|
|
173
|
+
if result.status != CredentialStatus.VALID:
|
|
174
|
+
invalid_services.append("gdrive")
|
|
175
|
+
|
|
176
|
+
if request.dropbox:
|
|
177
|
+
result = manager.check_dropbox_credentials()
|
|
178
|
+
if result.status != CredentialStatus.VALID:
|
|
179
|
+
invalid_services.append("dropbox")
|
|
180
|
+
|
|
181
|
+
if invalid_services:
|
|
182
|
+
return CredentialValidationResponse(
|
|
183
|
+
valid=False,
|
|
184
|
+
invalid_services=invalid_services,
|
|
185
|
+
message=f"The following services need re-authorization: {', '.join(invalid_services)}"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return CredentialValidationResponse(
|
|
189
|
+
valid=True,
|
|
190
|
+
invalid_services=[],
|
|
191
|
+
message="All requested credentials are valid"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# ===========================================================================
|
|
196
|
+
# Device Authorization Flow Endpoints
|
|
197
|
+
# ===========================================================================
|
|
198
|
+
|
|
199
|
+
@router.post("/youtube/device", response_model=DeviceAuthStartResponse)
|
|
200
|
+
async def start_youtube_device_auth(request: Optional[DeviceAuthStartRequest] = None):
|
|
201
|
+
"""
|
|
202
|
+
Start YouTube device authorization flow.
|
|
203
|
+
|
|
204
|
+
This initiates a device auth flow that allows authorization
|
|
205
|
+
from any device. The user must visit the verification URL
|
|
206
|
+
and enter the user code.
|
|
207
|
+
|
|
208
|
+
Client credentials are loaded from Secret Manager ('youtube-client-credentials')
|
|
209
|
+
unless explicitly provided in the request body.
|
|
210
|
+
|
|
211
|
+
After starting, poll /auth/youtube/device/{device_code} to
|
|
212
|
+
check for completion.
|
|
213
|
+
"""
|
|
214
|
+
manager = get_credential_manager()
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
device_info = manager.start_youtube_device_auth(
|
|
218
|
+
client_id=request.client_id if request else None,
|
|
219
|
+
client_secret=request.client_secret if request else None
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return DeviceAuthStartResponse(
|
|
223
|
+
device_code=device_info.device_code,
|
|
224
|
+
user_code=device_info.user_code,
|
|
225
|
+
verification_url=device_info.verification_url,
|
|
226
|
+
expires_in=device_info.expires_in,
|
|
227
|
+
interval=device_info.interval,
|
|
228
|
+
instructions=f"Visit {device_info.verification_url} and enter code: {device_info.user_code}"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
except Exception as e:
|
|
232
|
+
logger.error(f"Failed to start YouTube device auth: {e}")
|
|
233
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@router.get("/youtube/device/{device_code}", response_model=DeviceAuthPollResponse)
|
|
237
|
+
async def poll_youtube_device_auth(device_code: str):
|
|
238
|
+
"""
|
|
239
|
+
Poll for YouTube device authorization completion.
|
|
240
|
+
|
|
241
|
+
Call this endpoint at the interval specified in the device auth
|
|
242
|
+
start response. Keep polling while status is "pending".
|
|
243
|
+
|
|
244
|
+
Status values:
|
|
245
|
+
- pending: User has not yet authorized
|
|
246
|
+
- complete: Authorization successful, credentials saved
|
|
247
|
+
- expired: Device code expired, start new flow
|
|
248
|
+
- error: An error occurred
|
|
249
|
+
"""
|
|
250
|
+
manager = get_credential_manager()
|
|
251
|
+
|
|
252
|
+
status, data = manager.poll_device_auth("youtube", device_code)
|
|
253
|
+
|
|
254
|
+
return DeviceAuthPollResponse(
|
|
255
|
+
status=status,
|
|
256
|
+
message=data.get("message") if data else None,
|
|
257
|
+
credentials_saved=(status == "complete")
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@router.post("/gdrive/device", response_model=DeviceAuthStartResponse)
|
|
262
|
+
async def start_gdrive_device_auth(request: Optional[DeviceAuthStartRequest] = None):
|
|
263
|
+
"""
|
|
264
|
+
Start Google Drive device authorization flow.
|
|
265
|
+
|
|
266
|
+
Client credentials are loaded from Secret Manager ('gdrive-client-credentials')
|
|
267
|
+
unless explicitly provided in the request body.
|
|
268
|
+
|
|
269
|
+
After starting, poll /auth/gdrive/device/{device_code} to check for completion.
|
|
270
|
+
"""
|
|
271
|
+
manager = get_credential_manager()
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
device_info = manager.start_gdrive_device_auth(
|
|
275
|
+
client_id=request.client_id if request else None,
|
|
276
|
+
client_secret=request.client_secret if request else None
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
return DeviceAuthStartResponse(
|
|
280
|
+
device_code=device_info.device_code,
|
|
281
|
+
user_code=device_info.user_code,
|
|
282
|
+
verification_url=device_info.verification_url,
|
|
283
|
+
expires_in=device_info.expires_in,
|
|
284
|
+
interval=device_info.interval,
|
|
285
|
+
instructions=f"Visit {device_info.verification_url} and enter code: {device_info.user_code}"
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
except Exception as e:
|
|
289
|
+
logger.error(f"Failed to start Google Drive device auth: {e}")
|
|
290
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@router.get("/gdrive/device/{device_code}", response_model=DeviceAuthPollResponse)
|
|
294
|
+
async def poll_gdrive_device_auth(device_code: str):
|
|
295
|
+
"""
|
|
296
|
+
Poll for Google Drive device authorization completion.
|
|
297
|
+
"""
|
|
298
|
+
manager = get_credential_manager()
|
|
299
|
+
|
|
300
|
+
status, data = manager.poll_device_auth("gdrive", device_code)
|
|
301
|
+
|
|
302
|
+
return DeviceAuthPollResponse(
|
|
303
|
+
status=status,
|
|
304
|
+
message=data.get("message") if data else None,
|
|
305
|
+
credentials_saved=(status == "complete")
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ===========================================================================
|
|
310
|
+
# Alert Testing
|
|
311
|
+
# ===========================================================================
|
|
312
|
+
|
|
313
|
+
@router.post("/test-alert")
|
|
314
|
+
async def test_credential_alert(background_tasks: BackgroundTasks):
|
|
315
|
+
"""
|
|
316
|
+
Test the credential alert mechanism.
|
|
317
|
+
|
|
318
|
+
Sends a test alert to Discord if configured.
|
|
319
|
+
"""
|
|
320
|
+
settings = get_settings()
|
|
321
|
+
discord_url = settings.get_secret("discord-alert-webhook")
|
|
322
|
+
|
|
323
|
+
if not discord_url:
|
|
324
|
+
raise HTTPException(
|
|
325
|
+
status_code=400,
|
|
326
|
+
detail="Discord alert webhook not configured"
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
manager = get_credential_manager()
|
|
330
|
+
|
|
331
|
+
# Create a fake invalid result for testing
|
|
332
|
+
from backend.services.credential_manager import CredentialCheckResult
|
|
333
|
+
test_results = [
|
|
334
|
+
CredentialCheckResult(
|
|
335
|
+
service="test",
|
|
336
|
+
status=CredentialStatus.INVALID,
|
|
337
|
+
message="This is a test alert",
|
|
338
|
+
last_checked=datetime.utcnow()
|
|
339
|
+
)
|
|
340
|
+
]
|
|
341
|
+
|
|
342
|
+
background_tasks.add_task(
|
|
343
|
+
manager.send_credential_alert,
|
|
344
|
+
test_results,
|
|
345
|
+
discord_url
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
return {"message": "Test alert queued"}
|