Skip to content
This repository was archived by the owner on Apr 16, 2025. It is now read-only.

Commit 60b6d12

Browse files
committed
Added login endpoint, implemented authentication function, and created /users/me endpoint requiring authorization.
1 parent c989e33 commit 60b6d12

File tree

14 files changed

+298
-39
lines changed

14 files changed

+298
-39
lines changed

nodemon.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{
2-
"watch": ["src"],
3-
"ext": "ts",
4-
"ignore": ["node_modules", "dist", "tests"],
5-
"exec": "ts-node ./src/index.ts",
6-
"env": {
7-
"NODE_ENV": "development"
8-
},
9-
"delay": 1000,
10-
"verbose": true
11-
}
2+
"watch": ["src"],
3+
"ext": "ts",
4+
"ignore": ["node_modules", "dist", "tests"],
5+
"exec": "ts-node --files ./src/index.ts",
6+
"env": {
7+
"NODE_ENV": "development"
8+
},
9+
"delay": 1000,
10+
"verbose": true
11+
}

src/app.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,12 @@ app.use(express.urlencoded({ extended: true }));
4848
*/
4949
app.use(
5050
morgan(
51-
configuration.NODE_ENV === "development"
52-
? "dev"
53-
: "combined",
51+
configuration.NODE_ENV === "production"
52+
? "combined"
53+
: "dev",
54+
{
55+
skip: () => configuration.NODE_ENV === "test",
56+
},
5457
),
5558
);
5659

src/controllers/UserController.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import mongoose from "mongoose";
12
import UserModel, {
23
IUserDocument,
34
} from "../database/models/UserModel"; // Import the user model and IUserDocument interface
@@ -55,4 +56,18 @@ export default class UserController {
5556
// Return true if a document was deleted, otherwise return false
5657
return result.deletedCount > 0;
5758
}
59+
60+
public static async findUserById(
61+
id: mongoose.Types.ObjectId,
62+
): Promise<Omit<IUserDocument, "password"> | null> {
63+
const user = await UserModel.findOne<IUserDocument>({
64+
$or: [
65+
{
66+
_id: id,
67+
},
68+
],
69+
}).select("-password");
70+
71+
return user;
72+
}
5873
}

src/errors/ForbiddenError.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import BaseError from "./BaseError";
2+
3+
/**
4+
* Class representing a ForbiddenError.
5+
* This error is used to indicate that the client does not have permission
6+
* to access a specific resource or perform a specific action.
7+
* It extends the BaseError class to include a custom error message and a 403 status code.
8+
*/
9+
export default class ForbiddenError extends BaseError {
10+
/**
11+
* Constructor for ForbiddenError.
12+
* By default, it sets the message to "ForbiddenError" and the HTTP status code to 403.
13+
*
14+
* @param message - Custom error message (optional).
15+
*/
16+
public constructor(message = "ForbiddenError") {
17+
// Pass the message and status code to the BaseError constructor
18+
super(message, 403);
19+
}
20+
}

src/errors/UnauthorizedError.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import BaseError from "./BaseError";
2+
3+
/**
4+
* Class representing an UnauthorizedError.
5+
* This error is used when authentication is required but has failed or has not been provided.
6+
* It extends the BaseError class to include a custom error message and a 401 status code.
7+
*/
8+
export default class UnauthorizedError extends BaseError {
9+
/**
10+
* Constructor for UnauthorizedError.
11+
* By default, it sets the message to "Unauthorized" and the HTTP status code to 401.
12+
*
13+
* @param message - Custom error message (optional).
14+
*/
15+
public constructor(message = "Unauthorized") {
16+
// Pass the message and status code to the BaseError constructor
17+
super(message, 401);
18+
}
19+
}

src/middlewares/authentication.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type {
2+
Request,
3+
Response,
4+
NextFunction,
5+
} from "express";
6+
import UnauthorizedError from "../errors/UnauthorizedError"; // Import the custom UnauthorizedError class for handling authorization errors
7+
import { verifyToken } from "../utils/jsonwebtoken"; // Import the utility function to verify JWT tokens
8+
import ForbiddenError from "../errors/ForbiddenError"; // Import the custom ForbiddenError class for handling access errors
9+
10+
/**
11+
* Middleware function for authenticating requests using JWT.
12+
* This function checks for the presence of a valid JWT in the Authorization header.
13+
*
14+
* @param req - The request object.
15+
* @param res - The response object.
16+
* @param next - The next middleware function to call.
17+
*/
18+
export function authentication(
19+
req: Request,
20+
res: Response,
21+
next: NextFunction,
22+
): void {
23+
// Retrieve the Authorization header from the request
24+
const authHeader = req.headers["authorization"];
25+
26+
// Check if the Authorization header is missing
27+
if (!authHeader) {
28+
// If missing, call the next middleware with an UnauthorizedError
29+
return next(
30+
new UnauthorizedError(
31+
"Authorization header is missing.", // Error message for missing header
32+
),
33+
);
34+
}
35+
36+
// Extract the token from the Authorization header (format: "Bearer <token>")
37+
const token = authHeader.split(" ")[1];
38+
39+
// Verify the token using the verifyToken utility function
40+
const payload = verifyToken(token);
41+
42+
// Check if the payload is invalid or missing
43+
if (!payload) {
44+
// If verification fails, call the next middleware with a ForbiddenError
45+
return next(
46+
new ForbiddenError("Invalid or expired token."), // Error message for invalid/expired token
47+
);
48+
}
49+
50+
// Attach the decoded user payload to the request object for later use
51+
req.user = payload;
52+
53+
// Proceed to the next middleware or route handler
54+
return next();
55+
}

