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:
@@ -1361,3 +1361,102 @@ export function getYoutubeMetricsToday() {
|
||||
export function countYoutubeCacheRows() {
|
||||
try { return db.prepare(`SELECT COUNT(1) AS n FROM youtube_search_cache`).get()?.n || 0; } catch { return 0; }
|
||||
}
|
||||
|
||||
// -------------------- OAuth : connexions Google / Twitch (import favoris/abos) --------------------
|
||||
function ensureOAuthTables() {
|
||||
try {
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS oauth_connections (
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
provider TEXT NOT NULL,
|
||||
external_user_id TEXT,
|
||||
display_name TEXT,
|
||||
avatar_url TEXT,
|
||||
access_token TEXT,
|
||||
refresh_token TEXT,
|
||||
expires_at INTEGER,
|
||||
scopes TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (user_id, provider)
|
||||
);`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_oauth_connections_user ON oauth_connections(user_id);`);
|
||||
} catch {}
|
||||
}
|
||||
ensureOAuthTables();
|
||||
|
||||
function oauthRowToDto(row, withTokens = false) {
|
||||
if (!row) return null;
|
||||
const dto = {
|
||||
provider: row.provider,
|
||||
externalUserId: row.external_user_id || null,
|
||||
displayName: row.display_name || null,
|
||||
avatarUrl: row.avatar_url || null,
|
||||
scopes: row.scopes || null,
|
||||
expiresAt: row.expires_at ?? null,
|
||||
connectedAt: row.created_at ?? null,
|
||||
updatedAt: row.updated_at ?? null,
|
||||
};
|
||||
if (withTokens) {
|
||||
dto.accessToken = row.access_token || null;
|
||||
dto.refreshToken = row.refresh_token || null;
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
|
||||
export function upsertOAuthConnection({ userId, provider, externalUserId, displayName, avatarUrl, accessToken, refreshToken, expiresAt, scopes }) {
|
||||
ensureOAuthTables();
|
||||
const now = Date.now();
|
||||
const existing = db.prepare(`SELECT * FROM oauth_connections WHERE user_id = ? AND provider = ?`).get(userId, provider);
|
||||
if (existing) {
|
||||
db.prepare(`UPDATE oauth_connections SET external_user_id = ?, display_name = ?, avatar_url = ?,
|
||||
access_token = COALESCE(?, access_token), refresh_token = COALESCE(?, refresh_token),
|
||||
expires_at = COALESCE(?, expires_at), scopes = COALESCE(?, scopes), updated_at = ?
|
||||
WHERE user_id = ? AND provider = ?`)
|
||||
.run(
|
||||
externalUserId ?? existing.external_user_id, displayName ?? existing.display_name,
|
||||
avatarUrl ?? existing.avatar_url, accessToken ?? null, refreshToken ?? null,
|
||||
expiresAt ?? null, scopes ?? null, now, userId, provider,
|
||||
);
|
||||
} else {
|
||||
db.prepare(`INSERT INTO oauth_connections (user_id, provider, external_user_id, display_name, avatar_url,
|
||||
access_token, refresh_token, expires_at, scopes, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
||||
.run(userId, provider, externalUserId || null, displayName || null, avatarUrl || null,
|
||||
accessToken || null, refreshToken || null, expiresAt ?? null, scopes || null, now, now);
|
||||
}
|
||||
return oauthRowToDto(db.prepare(`SELECT * FROM oauth_connections WHERE user_id = ? AND provider = ?`).get(userId, provider));
|
||||
}
|
||||
|
||||
export function listOAuthConnections(userId) {
|
||||
try {
|
||||
ensureOAuthTables();
|
||||
return db.prepare(`SELECT * FROM oauth_connections WHERE user_id = ?`).all(userId).map((r) => oauthRowToDto(r));
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
export function getOAuthConnection(userId, provider, withTokens = false) {
|
||||
try {
|
||||
ensureOAuthTables();
|
||||
return oauthRowToDto(
|
||||
db.prepare(`SELECT * FROM oauth_connections WHERE user_id = ? AND provider = ?`).get(userId, provider),
|
||||
withTokens,
|
||||
);
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
export function updateOAuthTokens(userId, provider, { accessToken, refreshToken, expiresAt }) {
|
||||
try {
|
||||
ensureOAuthTables();
|
||||
db.prepare(`UPDATE oauth_connections SET access_token = COALESCE(?, access_token),
|
||||
refresh_token = COALESCE(?, refresh_token), expires_at = COALESCE(?, expires_at), updated_at = ?
|
||||
WHERE user_id = ? AND provider = ?`)
|
||||
.run(accessToken ?? null, refreshToken ?? null, expiresAt ?? null, Date.now(), userId, provider);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export function deleteOAuthConnection(userId, provider) {
|
||||
try {
|
||||
ensureOAuthTables();
|
||||
return db.prepare(`DELETE FROM oauth_connections WHERE user_id = ? AND provider = ?`).run(userId, provider);
|
||||
} catch { return { changes: 0 }; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user