Files
Emailsorter/server/routes/oauth.mjs
ANDJ abf761db07 Email Sorter Beta
Ich habe soweit automatisiert the Emails sortieren aber ich muss noch schauen was es fur bugs es gibt wenn die app online  ist deswegen wurde ich mit diesen Commit die website veroffentlichen obwohjl es sein konnte  das es noch nicht fertig ist und verkaufs bereit
2026-01-22 19:32:12 +01:00

389 lines
12 KiB
JavaScript

/**
* OAuth Routes
* Gmail and Outlook OAuth2 authentication
*/
import express from 'express'
import { OAuth2Client } from 'google-auth-library'
import { ConfidentialClientApplication } from '@azure/msal-node'
import { asyncHandler, ValidationError, AppError } from '../middleware/errorHandler.mjs'
import { respond } from '../utils/response.mjs'
import { emailAccounts } from '../services/database.mjs'
import { config, features } from '../config/index.mjs'
import { log } from '../middleware/logger.mjs'
const router = express.Router()
// Google OAuth client (lazy initialization)
let googleClient = null
function getGoogleClient() {
if (!googleClient && features.gmail()) {
googleClient = new OAuth2Client(
config.google.clientId,
config.google.clientSecret,
config.google.redirectUri
)
}
return googleClient
}
// Microsoft OAuth client (lazy initialization)
let msalClient = null
function getMsalClient() {
if (!msalClient && features.outlook()) {
msalClient = new ConfidentialClientApplication({
auth: {
clientId: config.microsoft.clientId,
clientSecret: config.microsoft.clientSecret,
authority: 'https://login.microsoftonline.com/common',
},
})
}
return msalClient
}
/**
* Gmail OAuth scopes
*/
const GMAIL_SCOPES = [
'https://www.googleapis.com/auth/gmail.modify',
'https://www.googleapis.com/auth/gmail.labels',
'https://www.googleapis.com/auth/userinfo.email',
]
/**
* Microsoft Graph scopes
*/
const OUTLOOK_SCOPES = [
'Mail.ReadWrite',
'User.Read',
'offline_access',
]
// ═══════════════════════════════════════════════════════════════════════════
// GMAIL OAUTH
// ═══════════════════════════════════════════════════════════════════════════
/**
* GET /api/oauth/gmail/connect
* Initiate Gmail OAuth flow
*/
router.get('/gmail/connect', asyncHandler(async (req, res) => {
const { userId } = req.query
if (!userId) {
throw new ValidationError('userId ist erforderlich')
}
if (!features.gmail()) {
throw new AppError('Gmail OAuth ist nicht konfiguriert', 503, 'FEATURE_DISABLED')
}
const client = getGoogleClient()
const authUrl = client.generateAuthUrl({
access_type: 'offline',
scope: GMAIL_SCOPES,
prompt: 'consent',
state: JSON.stringify({ userId }),
include_granted_scopes: true,
})
respond.success(res, { url: authUrl })
}))
/**
* GET /api/oauth/gmail/callback
* Gmail OAuth callback
*/
router.get('/gmail/callback', asyncHandler(async (req, res) => {
const { code, state, error, error_description } = req.query
log.info('Gmail OAuth Callback erhalten', {
hasCode: !!code,
hasState: !!state,
error: error || 'none'
})
if (error) {
log.warn('Gmail OAuth abgelehnt', { error, error_description })
return res.redirect(`${config.frontendUrl}/settings?error=oauth_denied&message=${encodeURIComponent(error_description || error)}`)
}
if (!code || !state) {
log.error('Gmail OAuth: Code oder State fehlt')
return res.redirect(`${config.frontendUrl}/settings?error=missing_params`)
}
let userId
try {
const stateData = JSON.parse(state)
userId = stateData.userId
} catch (e) {
log.error('Gmail OAuth: State konnte nicht geparst werden', { state })
return res.redirect(`${config.frontendUrl}/settings?error=invalid_state`)
}
// Create a FRESH OAuth client for this request
const client = new OAuth2Client(
config.google.clientId,
config.google.clientSecret,
config.google.redirectUri
)
try {
log.info('Gmail OAuth: Tausche Code gegen Token...')
// Exchange code for tokens
const { tokens } = await client.getToken(code)
log.info('Gmail OAuth: Token erhalten', {
hasAccessToken: !!tokens.access_token,
hasRefreshToken: !!tokens.refresh_token,
expiresIn: tokens.expiry_date
})
client.setCredentials(tokens)
// Get user email
const { google } = await import('googleapis')
const oauth2 = google.oauth2({ version: 'v2', auth: client })
const { data: userInfo } = await oauth2.userinfo.get()
log.info('Gmail OAuth: User Info erhalten', { email: userInfo.email })
// Check if account already exists
const existingAccounts = await emailAccounts.getByUser(userId)
const alreadyConnected = existingAccounts.find(a => a.email === userInfo.email)
if (alreadyConnected) {
// Update existing account with new tokens
const { db, Collections } = await import('../services/database.mjs')
await db.update(Collections.EMAIL_ACCOUNTS, alreadyConnected.$id, {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token || alreadyConnected.refreshToken,
expiresAt: tokens.expiry_date || 0,
isActive: true,
})
log.success(`Gmail aktualisiert: ${userInfo.email}`)
} else {
// Save new account to database
await emailAccounts.create({
userId,
provider: 'gmail',
email: userInfo.email,
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token || '',
expiresAt: tokens.expiry_date || 0,
isActive: true,
})
log.success(`Gmail verbunden: ${userInfo.email}`)
}
res.redirect(`${config.frontendUrl}/settings?gmail=connected&email=${encodeURIComponent(userInfo.email)}`)
} catch (tokenError) {
log.error('Gmail OAuth Token-Fehler', {
error: tokenError.message,
code: tokenError.code,
response: tokenError.response?.data
})
// Provide more specific error messages
let errorMessage = 'token_error'
if (tokenError.message.includes('invalid_grant')) {
errorMessage = 'invalid_grant'
} else if (tokenError.message.includes('invalid_client')) {
errorMessage = 'invalid_client'
}
res.redirect(`${config.frontendUrl}/settings?error=${errorMessage}&details=${encodeURIComponent(tokenError.message)}`)
}
}))
/**
* POST /api/oauth/gmail/refresh
* Refresh Gmail access token
*/
router.post('/gmail/refresh', asyncHandler(async (req, res) => {
const { accountId } = req.body
if (!accountId) {
throw new ValidationError('accountId ist erforderlich')
}
const account = await emailAccounts.get(accountId)
if (account.provider !== 'gmail') {
throw new ValidationError('Kein Gmail-Konto')
}
if (!account.refreshToken) {
throw new AppError('Kein Refresh Token verfügbar. Konto erneut verbinden.', 400, 'NO_REFRESH_TOKEN')
}
const client = getGoogleClient()
client.setCredentials({ refresh_token: account.refreshToken })
const { credentials } = await client.refreshAccessToken()
// Update tokens
const { db, Collections } = await import('../services/database.mjs')
await db.update(Collections.EMAIL_ACCOUNTS, accountId, {
accessToken: credentials.access_token,
expiresAt: credentials.expiry_date || 0,
})
respond.success(res, {
accessToken: credentials.access_token,
expiresAt: credentials.expiry_date,
})
}))
// ═══════════════════════════════════════════════════════════════════════════
// OUTLOOK OAUTH
// ═══════════════════════════════════════════════════════════════════════════
/**
* GET /api/oauth/outlook/connect
* Initiate Outlook OAuth flow
*/
router.get('/outlook/connect', asyncHandler(async (req, res) => {
const { userId } = req.query
if (!userId) {
throw new ValidationError('userId ist erforderlich')
}
if (!features.outlook()) {
throw new AppError('Outlook OAuth ist nicht konfiguriert', 503, 'FEATURE_DISABLED')
}
const client = getMsalClient()
const authUrl = await client.getAuthCodeUrl({
scopes: OUTLOOK_SCOPES,
redirectUri: config.microsoft.redirectUri,
state: JSON.stringify({ userId }),
prompt: 'select_account',
})
respond.success(res, { url: authUrl })
}))
/**
* GET /api/oauth/outlook/callback
* Outlook OAuth callback
*/
router.get('/outlook/callback', asyncHandler(async (req, res) => {
const { code, state, error, error_description } = req.query
if (error) {
log.warn('Outlook OAuth abgelehnt', { error, error_description })
return respond.redirect(res, `${config.frontendUrl}/settings?error=oauth_denied`)
}
if (!code || !state) {
throw new ValidationError('Code und State sind erforderlich')
}
const { userId } = JSON.parse(state)
const client = getMsalClient()
// Exchange code for tokens
const tokenResponse = await client.acquireTokenByCode({
code,
scopes: OUTLOOK_SCOPES,
redirectUri: config.microsoft.redirectUri,
})
// Get user email from Graph API
const graphResponse = await fetch('https://graph.microsoft.com/v1.0/me', {
headers: {
Authorization: `Bearer ${tokenResponse.accessToken}`,
},
})
const userInfo = await graphResponse.json()
// Save to database
await emailAccounts.create({
userId,
provider: 'outlook',
email: userInfo.mail || userInfo.userPrincipalName,
accessToken: tokenResponse.accessToken,
refreshToken: '', // MSAL handles token caching differently
expiresAt: tokenResponse.expiresOn ? new Date(tokenResponse.expiresOn).getTime() : 0,
isActive: true,
})
log.success(`Outlook verbunden: ${userInfo.mail || userInfo.userPrincipalName}`)
respond.redirect(res, `${config.frontendUrl}/settings?outlook=connected`)
}))
/**
* POST /api/oauth/outlook/refresh
* Refresh Outlook access token
*/
router.post('/outlook/refresh', asyncHandler(async (req, res) => {
const { accountId } = req.body
if (!accountId) {
throw new ValidationError('accountId ist erforderlich')
}
const account = await emailAccounts.get(accountId)
if (account.provider !== 'outlook') {
throw new ValidationError('Kein Outlook-Konto')
}
const client = getMsalClient()
// MSAL handles token refresh silently via cache
const tokenResponse = await client.acquireTokenSilent({
scopes: OUTLOOK_SCOPES,
account: {
homeAccountId: account.email,
},
}).catch(async () => {
// If silent refresh fails, need to re-authenticate
throw new AppError('Token abgelaufen. Konto erneut verbinden.', 401, 'TOKEN_EXPIRED')
})
// Update tokens
const { db, Collections } = await import('../services/database.mjs')
await db.update(Collections.EMAIL_ACCOUNTS, accountId, {
accessToken: tokenResponse.accessToken,
expiresAt: tokenResponse.expiresOn ? new Date(tokenResponse.expiresOn).getTime() : 0,
})
respond.success(res, {
accessToken: tokenResponse.accessToken,
expiresAt: tokenResponse.expiresOn,
})
}))
// ═══════════════════════════════════════════════════════════════════════════
// STATUS CHECK
// ═══════════════════════════════════════════════════════════════════════════
/**
* GET /api/oauth/status
* Check which OAuth providers are configured
*/
router.get('/status', (req, res) => {
respond.success(res, {
gmail: {
enabled: features.gmail(),
scopes: features.gmail() ? GMAIL_SCOPES : [],
},
outlook: {
enabled: features.outlook(),
scopes: features.outlook() ? OUTLOOK_SCOPES : [],
},
})
})
export default router