Skip to content

Commit 41dfc5e

Browse files
authored
Merge pull request #582 from coderolisa/feature/secure-sharing
Implement secure link sharing backend
2 parents 479b466 + f9d49d9 commit 41dfc5e

6 files changed

Lines changed: 162 additions & 0 deletions

File tree

backend/index.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const express = require('express');
2+
const mongoose = require('mongoose');
3+
const shareRoutes = require('./shareRoutes');
4+
5+
const app = express();
6+
7+
app.use(express.json());
8+
app.use('/', shareRoutes);
9+
10+
mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/shares');
11+
12+
const PORT = process.env.PORT || 3000;
13+
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

backend/models/AccessLog.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const mongoose = require('mongoose');
2+
3+
const accessLogSchema = new mongoose.Schema({
4+
tokenHash: { type: String, required: true, index: true },
5+
ip: { type: String, required: true },
6+
timestamp: { type: Date, default: Date.now },
7+
success: { type: Boolean, required: true }
8+
});
9+
10+
module.exports = mongoose.model('AccessLog', accessLogSchema);

backend/models/Share.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const mongoose = require('mongoose');
2+
3+
const shareSchema = new mongoose.Schema({
4+
tokenHash: { type: String, required: true, unique: true, index: true },
5+
resourceId: { type: String, required: true },
6+
ownerId: { type: String, required: true },
7+
expiresAt: { type: Date, required: true },
8+
revoked: { type: Boolean, default: false },
9+
createdAt: { type: Date, default: Date.now }
10+
});
11+
12+
module.exports = mongoose.model('Share', shareSchema);

backend/package.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "secure-sharing-backend",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"scripts": {
6+
"start": "node index.js"
7+
},
8+
"dependencies": {
9+
"express": "^4.18.2",
10+
"mongoose": "^7.5.0"
11+
}
12+
}

backend/shareRoutes.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
const express = require('express');
2+
const router = express.Router();
3+
const shareService = require('./shareService');
4+
5+
// Placeholder auth middleware - replace with actual auth
6+
const auth = (req, res, next) => {
7+
req.user = { id: 'testOwner' }; // Replace with actual user from auth
8+
next();
9+
};
10+
11+
router.post('/share', auth, async (req, res) => {
12+
try {
13+
const { resourceId, expiresAt } = req.body;
14+
const ownerId = req.user.id;
15+
const result = await shareService.createShareLink({ resourceId, ownerId, expiresAt });
16+
res.json(result);
17+
} catch (error) {
18+
res.status(400).json({ error: error.message });
19+
}
20+
});
21+
22+
router.get('/share/:token', async (req, res) => {
23+
try {
24+
const { token } = req.params;
25+
const ip = req.ip || req.connection.remoteAddress;
26+
const resource = await shareService.getSharedResource(token, ip);
27+
res.json(resource);
28+
} catch (error) {
29+
res.status(403).json({ error: error.message });
30+
}
31+
});
32+
33+
router.delete('/share/:token', auth, async (req, res) => {
34+
try {
35+
const { token } = req.params;
36+
await shareService.revokeShare(token);
37+
res.json({ message: 'Revoked' });
38+
} catch (error) {
39+
res.status(400).json({ error: error.message });
40+
}
41+
});
42+
43+
router.get('/share/:token/logs', auth, async (req, res) => {
44+
try {
45+
const { token } = req.params;
46+
const logs = await shareService.getAccessLogs(token);
47+
res.json(logs);
48+
} catch (error) {
49+
res.status(400).json({ error: error.message });
50+
}
51+
});
52+
53+
module.exports = router;

backend/shareService.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
const crypto = require('crypto');
2+
const Share = require('./models/Share');
3+
const AccessLog = require('./models/AccessLog');
4+
5+
async function createShareLink({ resourceId, ownerId, expiresAt }) {
6+
const token = crypto.randomBytes(32).toString('hex');
7+
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
8+
const share = new Share({
9+
tokenHash,
10+
resourceId,
11+
ownerId,
12+
expiresAt: new Date(expiresAt),
13+
revoked: false
14+
});
15+
await share.save();
16+
return { token, url: `/share/${token}` };
17+
}
18+
19+
async function getSharedResource(token, ip) {
20+
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
21+
const share = await Share.findOne({ tokenHash });
22+
if (!share) {
23+
await logAccess(tokenHash, ip, false);
24+
throw new Error('Invalid token');
25+
}
26+
if (share.revoked) {
27+
await logAccess(tokenHash, ip, false);
28+
throw new Error('Link revoked');
29+
}
30+
if (new Date() > share.expiresAt) {
31+
await logAccess(tokenHash, ip, false);
32+
throw new Error('Link expired');
33+
}
34+
await logAccess(tokenHash, ip, true);
35+
return { resourceId: share.resourceId, ownerId: share.ownerId };
36+
}
37+
38+
async function revokeShare(token) {
39+
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
40+
await Share.updateOne({ tokenHash }, { revoked: true });
41+
}
42+
43+
async function getAccessLogs(token) {
44+
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
45+
return await AccessLog.find({ tokenHash }).sort({ timestamp: -1 });
46+
}
47+
48+
async function logAccess(tokenHash, ip, success) {
49+
const log = new AccessLog({
50+
tokenHash,
51+
ip,
52+
success
53+
});
54+
await log.save();
55+
}
56+
57+
module.exports = {
58+
createShareLink,
59+
getSharedResource,
60+
revokeShare,
61+
getAccessLogs
62+
};

0 commit comments

Comments
 (0)