Skip to content

Commit

Permalink
Merge pull request #1 from 0xTxbi/auth-service
Browse files Browse the repository at this point in the history
authentication microservice
  • Loading branch information
0xTxbi authored Dec 6, 2023
2 parents ea181d3 + aa4ab35 commit 02439ee
Show file tree
Hide file tree
Showing 10 changed files with 1,741 additions and 36 deletions.
55 changes: 51 additions & 4 deletions authentication-service/entities/User.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,60 @@
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
import { Entity, PrimaryGeneratedColumn, Column, BeforeInsert } from "typeorm";
import * as bcrypt from "bcrypt";
import { sign } from "jsonwebtoken";

@Entity()
export class User {
@PrimaryGeneratedColumn()
id!: number;
id: number;

@Column({ unique: true })
username: string;

@Column({ unique: true })
email: string;

@Column()
username!: string;
salt: string;

@Column()
password!: string;
hashedPassword: string;

@BeforeInsert()
async hashCredentials() {
this.salt = await bcrypt.genSalt(16);
this.hashedPassword = await bcrypt.hash(
this.hashedPassword,
this.salt
);
}

async compareCredentials(
usernameOrEmail: string,
password: string
): Promise<boolean> {
if (!usernameOrEmail || !password) {
return false;
}

if (
usernameOrEmail !== this.username &&
usernameOrEmail !== this.email
) {
return false;
}

const validPassword = await bcrypt.compare(
password,
this.hashedPassword
);
return validPassword;
}

generateJWT() {
const payload = { userId: this.id, username: this.username };
const secret = process.env.JWT_SECRET || "your-secret-key";
const token = sign(payload, secret, { expiresIn: "1h" });

return token;
}
}
209 changes: 209 additions & 0 deletions authentication-service/src/controllers/AuthController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import {
JsonController,
Post,
Body,
UseBefore,
Authorized,
Put,
CurrentUser,
Get,
Res,
} from "routing-controllers";
import {
IsNotEmpty,
IsEmail,
MinLength,
validateOrReject,
} from "class-validator";

import { getCustomRepository } from "../../../shared/utils/getCustomRepository";
import { User } from "../../entities/User";
import {
authRateLimit,
profileUpdateRateLimit,
} from "../middlewares/RateLimitMiddleware";
import { Response, CookieOptions } from "express";
import { AuthResponse } from "../../../types";

// create user dto
class CreateUserDto {
@IsNotEmpty({ message: "Username is required" })
username: string;

@IsNotEmpty({ message: "Email is required" })
@IsEmail({}, { message: "Invalid email format" })
email: string;

@IsNotEmpty({ message: "Password is required" })
@MinLength(8, {
message: "Password must be at least 8 characters long",
})
hashedPassword: string;
}

// login user dto
class LoginUserDto {
@IsNotEmpty({ message: "Username or email is required" })
usernameOrEmail: string;

@IsNotEmpty({ message: "Password is required" })
password: string;
}

// update user dto
export class UpdateUserDto {
@IsNotEmpty({ message: "Email is required" })
@IsEmail({}, { message: "Invalid email format" })
email?: string;

@IsNotEmpty({ message: "Username is required" })
username?: string;
}

@JsonController()
export class AuthController {
private readonly userRepository = getCustomRepository(User);

@Post("/register")
async register(@Body() userData: CreateUserDto): Promise<AuthResponse> {
try {
// validate user input using class-validator
await validateOrReject(userData);

// check if the username or email is already registered
const existingUser = await this.userRepository.findOne({
where: [
{ username: userData.username },
{ email: userData.email },
],
});

if (existingUser) {
throw new Error(
"Username or email already exists."
);
}

// create a new user
const newUser = this.userRepository.create(userData);

// save the newly created user to the database
await this.userRepository.save(newUser);

// generate a JWT token for the new user
const token = newUser.generateJWT();

// return success message, user details, and JWT
return {
message: "Registered successfully",
successCode: 201,
userDetails: {
id: newUser.id,
username: newUser.username,
email: newUser.email,
},
token: token,
};
} catch (errors) {
console.error("Registration failed:", errors);
return {
message: "Registration failed. Please check your input and try again.",
successCode: 500,
};
}
}

@UseBefore(authRateLimit)
@Post("/login")
async login(@Body() loginData: LoginUserDto): Promise<AuthResponse> {
try {
// validate user input using class-validator
await validateOrReject(loginData);

// find the user by username or email
const user = await this.userRepository.findOne({
where: [
{ username: loginData.usernameOrEmail },
{ email: loginData.usernameOrEmail },
],
});

// throw an error if the user isn't found or the password is incorrect
if (
!user ||
!(await user.compareCredentials(
loginData.usernameOrEmail,
loginData.password
))
) {
return {
message: "Authentication failed. Please check your input and try again.",
successCode: 401,
};
}

// generate a JWT token for the authenticated user
const token = user.generateJWT();

return {
message: "Authenticated successfully",
successCode: 201,
userDetails: {
username: user.username,
email: user.email,
},
token: token,
};
} catch (errors) {
console.error("Login failed:", errors);
return {
message: "Authentication failed. Please check your input and try again.",
successCode: 500,
};
}
}

@Authorized()
@UseBefore(profileUpdateRateLimit)
@Put("/profile")
async updateProfile(
@CurrentUser({ required: true }) currentUser: User,
@Body() updateData: UpdateUserDto
): Promise<{ message: string } | { error: string }> {
try {
// validate user input using class-validator
await validateOrReject(updateData);

// update the user's profile
currentUser.email =
updateData.email || currentUser.email;
currentUser.username =
updateData.username || currentUser.username;

// save the updated user to the database
await this.userRepository.save(currentUser);

return { message: "Profile updated successfully" };
} catch (errors) {
console.error("Profile update failed:", errors);
return {
error: "Profile update failed. Please check your input and try again.",
};
}
}

@Get("/")
async getCurrentUser(@CurrentUser({ required: false }) user: User) {
if (!user) {
return { isAuthenticated: false };
}
const { email, username } = user;
return {
isAuthenticated: true,
user: {
email,
username,
},
};
}
}
61 changes: 61 additions & 0 deletions authentication-service/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import "reflect-metadata";
import { Action, createExpressServer } from "routing-controllers";
import { AuthController } from "./controllers/AuthController";
import { getCustomRepository } from "../../shared/utils/getCustomRepository";
import { User } from "../entities/User";
import { verify } from "jsonwebtoken";

// set up express server
const app = createExpressServer({
controllers: [AuthController],
currentUserChecker: async (action: Action) => {
return new Promise(async (resolve, reject) => {
const authorizationHeader =
action.request.headers["authorization"];
if (authorizationHeader) {
const token =
authorizationHeader.match(
/Bearer\s(\S+)/
)[1];
try {
const decodedToken = verify(
token,
process.env.JWT_SECRET ||
"your-secret-key"
);
// check if userId is defined
if (decodedToken["userId"]) {
const userRepository =
getCustomRepository(
User
);
const user =
await userRepository.findOneBy(
decodedToken[
"userId"
]
);
resolve(user);
} else {
// handle the case where userId is not defined
reject(
new Error(
"User ID is not defined in the token"
)
);
}
} catch (error) {
reject(error);
}
} else {
resolve(undefined);
}
});
},
});

// start the server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Authentication service is running on port ${PORT}`);
});
Loading

0 comments on commit 02439ee

Please sign in to comment.