diff --git a/middleware/authenticateToken.js b/middleware/authenticateToken.js index 772411b..463b85b 100644 --- a/middleware/authenticateToken.js +++ b/middleware/authenticateToken.js @@ -1,94 +1,68 @@ const authService = require('../services/authService'); /** - * Enhanced 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]; - - 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' }); } - req.user = decoded; // THIS FIXES PROFILE FETCH - next(); + const token = parts[1]; - } catch (error) { - if (error.name === 'TokenExpiredError') { + const decoded = authService.verifyAccessToken(token); + + // Ensure only access tokens are accepted + if (!decoded || decoded.type !== 'access') { return res.status(401).json({ success: false, - error: 'Access token expired', - code: 'TOKEN_EXPIRED' + error: 'Invalid token type', + code: 'INVALID_TOKEN_TYPE' }); } - 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({ + // Attach user to request + req.user = { + userId: decoded.userId, + email: decoded.email, + role: decoded.role + }; + + next(); + } catch (error) { + return res.status(401).json({ success: false, - error: 'Internal server error', - code: 'INTERNAL_ERROR' + error: 'Invalid or expired access token', + code: 'TOKEN_INVALID' }); } }; -/** - * 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..a635c66 100644 --- a/services/authService.js +++ b/services/authService.js @@ -4,324 +4,333 @@ 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 ); -class AuthService { - constructor() { - this.accessTokenExpiry = '15m'; // 15 minutes - this.refreshTokenExpiry = 7 * 24 * 60 * 60 * 1000; // 7 days - } - - /** - * 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}`); - } - } +const supabaseService = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_SERVICE_ROLE_KEY +); - /** - * 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; - } +class AuthService { + 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; + + try { + const { data: existingUser } = await supabaseAnon + .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 supabaseAnon + .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}`); } - - /** - * 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 - const refreshToken = crypto.randomBytes(40).toString('hex'); - const expiresAt = new Date(Date.now() + this.refreshTokenExpiry); - - // 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; - - return { - accessToken, - refreshToken, - expiresIn: 15 * 60, // 15 minutes in seconds - tokenType: 'Bearer' - }; - - } catch (error) { - throw new Error(`Token generation failed: ${error.message}`); - } + } + + /* ========================= + Login + ========================= */ + async login(loginData, deviceInfo = {}) { + const { email, password } = loginData; + + try { + const { data: user, error } = await supabaseAnon + .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 supabaseAnon + .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 - */ - 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(); - - if (error || !session) { - throw new Error('Invalid refresh token'); - } - - // Check if the token is expired - if (new Date(session.expires_at) < new Date()) { - throw new Error('Refresh token expired'); - } - - // Checking User Status - const user = session.users; - if (user.account_status !== 'active') { - throw new Error('Account is not active'); - } - - // Generate a new token pair - const newTokens = await this.generateTokenPair(user, deviceInfo); - - // Invalidate old refresh tokens - await supabase - .from('user_session') - .update({ is_active: false }) - .eq('id', session.id); - - return { - success: true, - ...newTokens - }; - - } catch (error) { - throw new Error(`Token refresh failed: ${error.message}`); - } + } + + /* ========================= + Generate Tokens + ========================= */ + async generateTokenPair(user, deviceInfo = {}) { + try { + 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 supabaseService + .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 supabaseService + .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 generation failed: ${error.message}`); } - - /** - * Logout - */ - async logout(refreshToken) { - try { - if (refreshToken) { - await supabase - .from('user_session') - .update({ is_active: false }) - .eq('refresh_token', refreshToken); - } - - return { success: true, message: 'Logout successful' }; - } catch (error) { - throw new Error(`Logout failed: ${error.message}`); - } + } + + /* ========================= + Refresh Token + ========================= */ + async refreshAccessToken(refreshToken, deviceInfo = {}) { + try { + + + const lookupHash = this.createLookupHash(refreshToken); + + const { data: sessions, error } = await supabaseService + .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 supabaseAnon + .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 supabaseService + .from('user_sessiontoken') + .update({ is_active: false }) + .eq('id', session.id); + + return { + success: true, + ...newTokens + }; + } catch (error) { + console.error('REFRESH FAILED:', error.message); + throw new Error(`Token refresh 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 + ========================= */ + async logout(refreshToken) { + try { + const lookupHash = this.createLookupHash(refreshToken); + + await supabaseService + .from('user_sessiontoken') + .update({ is_active: false }) + .eq('refresh_token_lookup', lookupHash); + + return { success: true, message: 'Logout successful' }; + } catch (error) { + throw new Error(`Logout failed: ${error.message}`); } - - /** - * 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'); - } + } + + /* ========================= + Logout All + ========================= */ + async logoutAll(userId) { + try { + await supabaseService + .from('user_sessiontoken') + .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}`); } - - /** - * 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 supabaseAnon + .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 supabaseService + .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 new file mode 100644 index 0000000..a396c63 --- /dev/null +++ b/services/supabaseClient.js @@ -0,0 +1,16 @@ +const { createClient } = require('@supabase/supabase-js'); + +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 +); + +module.exports = { + supabaseAnon, + supabaseService +};