feat(auth): secure messenger start token flow
This commit is contained in:
103
src/server.js
103
src/server.js
@@ -8,7 +8,14 @@ import express from 'express';
|
||||
import { ApolloServer } from '@apollo/server';
|
||||
import { expressMiddleware } from '@as-integrations/express5';
|
||||
|
||||
import { issueTemporaryLoginToken } from './auth.js';
|
||||
import {
|
||||
createMessengerStartSession,
|
||||
consumeMessengerStartSession,
|
||||
extractAuthTokenFromRequest,
|
||||
hasMessengerStartSession,
|
||||
issueTemporaryLoginToken,
|
||||
verifyAccessToken,
|
||||
} from './auth.js';
|
||||
import { buildContext } from './context.js';
|
||||
import { sendMessengerMessage } from './messenger.js';
|
||||
import { prisma } from './prisma-client.js';
|
||||
@@ -73,6 +80,49 @@ async function resolveUserForMessenger({ userId, email }) {
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveAuthenticatedUserFromRequest(req) {
|
||||
const authToken = extractAuthTokenFromRequest(req);
|
||||
const auth = verifyAccessToken(authToken);
|
||||
if (!auth?.userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return prisma.user.findUnique({
|
||||
where: { id: auth.userId },
|
||||
});
|
||||
}
|
||||
|
||||
app.post('/auth/messenger-start', async (req, res) => {
|
||||
const channel = String(req.body?.channel || '').toUpperCase();
|
||||
if (channel !== 'TELEGRAM' && channel !== 'MAX') {
|
||||
res.status(400).json({ error: 'Unsupported channel.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const authenticatedUser = await resolveAuthenticatedUserFromRequest(req);
|
||||
const providedEmail = String(req.body?.email || '').trim().toLowerCase();
|
||||
const email = authenticatedUser?.email?.trim().toLowerCase() || providedEmail;
|
||||
const userId = authenticatedUser?.id ?? null;
|
||||
|
||||
if (!userId && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
res.status(400).json({ error: 'A valid email is required.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const session = createMessengerStartSession({
|
||||
channel,
|
||||
email,
|
||||
userId,
|
||||
});
|
||||
|
||||
res.json({
|
||||
ok: true,
|
||||
startToken: session.startToken,
|
||||
expiresAt: session.expiresAt.toISOString(),
|
||||
mode: userId ? 'connect' : 'login',
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/bot/messenger-login', async (req, res) => {
|
||||
const webhookToken = process.env.BOT_LOGIN_WEBHOOK_TOKEN;
|
||||
const providedToken = req.body?.token;
|
||||
@@ -87,18 +137,29 @@ app.post('/bot/messenger-login', async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = String(req.body?.userId || '').trim();
|
||||
const email = String(req.body?.email || '').trim().toLowerCase();
|
||||
const startToken = String(req.body?.startToken || '').trim();
|
||||
const channelId = String(req.body?.channelId || '').trim();
|
||||
const skipDispatch = req.body?.skipDispatch === true;
|
||||
if (!channelId || (!userId && !email)) {
|
||||
res.status(400).json({ error: 'channelId and (userId or email) are required.' });
|
||||
if (!channelId || !startToken) {
|
||||
res.status(400).json({ error: 'channelId and startToken are required.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasMessengerStartSession(startToken)) {
|
||||
res.status(400).json({ error: 'Messenger start token is invalid or expired.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const startSession = consumeMessengerStartSession(startToken);
|
||||
|
||||
if (startSession.channel !== channel) {
|
||||
res.status(400).json({ error: 'Start token channel mismatch.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await resolveUserForMessenger({
|
||||
userId,
|
||||
email,
|
||||
userId: startSession.userId,
|
||||
email: startSession.email,
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
@@ -106,34 +167,13 @@ app.post('/bot/messenger-login', async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.messengerConnection.updateMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
type: channel,
|
||||
isActive: true,
|
||||
NOT: { channelId },
|
||||
},
|
||||
data: { isActive: false },
|
||||
});
|
||||
|
||||
await prisma.messengerConnection.upsert({
|
||||
where: {
|
||||
userId_type_channelId: {
|
||||
userId: user.id,
|
||||
type: channel,
|
||||
channelId,
|
||||
},
|
||||
},
|
||||
update: { isActive: true },
|
||||
create: {
|
||||
userId: user.id,
|
||||
const login = issueTemporaryLoginToken({
|
||||
userId: user.id,
|
||||
messengerConnection: {
|
||||
type: channel,
|
||||
channelId,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
const login = issueTemporaryLoginToken(user.id);
|
||||
const frontendUrl = (
|
||||
process.env.WEB_FRONTEND_URL ||
|
||||
process.env.NUXT_PUBLIC_SITE_URL ||
|
||||
@@ -158,6 +198,7 @@ app.post('/bot/messenger-login', async (req, res) => {
|
||||
|
||||
res.json({
|
||||
ok: true,
|
||||
mode: startSession.userId ? 'connect' : 'login',
|
||||
loginUrl,
|
||||
expiresAt: login.expiresAt.toISOString(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user