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:
@@ -19,6 +19,10 @@ class OAuthService {
|
||||
* @returns {Promise<Object>} Token und Benutzer-Info
|
||||
*/
|
||||
async authenticateWithProvider(profile, provider) {
|
||||
return this.completeOAuthLogin(profile, provider);
|
||||
}
|
||||
|
||||
async completeOAuthLogin(profile, provider, options = {}) {
|
||||
const { User, AuthInfo, AuthIdentity, AuthToken } = database.getModels();
|
||||
|
||||
const providerId = profile.id;
|
||||
@@ -51,68 +55,150 @@ class OAuthService {
|
||||
// Bestehender OAuth-Benutzer
|
||||
authInfo = authIdentity.authInfo;
|
||||
user = authInfo.user;
|
||||
} else {
|
||||
// Neuer OAuth-Benutzer oder Verknüpfung mit bestehendem Account
|
||||
|
||||
// Prüfen ob Benutzer mit dieser E-Mail bereits existiert
|
||||
if (email) {
|
||||
authInfo = await AuthInfo.findOne({
|
||||
where: { email },
|
||||
include: [{
|
||||
model: User,
|
||||
as: 'user'
|
||||
}]
|
||||
});
|
||||
if (options.linkUserId && Number(user.id) !== Number(options.linkUserId)) {
|
||||
throw new Error('Dieser Google-Account ist bereits mit einem anderen Benutzer verknüpft');
|
||||
}
|
||||
} else {
|
||||
if (options.linkUserId) {
|
||||
authInfo = await AuthInfo.findOne({
|
||||
where: { user_id: options.linkUserId },
|
||||
include: [{ model: User, as: 'user' }]
|
||||
});
|
||||
|
||||
if (authInfo) {
|
||||
// Verknüpfe OAuth mit bestehendem Account
|
||||
if (!authInfo) {
|
||||
throw new Error('Benutzer für Verknüpfung nicht gefunden');
|
||||
}
|
||||
|
||||
authIdentity = await AuthIdentity.create({
|
||||
auth_info_id: authInfo.id,
|
||||
provider,
|
||||
identity: providerId,
|
||||
version: 0
|
||||
});
|
||||
user = authInfo.user;
|
||||
|
||||
authIdentity = await AuthIdentity.create({
|
||||
auth_info_id: authInfo.id,
|
||||
provider,
|
||||
identity: providerId,
|
||||
version: 0
|
||||
});
|
||||
} else {
|
||||
// Neuen Benutzer erstellen
|
||||
user = await User.create({
|
||||
full_name: displayName,
|
||||
role: 0,
|
||||
daily_hours: 8,
|
||||
week_hours: 40,
|
||||
week_workdays: 5,
|
||||
preferred_title_type: 0,
|
||||
version: 0,
|
||||
last_change: new Date()
|
||||
});
|
||||
// Neuer OAuth-Benutzer oder explizite Verknüpfung mit bestehendem Account
|
||||
|
||||
// Auth-Info erstellen (ohne Passwort für OAuth-only Accounts)
|
||||
authInfo = await AuthInfo.create({
|
||||
user_id: user.id,
|
||||
email: email || `${provider}_${providerId}@oauth.local`,
|
||||
password_hash: '', // Kein Passwort für OAuth-only
|
||||
password_method: 'oauth',
|
||||
password_salt: '',
|
||||
status: 1,
|
||||
failed_login_attempts: 0,
|
||||
email_token: '',
|
||||
email_token_role: 0,
|
||||
unverified_email: '',
|
||||
version: 0
|
||||
});
|
||||
// Prüfen ob Benutzer mit dieser E-Mail bereits existiert
|
||||
if (email) {
|
||||
authInfo = await AuthInfo.findOne({
|
||||
where: { email },
|
||||
include: [{
|
||||
model: User,
|
||||
as: 'user'
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
// OAuth-Identity erstellen
|
||||
authIdentity = await AuthIdentity.create({
|
||||
auth_info_id: authInfo.id,
|
||||
provider,
|
||||
identity: providerId,
|
||||
version: 0
|
||||
});
|
||||
if (authInfo) {
|
||||
return {
|
||||
requiresLink: true,
|
||||
pendingToken: this.createPendingToken(profile, provider),
|
||||
email,
|
||||
displayName
|
||||
};
|
||||
} else {
|
||||
// Neuen Benutzer erstellen
|
||||
user = await User.create({
|
||||
full_name: displayName,
|
||||
role: 0,
|
||||
daily_hours: 8,
|
||||
week_hours: 40,
|
||||
week_workdays: 5,
|
||||
preferred_title_type: 0,
|
||||
version: 0,
|
||||
last_change: new Date()
|
||||
});
|
||||
|
||||
// Auth-Info erstellen (ohne Passwort für OAuth-only Accounts)
|
||||
authInfo = await AuthInfo.create({
|
||||
user_id: user.id,
|
||||
email: email || `${provider}_${providerId}@oauth.local`,
|
||||
password_hash: '', // Kein Passwort für OAuth-only
|
||||
password_method: 'oauth',
|
||||
password_salt: '',
|
||||
status: 1,
|
||||
failed_login_attempts: 0,
|
||||
email_token: '',
|
||||
email_token_role: 0,
|
||||
unverified_email: '',
|
||||
version: 0
|
||||
});
|
||||
|
||||
// OAuth-Identity erstellen
|
||||
authIdentity = await AuthIdentity.create({
|
||||
auth_info_id: authInfo.id,
|
||||
provider,
|
||||
identity: providerId,
|
||||
version: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.createLoginResult(user, authInfo, provider);
|
||||
}
|
||||
|
||||
createPendingToken(profile, provider) {
|
||||
return jwt.sign(
|
||||
{
|
||||
provider,
|
||||
providerId: profile.id,
|
||||
email: profile.emails && profile.emails[0] ? profile.emails[0].value : null,
|
||||
displayName: profile.displayName || profile.name || null,
|
||||
type: 'oauth-pending'
|
||||
},
|
||||
this.jwtSecret,
|
||||
{ expiresIn: '15m' }
|
||||
);
|
||||
}
|
||||
|
||||
verifyPendingToken(pendingToken) {
|
||||
const data = jwt.verify(pendingToken, this.jwtSecret);
|
||||
if (data.type !== 'oauth-pending' || !data.provider || !data.providerId) {
|
||||
throw new Error('Ungültiger OAuth-Verknüpfungstoken');
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
createStateToken(data) {
|
||||
return jwt.sign({ ...data, type: 'oauth-state' }, this.jwtSecret, { expiresIn: '15m' });
|
||||
}
|
||||
|
||||
verifyStateToken(stateToken) {
|
||||
if (!stateToken) return {};
|
||||
const data = jwt.verify(stateToken, this.jwtSecret);
|
||||
if (data.type !== 'oauth-state') {
|
||||
throw new Error('Ungültiger OAuth-State');
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
async linkPendingToPasswordAccount(pendingToken, email, password) {
|
||||
const authService = require('./AuthService');
|
||||
const pending = this.verifyPendingToken(pendingToken);
|
||||
const loginResult = await authService.login(email, password, '0');
|
||||
const profile = {
|
||||
id: pending.providerId,
|
||||
displayName: pending.displayName || loginResult.user.full_name,
|
||||
emails: pending.email ? [{ value: pending.email }] : []
|
||||
};
|
||||
return this.completeOAuthLogin(profile, pending.provider, { linkUserId: loginResult.user.id });
|
||||
}
|
||||
|
||||
async linkPendingToAuthenticatedUser(pendingToken, userId) {
|
||||
const pending = this.verifyPendingToken(pendingToken);
|
||||
const profile = {
|
||||
id: pending.providerId,
|
||||
displayName: pending.displayName || null,
|
||||
emails: pending.email ? [{ value: pending.email }] : []
|
||||
};
|
||||
return this.completeOAuthLogin(profile, pending.provider, { linkUserId: userId });
|
||||
}
|
||||
|
||||
createLoginResult(user, authInfo, provider) {
|
||||
const { AuthToken } = database.getModels();
|
||||
|
||||
// JWT Token generieren
|
||||
const token = jwt.sign(
|
||||
{
|
||||
@@ -129,14 +215,12 @@ class OAuthService {
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setHours(expiresAt.getHours() + 24);
|
||||
|
||||
await AuthToken.create({
|
||||
return AuthToken.create({
|
||||
auth_info_id: authInfo.id,
|
||||
value: crypto.createHash('sha256').update(token).digest('hex'),
|
||||
expires: expiresAt,
|
||||
version: 0
|
||||
});
|
||||
|
||||
return {
|
||||
}).then(() => ({
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
@@ -145,7 +229,7 @@ class OAuthService {
|
||||
role: user.role,
|
||||
provider
|
||||
}
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -205,5 +289,3 @@ class OAuthService {
|
||||
|
||||
module.exports = new OAuthService();
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user