Implement Google OAuth linking functionality. Update backend to handle linking existing accounts with Google, including state token management. Enhance frontend to support linking process, including new UI components for user input and feedback. Update mobile app to handle OAuth callbacks and integrate linking features. Refactor related services and controllers for improved error handling and user experience.

This commit is contained in:
Torsten Schulz (local)
2026-05-15 08:27:36 +02:00
parent 95b611fd04
commit c16d2a6e4d
20 changed files with 768 additions and 127 deletions

View File

@@ -1,4 +1,5 @@
const passport = require('passport');
const oauthService = require('../services/OAuthService');
/**
* OAuth Controller
@@ -10,12 +11,38 @@ class OAuthController {
* GET /api/auth/google
*/
googleAuth(req, res, next) {
const state = req.query.stateToken || oauthService.createStateToken({
platform: req.query.platform === 'android' ? 'android' : 'web'
});
passport.authenticate('google', {
scope: ['profile', 'email'],
session: false
session: false,
state
})(req, res, next);
}
/**
* Google OAuth-Verknüpfung für eingeloggte Benutzer starten
* POST /api/auth/google/link-url
*/
createGoogleLinkUrl(req, res) {
try {
const platform = req.body?.platform === 'android' ? 'android' : 'web';
const stateToken = oauthService.createStateToken({
mode: 'link',
platform,
userId: req.user.userId
});
const baseUrl = process.env.API_PUBLIC_URL || `${req.protocol}://${req.get('host')}/api`;
res.json({
success: true,
url: `${baseUrl}/auth/google?stateToken=${encodeURIComponent(stateToken)}`
});
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
}
/**
* Google OAuth Callback
* GET /api/auth/google/callback
@@ -24,18 +51,67 @@ class OAuthController {
passport.authenticate('google', {
session: false,
failureRedirect: `${process.env.FRONTEND_URL || 'http://localhost:5010'}/login?error=oauth_failed`
}, (err, result) => {
}, async (err, result) => {
if (err || !result) {
console.error('Google OAuth Fehler:', err);
return res.redirect(`${process.env.FRONTEND_URL || 'http://localhost:5010'}/login?error=oauth_failed`);
}
// Redirect zum Frontend mit Token
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5010';
res.redirect(`${frontendUrl}/oauth-callback?token=${result.token}`);
try {
const state = oauthService.verifyStateToken(req.query.state);
const authResult = await oauthService.completeOAuthLogin(result.profile, result.provider, {
linkUserId: state.mode === 'link' ? state.userId : null
});
const target = state.platform === 'android'
? 'timeclock://oauth-callback'
: `${process.env.FRONTEND_URL || 'http://localhost:5010'}/oauth-callback`;
if (authResult.requiresLink) {
const params = new URLSearchParams({
pending: authResult.pendingToken,
email: authResult.email || '',
provider: result.provider
});
return res.redirect(`${target}?${params.toString()}`);
}
return res.redirect(`${target}?token=${encodeURIComponent(authResult.token)}`);
} catch (callbackError) {
console.error('Google OAuth Callback-Verarbeitung fehlgeschlagen:', callbackError);
return res.redirect(`${process.env.FRONTEND_URL || 'http://localhost:5010'}/login?error=oauth_failed`);
}
})(req, res, next);
}
async linkExistingAccount(req, res) {
try {
const { pendingToken, email, password } = req.body;
if (!pendingToken || !email || !password) {
return res.status(400).json({ success: false, error: 'pendingToken, E-Mail und Passwort sind erforderlich' });
}
const result = await oauthService.linkPendingToPasswordAccount(pendingToken, email, password);
res.json({ success: true, token: result.token, user: result.user });
} catch (error) {
console.error('OAuth-Verknüpfung mit bestehendem Account fehlgeschlagen:', error);
res.status(401).json({ success: false, error: error.message });
}
}
async linkPendingToCurrentUser(req, res) {
try {
const { pendingToken } = req.body;
if (!pendingToken) {
return res.status(400).json({ success: false, error: 'pendingToken ist erforderlich' });
}
const result = await oauthService.linkPendingToAuthenticatedUser(pendingToken, req.user.userId);
res.json({ success: true, token: result.token, user: result.user });
} catch (error) {
console.error('OAuth-Verknüpfung fehlgeschlagen:', error);
res.status(400).json({ success: false, error: error.message });
}
}
/**
* OAuth-Identities für Benutzer abrufen
* GET /api/auth/identities
@@ -43,8 +119,6 @@ class OAuthController {
async getIdentities(req, res) {
try {
const userId = req.user.userId;
const oauthService = require('../services/OAuthService');
const identities = await oauthService.getUserIdentities(userId);
res.json({
@@ -69,8 +143,6 @@ class OAuthController {
try {
const userId = req.user.userId;
const { provider } = req.params;
const oauthService = require('../services/OAuthService');
const unlinked = await oauthService.unlinkProvider(userId, provider);
if (unlinked) {
@@ -98,4 +170,3 @@ class OAuthController {
module.exports = new OAuthController();