Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/database/database.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,18 @@ export const client = new MongoClient(url);
* @property {Date} updatedAt
*/

/**
* @typedef {object} Session
* @property {string} _id - Session ID (prefixed with "sess:")
* @property {string | object} session - Session data (JSON string or object)
* @property {Date} expires - Session expiration date
*/

/**
* @typedef {object} DatabaseConnection
* @property {mongo.Collection<User>} users
* @property {mongo.Collection<Repository>} repositories
* @property {mongo.Collection<Session>} sessions
*/

/**
Expand Down Expand Up @@ -73,6 +81,13 @@ class Database {
get repositories() {
return this.getCollection('repositories');
}

/**
* @return {mongo.Collection<Session>}
*/
get sessions() {
return this.getCollection('sessions');
}
}

export const database = new Database();
Expand Down
52 changes: 52 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
import { removePatAuthentication } from '../scripts/remove-pat-auth.js';
import { removeConfiguredField } from '../scripts/remove-configured-field.js';
import { migrateDatabase as migrateUserGithubIds } from '../scripts/migrate-user-github-ids.js';
import { sessionAuthMiddleware } from './middleware/sessionAuth.js';

const mongoSessionStore = MongoStore.create({
clientPromise: client.connect(),
Expand Down Expand Up @@ -104,6 +105,9 @@ async function startServer() {
app.use(express.json());
app.use(session(sess));

// Session authentication middleware (supports both cookies and Authorization header)
app.use(sessionAuthMiddleware);

// Static file serving
app.use(express.static('./static'));
if (isProduction) {
Expand Down Expand Up @@ -217,6 +221,54 @@ async function startServer() {
}
});

// API Authentication endpoints (for webapp compatibility)
app.get('/api/auth/status', async function (req, res) {
if (!req.session.userId) {
return res.status(401).json({ authenticated: false });
}

const user = await User.findById(req.session.userId);
if (!user) {
return res.status(401).json({ authenticated: false });
}

try {
const response = await fetch('https://api.github.com/user', {
headers: {
Accept: 'application/vnd.github.v3+json',
Authorization: `token ${user.githubAccessToken}`,
},
});

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

const userData = await response.json();
res.json({
authenticated: true,
user: {
name: userData.name,
login: userData.login,
avatarUrl: userData.avatar_url,
},
});
} catch (e) {
console.error('Failed to fetch user data:', e);
res.status(503).json({ error: 'Failed to fetch user data' });
}
});

app.post('/api/auth/logout', function (req, res) {
req.session.destroy(err => {
if (err) {
console.error('Session destruction error:', err);
return res.status(500).json({ error: 'Failed to logout' });
}
res.json({ success: true });
});
});

app.get('/v1/user', async function (req, res) {
if (!req.session.userId) {
return res.status(401).end();
Expand Down
79 changes: 79 additions & 0 deletions src/middleware/sessionAuth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Session Authentication Middleware
*
* Supports both cookie-based and header-based session authentication:
* 1. Cookie-based: Traditional express-session (current frontend)
* 2. Header-based: Authorization: SESSION <sessionId> (webapp proxy)
*
* This allows the webapp proxy to send session IDs via headers while
* maintaining backward compatibility with cookie-based authentication.
*/

import { database } from '../database/database.js';

/**
* Middleware to extract session ID from Authorization header
* and populate req.session.userId for existing authentication logic
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @param {Function} next - Express next middleware function
*/
export async function sessionAuthMiddleware(req, res, next) {
// Check if Authorization header contains a session ID
const authHeader = req.headers.authorization;

// If no Authorization header or session already set via cookie, continue
if (!authHeader || req.session.userId) {
return next();
}

// Parse Authorization header format: "SESSION <sessionId>"
const parts = authHeader.split(' ');
if (parts.length !== 2 || parts[0] !== 'SESSION') {
// Invalid format, continue without setting session
return next();
}

const sessionId = parts[1];

try {
// Query MongoDB session store to get session data
// Session ID in MongoDB is prefixed with "sess:"
const sessionDoc = await database.sessions.findOne({
_id: `sess:${sessionId}`,
});

if (!sessionDoc || !sessionDoc.session) {
// Session not found or invalid
console.warn(`Invalid session ID in Authorization header: ${sessionId}`);
return next();
}

// Parse session data (stored as JSON string in MongoDB)
const sessionData =
typeof sessionDoc.session === 'string'
? JSON.parse(sessionDoc.session)
: sessionDoc.session;

// Check if session has expired
if (sessionDoc.expires && new Date(sessionDoc.expires) < new Date()) {
console.warn(`Expired session ID in Authorization header: ${sessionId}`);
return next();
}

// Extract userId from session data and set it on req.session
if (sessionData.userId) {
req.session.userId = sessionData.userId;
console.log(
`Authenticated via Authorization header: userId=${sessionData.userId}`
);
}

next();
} catch (error) {
console.error('Error processing session from Authorization header:', error);
// Don't fail the request, just continue without authentication
next();
}
}