Add OAuth integration for multiple providers and implement user linking
Some checks failed
Deploy to production / deploy (push) Failing after 49s

- Created OAuth credentials setup guide for Google, Microsoft, Keycloak, ORY, and ZITADEL.
- Added migration for oauth_identity table to store OAuth identities linked to users.
- Implemented OAuthIdentity model for managing OAuth identities in the database.
- Developed oauthService to handle OAuth login, user creation, and identity linking.
- Created OAuthCallbackView and OAuthUserCallbackView components for handling OAuth responses in the frontend.
- Added error handling and user feedback during the OAuth process.
This commit is contained in:
Torsten Schulz (local)
2026-05-15 13:59:40 +02:00
parent 464208e30e
commit ac57931928
16 changed files with 7620 additions and 949 deletions

View File

@@ -1,4 +1,5 @@
import * as userService from '../services/authService.js';
import * as oauthService from '../services/oauthService.js';
class AuthController {
constructor() {
@@ -7,6 +8,13 @@ class AuthController {
this.forgotPassword = this.forgotPassword.bind(this);
this.activateAccount = this.activateAccount.bind(this);
this.logout = this.logout.bind(this);
this.oauthProviders = this.oauthProviders.bind(this);
this.oauthStart = this.oauthStart.bind(this);
this.oauthExchange = this.oauthExchange.bind(this);
this.oauthUserIdentities = this.oauthUserIdentities.bind(this);
this.oauthUserStart = this.oauthUserStart.bind(this);
this.oauthUserExchange = this.oauthUserExchange.bind(this);
this.oauthUserRemove = this.oauthUserRemove.bind(this);
}
async register(req, res) {
@@ -43,6 +51,133 @@ class AuthController {
res.status(200).json({ result: 'loggedout' });
}
async oauthProviders(req, res) {
try {
const providers = await oauthService.getOAuthProviders();
res.status(200).json({ providers });
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async oauthStart(req, res) {
const { provider } = req.params;
try {
const redirectTo = await oauthService.startOAuthLogin({ providerSlug: provider });
res.redirect(302, redirectTo.toString());
} catch (error) {
const status = error.message === 'providernotconfigured' ? 503 : 500;
res.status(status).json({ error: error.message });
}
}
async oauthExchange(req, res) {
const { code, state } = req.body;
try {
const result = await oauthService.exchangeOAuthLogin({ code, state });
res.status(200).json(result);
} catch (error) {
const knownErrors = new Set([
'oauthcodemissing',
'oauthstatemissing',
'oauthsubjectmissing',
'providernotconfigured',
'oauthidentityconflict'
]);
const status = knownErrors.has(error.message) ? 400 : 500;
res.status(status).json({ error: error.message });
}
}
async oauthUserIdentities(req, res) {
const { userid: hashedUserId } = req.headers;
try {
const User = (await import('../models/community/user.js')).default;
const user = await User.findOne({ where: { hashedId: hashedUserId } });
if (!user) {
return res.status(404).json({ error: 'usernotfound' });
}
const identities = await oauthService.getUserOAuthIdentities(user.id);
res.status(200).json({ identities });
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async oauthUserStart(req, res) {
const { userid: hashedUserId } = req.headers;
const { provider } = req.params;
try {
const User = (await import('../models/community/user.js')).default;
const user = await User.findOne({ where: { hashedId: hashedUserId } });
if (!user) {
return res.status(404).json({ error: 'usernotfound' });
}
const redirectTo = await oauthService.startOAuthLoginForUser({
userId: user.id,
providerSlug: provider
});
res.redirect(302, redirectTo.toString());
} catch (error) {
const status = error.message === 'providernotconfigured' ? 503 : 500;
res.status(status).json({ error: error.message });
}
}
async oauthUserExchange(req, res) {
const { userid: hashedUserId } = req.headers;
const { code, state } = req.body;
try {
const User = (await import('../models/community/user.js')).default;
const user = await User.findOne({ where: { hashedId: hashedUserId } });
if (!user) {
return res.status(404).json({ error: 'usernotfound' });
}
const result = await oauthService.exchangeOAuthLoginForUser({
userId: user.id,
code,
state
});
res.status(200).json(result);
} catch (error) {
const knownErrors = new Set([
'oauthcodemissing',
'oauthstatemissing',
'oauthsubjectmissing',
'providernotconfigured',
'oauthuseridmismatch',
'oauthidentityalreadylinked'
]);
const status = knownErrors.has(error.message) ? 400 : 500;
res.status(status).json({ error: error.message });
}
}
async oauthUserRemove(req, res) {
const { userid: hashedUserId } = req.headers;
const { identityId } = req.params;
try {
const User = (await import('../models/community/user.js')).default;
const user = await User.findOne({ where: { hashedId: hashedUserId } });
if (!user) {
return res.status(404).json({ error: 'usernotfound' });
}
const result = await oauthService.removeOAuthIdentity({
userId: user.id,
identityId: parseInt(identityId, 10)
});
res.status(200).json(result);
} catch (error) {
const status = error.message === 'forbidden' ? 403 : 500;
res.status(status).json({ error: error.message });
}
}
async forgotPassword(req, res) {
const { email } = req.body;
try {