From 0cb1902aac103f9aaf88486f9b31486bf37515b6 Mon Sep 17 00:00:00 2001 From: Dinakar Date: Mon, 15 Dec 2025 23:10:57 +1100 Subject: [PATCH 1/3] Added secure token revocation, session lifecycle logic, Supabase integration, and auth test script --- middleware/authenticateToken.js | 54 ++++----- package-lock.json | 107 +++++++++------- package.json | 4 +- routes/auth.js | 2 +- services/authService.js | 208 +++++++++++++++++++------------- services/supabaseClient.js | 12 ++ test-authflow.js | 108 +++++++++++++++++ testcapstone.js | 24 ++++ 8 files changed, 362 insertions(+), 157 deletions(-) create mode 100644 services/supabaseClient.js create mode 100644 test-authflow.js create mode 100644 testcapstone.js diff --git a/middleware/authenticateToken.js b/middleware/authenticateToken.js index 772411b..4243292 100644 --- a/middleware/authenticateToken.js +++ b/middleware/authenticateToken.js @@ -1,9 +1,10 @@ const authService = require('../services/authService'); /** - * Enhanced authentication middleware + * Clean Access Token Authentication Middleware */ const authenticateToken = (req, res, next) => { + const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; @@ -34,6 +35,29 @@ const authenticateToken = (req, res, next) => { error: 'Invalid token payload', code: 'INVALID_TOKEN' }); + + const authHeader = req.headers["authorization"]; + const token = authHeader && authHeader.split(" ")[1]; + + if (!token) { + return res.status(401).json({ + success: false, + error: "Access token required", + code: "TOKEN_MISSING" + }); + } + + try { + const decoded = authService.verifyAccessToken(token); + req.user = decoded; + next(); + } catch (error) { + return res.status(401).json({ + success: false, + error: "Invalid or expired access token", + code: "TOKEN_INVALID" + }); + } req.user = decoded; // THIS FIXES PROFILE FETCH @@ -65,30 +89,4 @@ const authenticateToken = (req, res, next) => { } }; -/** - * Optional authentication middleware - * (attaches user if token exists, otherwise continues without blocking) - */ -const optionalAuth = (req, res, next) => { - const authHeader = req.headers['authorization']; - const token = authHeader && authHeader.split(' ')[1]; - - if (!token) { - req.user = null; - return next(); - } - - try { - const decoded = authService.verifyAccessToken(token); - req.user = decoded; - } catch (error) { - req.user = null; - } - - next(); -}; - -module.exports = { - authenticateToken, - optionalAuth -}; +module.exports = { authenticateToken }; diff --git a/package-lock.json b/package-lock.json index c33089a..97dd77f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "ISC", "dependencies": { "@sendgrid/mail": "^8.1.3", - "@supabase/supabase-js": "^2.40.0", + "@supabase/supabase-js": "^2.86.0", "base64-arraybuffer": "^1.0.2", "bcrypt": "^5.1.1", "bcryptjs": "^2.4.3", @@ -33,7 +33,7 @@ "yamljs": "^0.3.0" }, "devDependencies": { - "axios": "^1.11.0", + "axios": "^1.13.2", "chai": "^6.0.1", "chai-http": "^5.1.2", "concurrently": "^8.2.2", @@ -76,6 +76,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -1488,77 +1489,83 @@ "license": "(Unlicense OR Apache-2.0)" }, "node_modules/@supabase/auth-js": { - "version": "2.71.1", - "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.71.1.tgz", - "integrity": "sha512-mMIQHBRc+SKpZFRB2qtupuzulaUhFYupNyxqDj5Jp/LyPvcWvjaJzZzObv6URtL/O6lPxkanASnotGtNpS3H2Q==", + "version": "2.86.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.86.0.tgz", + "integrity": "sha512-3xPqMvBWC6Haqpr6hEWmSUqDq+6SA1BAEdbiaHdAZM9QjZ5uiQJ+6iD9pZOzOa6MVXZh4GmwjhC9ObIG0K1NcA==", "license": "MIT", "dependencies": { - "@supabase/node-fetch": "^2.6.14" + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@supabase/functions-js": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.5.tgz", - "integrity": "sha512-v5GSqb9zbosquTo6gBwIiq7W9eQ7rE5QazsK/ezNiQXdCbY+bH8D9qEaBIkhVvX4ZRW5rP03gEfw5yw9tiq4EQ==", + "version": "2.86.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.86.0.tgz", + "integrity": "sha512-AlOoVfeaq9XGlBFIyXTmb+y+CZzxNO4wWbfgRM6iPpNU5WCXKawtQYSnhivi3UVxS7GA0rWovY4d6cIAxZAojA==", "license": "MIT", "dependencies": { - "@supabase/node-fetch": "^2.6.14" - } - }, - "node_modules/@supabase/node-fetch": { - "version": "2.6.15", - "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", - "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" + "tslib": "2.8.1" }, "engines": { - "node": "4.x || >=6.0.0" + "node": ">=20.0.0" } }, "node_modules/@supabase/postgrest-js": { - "version": "1.21.3", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.21.3.tgz", - "integrity": "sha512-rg3DmmZQKEVCreXq6Am29hMVe1CzemXyIWVYyyua69y6XubfP+DzGfLxME/1uvdgwqdoaPbtjBDpEBhqxq1ZwA==", + "version": "2.86.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.86.0.tgz", + "integrity": "sha512-QVf+wIXILcZJ7IhWhWn+ozdf8B+oO0Ulizh2AAPxD/6nQL+x3r9lJ47a+fpc/jvAOGXMbkeW534Kw6jz7e8iIA==", "license": "MIT", "dependencies": { - "@supabase/node-fetch": "^2.6.14" + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@supabase/realtime-js": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.15.4.tgz", - "integrity": "sha512-e/FYIWjvQJHOCNACWehnKvg26zosju3694k0NMUNb+JGLdvHJzEa29ZVVLmawd2kvx4hdbv8mxSqfttRnH3+DA==", + "version": "2.86.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.86.0.tgz", + "integrity": "sha512-dyS8bFoP29R/sj5zLi0AP3JfgG8ar1nuImcz5jxSx7UIW7fbFsXhUCVrSY2Ofo0+Ev6wiATiSdBOzBfWaiFyPA==", "license": "MIT", "dependencies": { - "@supabase/node-fetch": "^2.6.13", "@types/phoenix": "^1.6.6", "@types/ws": "^8.18.1", + "tslib": "2.8.1", "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@supabase/storage-js": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.11.0.tgz", - "integrity": "sha512-Y+kx/wDgd4oasAgoAq0bsbQojwQ+ejIif8uczZ9qufRHWFLMU5cODT+ApHsSrDufqUcVKt+eyxtOXSkeh2v9ww==", + "version": "2.86.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.86.0.tgz", + "integrity": "sha512-PM47jX/Mfobdtx7NNpoj9EvlrkapAVTQBZgGGslEXD6NS70EcGjhgRPBItwHdxZPM5GwqQ0cGMN06uhjeY2mHQ==", "license": "MIT", "dependencies": { - "@supabase/node-fetch": "^2.6.14" + "iceberg-js": "^0.8.0", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@supabase/supabase-js": { - "version": "2.56.1", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.56.1.tgz", - "integrity": "sha512-cb/kS0d6G/qbcmUFItkqVrQbxQHWXzfRZuoiSDv/QiU6RbGNTn73XjjvmbBCZ4MMHs+5teihjhpEVluqbXISEg==", + "version": "2.86.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.86.0.tgz", + "integrity": "sha512-BaC9sv5+HGNy1ulZwY8/Ev7EjfYYmWD4fOMw9bDBqTawEj6JHAiOHeTwXLRzVaeSay4p17xYLN2NSCoGgXMQnw==", "license": "MIT", "dependencies": { - "@supabase/auth-js": "2.71.1", - "@supabase/functions-js": "2.4.5", - "@supabase/node-fetch": "2.6.15", - "@supabase/postgrest-js": "1.21.3", - "@supabase/realtime-js": "2.15.4", - "@supabase/storage-js": "^2.10.4" + "@supabase/auth-js": "2.86.0", + "@supabase/functions-js": "2.86.0", + "@supabase/postgrest-js": "2.86.0", + "@supabase/realtime-js": "2.86.0", + "@supabase/storage-js": "2.86.0" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@tybys/wasm-util": { @@ -2173,9 +2180,9 @@ } }, "node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -2420,6 +2427,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001737", "electron-to-chromium": "^1.5.211", @@ -3345,6 +3353,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -4050,6 +4059,15 @@ "node": ">=10.17.0" } }, + "node_modules/iceberg-js": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.0.tgz", + "integrity": "sha512-kmgmea2nguZEvRqW79gDqNXyxA3OS5WIgMVffrHpqXV4F/J4UmNIw2vstixioLTNSkd5rFB8G0s3Lwzogm6OFw==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -7336,7 +7354,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/twilio": { diff --git a/package.json b/package.json index 603bb4c..f6c4b4c 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "license": "ISC", "dependencies": { "@sendgrid/mail": "^8.1.3", - "@supabase/supabase-js": "^2.40.0", + "@supabase/supabase-js": "^2.86.0", "base64-arraybuffer": "^1.0.2", "bcrypt": "^5.1.1", "bcryptjs": "^2.4.3", @@ -48,7 +48,7 @@ "yamljs": "^0.3.0" }, "devDependencies": { - "axios": "^1.11.0", + "axios": "^1.13.2", "chai": "^6.0.1", "chai-http": "^5.1.2", "concurrently": "^8.2.2", diff --git a/routes/auth.js b/routes/auth.js index bed0e8f..27a6cac 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -34,4 +34,4 @@ router.get('/health', (req, res) => { }); }); -module.exports = router; +module.exports = router; \ No newline at end of file diff --git a/services/authService.js b/services/authService.js index db0f437..fd00487 100644 --- a/services/authService.js +++ b/services/authService.js @@ -158,23 +158,41 @@ class AuthService { console.log("āœ… Generated accessToken:", accessToken); // Generate a refresh token - const refreshToken = crypto.randomBytes(40).toString('hex'); - const expiresAt = new Date(Date.now() + this.refreshTokenExpiry); + // Generate raw refresh token +const rawRefreshToken = crypto.randomBytes(40).toString('hex'); + +// Hash refresh token before storing (OWASP recommended) +const hashedRefreshToken = await bcrypt.hash(rawRefreshToken, 12); + +const expiresAt = new Date(Date.now() + this.refreshTokenExpiry); + +// Hash the refresh token before storing (security requirement) +const hashedToken = await bcrypt.hash(refreshToken, 12); + +// Store hashed refresh token in database +const { error } = await supabase + .from('user_session') + .insert({ + user_id: user.user_id, + refresh_token: hashedToken, // āœ” hashed version stored + token_type: 'refresh', + device_info: deviceInfo, + ip_address: deviceInfo.ip || null, + user_agent: deviceInfo.userAgent || null, + expires_at: expiresAt.toISOString(), + is_active: true, + }); + + +if (error) throw error; + +return { + accessToken, + refreshToken: rawRefreshToken, // send ONLY raw token to client + expiresIn: 15 * 60, + tokenType: 'Bearer' +}; - // Store refresh token in database - const { error } = await supabase - .from('user_session') - .insert({ - user_id: user.user_id, - session_token: refreshToken, - refresh_token: refreshToken, - token_type: 'refresh', - device_info: deviceInfo, - ip_address: deviceInfo.ip || null, - user_agent: deviceInfo.userAgent || null, - expires_at: expiresAt.toISOString(), - is_active: true, - }); if (error) throw error; @@ -192,89 +210,117 @@ class AuthService { /** * Refresh Access Token - */ - async refreshAccessToken(refreshToken, deviceInfo = {}) { - try { - // Verifying the refresh token - const { data: session, error } = await supabase - .from('user_session') - .select(` - id, user_id, expires_at, is_active, - users!inner(user_id, email, name, role_id, account_status, - user_roles!inner(id, role_name) - ) - `) - .eq('refresh_token', refreshToken) - .eq('is_active', true) - .single(); + *//** + * Refresh Access Token (secure hashed-token version) + */ +async refreshAccessToken(refreshToken, deviceInfo = {}) { + try { + // Fetch all active sessions + const { data: sessions, error } = await supabase + .from('user_session') + .select(` + id, user_id, refresh_token, expires_at, is_active, + users!inner(user_id, email, name, role_id, account_status, + user_roles!inner(id, role_name) + ) + `) + .eq('is_active', true); + + if (error || !sessions || sessions.length === 0) { + throw new Error('Invalid refresh token'); + } - if (error || !session) { - throw new Error('Invalid refresh token'); + // Compare hashed refresh tokens + let session = null; + for (const s of sessions) { + const match = await bcrypt.compare(refreshToken, s.refresh_token); + if (match) { + session = s; + break; } + } - // Check if the token is expired - if (new Date(session.expires_at) < new Date()) { - throw new Error('Refresh token expired'); - } + if (!session) { + throw new Error('Invalid refresh token'); + } - // Checking User Status - const user = session.users; - if (user.account_status !== 'active') { - throw new Error('Account is not active'); - } + // Check expiration + if (new Date(session.expires_at) < new Date()) { + throw new Error('Refresh token expired'); + } - // Generate a new token pair - const newTokens = await this.generateTokenPair(user, deviceInfo); + const user = session.users; + if (user.account_status !== "active") { + throw new Error("Account is not active"); + } - // Invalidate old refresh tokens - await supabase - .from('user_session') - .update({ is_active: false }) - .eq('id', session.id); + // Generate new token pair + const newTokens = await this.generateTokenPair(user, deviceInfo); - return { - success: true, - ...newTokens - }; + // Deactivate old session + await supabase + .from("user_session") + .update({ is_active: false }) + .eq("id", session.id); - } catch (error) { - throw new Error(`Token refresh failed: ${error.message}`); - } + return { + success: true, + ...newTokens, + }; + + } catch (error) { + throw new Error(`Token refresh failed: ${error.message}`); } +} - /** - * Logout - */ - async logout(refreshToken) { - try { - if (refreshToken) { +/** + * Logout (secure hashed-token version) + */ +async logout(refreshToken) { + try { + // Get all active sessions + const { data: sessions } = await supabase + .from("user_session") + .select("id, refresh_token, is_active") + .eq("is_active", true); + + if (!sessions) return { success: true }; + + // Find matching hashed token + for (const s of sessions) { + if (await bcrypt.compare(refreshToken, s.refresh_token)) { await supabase - .from('user_session') + .from("user_session") .update({ is_active: false }) - .eq('refresh_token', refreshToken); + .eq("id", s.id); + break; } - - return { success: true, message: 'Logout successful' }; - } catch (error) { - throw new Error(`Logout failed: ${error.message}`); } + + return { success: true, message: "Logout successful" }; + } catch (error) { + throw new Error(`Logout failed: ${error.message}`); } +} - /** - * Log out of all devices - */ - async logoutAll(userId) { - try { - await supabase - .from('user_session') - .update({ is_active: false }) - .eq('user_id', userId); - return { success: true, message: 'Logged out from all devices' }; - } catch (error) { - throw new Error(`Logout all failed: ${error.message}`); - } + +/** + * Logout from all devices (same logic, we deactivate all sessions) + */ +async logoutAll(userId) { + try { + await supabase + .from("user_session") + .update({ is_active: false }) + .eq("user_id", userId); + + return { success: true, message: "Logged out from all devices" }; + } catch (error) { + throw new Error(`Logout all failed: ${error.message}`); } +} + /** * Verifying the Access Token diff --git a/services/supabaseClient.js b/services/supabaseClient.js new file mode 100644 index 0000000..a73fdb3 --- /dev/null +++ b/services/supabaseClient.js @@ -0,0 +1,12 @@ +require('dotenv').config(); +const { createClient } = require('@supabase/supabase-js'); + +console.log("URL:", process.env.PERSONAL_SUPABASE_URL); // TEMP DEBUG +console.log("KEY:", process.env.PERSONAL_SUPABASE_ANON_KEY); // TEMP DEBUG + +const supabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_ANON_KEY +); + +module.exports = supabase; diff --git a/test-authflow.js b/test-authflow.js new file mode 100644 index 0000000..1ed8592 --- /dev/null +++ b/test-authflow.js @@ -0,0 +1,108 @@ +require("dotenv").config(); +const axios = require("axios"); + +const BASE_URL = "http://localhost/api/auth"; + +let accessToken = ""; +let refreshToken = ""; + +// Helper for pretty logs +function log(title, data) { + console.log(`\nšŸ”µ ${title}...`); + console.log(JSON.stringify(data, null, 2)); +} + +// STEP 1 — SIGNUP user if not exists +async function testSignup() { + try { + const res = await axios.post("http://localhost/api/signup", { + email: "test@email.com", + password: "test123" + }); + + log("Signup Success", res.data); + } catch (err) { + console.log("šŸ”ø Signup skipped (user may already exist)"); + } +} + +// STEP 2 — LOGIN +async function testLogin() { + try { + const res = await axios.post(`${BASE_URL}/login`, { + email: "john@nutrihep.com", + password: "SecurePassword123!" + }); + + accessToken = res.data.accessToken; + refreshToken = res.data.refreshToken; + + log("Login Success", { + accessToken, + refreshToken + }); + } catch (err) { + console.log("āŒ Login failed:", err.response?.data || err.message); + } +} + +// STEP 3 — CALL PROTECTED ROUTE +async function testProtectedRoute() { + try { + const res = await axios.get(`${BASE_URL}/profile`, { + headers: { Authorization: `Bearer ${accessToken}` } + }); + + log("Protected Route Success", res.data); + } catch (err) { + console.log("āŒ Protected route failed:", err.response?.data || err.message); + } +} + +// STEP 4 — REFRESH TOKEN +async function testRefresh() { + try { + const res = await axios.post(`${BASE_URL}/refresh`, { + refreshToken + }); + + accessToken = res.data.accessToken; + refreshToken = res.data.refreshToken; + + log("Token Refresh Success", { + accessToken, + refreshToken + }); + + } catch (err) { + console.log("āŒ Refresh failed:", err.response?.data || err.message); + } +} + +// STEP 5 — LOGOUT +async function testLogout() { + try { + const res = await axios.post(`${BASE_URL}/logout`, { + refreshToken + }); + + log("Logout Success", res.data); + } catch (err) { + console.log("āŒ Logout failed:", err.response?.data || err.message); + } +} + +// RUN EVERYTHING IN ORDER +(async () => { + console.log("\nšŸš€ Starting full Auth Flow Test...\n"); + + await testSignup(); + await testLogin(); + await testProtectedRoute(); + await testRefresh(); + await testLogout(); + + console.log("\n✨ ALL TESTS COMPLETED\n"); +})(); + + diff --git a/testcapstone.js b/testcapstone.js new file mode 100644 index 0000000..5845e88 --- /dev/null +++ b/testcapstone.js @@ -0,0 +1,24 @@ +require("dotenv").config(); +const { createClient } = require("@supabase/supabase-js"); + +console.log("šŸ” Testing Capstone Supabase..."); + +const supabase = createClient( + process.env.SUPABASE_URL, // should be Capstone URL + process.env.SUPABASE_ANON_KEY // Capstone Anon Key +); + +async function test() { + console.log("šŸ”¹ SUPABASE_URL:", process.env.SUPABASE_URL); + console.log("šŸ”¹ SUPABASE_ANON_KEY:", process.env.SUPABASE_ANON_KEY); + + const { data, error } = await supabase + .from("users") + .select("user_id, email") + .limit(1); + + console.log("šŸ“Œ DATA:", data); + console.log("āŒ ERROR:", error); +} + +test(); From 3c5c31ab3f1585272c1bfe8b50ef6a55c47952fd Mon Sep 17 00:00:00 2001 From: Dinakar Date: Sun, 4 Jan 2026 23:37:13 +1100 Subject: [PATCH 2/3] Fix secure refresh token flow and clean up auth logic --- middleware/authenticateToken.js | 43 ++- services/authService.js | 629 +++++++++++++++----------------- services/supabaseClient.js | 3 +- test-authflow.js | 108 ------ testcapstone.js | 24 -- 5 files changed, 330 insertions(+), 477 deletions(-) delete mode 100644 test-authflow.js delete mode 100644 testcapstone.js diff --git a/middleware/authenticateToken.js b/middleware/authenticateToken.js index 4243292..6e626f5 100644 --- a/middleware/authenticateToken.js +++ b/middleware/authenticateToken.js @@ -1,10 +1,13 @@ const authService = require('../services/authService'); /** - * Clean Access Token Authentication Middleware + * Access Token Authentication Middleware + * - Verifies JWT access tokens only + * - Attaches decoded user payload to req.user */ const authenticateToken = (req, res, next) => { + const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; @@ -39,14 +42,31 @@ const authenticateToken = (req, res, next) => { const authHeader = req.headers["authorization"]; const token = authHeader && authHeader.split(" ")[1]; - if (!token) { - return res.status(401).json({ - success: false, - error: "Access token required", - code: "TOKEN_MISSING" - }); + const authHeader = req.headers.authorization; + const token = authHeader?.split(' ')[1]; // Bearer + + + if (!token) { + return res.status(401).json({ + success: false, + error: 'Access token missing', + code: 'TOKEN_MISSING' + }); + } + + try { + const decoded = authService.verifyAccessToken(token); + + // Ensure only access tokens are accepted + if (!decoded || decoded.type !== 'access') { + return res.status(401).json({ + success: false, + error: 'Invalid token type', + code: 'INVALID_TOKEN_TYPE' + }); } + try { const decoded = authService.verifyAccessToken(token); req.user = decoded; @@ -85,6 +105,15 @@ const authenticateToken = (req, res, next) => { success: false, error: 'Internal server error', code: 'INTERNAL_ERROR' + + req.user = decoded; + next(); + } catch (err) { + return res.status(401).json({ + success: false, + error: 'Invalid or expired access token', + code: 'TOKEN_INVALID' + }); } }; diff --git a/services/authService.js b/services/authService.js index fd00487..7aba86b 100644 --- a/services/authService.js +++ b/services/authService.js @@ -1,4 +1,3 @@ -console.log("🟢 Loaded AuthService from:", __filename); const { createClient } = require('@supabase/supabase-js'); const jwt = require('jsonwebtoken'); const bcrypt = require('bcrypt'); @@ -10,364 +9,322 @@ const supabase = createClient( ); class AuthService { - constructor() { - this.accessTokenExpiry = '15m'; // 15 minutes - this.refreshTokenExpiry = 7 * 24 * 60 * 60 * 1000; // 7 days - } + constructor() { + this.accessTokenExpiry = '15m'; + this.refreshTokenExpiry = 7 * 24 * 60 * 60 * 1000; // 7 days + } + + /* ========================= + Helper + ========================= */ + createLookupHash(token) { + return crypto + .createHash('sha256') + .update(token) + .digest('hex') + .slice(0, 16); + } + + /* ========================= + Register + ========================= */ + async register(userData) { + const { name, email, password, first_name, last_name } = userData; - /** - * User Registration - */ - async register(userData) { - const { name, email, password, first_name, last_name } = userData; - - try { - // Check if the user already exists - const { data: existingUser } = await supabase - .from('users') - .select('user_id') - .eq('email', email) - .single(); - - if (existingUser) { - throw new Error('User already exists'); - } - - // Hashed Passwords - const hashedPassword = await bcrypt.hash(password, 12); - - // Create User - const { data: newUser, error } = await supabase - .from('users') - .insert({ - name, - email, - password: hashedPassword, - first_name, - last_name, - role_id: 7, - account_status: 'active', - email_verified: false, - mfa_enabled: false, - registration_date: new Date().toISOString() - }) - .select('user_id, email, name') - .single(); - - if (error) throw error; - - return { - success: true, - user: newUser, - message: 'User registered successfully' - }; - - } catch (error) { - throw new Error(`Registration failed: ${error.message}`); - } + try { + const { data: existingUser } = await supabase + .from('users') + .select('user_id') + .eq('email', email) + .single(); + + if (existingUser) { + throw new Error('User already exists'); + } + + const hashedPassword = await bcrypt.hash(password, 12); + + const { data: newUser, error } = await supabase + .from('users') + .insert({ + name, + email, + password: hashedPassword, + first_name, + last_name, + role_id: 7, + account_status: 'active', + email_verified: false, + mfa_enabled: false, + registration_date: new Date().toISOString() + }) + .select('user_id, email, name') + .single(); + + if (error) throw error; + + return { + success: true, + user: newUser, + message: 'User registered successfully' + }; + } catch (error) { + throw new Error(`Registration failed: ${error.message}`); } + } - /** - * User login - */ - async login(loginData, deviceInfo = {}) { - const { email, password } = loginData; - - try { - // Find User - const { data: user, error } = await supabase - .from('users') - .select(` - user_id, email, password, name, role_id, - account_status, email_verified, - user_roles!inner(id,role_name) - `) - .eq('email', email) - .single(); - - if (error || !user) { - throw new Error('Invalid credentials'); - } - - // Check account status - if (user.account_status !== 'active') { - throw new Error('Account is not active'); - } - - // Verify Password - const validPassword = await bcrypt.compare(password, user.password); - if (!validPassword) { - throw new Error('Invalid credentials'); - } - - // Generate token pair - const tokens = await this.generateTokenPair(user, deviceInfo); - - // Update last login time - await supabase - .from('users') - .update({ last_login: new Date().toISOString() }) - .eq('user_id', user.user_id); - - // Record successful login - await this.logAuthAttempt(user.user_id, email, true, deviceInfo); - - return { - success: true, - user: { - id: user.user_id, - email: user.email, - name: user.name, - role: user.user_roles?.role_name || 'user' - }, - ...tokens - }; - - } catch (error) { - // Login failures - await this.logAuthAttempt(null, email, false, deviceInfo); - throw error; - } - } + /* ========================= + Login + ========================= */ + async login(loginData, deviceInfo = {}) { + const { email, password } = loginData; - /** - * Generate access token and refresh token - */ - async generateTokenPair(user, deviceInfo = {}) { - try { - // Build access token payload - const accessPayload = { - userId: user.user_id, - email: user.email, - role: user.user_roles?.role_name || 'user', - type: 'access' - }; - - console.log("šŸ”‘ Signing access token with payload:", accessPayload); - - // Generate Access Token - const accessToken = jwt.sign( - accessPayload, - process.env.JWT_TOKEN, - { - expiresIn: this.accessTokenExpiry, - algorithm: 'HS256' - } - ); - - console.log("āœ… Generated accessToken:", accessToken); - - // Generate a refresh token - // Generate raw refresh token -const rawRefreshToken = crypto.randomBytes(40).toString('hex'); - -// Hash refresh token before storing (OWASP recommended) -const hashedRefreshToken = await bcrypt.hash(rawRefreshToken, 12); - -const expiresAt = new Date(Date.now() + this.refreshTokenExpiry); - -// Hash the refresh token before storing (security requirement) -const hashedToken = await bcrypt.hash(refreshToken, 12); - -// Store hashed refresh token in database -const { error } = await supabase - .from('user_session') - .insert({ - user_id: user.user_id, - refresh_token: hashedToken, // āœ” hashed version stored - token_type: 'refresh', - device_info: deviceInfo, - ip_address: deviceInfo.ip || null, - user_agent: deviceInfo.userAgent || null, - expires_at: expiresAt.toISOString(), - is_active: true, - }); - - -if (error) throw error; - -return { - accessToken, - refreshToken: rawRefreshToken, // send ONLY raw token to client - expiresIn: 15 * 60, - tokenType: 'Bearer' -}; - - - if (error) throw error; - - return { - accessToken, - refreshToken, - expiresIn: 15 * 60, // 15 minutes in seconds - tokenType: 'Bearer' - }; - - } catch (error) { - throw new Error(`Token generation failed: ${error.message}`); - } + try { + const { data: user, error } = await supabase + .from('users') + .select(` + user_id, email, password, name, role_id, + account_status, email_verified, + user_roles!inner(id, role_name) + `) + .eq('email', email) + .single(); + + if (error || !user) throw new Error('Invalid credentials'); + if (user.account_status !== 'active') throw new Error('Account is not active'); + + const validPassword = await bcrypt.compare(password, user.password); + if (!validPassword) throw new Error('Invalid credentials'); + + const tokens = await this.generateTokenPair(user, deviceInfo); + + await supabase + .from('users') + .update({ last_login: new Date().toISOString() }) + .eq('user_id', user.user_id); + + await this.logAuthAttempt(user.user_id, email, true, deviceInfo); + + return { + success: true, + user: { + id: user.user_id, + email: user.email, + name: user.name, + role: user.user_roles?.role_name || 'user' + }, + ...tokens + }; + } catch (error) { + await this.logAuthAttempt(null, email, false, deviceInfo); + throw error; } + } - /** - * Refresh Access Token - *//** - * Refresh Access Token (secure hashed-token version) - */ -async refreshAccessToken(refreshToken, deviceInfo = {}) { + /* ========================= + Generate Tokens + ========================= */ + async generateTokenPair(user, deviceInfo = {}) { try { - // Fetch all active sessions - const { data: sessions, error } = await supabase - .from('user_session') - .select(` - id, user_id, refresh_token, expires_at, is_active, - users!inner(user_id, email, name, role_id, account_status, - user_roles!inner(id, role_name) - ) - `) - .eq('is_active', true); - - if (error || !sessions || sessions.length === 0) { - throw new Error('Invalid refresh token'); - } - - // Compare hashed refresh tokens - let session = null; - for (const s of sessions) { - const match = await bcrypt.compare(refreshToken, s.refresh_token); - if (match) { - session = s; - break; - } - } - - if (!session) { - throw new Error('Invalid refresh token'); - } - - // Check expiration - if (new Date(session.expires_at) < new Date()) { - throw new Error('Refresh token expired'); - } - - const user = session.users; - if (user.account_status !== "active") { - throw new Error("Account is not active"); - } - - // Generate new token pair - const newTokens = await this.generateTokenPair(user, deviceInfo); - - // Deactivate old session - await supabase - .from("user_session") - .update({ is_active: false }) - .eq("id", session.id); - - return { - success: true, - ...newTokens, - }; - + const accessPayload = { + userId: user.user_id, + email: user.email, + role: user.user_roles?.role_name || 'user', + type: 'access' + }; + + const accessToken = jwt.sign( + accessPayload, + process.env.JWT_TOKEN, + { expiresIn: this.accessTokenExpiry, algorithm: 'HS256' } + ); + + await supabase + .from('user_sessiontoken') + .update({ is_active: false }) + .eq('user_id', user.user_id); + + const rawRefreshToken = crypto.randomBytes(32).toString('hex'); + const hashedRefreshToken = await bcrypt.hash(rawRefreshToken, 12); + const lookupHash = this.createLookupHash(rawRefreshToken); + const expiresAt = new Date(Date.now() + this.refreshTokenExpiry); + + const { error } = await supabase + .from('user_sessiontoken') + .insert({ + user_id: user.user_id, + refresh_token: hashedRefreshToken, + refresh_token_lookup: lookupHash, + token_type: 'refresh', + device_info: deviceInfo, + ip_address: deviceInfo.ip || null, + user_agent: deviceInfo.userAgent || null, + expires_at: expiresAt.toISOString(), + is_active: true + }); + + if (error) throw error; + + return { + accessToken, + refreshToken: rawRefreshToken, + expiresIn: 15 * 60, + tokenType: 'Bearer' + }; } catch (error) { - throw new Error(`Token refresh failed: ${error.message}`); + throw new Error(`Token generation failed: ${error.message}`); } -} + } -/** - * Logout (secure hashed-token version) - */ -async logout(refreshToken) { + /* ========================= + Refresh Token + ========================= */ + async refreshAccessToken(refreshToken, deviceInfo = {}) { try { - // Get all active sessions - const { data: sessions } = await supabase - .from("user_session") - .select("id, refresh_token, is_active") - .eq("is_active", true); - - if (!sessions) return { success: true }; - - // Find matching hashed token - for (const s of sessions) { - if (await bcrypt.compare(refreshToken, s.refresh_token)) { - await supabase - .from("user_session") - .update({ is_active: false }) - .eq("id", s.id); - break; - } - } - - return { success: true, message: "Logout successful" }; + + + const lookupHash = this.createLookupHash(refreshToken); + + const { data: sessions, error } = await supabase + .from('user_sessiontoken') + .select(` + id, + user_id, + refresh_token, + refresh_token_lookup, + expires_at, + is_active + `) + .eq('refresh_token_lookup', lookupHash) + .eq('is_active', true) + .limit(1); + + console.log('supabase query result:', { sessions, error}); + + if (error || !sessions || sessions.length === 0) { + throw new Error('Invalid refresh token'); + } + + const session = sessions[0]; + + const match = await bcrypt.compare(refreshToken, session.refresh_token); + if (!match) throw new Error('Invalid refresh token'); + + if (new Date(session.expires_at) < new Date()) { + throw new Error('Refresh token expired'); + } + + const { data: user, error: userError } = await supabase + .from('users') + .select(` + user_id, + email, + name, + role_id, + account_status + `) + .eq('user_id', session.user_id) + .single(); + + if (userError || !user) { + throw new Error('User not found'); + } + + if (user.account_status !== 'active') { + throw new Error('Account is not active'); + } + + + const newTokens = await this.generateTokenPair(user, deviceInfo); + + await supabase + .from('user_sessiontoken') + .update({ is_active: false }) + .eq('id', session.id); + + return { + success: true, + ...newTokens + }; } catch (error) { - throw new Error(`Logout failed: ${error.message}`); + console.error('REFRESH FAILED:', error.message); + throw new Error(`Token refresh failed: ${error.message}`); } -} + } - - -/** - * Logout from all devices (same logic, we deactivate all sessions) - */ -async logoutAll(userId) { + /* ========================= + Logout + ========================= */ + async logout(refreshToken) { try { - await supabase - .from("user_session") - .update({ is_active: false }) - .eq("user_id", userId); + const lookupHash = this.createLookupHash(refreshToken); + + await supabase + .from('user_sessiontoken') + .update({ is_active: false }) + .eq('refresh_token_lookup', lookupHash); - return { success: true, message: "Logged out from all devices" }; + return { success: true, message: 'Logout successful' }; } catch (error) { - throw new Error(`Logout all failed: ${error.message}`); + throw new Error(`Logout failed: ${error.message}`); } -} + } + /* ========================= + Logout All + ========================= */ + async logoutAll(userId) { + try { + await supabase + .from('user_sessiontoken') + .update({ is_active: false }) + .eq('user_id', userId); - /** - * Verifying the Access Token - */ - verifyAccessToken(token) { - try { - const decoded = jwt.verify(token, process.env.JWT_TOKEN); - console.log("šŸ” Decoded token payload:", decoded); - return decoded; - } catch (error) { - console.error("āŒ Token verification failed:", error.message); - throw new Error('Invalid access token'); - } + return { success: true, message: 'Logged out from all devices' }; + } catch (error) { + throw new Error(`Logout all failed: ${error.message}`); } - - /** - * Logging authentication attempts - */ - async logAuthAttempt(userId, email, success, deviceInfo) { - try { - await supabase - .from('auth_logs') - .insert({ - user_id: userId, - email: email, - success: success, - ip_address: deviceInfo.ip || null, - created_at: new Date().toISOString() - }); - } catch (error) { - console.error('Failed to log auth attempt:', error); - } + } + + /* ========================= + Verify Access Token + ========================= */ + verifyAccessToken(token) { + return jwt.verify(token, process.env.JWT_TOKEN); + } + + /* ========================= + Auth Logs + ========================= */ + async logAuthAttempt(userId, email, success, deviceInfo) { + try { + await supabase + .from('auth_logs') + .insert({ + user_id: userId, + email, + success, + ip_address: deviceInfo.ip || null, + created_at: new Date().toISOString() + }); + } catch { + // silent by design } + } - /** - * Clean up expired sessions - */ - async cleanupExpiredSessions() { - try { - await supabase - .from('user_session') - .update({ is_active: false }) - .lt('expires_at', new Date().toISOString()); - } catch (error) { - console.error('Failed to cleanup expired sessions:', error); - } + /* ========================= + Cleanup + ========================= */ + async cleanupExpiredSessions() { + try { + await supabase + .from('user_sessiontoken') + .update({ is_active: false }) + .lt('expires_at', new Date().toISOString()); + } catch { + // silent by design } + } } -module.exports = new AuthService(); \ No newline at end of file +module.exports = new AuthService(); diff --git a/services/supabaseClient.js b/services/supabaseClient.js index a73fdb3..e7302a8 100644 --- a/services/supabaseClient.js +++ b/services/supabaseClient.js @@ -1,8 +1,7 @@ require('dotenv').config(); const { createClient } = require('@supabase/supabase-js'); -console.log("URL:", process.env.PERSONAL_SUPABASE_URL); // TEMP DEBUG -console.log("KEY:", process.env.PERSONAL_SUPABASE_ANON_KEY); // TEMP DEBUG + const supabase = createClient( process.env.SUPABASE_URL, diff --git a/test-authflow.js b/test-authflow.js deleted file mode 100644 index 1ed8592..0000000 --- a/test-authflow.js +++ /dev/null @@ -1,108 +0,0 @@ -require("dotenv").config(); -const axios = require("axios"); - -const BASE_URL = "http://localhost/api/auth"; - -let accessToken = ""; -let refreshToken = ""; - -// Helper for pretty logs -function log(title, data) { - console.log(`\nšŸ”µ ${title}...`); - console.log(JSON.stringify(data, null, 2)); -} - -// STEP 1 — SIGNUP user if not exists -async function testSignup() { - try { - const res = await axios.post("http://localhost/api/signup", { - email: "test@email.com", - password: "test123" - }); - - log("Signup Success", res.data); - } catch (err) { - console.log("šŸ”ø Signup skipped (user may already exist)"); - } -} - -// STEP 2 — LOGIN -async function testLogin() { - try { - const res = await axios.post(`${BASE_URL}/login`, { - email: "john@nutrihep.com", - password: "SecurePassword123!" - }); - - accessToken = res.data.accessToken; - refreshToken = res.data.refreshToken; - - log("Login Success", { - accessToken, - refreshToken - }); - } catch (err) { - console.log("āŒ Login failed:", err.response?.data || err.message); - } -} - -// STEP 3 — CALL PROTECTED ROUTE -async function testProtectedRoute() { - try { - const res = await axios.get(`${BASE_URL}/profile`, { - headers: { Authorization: `Bearer ${accessToken}` } - }); - - log("Protected Route Success", res.data); - } catch (err) { - console.log("āŒ Protected route failed:", err.response?.data || err.message); - } -} - -// STEP 4 — REFRESH TOKEN -async function testRefresh() { - try { - const res = await axios.post(`${BASE_URL}/refresh`, { - refreshToken - }); - - accessToken = res.data.accessToken; - refreshToken = res.data.refreshToken; - - log("Token Refresh Success", { - accessToken, - refreshToken - }); - - } catch (err) { - console.log("āŒ Refresh failed:", err.response?.data || err.message); - } -} - -// STEP 5 — LOGOUT -async function testLogout() { - try { - const res = await axios.post(`${BASE_URL}/logout`, { - refreshToken - }); - - log("Logout Success", res.data); - } catch (err) { - console.log("āŒ Logout failed:", err.response?.data || err.message); - } -} - -// RUN EVERYTHING IN ORDER -(async () => { - console.log("\nšŸš€ Starting full Auth Flow Test...\n"); - - await testSignup(); - await testLogin(); - await testProtectedRoute(); - await testRefresh(); - await testLogout(); - - console.log("\n✨ ALL TESTS COMPLETED\n"); -})(); - - diff --git a/testcapstone.js b/testcapstone.js deleted file mode 100644 index 5845e88..0000000 --- a/testcapstone.js +++ /dev/null @@ -1,24 +0,0 @@ -require("dotenv").config(); -const { createClient } = require("@supabase/supabase-js"); - -console.log("šŸ” Testing Capstone Supabase..."); - -const supabase = createClient( - process.env.SUPABASE_URL, // should be Capstone URL - process.env.SUPABASE_ANON_KEY // Capstone Anon Key -); - -async function test() { - console.log("šŸ”¹ SUPABASE_URL:", process.env.SUPABASE_URL); - console.log("šŸ”¹ SUPABASE_ANON_KEY:", process.env.SUPABASE_ANON_KEY); - - const { data, error } = await supabase - .from("users") - .select("user_id, email") - .limit(1); - - console.log("šŸ“Œ DATA:", data); - console.log("āŒ ERROR:", error); -} - -test(); From cd0b0397f4ed0df9b54ed7a0b4d610418703835a Mon Sep 17 00:00:00 2001 From: Dinakar Date: Fri, 23 Jan 2026 12:14:29 +1100 Subject: [PATCH 3/3] Finalize auth middleware and token handling --- middleware/authenticateToken.js | 93 +++++++-------------------------- services/authService.js | 34 +++++++----- services/supabaseClient.js | 15 ++++-- 3 files changed, 50 insertions(+), 92 deletions(-) diff --git a/middleware/authenticateToken.js b/middleware/authenticateToken.js index 6e626f5..463b85b 100644 --- a/middleware/authenticateToken.js +++ b/middleware/authenticateToken.js @@ -6,55 +6,28 @@ const authService = require('../services/authService'); * - Attaches decoded user payload to req.user */ const authenticateToken = (req, res, next) => { - - - const authHeader = req.headers['authorization']; - const token = authHeader && authHeader.split(' ')[1]; - - if (!token) { - return res.status(401).json({ - success: false, - error: 'Access token required', - code: 'TOKEN_MISSING' - }); - } - try { - const decoded = authService.verifyAccessToken(token); + const authHeader = req.headers.authorization; - // optional check - if (decoded.type && decoded.type !== 'access') { + if (!authHeader) { return res.status(401).json({ success: false, - error: 'Invalid token type', - code: 'INVALID_TOKEN_TYPE' + error: 'Authorization header missing', + code: 'TOKEN_MISSING' }); } - // āœ… safer payload validation - if (!decoded.userId || !decoded.role) { + const parts = authHeader.split(' '); + if (parts.length !== 2 || parts[0] !== 'Bearer') { return res.status(401).json({ success: false, - error: 'Invalid token payload', - code: 'INVALID_TOKEN' + error: 'Invalid authorization format', + code: 'INVALID_AUTH_HEADER' }); + } - const authHeader = req.headers["authorization"]; - const token = authHeader && authHeader.split(" ")[1]; - - const authHeader = req.headers.authorization; - const token = authHeader?.split(' ')[1]; // Bearer - - - if (!token) { - return res.status(401).json({ - success: false, - error: 'Access token missing', - code: 'TOKEN_MISSING' - }); - } + const token = parts[1]; - try { const decoded = authService.verifyAccessToken(token); // Ensure only access tokens are accepted @@ -66,54 +39,28 @@ const authenticateToken = (req, res, next) => { }); } - - try { - const decoded = authService.verifyAccessToken(token); - req.user = decoded; - next(); - } catch (error) { - return res.status(401).json({ - success: false, - error: "Invalid or expired access token", - code: "TOKEN_INVALID" - }); - - } - - req.user = decoded; // THIS FIXES PROFILE FETCH - next(); - - } catch (error) { - if (error.name === 'TokenExpiredError') { - return res.status(401).json({ - success: false, - error: 'Access token expired', - code: 'TOKEN_EXPIRED' - }); - } - - if (error.name === 'JsonWebTokenError') { + // Validate payload + if (!decoded.userId || !decoded.role) { return res.status(401).json({ success: false, - error: 'Invalid access token', + error: 'Invalid token payload', code: 'INVALID_TOKEN' }); } - console.error('Token verification error:', error); - return res.status(500).json({ - success: false, - error: 'Internal server error', - code: 'INTERNAL_ERROR' + // Attach user to request + req.user = { + userId: decoded.userId, + email: decoded.email, + role: decoded.role + }; - req.user = decoded; next(); - } catch (err) { + } catch (error) { return res.status(401).json({ success: false, error: 'Invalid or expired access token', code: 'TOKEN_INVALID' - }); } }; diff --git a/services/authService.js b/services/authService.js index 7aba86b..a635c66 100644 --- a/services/authService.js +++ b/services/authService.js @@ -1,13 +1,19 @@ +console.log("🟢 Loaded AuthService from:", __filename); const { createClient } = require('@supabase/supabase-js'); const jwt = require('jsonwebtoken'); const bcrypt = require('bcrypt'); const crypto = require('crypto'); -const supabase = createClient( +const supabaseAnon = createClient( process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY ); +const supabaseService = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_SERVICE_ROLE_KEY +); + class AuthService { constructor() { this.accessTokenExpiry = '15m'; @@ -32,7 +38,7 @@ class AuthService { const { name, email, password, first_name, last_name } = userData; try { - const { data: existingUser } = await supabase + const { data: existingUser } = await supabaseAnon .from('users') .select('user_id') .eq('email', email) @@ -44,7 +50,7 @@ class AuthService { const hashedPassword = await bcrypt.hash(password, 12); - const { data: newUser, error } = await supabase + const { data: newUser, error } = await supabaseAnon .from('users') .insert({ name, @@ -80,7 +86,7 @@ class AuthService { const { email, password } = loginData; try { - const { data: user, error } = await supabase + const { data: user, error } = await supabaseAnon .from('users') .select(` user_id, email, password, name, role_id, @@ -98,7 +104,7 @@ class AuthService { const tokens = await this.generateTokenPair(user, deviceInfo); - await supabase + await supabaseAnon .from('users') .update({ last_login: new Date().toISOString() }) .eq('user_id', user.user_id); @@ -139,7 +145,7 @@ class AuthService { { expiresIn: this.accessTokenExpiry, algorithm: 'HS256' } ); - await supabase + await supabaseService .from('user_sessiontoken') .update({ is_active: false }) .eq('user_id', user.user_id); @@ -149,7 +155,7 @@ class AuthService { const lookupHash = this.createLookupHash(rawRefreshToken); const expiresAt = new Date(Date.now() + this.refreshTokenExpiry); - const { error } = await supabase + const { error } = await supabaseService .from('user_sessiontoken') .insert({ user_id: user.user_id, @@ -185,7 +191,7 @@ class AuthService { const lookupHash = this.createLookupHash(refreshToken); - const { data: sessions, error } = await supabase + const { data: sessions, error } = await supabaseService .from('user_sessiontoken') .select(` id, @@ -214,7 +220,7 @@ class AuthService { throw new Error('Refresh token expired'); } - const { data: user, error: userError } = await supabase + const { data: user, error: userError } = await supabaseAnon .from('users') .select(` user_id, @@ -237,7 +243,7 @@ class AuthService { const newTokens = await this.generateTokenPair(user, deviceInfo); - await supabase + await supabaseService .from('user_sessiontoken') .update({ is_active: false }) .eq('id', session.id); @@ -259,7 +265,7 @@ class AuthService { try { const lookupHash = this.createLookupHash(refreshToken); - await supabase + await supabaseService .from('user_sessiontoken') .update({ is_active: false }) .eq('refresh_token_lookup', lookupHash); @@ -275,7 +281,7 @@ class AuthService { ========================= */ async logoutAll(userId) { try { - await supabase + await supabaseService .from('user_sessiontoken') .update({ is_active: false }) .eq('user_id', userId); @@ -298,7 +304,7 @@ class AuthService { ========================= */ async logAuthAttempt(userId, email, success, deviceInfo) { try { - await supabase + await supabaseAnon .from('auth_logs') .insert({ user_id: userId, @@ -317,7 +323,7 @@ class AuthService { ========================= */ async cleanupExpiredSessions() { try { - await supabase + await supabaseService .from('user_sessiontoken') .update({ is_active: false }) .lt('expires_at', new Date().toISOString()); diff --git a/services/supabaseClient.js b/services/supabaseClient.js index e7302a8..a396c63 100644 --- a/services/supabaseClient.js +++ b/services/supabaseClient.js @@ -1,11 +1,16 @@ -require('dotenv').config(); const { createClient } = require('@supabase/supabase-js'); - - -const supabase = createClient( +const supabaseAnon = createClient( process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY ); -module.exports = supabase; +const supabaseService = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_SERVICE_ROLE_KEY +); + +module.exports = { + supabaseAnon, + supabaseService +};