feat(oauth): import favoris/abos Google (YouTube) et Twitch vers abonnements/likes

- Backend: server/oauth.mjs (auth URL, echange code, refresh, fetch
  subscriptions/likes Google, follows Twitch), table oauth_connections
  + migration, routes /api/oauth/:provider/{url,callback,preview,import},
  /api/oauth/{status,connections}
- Front: /library/import (connect, preview, import), service
  oauth-import, lien depuis Abonnements
- Config: GOOGLE_CLIENT_ID/SECRET/REDIRECT_URI, TWITCH_REDIRECT_URI,
  OAUTH_APP_BASE_URL documentes (.env.example, docker-compose),
  passthrough docker-compose.local.yml, roadmap README cochee
This commit is contained in:
2026-09-26 18:47:02 -04:00
parent 21f2296c41
commit df53445efe
13 changed files with 971 additions and 1 deletions
+62
View File
@@ -0,0 +1,62 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface OAuthConnection {
provider: string;
externalUserId: string | null;
displayName: string | null;
avatarUrl: string | null;
scopes: string | null;
expiresAt: number | null;
connectedAt: number | null;
updatedAt: number | null;
}
export interface OAuthPreview {
provider: string;
subscriptions: Array<{ provider: string; externalId: string; title: string; handle?: string | null; avatarUrl?: string | null; url?: string | null }>;
likes: Array<{ provider: string; videoId: string; title: string; thumbnail?: string }>;
subscriptionCount: number;
likeCount: number;
}
export interface OAuthImportResult {
provider: string;
importedSubscriptions: number;
importedLikes: number;
skippedSubscriptions: number;
skippedLikes: number;
}
@Injectable({ providedIn: 'root' })
export class OAuthImportService {
private http = inject(HttpClient);
status(): Observable<{ google: { configured: boolean; missing: string[] }; twitch: { configured: boolean; missing: string[] } }> {
return this.http.get<any>('/api/oauth/status');
}
connections(): Observable<{ connections: OAuthConnection[]; status: any }> {
return this.http.get<any>('/api/oauth/connections');
}
async connect(provider: 'google' | 'twitch'): Promise<void> {
const res: any = await this.http.get(`/api/oauth/${provider}/url`).toPromise().catch((e) => { throw e; });
const url = res?.url;
if (!url) throw new Error('oauth_url_failed');
window.location.href = url;
}
disconnect(provider: string): Observable<void> {
return this.http.delete<void>(`/api/oauth/${provider}`);
}
preview(provider: string): Observable<OAuthPreview> {
return this.http.get<OAuthPreview>(`/api/oauth/${provider}/preview`);
}
importAll(provider: string, types: string[]): Observable<OAuthImportResult> {
return this.http.post<OAuthImportResult>(`/api/oauth/${provider}/import`, { types });
}
}