From 90554cfad41e0a58352ecd56abef8318bb554ee4 Mon Sep 17 00:00:00 2001 From: rxmox Date: Thu, 27 Nov 2025 16:04:54 -0700 Subject: [PATCH 1/5] setup jwt utility functions --- shatter-backend/src/utils/jwt_utils.ts | 70 ++++++++++++++++++++++++++ shatter-backend/src/utils/test_jwt.ts | 51 +++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 shatter-backend/src/utils/jwt_utils.ts create mode 100644 shatter-backend/src/utils/test_jwt.ts diff --git a/shatter-backend/src/utils/jwt_utils.ts b/shatter-backend/src/utils/jwt_utils.ts new file mode 100644 index 0000000..5442035 --- /dev/null +++ b/shatter-backend/src/utils/jwt_utils.ts @@ -0,0 +1,70 @@ +import jwt from 'jsonwebtoken'; + +// Get JWT secret from .env +const JWT_SECRET = process.env.JWT_SECRET || ''; +const JWT_EXPIRATION = '30d'; // set token to expire in 30 days + +// Validate that secret actually exists +if (!JWT_SECRET) { + console.warn('WARNING: JWT_SECRET not set in environment variables!'); +} + +/** + * Generate a JWT token for a user + * + * @param userId - The user's MongoDB _id + * @returns A signed JWT token string + * + * @example + * const token = generateToken('673abc123def456'); + * // Returns: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + */ +export const generateToken = (userId: string): string => { + try{ + // create and sign the token + const token = jwt.sign( + { userId }, // payload - data we want to store + JWT_SECRET, // Secret key - proves token is real + { expiresIn: JWT_EXPIRATION} // Options - token expires in 30 days + ); + + return token; + + } catch (error) { + console.error('Error generating JWT token:', error); + throw new Error('Failed to generate authentication token'); + } +}; + +/** + * Verify a JWT token and extract the userId + * + * @param token - The JWT token string to verify + * @returns Object containing the userId + * @throws Error if token is invalid or expired + * + * @example + * const decoded = verifyToken('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'); + * console.log(decoded.userId); // "673abc123def456" + */ +export const verifyToken = (token: string): { userId: string } => { + try { + // verify the token signature and decode + const decoded = jwt.verify(token, JWT_SECRET) as { userId: string }; + + return decoded; + + } catch (error) { + // Handle specific JWT errors + if (error instanceof jwt.TokenExpiredError) { + throw new Error('Token expired'); + } + if (error instanceof jwt.JsonWebTokenError) { + throw new Error('Invalid token'); + } + + // Generic error + console.error('Error verifying JWT token:', error); + throw new Error('Token verification failed'); + } +}; diff --git a/shatter-backend/src/utils/test_jwt.ts b/shatter-backend/src/utils/test_jwt.ts new file mode 100644 index 0000000..edd99e8 --- /dev/null +++ b/shatter-backend/src/utils/test_jwt.ts @@ -0,0 +1,51 @@ +import dotenv from 'dotenv'; +// Load environment variables +dotenv.config(); + +import { generateToken, verifyToken } from './jwt_utils'; + + +function testJWT() { + console.log('๐Ÿงช Testing JWT Utilities...\n'); + + // Test 1: Generate a token + console.log('๐Ÿ“ Test 1: Generating token...'); + const userId = '673abc123def456789'; + const token = generateToken(userId); + console.log('โœ… Token generated:', token); + console.log(' Token length:', token.length, 'characters\n'); + + // Test 2: Verify a valid token + console.log('๐Ÿ“ Test 2: Verifying valid token...'); + try { + const decoded = verifyToken(token); + console.log('โœ… Token verified successfully'); + console.log(' Decoded userId:', decoded.userId); + console.log(' Matches original?', decoded.userId === userId ? 'YES โœ…' : 'NO โŒ\n'); + } catch (error: any) { + console.log('โŒ Verification failed:', error.message); + } + + // Test 3: Try to verify an invalid token + console.log('\n๐Ÿ“ Test 3: Testing invalid token...'); + try { + const fakeToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.fake.signature'; + verifyToken(fakeToken); + console.log('โŒ Invalid token was accepted (this should not happen!)'); + } catch (error: any) { + console.log('โœ… Invalid token correctly rejected'); + console.log(' Error:', error.message); + } + + // Test 4: Token structure + console.log('\n๐Ÿ“ Test 4: Token structure...'); + const parts = token.split('.'); + console.log('โœ… Token has', parts.length, 'parts (should be 3)'); + console.log(' Part 1 (Header):', parts[0].substring(0, 20) + '...'); + console.log(' Part 2 (Payload):', parts[1].substring(0, 20) + '...'); + console.log(' Part 3 (Signature):', parts[2].substring(0, 20) + '...'); + + console.log('\n๐ŸŽ‰ All JWT tests completed!'); +} + +testJWT(); From 9d33f6d062c51e5f11b1941efd0f2214dd94be3a Mon Sep 17 00:00:00 2001 From: rxmox Date: Thu, 27 Nov 2025 16:12:31 -0700 Subject: [PATCH 2/5] updating json files --- shatter-backend/package-lock.json | 121 +++++++++++++++++++++++++++++- shatter-backend/package.json | 2 + 2 files changed, 122 insertions(+), 1 deletion(-) diff --git a/shatter-backend/package-lock.json b/shatter-backend/package-lock.json index b537f61..f98aee8 100644 --- a/shatter-backend/package-lock.json +++ b/shatter-backend/package-lock.json @@ -12,6 +12,7 @@ "bcryptjs": "^3.0.3", "dotenv": "^17.2.3", "express": "^5.1.0", + "jsonwebtoken": "^9.0.2", "mongoose": "^8.19.2", "zod": "^4.1.12" }, @@ -19,6 +20,7 @@ "@eslint/js": "^9.38.0", "@types/bcryptjs": "^2.4.6", "@types/express": "^5.0.5", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.9.2", "eslint": "^9.38.0", "globals": "^16.4.0", @@ -466,6 +468,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -473,6 +486,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.9.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", @@ -1009,6 +1029,12 @@ "node": ">=16.20.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -1273,6 +1299,15 @@ "xtend": "^4.0.0" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2091,6 +2126,49 @@ "dev": true, "license": "MIT" }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kareem": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", @@ -2140,6 +2218,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2147,6 +2261,12 @@ "dev": true, "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/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -2814,7 +2934,6 @@ "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" diff --git a/shatter-backend/package.json b/shatter-backend/package.json index b3696eb..0eb8397 100644 --- a/shatter-backend/package.json +++ b/shatter-backend/package.json @@ -16,6 +16,7 @@ "bcryptjs": "^3.0.3", "dotenv": "^17.2.3", "express": "^5.1.0", + "jsonwebtoken": "^9.0.2", "mongoose": "^8.19.2", "zod": "^4.1.12" }, @@ -23,6 +24,7 @@ "@eslint/js": "^9.38.0", "@types/bcryptjs": "^2.4.6", "@types/express": "^5.0.5", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.9.2", "eslint": "^9.38.0", "globals": "^16.4.0", From 69ca14d18e81496009e63f1cbc302a2b4181a9ea Mon Sep 17 00:00:00 2001 From: rxmox Date: Thu, 27 Nov 2025 20:01:15 -0700 Subject: [PATCH 3/5] started auth middleware --- .../src/middleware/auth_middleware.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 shatter-backend/src/middleware/auth_middleware.ts diff --git a/shatter-backend/src/middleware/auth_middleware.ts b/shatter-backend/src/middleware/auth_middleware.ts new file mode 100644 index 0000000..5b80f02 --- /dev/null +++ b/shatter-backend/src/middleware/auth_middleware.ts @@ -0,0 +1,32 @@ +import { Request, Response, NextFunction } from 'express'; +import { verifyToken } from '../utils/jwt_utils'; + +/** + * Extend Express Request type to include user property + * This allows us to attach user info to the request object + */ +declare global { + namespace Express { + interface Request { + user?: { + userId: string; + }; + } + } +} + +/** + * Authentication Middleware + * Verifies JWT token and attaches user info to request + * + * Usage: + * router.get('/protected', authMiddleware, controller); + * + * Request must include: + * Authorization: Bearer + */ +export const authMiddleware = async ( + req: Request, + res: Response, + next: NextFunction +) => From 8511c2b0291ff192467c3ffa9e993e99bad8d3cd Mon Sep 17 00:00:00 2001 From: rxmox Date: Thu, 4 Dec 2025 19:20:07 -0700 Subject: [PATCH 4/5] created Auth middleware --- .../src/middleware/auth_middleware.ts | 67 ++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/shatter-backend/src/middleware/auth_middleware.ts b/shatter-backend/src/middleware/auth_middleware.ts index 5b80f02..69d4c1f 100644 --- a/shatter-backend/src/middleware/auth_middleware.ts +++ b/shatter-backend/src/middleware/auth_middleware.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-namespace */ import { Request, Response, NextFunction } from 'express'; import { verifyToken } from '../utils/jwt_utils'; @@ -29,4 +30,68 @@ export const authMiddleware = async ( req: Request, res: Response, next: NextFunction -) => +) => { + try { + // step 1: Get Authorization header + const authHeader = req.headers.authorization; + + if (!authHeader) { + return res.status(401).json({ + error: 'Authorization header missing', + }); + } + + // Step 2: extract token from "Bearer " format + const parts = authHeader.split(' '); + + if (parts.length !== 2) { + return res.status(401).json({ + error: 'Invalid authorization format. Use: Bearer ', + }); + } + + if (parts[0] !== 'Bearer') { + return res.status(401).json({ + error: 'Invalid authorization format. Must start with "Bearer"', + }); + } + + const token = parts[1]; + + if (!token) { + return res.status(401).json({ + error: 'Token is empty', + }); + } + + // Step 3: verify token using JWT utility + const decoded = verifyToken(token) + + // Step 4: Attach user info to request object + req.user = { + userId: decoded.userId, + }; + + // step 5: Continue to next Middleware/controller + next(); + } catch (error: any) { + // Handle specific JWT errors thrown by jwt_utils + if(error?.message === 'Token expired') { + return res.status(401).json({ + error: 'Token expired. Please login again.', + }); + } + + if (error?.message === 'Invalid token') { + return res.status(401).json({ + error: 'Invalid token. Please login again.', + }); + } + + // Generic error + console.error('Auth middleware error:', error); + return res.status(401).json({ + error: 'Authentication failed', + }); + } +}; From 8d4b7c166b189a01eb26db358a8d7671a53cc37f Mon Sep 17 00:00:00 2001 From: rxmox Date: Thu, 15 Jan 2026 13:12:05 -0700 Subject: [PATCH 5/5] Complete JWT token implementation (TICKET-17 & TICKET-18) - Add JWT token to login response in auth_controller.ts * Import generateToken from jwt_utils * Generate token after successful login * Return token alongside userId in response - Add protected /me endpoint in user_route.ts * Import and apply authMiddleware * Returns current user's profile (excluding password) * Demonstrates JWT authentication working This completes the JWT authentication implementation. Users now receive a token on login that can be used to access protected routes. --- .../src/controllers/auth_controller.ts | 10 ++++++--- shatter-backend/src/routes/user_route.ts | 22 ++++++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/shatter-backend/src/controllers/auth_controller.ts b/shatter-backend/src/controllers/auth_controller.ts index 13bba1f..d796701 100644 --- a/shatter-backend/src/controllers/auth_controller.ts +++ b/shatter-backend/src/controllers/auth_controller.ts @@ -1,6 +1,7 @@ import { Request, Response } from 'express'; import { User } from '../models/user_model'; import { hashPassword, comparePassword } from '../utils/password_hash'; +import { generateToken } from '../utils/jwt_utils'; // Email validation regex const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/; @@ -148,11 +149,14 @@ export const login = async (req: Request, res: Response) => { user.lastLogin = new Date(); await user.save(); // save the updated user - // 9 - return success + // 9 - generate JWT token for the user + const token = generateToken(user._id.toString()); + + // 10 - return success with token res.status(200).json({ message: 'Login successful', - userId: user._id - // TODO: figure out a way to add JWT token here + userId: user._id, + token }); } catch (err: any) { diff --git a/shatter-backend/src/routes/user_route.ts b/shatter-backend/src/routes/user_route.ts index a86e2cf..a3d9720 100644 --- a/shatter-backend/src/routes/user_route.ts +++ b/shatter-backend/src/routes/user_route.ts @@ -1,9 +1,25 @@ -import { Router } from 'express'; +import { Router, Request, Response } from 'express'; import { getUsers, createUser } from '../controllers/user_controller'; +import { authMiddleware } from '../middleware/auth_middleware'; +import { User } from '../models/user_model'; const router = Router(); -router.get('/', getUsers); -router.post('/', createUser); +router.get('/', getUsers); +router.post('/', createUser); + +// Protected route example - returns current user's info +router.get('/me', authMiddleware, async (req: Request, res: Response) => { + try { + const user = await User.findById(req.user?.userId).select('-passwordHash'); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + res.json(user); + } catch (err) { + console.error('GET /api/users/me error:', err); + res.status(500).json({ error: 'Failed to fetch user' }); + } +}); export default router;