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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
6 changes: 6 additions & 0 deletions backend/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
PORT=5000
MONGODB_URI=mongodb://localhost:27017/Vi-Notes
JWT_SECRET= sath123
JWT_EXPIRES_IN=7d
NODE_ENV=development
CLIENT_URL=http://localhost:3000
77 changes: 77 additions & 0 deletions backend/controllers/auth.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
const jwt = require('jsonwebtoken');
const { validationResult } = require('express-validator');
const User = require('../models/User');

const signToken = (id) => {
return jwt.sign({ id }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
});
};

const sendTokenResponse = (user, statusCode, res) => {
const token = signToken(user._id);
res.status(statusCode).json({
token,
user: {
id: user._id,
email: user.email,
createdAt: user.createdAt,
},
});
};

// POST /api/auth/register
const register = async (req, res, next) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ message: errors.array()[0].msg });
}

const { email, password } = req.body;

const existingUser = await User.findOne({ email: email.toLowerCase() });
if (existingUser) {
return res.status(409).json({ message: 'Email is already registered.' });
}

const user = await User.create({ email, password });
sendTokenResponse(user, 201, res);
} catch (err) {
next(err);
}
};

// POST /api/auth/login
const login = async (req, res, next) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ message: errors.array()[0].msg });
}

const { email, password } = req.body;

const user = await User.findOne({ email: email.toLowerCase() }).select('+password');
if (!user || !(await user.comparePassword(password))) {
return res.status(401).json({ message: 'Invalid email or password.' });
}

sendTokenResponse(user, 200, res);
} catch (err) {
next(err);
}
};

// GET /api/auth/me
const getMe = async (req, res) => {
res.json({
user: {
id: req.user._id,
email: req.user.email,
createdAt: req.user.createdAt,
},
});
};

module.exports = { register, login, getMe };
102 changes: 102 additions & 0 deletions backend/controllers/session.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
const Session = require('../models/Session');

// POST /api/sessions
const saveSession = async (req, res, next) => {
try {
const {
sessionId,
title,
content,
keystrokes,
totalKeystrokes,
deletions,
bursts,
pauses,
wordCount,
startedAt,
duration,
score,
} = req.body;

if (!sessionId) {
return res.status(400).json({ message: 'sessionId is required.' });
}

// Upsert: update if session exists, otherwise create
const session = await Session.findOneAndUpdate(
{ sessionId, user: req.user._id },
{
user: req.user._id,
sessionId,
title: title || 'Untitled document',
content,
keystrokes: keystrokes || [],
totalKeystrokes: totalKeystrokes || 0,
deletions: deletions || 0,
bursts: bursts || 0,
pauses: pauses || [],
wordCount: wordCount || 0,
startedAt: startedAt ? new Date(startedAt) : new Date(),
savedAt: new Date(),
duration: duration || 0,
},
{ new: true, upsert: true, runValidators: true }
);

res.status(200).json({ session });
} catch (err) {
next(err);
}
};

// GET /api/sessions
const getSessions = async (req, res, next) => {
try {
const sessions = await Session.find({ user: req.user._id })
.select('-keystrokes -pauses -content') // Exclude heavy fields from list
.sort({ savedAt: -1 })
.limit(50);

res.json({ sessions });
} catch (err) {
next(err);
}
};

// GET /api/sessions/:id
const getSession = async (req, res, next) => {
try {
const session = await Session.findOne({
sessionId: req.params.id,
user: req.user._id,
});

if (!session) {
return res.status(404).json({ message: 'Session not found.' });
}

res.json({ session });
} catch (err) {
next(err);
}
};

// DELETE /api/sessions/:id
const deleteSession = async (req, res, next) => {
try {
const session = await Session.findOneAndDelete({
sessionId: req.params.id,
user: req.user._id,
});

if (!session) {
return res.status(404).json({ message: 'Session not found.' });
}

res.json({ message: 'Session deleted.' });
} catch (err) {
next(err);
}
};

module.exports = { saveSession, getSessions, getSession, deleteSession };
38 changes: 38 additions & 0 deletions backend/middleware/auth.middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const jwt = require('jsonwebtoken');
const User = require('../models/User');

