Skip to content

Commit

Permalink
Merge pull request #40 from PreciousIfeaka/feat/36-user_authentication
Browse files Browse the repository at this point in the history
[FEAT]: add user registration auth
  • Loading branch information
Idimmusix authored Jul 20, 2024
2 parents 63adced + a66f276 commit 20a99ae
Show file tree
Hide file tree
Showing 23 changed files with 8,242 additions and 75 deletions.
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
port=8000
auth-secret=
AUTH_SECRET=
DB_USER=
DB_HOST=
DB_PASSWORD=
Expand Down
5,888 changes: 5,888 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

89 changes: 49 additions & 40 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,42 +1,51 @@
{
"name": "hng_boilerplate_expressjs",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start:dev": "ts-node-dev --respawn --transpile-only ./src/index",
"start": "ts-node src/index.ts",
"test": "jest",
"typeorm": "typeorm-ts-node-commonjs"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/express": "^4.17.21",
"@types/jest": "^29.5.12",
"@types/node": "^16.11.10",
"@types/supertest": "^6.0.2",
"ts-node": "10.9.1",
"typescript": "4.5.2"
},
"dependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/config": "^3.3.4",
"bcryptjs": "^2.4.3",
"class-validator": "^0.14.1",
"config": "^3.3.12",
"dayjs": "^1.11.12",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"jest": "^29.7.0",
"pg": "^8.4.0",
"pino": "^9.3.1",
"pino-pretty": "^11.2.1",
"reflect-metadata": "^0.1.13",
"supertest": "^7.0.0",
"ts-jest": "^29.2.3",
"ts-node-dev": "^2.0.0",
"typeorm": "0.3.20"
}
"name": "hng_boilerplate_expressjs",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start:dev": "ts-node-dev --respawn --transpile-only ./src/index",
"start": "ts-node --transpile-only src/index.ts",
"test": "jest",
"typeorm": "typeorm-ts-node-commonjs",
"build": "tsc",
"prod": "node dist/index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/express": "^4.17.21",
"@types/jest": "^29.5.12",
"@types/node": "^16.18.103",
"@types/supertest": "^6.0.2",
"ts-node": "^10.9.1",
"typescript": "4.5.2"
},
"dependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/config": "^3.3.4",
"@types/cors": "^2.8.17",
"@types/jsonwebtoken": "^9.0.6",
"@types/nodemailer": "^6.4.15",
"bcryptjs": "^2.4.3",
"class-validator": "^0.14.1",
"config": "^3.3.12",
"cors": "^2.8.5",
"dayjs": "^1.11.12",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"handlebars": "^4.7.8",
"jest": "^29.7.0",
"jsonwebtoken": "^9.0.2",
"node-mailer": "^0.1.1",
"pg": "^8.12.0",
"pino": "^9.3.1",
"pino-pretty": "^11.2.1",
"reflect-metadata": "^0.1.14",
"supertest": "^7.0.0",
"ts-jest": "^29.2.3",
"ts-node-dev": "^2.0.0",
"typeorm": "^0.3.20"
}
}
7 changes: 6 additions & 1 deletion src/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import dotenv from "dotenv";

dotenv.config();

const config = {
port: process.env.PORT ?? 8000,
"api-prefix": "api/v1",
Expand All @@ -9,6 +9,11 @@ const config = {
DB_PASSWORD: process.env.DB_PASSWORD,
DB_PORT: process.env.DB_PORT,
DB_NAME: process.env.DB_NAME,
TOKEN_SECRET: process.env.AUTH_SECRET,
SMTP_USER: process.env.SMTP_USER,
SMTP_PASSWORD: process.env.SMTP_PASSWORD,
SMTP_HOST: process.env.SMTP_HOST,
SMTP_SERVICE: process.env.SMTP_SERVICE,
};

export default config;
30 changes: 30 additions & 0 deletions src/controllers/AuthController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Request, Response, NextFunction } from "express";

import { AuthService } from "../services";

/*
*TODO: add validation
*/
const authService = new AuthService();
const signUp = async (req: Request, res: Response, next: NextFunction) => {
try {
const { mailSent, newUser, access_token } = await authService.signUp(
req.body
);
res.status(201).json({ mailSent, newUser, access_token });
} catch (error) {
next(error);
}
};

const verifyOtp = async (req: Request, res: Response, next: NextFunction) => {
try {
const { otp, token } = req.body;
const { message } = await authService.verifyEmail(token as string, otp);
res.status(200).json({ message });
} catch (error) {
next(error);
}
};

