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
This commit is contained in:
388
server/routes/oauth.mjs
Normal file
388
server/routes/oauth.mjs
Normal file
@@ -0,0 +1,388 @@
|
||||
/**
|
||||
* 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
|
||||
Reference in New Issue
Block a user