src/routes/auth/login.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export async function login(
6262

6363
// Generate a JWT access token for the authenticated user
6464
const accessTokenResult = generateAccessToken({
65+
id: user.id,
6566
username: user.username,
6667
});
6768

src/routes/routes.ts

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
import {
2-
type Request, // Type for the HTTP request object from Express
3-
type Response, // Type for the HTTP response object from Express
4-
Router, // Function from Express used to create routers
2+
type Request, // Type definition for the HTTP request object from Express.
3+
type Response, // Type definition for the HTTP response object from Express.
4+
Router, // Function from Express used to create a new router instance.
55
} from "express";
6-
import configuration from "../utils/configuration"; // Import the configuration utility for API versioning
7-
import getHome from "./homes"; // Import the getHome function to handle the home route
8-
import register from "./auth/register"; // Import the register function to handle user registration
9-
import { login } from "./auth/login"; // Import the login function to handle user login
6+
import configuration from "../utils/configuration"; // Import the configuration utility for accessing application settings, including API versioning.
7+
import getHome from "./homes"; // Import the getHome function to handle requests to the home route.
8+
import register from "./auth/register"; // Import the register function for handling user registration requests.
9+
import { login } from "./auth/login"; // Import the login function for handling user login requests.
10+
import { authentication } from "../middlewares/authentication"; // Import the authentication middleware to protect routes that require authentication.
11+
import { profile } from "./users"; // Import the profile function to handle user profile retrieval.
1012

1113
/**
12-
* Create a new Router instance for defining API routes.
14+
* Create a new Router instance to define the API routes.
1315
*/
1416
const router = Router();
1517

1618
/**
17-
* Define the base URL for API versioning.
18-
* The version is fetched from the configuration utility.
19+
* Define the base URL for the API, incorporating versioning from the configuration.
20+
* The version is fetched dynamically to ensure flexibility during updates.
1921
*/
2022
const baseURL = `/api/v${configuration.API_VERSION}`;
2123

@@ -25,38 +27,50 @@ const baseURL = `/api/v${configuration.API_VERSION}`;
2527
const authURL = `${baseURL}/auth`;
2628

2729
/**
28-
* Redirects from the root endpoint to the base API URL.
29-
* This route handles GET requests to the root ("/") and redirects them to the base URL.
30+
* Construct the users URL using the base URL.
31+
*/
32+
const usersURL = `${baseURL}/users`;
33+
34+
/**
35+
* Redirect from the root endpoint ("/") to the base API URL.
36+
* This route handles GET requests to the root and redirects them to the defined base URL.
3037
*
3138
* @param req - The HTTP request object.
3239
* @param res - The HTTP response object.
3340
* @returns A redirection response to the base URL.
3441
*/
3542
router.get("/", (req: Request, res: Response) => {
36-
return res.redirect(baseURL); // Redirect to the base URL
43+
return res.redirect(baseURL); // Redirect to the base API URL.
3744
});
3845

3946
/**
4047
* Define the GET route for the base API URL.
41-
* This route handles GET requests to the base URL and executes the getHome function.
48+
* This route handles GET requests to the base URL and executes the getHome function,
49+
* which returns relevant home data.
4250
*
4351
* @returns A JSON response containing home data.
4452
*/
45-
router.get(baseURL, getHome); // Handle GET requests to the base URL
53+
router.get(baseURL, getHome); // Handle GET requests to the base URL.
4654

4755
/**
4856
* Define the POST route for user registration.
49-
* This route handles POST requests to the authentication URL for user registration.
57+
* This route handles POST requests to the authentication URL for registering new users.
5058
*/
51-
router.post(`${authURL}/register`, register); // Handle user registration
59+
router.post(`${authURL}/register`, register); // Handle user registration requests.
5260

5361
/**
5462
* Define the POST route for user login.
55-
* This route handles POST requests to the authentication URL for user login.
63+
* This route handles POST requests to the authentication URL for user login operations.
64+
*/
65+
router.post(`${authURL}/login`, login); // Handle user login requests.
66+
67+
/**
68+
* Define the GET route for retrieving the authenticated user's profile.
69+
* This route requires authentication middleware to ensure only logged-in users can access it.
5670
*/
57-
router.post(`${authURL}/login`, login); // Handle user login
71+
router.get(`${usersURL}/me`, authentication, profile); // Handle GET requests for the authenticated user's profile.
5872

5973
/**
60-
* Export the router instance for use in other modules.
74+
* Export the configured router instance for use in other modules.
6175
*/
62-
export default router; // Export the configured router instance
76+
export default router; // Export the router instance for application routing.

src/routes/users.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type {
2+
Request,
3+
Response,
4+
NextFunction,
5+
} from "express"; // Importing necessary types from Express framework.
6+
import UserController from "../controllers/UserController"; // Importing the UserController to handle user-related operations.
7+
import NotFoundError from "../errors/NotFoundError"; // Importing a custom error class for handling not found errors.
8+
import { IDataResponse } from "../types/response"; // Importing the response interface for standardized responses.
9+
import { IUserDocument } from "../database/models/UserModel"; // Importing the user document interface to define the structure of user data.
10+
11+
/**
12+
* Middleware function to retrieve and return the profile information of the authenticated user.
13+
* @param req - The Express request object, containing user information in req.user.
14+
* @param res - The Express response object used to send back the desired HTTP response.
15+
* @param next - The next middleware function in the Express stack to call if an error occurs.
16+
* @returns A JSON response containing user data without the password field, or an error if the user is not found.
17+
*/
18+
export async function profile(
19+
req: Request,
20+
res: Response,
21+
next: NextFunction,
22+
): Promise<Response<
23+
IDataResponse<Omit<IUserDocument, "password">>
24+
> | void> {
25+
// Attempt to find the user by ID from the authenticated request.
26+
const user = await UserController.findUserById(
27+
req.user.id,
28+
);
29+
30+
// If the user is not found, call the next middleware with a NotFoundError.
31+
if (!user) {
32+
return next(new NotFoundError("User Not Found."));
33+
}
34+
35+
// Prepare the response data, excluding the password field for security reasons.
36+
const response: IDataResponse<
37+
Omit<IUserDocument, "password">
38+
> = {
39+
message: "User Data", // Message indicating success.
40+
status: "success", // Status of the response.
41+
data: user, // User data returned in the response.
42+
};
43+
44+
// Send a 200 OK response with the user data in JSON format.
45+
return res.status(200).json(response);
46+
}

src/types/Payload.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import { IUser } from "../database/models/UserModel"; // Import the IUser interface, representing a user schema from the database
1+
import mongoose from "mongoose";
22

33
/**
44
* Type definition for the JWT payload.
55
* The payload will contain user data, but the password field is omitted for security reasons.
66
*/
7-
export type Payload = Omit<IUser, "password">; // Omit the 'password' field from IUser, as it should not be included in the token
7+
export type Payload = {
8+
id: mongoose.Types.ObjectId;
9+
username: string;
10+
}; // Omit the 'password' field from IUser, as it should not be included in the token

src/types/declare.d.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { Payload } from "./Payload";
2+
13
/**
24
* Augmenting the global NodeJS namespace to include custom environment variable types.
35
* This allows TypeScript to understand the structure of the process.env object.
@@ -52,16 +54,34 @@ declare global {
5254
* affecting the time taken to hash a password. Higher values increase security but also the time required.
5355
*/
5456
SALT_ROUNDS: string;
57+
5558
/**
56-
* JWT Secret Key
59+
* The secret key used for signing JWT tokens.
60+
* This is a critical value for token security and should be kept secret.
5761
*/
5862
SECRET_KEY: string;
63+
5964
/**
60-
* Jwt Access Token EXPIRES_IN
65+
* Defines the expiration time for JWT tokens.
66+
* Typically in a format such as '1h' (1 hour) or '7d' (7 days).
6167
*/
6268
EXPIRES_IN: string;
6369
}
6470
}
71+
72+
/**
73+
* Augmenting the Express namespace to include custom user data in the Request object.
74+
* This allows access to the authenticated user's payload within request handlers.
75+
*/
76+
namespace Express {
77+
interface Request {
78+
/**
79+
* The user payload extracted from the JWT token.
80+
* This will be available on the request object after authentication.
81+
*/
82+
user: Payload;
83+
}
84+
}
6585
}
6686

6787
/**

src/utils/jsonwebtoken.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,26 @@ export function generateAccessToken(
3636
expiresIn: configuration.EXPIRES_IN, // Expiration time for the token
3737
};
3838
}
39+
40+
/**
41+
* Function to verify a JWT token.
42+
* This function checks the validity of the token using the secret key and returns the decoded payload.
43+
*
44+
* @param token - The JWT access token string to be verified.
45+
* @returns The decoded payload if the token is valid, otherwise null.
46+
*/
47+
export function verifyToken(token: string): Payload | null {
48+
try {
49+
// Verify the token using the secret key and decode the payload
50+
const payload = jwt.verify(
51+
token,
52+
configuration.SECRET_KEY,
53+
);
54+
55+
// Return the decoded payload casted to the Payload type
56+
return payload as Payload;
57+
} catch {
58+
// Return null if token verification fails
59+
return null;
60+
}
61+
}

0 commit comments

Comments
 (0)