const protect = async (req, res, next) => {
try {
// Get token from Authorization header
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ message: 'Not authorized. No token provided.' });
}

const token = authHeader.split(' ')[1];

// Verify token
let decoded;
try {
decoded = jwt.verify(token, process.env.JWT_SECRET);
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ message: 'Session expired. Please sign in again.' });
}
return res.status(401).json({ message: 'Invalid token.' });
}

// Attach user to request
const user = await User.findById(decoded.id);
if (!user) {
return res.status(401).json({ message: 'User no longer exists.' });
}

req.user = user;
next();
} catch (err) {
next(err);
}
};

module.exports = { protect };
89 changes: 89 additions & 0 deletions backend/models/Session.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
const mongoose = require('mongoose');

const keystrokeSchema = new mongoose.Schema(
{
t: { type: Number, required: true }, // timestamp (ms since epoch)
k: { type: String, required: true }, // key pressed
gap: { type: Number, default: null }, // ms since last keystroke
},
{ _id: false }
);

const sessionSchema = new mongoose.Schema(
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
index: true,
},
sessionId: {
type: String,
required: true,
unique: true,
},
title: {
type: String,
default: 'Untitled document',
trim: true,
},
content: {
type: String,
default: '',
},
// Keystroke telemetry
keystrokes: [keystrokeSchema],
totalKeystrokes: { type: Number, default: 0 },
deletions: { type: Number, default: 0 },
bursts: { type: Number, default: 0 },
pauses: [Number], // array of inter-keystroke gaps in ms
wordCount: { type: Number, default: 0 },
score: { type: Number, default: null },
// Computed authenticity metrics
metrics: {
avgPause: { type: Number, default: null },
pauseVariance: { type: Number, default: null },
coefficientOfVariation: { type: Number, default: null },
deletionRatio: { type: Number, default: null },
authenticityScore: { type: Number, default: null },
},

// Session lifecycle
startedAt: { type: Date, default: Date.now },
savedAt: { type: Date, default: null },
duration: { type: Number, default: 0 }, // seconds
},
{
timestamps: true,
}
);

// Compute metrics before saving
sessionSchema.pre('save', function (next) {
if (this.pauses && this.pauses.length > 0) {
const pauses = this.pauses;
const mean = pauses.reduce((a, b) => a + b, 0) / pauses.length;
const variance =
pauses.reduce((a, b) => a + (b - mean) ** 2, 0) / pauses.length;
const cv = mean > 0 ? Math.sqrt(variance) / mean : 0;
const delRatio =
this.totalKeystrokes > 0 ? this.deletions / this.totalKeystrokes : 0;

let score = 60;
if (cv > 0.4 && cv < 2.5) score += 20;
if (delRatio > 0.02 && delRatio < 0.25) score += 15;
if (this.bursts > 0) score += 5;
score = Math.min(99, Math.max(30, score));

this.metrics = {
avgPause: Math.round(mean),
pauseVariance: Math.round(variance),
coefficientOfVariation: parseFloat(cv.toFixed(3)),
deletionRatio: parseFloat(delRatio.toFixed(3)),
authenticityScore: score,
};
}
next();
});

module.exports = mongoose.model('Session', sessionSchema);
46 changes: 46 additions & 0 deletions backend/models/User.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = new mongoose.Schema(
{
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
lowercase: true,
trim: true,
match: [/^\S+@\S+\.\S+$/, 'Please enter a valid email'],
},
password: {
type: String,
required: [true, 'Password is required'],
minlength: [6, 'Password must be at least 6 characters'],
select: false, // Never return password by default
},
},
{
timestamps: true,
}
);

// Hash password before saving
userSchema.pre('save', async function (next) {
if (!this.isModified('password')) return next();
const salt = await bcrypt.genSalt(12);
this.password = await bcrypt.hash(this.password, salt);
next();
});

// Compare password method
userSchema.methods.comparePassword = async function (candidatePassword) {
return bcrypt.compare(candidatePassword, this.password);
};

// Remove sensitive fields when converting to JSON
userSchema.methods.toJSON = function () {
const obj = this.toObject();
delete obj.password;
return obj;
};

module.exports = mongoose.model('User', userSchema);
16 changes: 16 additions & 0 deletions backend/node_modules/.bin/mime

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

Loading