feat(auth): connexion avec un compte Google (find-or-create, import likes/abos inclus)

- Backend: GET /api/auth/google/{url,callback} (state login distinct du
  link import), echange code, profil userinfo, find-or-create (OAuth sub,
  puis email), session + cookies comme login interne, connexion stockee
  dans oauth_connections pour l'import direct
- Front: bouton 'Se connecter avec Google' sur /auth/login (visible si
  configure), page /auth/google/callback (initSession + prefs + redirect
  /library/import), cles i18n nav.import deja en place
- Comptes Google-only: password_hash aleatoire (login mot de passe impossible)
This commit is contained in:
2026-09-26 21:02:12 -04:00
parent ecb4295a59
commit 783003822d
9 changed files with 252 additions and 2 deletions
+9 -2
View File
@@ -36,9 +36,14 @@ function randomState() {
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;
}
export function createOAuthState(userId, provider) {
export function createOAuthState(userId, provider, purpose = 'link') {
const state = randomState();
pendingStates.set(state, { userId: String(userId), provider: String(provider), createdAt: Date.now() });
pendingStates.set(state, {
userId: userId == null ? null : String(userId),
provider: String(provider),
purpose: String(purpose || 'link'),
createdAt: Date.now(),
});
// Purge opportuniste.
try {
const now = Date.now();
@@ -233,6 +238,8 @@ export async function fetchGoogleProfile(accessToken) {
const data = await getJson('https://www.googleapis.com/oauth2/v2/userinfo', accessToken);
return {
id: String(data.id || ''),
email: String(data.email || ''),
verifiedEmail: data.verified_email !== false,
displayName: String(data.name || data.email || 'Google'),
avatarUrl: String(data.picture || ''),
};