export { signUp, verifyOtp };
2 changes: 2 additions & 0 deletions src/controllers/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
// silence is golden
export * from "./AuthController";
export * from "./UserController";
4 changes: 2 additions & 2 deletions src/data-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ export const AppDataSource = new DataSource({
username: config.DB_USER,
password: config.DB_PASSWORD,
database: config.DB_NAME,
synchronize: false,
synchronize: true,
logging: false,
entities: ["src/models/**/*.ts"],
ssl: true,
ssl: false,
extra: {
ssl: {
rejectUnauthorized: false,
Expand Down
5 changes: 5 additions & 0 deletions src/enums/userRoles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum UserRole {
SUPER_ADMIN = 'super_admin',
ADMIN = 'admin',
USER = 'user',
}
38 changes: 28 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,43 @@
import "reflect-metadata";
import { AppDataSource } from "./data-source";
import log from "./utils/logger";
import { seed } from "./seeder";
import express, { Express, Request, Response } from "express";
import config from "./config";
import userRouter from "./routes/user";
import dotenv from "dotenv";
import cors from "cors";
import { userRouter, authRoute } from "./routes";
import { routeNotFound, errorHandler } from "./middleware";

dotenv.config();

const port = config.port
const port = config.port;
const server: Express = express();
server.options("*", cors());
server.use(
cors({
origin: "*",
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: [
"Origin",
"X-Requested-With",
"Content-Type",
"Authorization",
],
})
);
server.use(express.json());
server.use(express.urlencoded({ extended: true }));
server.use(express.json());
server.get("/", (req: Request, res: Response) => {
res.send("Hello world");
});
server.use("/api/v1", userRouter);
server.use("/api/v1/auth", authRoute);
server.use(routeNotFound);
server.use(errorHandler);

AppDataSource.initialize()
.then(async () => {
// seed().catch(log.error);
server.use(express.json());
server.get("/", (req: Request, res: Response) => {
res.send("Hello world");
});
server.use("/api/v1", userRouter);

server.listen(port, () => {
log.info(`Server is listening on port ${port}`);
});
Expand Down
87 changes: 87 additions & 0 deletions src/middleware/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { NextFunction, Request, Response } from "express";

class HttpError extends Error {
status: number;
success: boolean = false;

constructor(statusCode: number, message: string) {
super(message);
this.name = this.constructor.name;
this.status = statusCode;
}
}

class BadRequest extends HttpError {
constructor(message: string) {
super(400, message);
}
}

class ResourceNotFound extends HttpError {
constructor(message: string) {
super(404, message);
}
}

class Unauthorized extends HttpError {
constructor(message: string) {
super(401, message);
}
}

class Forbidden extends HttpError {
constructor(message: string) {
super(403, message);
}
}

class Conflict extends HttpError {
constructor(message: string) {
super(409, message);
}
}

class InvalidInput extends HttpError {
constructor(message: string) {
super(422, message);
}
}

class ServerError extends HttpError {
constructor(message: string) {
super(500, message);
}
}

const routeNotFound = (req: Request, res: Response, next: NextFunction) => {
const message = `Route not found: ${req.originalUrl}`;
res.status(404).json({ success: false, status: 404, message });
};

const errorHandler = (
err: HttpError,
_req: Request,
res: Response,
_next: NextFunction
) => {
const { success, status, message } = err;
const cleanedMessage = message.replace(/"/g, "");
res.status(status).json({
success,
status,
message: cleanedMessage,
});
};

export {
ServerError,
Conflict,
Forbidden,
Unauthorized,
ResourceNotFound,
BadRequest,
InvalidInput,
HttpError,
routeNotFound,
errorHandler,
};
2 changes: 1 addition & 1 deletion src/middleware/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
// silence is golden
export * from "./error";
4 changes: 2 additions & 2 deletions src/models/Profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ export class Profile extends ExtendedBaseEntity {
id: string;

@Column()
firstName: string;
first_name: string;

@Column()
lastName: string;
last_name: string;

@Column()
phone: string;
Expand Down
30 changes: 29 additions & 1 deletion src/models/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ import {
Unique,
JoinTable,
JoinColumn,
ManyToOne,
CreateDateColumn,
UpdateDateColumn,
} from "typeorm";
import { Profile } from "./Profile";
import { Product } from "./Product";
import { Organization } from "./Organization";
import { IsEmail, Length } from "class-validator";
import { IsEmail } from "class-validator";
import ExtendedBaseEntity from "./extended-base-entity";
import { getIsInvalidMessage } from "../utils";
import { UserRole } from "../enums/userRoles";

@Entity()
@Unique(["email"])
Expand All @@ -32,10 +36,28 @@ export class User extends ExtendedBaseEntity {
@Column()
password: string;

@Column({
default: false,
})
isverified: boolean;

@OneToOne(() => Profile, (profile) => profile.user, { cascade: true })
@JoinColumn()
profile: Profile;

@Column({
type: "enum",
enum: UserRole,
default: UserRole.USER,
})
role: UserRole;

@Column()
otp: number;

@Column()
otp_expires_at: Date;

@OneToMany(() => Product, (product) => product.user, { cascade: true })
@JoinTable()
products: Product[];
Expand All @@ -45,4 +67,10 @@ export class User extends ExtendedBaseEntity {
})
@JoinTable()
organizations: Organization[];

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}
Loading

0 comments on commit 20a99ae

Please sign in to comment.