diff --git a/.env.example b/.env.example
index 3453588c..c03a259c 100644
--- a/.env.example
+++ b/.env.example
@@ -6,3 +6,9 @@ GEMINI_API_KEY=your_gen_ai_key_here
# Server Port (optional, defaults to 3000)
PORT=3000
+
+# Google OAuth 2.0 Client ID for Sign-In
+GOOGLE_CLIENT_ID=your_google_client_id_here
+
+# JWT Secret for Session Management
+JWT_SECRET=your_jwt_secret_here
diff --git a/database.js b/database.js
index d2a34f89..53d982b1 100644
--- a/database.js
+++ b/database.js
@@ -5,6 +5,16 @@ const db = new sqlite3.Database(path.join(__dirname, 'studyplan.db'));
function initDb() {
db.serialize(() => {
+ // Users Table
+ db.run(`CREATE TABLE IF NOT EXISTS users (
+ id TEXT PRIMARY KEY,
+ email TEXT UNIQUE NOT NULL,
+ name TEXT,
+ picture TEXT,
+ auth_provider TEXT DEFAULT 'local',
+ password TEXT,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ )`);
// Subjects Table
db.run(`CREATE TABLE IF NOT EXISTS subjects (
diff --git a/index.html b/index.html
index 43b82cc5..e5d3aaf9 100644
--- a/index.html
+++ b/index.html
@@ -11,6 +11,7 @@
+
@@ -33,6 +34,9 @@ Wel
Sign In
+
OR
+
+
Password must contain:
@@ -501,6 +505,9 @@ =12",
+ "npm": ">=6"
+ }
+ },
"node_modules/jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
@@ -1007,6 +1031,48 @@
"safe-buffer": "^5.0.1"
}
},
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
+ "license": "MIT"
+ },
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
diff --git a/package.json b/package.json
index 37df9f01..65c84742 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,8 @@
"cors": "^2.8.6",
"dotenv": "^17.4.2",
"express": "^5.2.1",
+ "google-auth-library": "^10.7.0",
+ "jsonwebtoken": "^9.0.3",
"sqlite3": "^6.0.1"
},
"engines": {
diff --git a/server.js b/server.js
index d908b575..ef62e612 100644
--- a/server.js
+++ b/server.js
@@ -5,6 +5,11 @@ const { db, initDb } = require('./database');
const { GoogleGenAI } = require('@google/genai');
const path = require('path');
const csvDownloadRouter = require('./backend/routers/csvDownload.router.js');
+const { OAuth2Client } = require('google-auth-library');
+const jwt = require('jsonwebtoken');
+
+const googleClient = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);
+const JWT_SECRET = process.env.JWT_SECRET || 'your-fallback-secret-key';
const app = express();
app.use(cors());
@@ -559,6 +564,7 @@ Text: "${text}"
const tasks = nlpExtractTasksFromText(text);
return res.json(tasks);
});
+
// ================= AUTH =================
// SIGNUP
@@ -566,75 +572,145 @@ app.post('/api/auth/signup', (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
- return res.status(400).json({
- error: 'Email and password required'
- });
+ return res.status(400).json({ error: 'Email and password required' });
}
- const id = 'user_' + Date.now();
+ db.get('SELECT * FROM users WHERE email = ?', [email], (err, row) => {
+ if (err) return res.status(500).json({ error: err.message });
- db.run(
- `INSERT INTO users (id, email, password)
- VALUES (?, ?, ?)`,
- [id, email, password],
- function(err) {
+ if (row) {
+ return res.status(400).json({ error: 'User already exists' });
+ }
- if (err) {
+ const id = 'user_' + Date.now() + Math.random().toString(36).substr(2, 5);
- if (err.message.includes('UNIQUE')) {
- return res.status(400).json({
- error: 'User already exists'
- });
- }
+ db.run(
+ 'INSERT INTO users (id, email, password, auth_provider) VALUES (?, ?, ?, ?)',
+ [id, email, password, 'local'],
+ function (err) {
+ if (err) return res.status(500).json({ error: err.message });
- return res.status(500).json({
- error: err.message
+ const token = jwt.sign(
+ { id, email },
+ JWT_SECRET,
+ { expiresIn: '7d' }
+ );
+
+ res.json({
+ success: true,
+ message: 'Account created successfully',
+ token
});
}
-
- res.json({
- success: true,
- message: 'Account created successfully'
- });
- }
- );
+ );
+ });
});
+
// LOGIN
app.post('/api/auth/login', (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
- return res.status(400).json({
- error: 'Email and password required'
- });
+ return res.status(400).json({ error: 'Email and password required' });
}
db.get(
- `SELECT * FROM users WHERE email = ?`,
+ 'SELECT * FROM users WHERE email = ?',
[email],
(err, user) => {
-
- if (err) {
- return res.status(500).json({
- error: err.message
- });
- }
+ if (err) return res.status(500).json({ error: err.message });
if (!user || user.password !== password) {
- return res.status(401).json({
- error: 'Invalid email or password'
- });
+ return res.status(401).json({ error: 'Invalid email or password' });
}
+ const token = jwt.sign(
+ { id: user.id, email: user.email },
+ JWT_SECRET,
+ { expiresIn: '7d' }
+ );
+
res.json({
success: true,
- email: user.email
+ email: user.email,
+ token
});
}
);
});
+
+// GOOGLE LOGIN
+app.post('/api/auth/google', async (req, res) => {
+ const { token } = req.body;
+
+ if (!token) {
+ return res.status(400).json({ error: 'Token is required' });
+ }
+
+ try {
+ const ticket = await googleClient.verifyIdToken({
+ idToken: token,
+ audience: process.env.GOOGLE_CLIENT_ID,
+ });
+
+ const payload = ticket.getPayload();
+ const { email, name, picture } = payload;
+
+ db.get(
+ 'SELECT * FROM users WHERE email = ?',
+ [email],
+ (err, user) => {
+ if (err) return res.status(500).json({ error: err.message });
+
+ if (user) {
+ const jwtToken = jwt.sign(
+ { id: user.id, email: user.email },
+ JWT_SECRET,
+ { expiresIn: '7d' }
+ );
+
+ return res.json({
+ success: true,
+ email: user.email,
+ token: jwtToken
+ });
+ }
+
+ const id = 'user_' + Date.now() + Math.random().toString(36).substr(2, 5);
+
+ db.run(
+ 'INSERT INTO users (id, email, name, picture, auth_provider) VALUES (?, ?, ?, ?, ?)',
+ [id, email, name, picture, 'google'],
+ function (err) {
+ if (err) {
+ return res.status(500).json({ error: err.message });
+ }
+
+ const jwtToken = jwt.sign(
+ { id, email },
+ JWT_SECRET,
+ { expiresIn: '7d' }
+ );
+
+ res.json({
+ success: true,
+ email,
+ token: jwtToken
+ });
+ }
+ );
+ }
+ );
+
+ } catch (err) {
+ console.error('Google Auth Error:', err);
+ res.status(401).json({ error: 'Invalid Google token' });
+ }
+});
+
+
// LOGOUT
app.post('/api/auth/logout', (req, res) => {
res.json({
@@ -642,7 +718,6 @@ app.post('/api/auth/logout', (req, res) => {
message: 'Logged out successfully'
});
});
-
// Intentional test route for verifying server error page behavior.
app.get('/debug/force-error', (req, res, next) => {
next(new Error('Intentional test error'));