Skip to content
Open
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
16 changes: 16 additions & 0 deletions backend/models/share.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const mongoose = require('mongoose');

const shareSchema = new mongoose.Schema({
token: { type: String, required: true, unique: true },
repo: { type: String, required: true },
branch: { type: String, default: 'main' },
owner: { type: String, required: true },
ownerToken: { type: String, required: true },
createdAt: { type: Date, default: Date.now },
expiresAt: { type: Date, required: true }
});

// TTL index → MongoDB auto-deletes expired docs
shareSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });

module.exports = mongoose.model('Share', shareSchema);
65 changes: 62 additions & 3 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"express": "^5.1.0",
"express-session": "^1.18.1",
"mongodb": "^6.17.0",
"mongoose": "^8.18.3",
"passport": "^0.7.0",
"passport-github": "^1.1.0",
"uuid": "^11.1.0"
Expand Down
102 changes: 69 additions & 33 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const GitHubStrategy = require('passport-github').Strategy;
const { Octokit } = require('@octokit/rest');
const cors = require('cors');
const crypto = require('crypto');
const Share = require('./models/share');

const app = express();
const MongoStore = require('connect-mongo');
Expand All @@ -19,7 +20,20 @@ for (const envVar of requiredEnvVars) {
}
}

// Middleware
// MongoDB connection
const mongoose = require('mongoose');

mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log("MongoDB connected"))
.catch(err => {
console.error("MongoDB connection error:", err);
process.exit(1);
});

// Middleware for parsing JSON and URL-encoded data
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cors({
Expand All @@ -30,7 +44,7 @@ app.use(cors({
exposedHeaders: ['set-cookie']
}));

// Session configuration
// Session configuration with MongoDB store
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
Expand All @@ -56,7 +70,7 @@ app.use(session({
app.use(passport.initialize());
app.use(passport.session());

// GitHub OAuth Strategy
// GitHub OAuth Strategy configuration
passport.use(new GitHubStrategy({
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
Expand All @@ -75,28 +89,31 @@ passport.use(new GitHubStrategy({
});
}));

// Passport serialization
// Passport serialization and deserialization
passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((user, done) => done(null, user));

// In-memory store for shares
const shares = new Map();


// Trust proxy in production
if (process.env.NODE_ENV === 'production') {
app.set('trust proxy', 1);
}

// Routes
// ROUTES
// GitHub OAuth routes
app.get('/auth/github', passport.authenticate('github'));

// GitHub OAuth callback
app.get('/auth/github/callback',
passport.authenticate('github', { failureRedirect: `${process.env.FRONTEND_URL}/login` }),
(req, res) => {
res.redirect(`${process.env.FRONTEND_URL}/`);
}
);


// Get current user info
app.get('/api/user', (req, res) => {
if (!req.isAuthenticated()) {
return res.status(200).json({ authenticated: false });
Expand All @@ -109,6 +126,8 @@ app.get('/api/user', (req, res) => {
});
});


// List user repositories
app.get('/api/repos', async (req, res) => {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: 'Unauthorized' });
Expand All @@ -128,50 +147,64 @@ app.get('/api/repos', async (req, res) => {
}
});

// Create a shareable link stored in MongoDB
app.post('/api/share', async (req, res) => {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: 'Unauthorized' });
}

const { repo, branch = 'main', expiresInHours = 24 } = req.body;
const { repo, branch = 'main', expiresInHours, expiresAt: clientExpiresAt } = req.body;
if (!repo || !repo.includes('/')) {
return res.status(400).json({ error: 'Invalid repository format' });
}

const shareToken = crypto.randomBytes(32).toString('hex');
const expiresAt = new Date(Date.now() + expiresInHours * 60 * 60 * 1000);

shares.set(shareToken, {
repo,
branch,
expiresAt,
owner: req.user.profile.id,
ownerToken: req.user.accessToken,
createdAt: new Date()
});

res.json({
shareToken,
shareLink: `${process.env.FRONTEND_URL}/share/${shareToken}`,
expiresAt: expiresAt.toISOString()
});
});

app.get('/api/repo-content/:token', async (req, res) => {
const share = shares.get(req.params.token);
if (!share) {
return res.status(404).json({ error: 'Share link not found' });
let expiresAt;
if (clientExpiresAt) {
// Use frontend-provided expiry datetime
expiresAt = new Date(clientExpiresAt);
} else {
// Default to hours-based expiry
const hours = Number(expiresInHours) || 24;
expiresAt = new Date(Date.now() + hours * 60 * 60 * 1000);
}

if (share.expiresAt < new Date()) {
shares.delete(req.params.token);
return res.status(410).json({ error: 'Share link expired' });
try {
const share = new Share({
token: shareToken,
repo,
branch,
owner: req.user.profile.id,
ownerToken: req.user.accessToken,
expiresAt
});

await share.save();

res.json({
shareToken,
shareLink: `${process.env.FRONTEND_URL}/share/${shareToken}`,
expiresAt: expiresAt.toISOString()
});
} catch (err) {
console.error("Error saving share:", err);
res.status(500).json({ error: 'Failed to create share' });
}
});


// Fetch repository content via share token stored in MongoDB
app.get('/api/repo-content/:token', async (req, res) => {
try {
const share = await Share.findOne({ token: req.params.token });
if (!share || share.expiresAt < new Date()) {
return res.status(410).json({ error: 'Share link expired' });
}

const [owner, repo] = share.repo.split('/');
const octokit = new Octokit({ auth: share.ownerToken });

const { data } = await octokit.repos.getContent({
owner,
repo,
Expand All @@ -186,6 +219,8 @@ app.get('/api/repo-content/:token', async (req, res) => {
}
});


// List branches of a repository
app.get('/api/repos/:owner/:repo/branches', async (req, res) => {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: 'Unauthorized' });
Expand All @@ -208,6 +243,7 @@ app.get('/api/repos/:owner/:repo/branches', async (req, res) => {
}
});

// Logout route
app.get('/logout', (req, res) => {
req.logout(() => {
req.session.destroy(() => {
Expand Down
Loading