Skip to content

Commit

Permalink
Init base project
Browse files Browse the repository at this point in the history
  • Loading branch information
mfjkri committed Sep 19, 2023
0 parents commit df2bc1b
Show file tree
Hide file tree
Showing 54 changed files with 3,804 additions and 0 deletions.
39 changes: 39 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Logs
logs
*.log

# Runtime data
# pids
# *.pid
# *.seed

venv/

# Directory for instrumented libs generated by jscoverage/JSCover
#lib-cov

# Coverage directory used by tools like istanbul
#coverage

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
#.grunt

# node-waf configuration
# .lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
# build/Release

# Dependency directory
# https://docs.npmjs.com/cli/shrinkwrap#caveats
node_modules

# Distribution directory
dist

# Debug log from npm
npm-debug.log

.DS_Store

.env
3 changes: 3 additions & 0 deletions app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Main from "./src/main";

Main();
40 changes: 40 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "gifty-backend",
"version": "1.0.0",
"description": "Backend for Gifty",
"main": "app.js",
"author": "ruishanteo, mfjkri",
"license": "MIT",
"dependencies": {
"bcrypt": "^5.1.0",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.1",
"nodemailer": "^6.9.4",
"pg": "^8.11.1",
"pgtools": "^1.0.0",
"sequelize": "^6.32.1"
},
"scripts": {
"build": "npx tsc",
"start": "node dist/app.js",
"dev": "concurrently \"npx tsc --watch\" \"nodemon -q dist/app.js\"",
"dropDB": "yarn tsc; node dist/cmd/db.js drop",
"seedDB": "yarn tsc; node dist/cmd/db.js seed",
"backupDB": "yarn tsc; node dist/cmd/db.js backup"
},
"devDependencies": {
"@types/bcrypt": "^5.0.0",
"@types/cors": "^2.8.13",
"@types/express": "^4.17.17",
"@types/jsonwebtoken": "^9.0.2",
"@types/node": "^20.4.5",
"@types/nodemailer": "^6.4.9",
"@types/pg": "^8.10.2",
"commander": "^11.0.0",
"concurrently": "^8.2.0",
"nodemon": "^3.0.1",
"typescript": "^5.1.6"
}
}
110 changes: 110 additions & 0 deletions src/config/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import dotenv from "dotenv";

type DBConfig = {
DBHostname: string;
DBPort: number;
DBName: string;
DBUsername: string;
DBPassword: string;
};

type Config = DBConfig & {
ServerPort: number;
JWTSecretKey: string;

UseLocalDB: boolean;

EmailUsername: string;
EmailPassword: string;
};

export function loadEnv() {
dotenv.config();
validateEnv();
}

export function getConfig(): Config {
const {
SERVER_PORT,

JWT_SECRET_KEY,

USE_LOCAL_DB,
DB_HOSTNAME,
DB_PORT,
DB_NAME,
DB_USERNAME,
DB_PASSWORD,
LOCAL_DB_HOSTNAME,
LOCAL_DB_PORT,
LOCAL_DB_NAME,
LOCAL_DB_USERNAME,
LOCAL_DB_PASSWORD,

EMAIL_USERNAME,
EMAIL_PASSWORD,
} = process.env;
const dbConfig: DBConfig =
USE_LOCAL_DB === "true"
? {
DBHostname: LOCAL_DB_HOSTNAME || "localhost",
DBPort: parseInt(LOCAL_DB_PORT || "5432"),
DBName: LOCAL_DB_NAME || "postgres",
DBUsername: LOCAL_DB_USERNAME || "postgres",
DBPassword: LOCAL_DB_PASSWORD || "postgres",
}
: {
DBHostname: DB_HOSTNAME || "localhost",
DBPort: parseInt(DB_PORT || "5432"),
DBName: DB_NAME || "postgres",
DBUsername: DB_USERNAME || "postgres",
DBPassword: DB_PASSWORD || "postgres",
};

return {
ServerPort: parseInt(SERVER_PORT || "8080"),

JWTSecretKey: JWT_SECRET_KEY || "secret",

UseLocalDB: USE_LOCAL_DB === "true",
...dbConfig,

EmailUsername: EMAIL_USERNAME || "",
EmailPassword: EMAIL_PASSWORD || "",
};
}

function validateEnv() {
const {
SERVER_PORT,
DB_HOSTNAME,
DB_PORT,
DB_NAME,
DB_USERNAME,
DB_PASSWORD,
JWT_SECRET_KEY,
} = process.env;

if (!SERVER_PORT) {
throw new Error("SERVER_PORT is not defined");
}

if (!DB_HOSTNAME) {
throw new Error("DB_HOSTNAME is not defined");
}
if (!DB_PORT) {
throw new Error("DB_PORT is not defined");
}
if (!DB_NAME) {
throw new Error("DB_NAME is not defined");
}
if (!DB_USERNAME) {
throw new Error("DB_USERNAME is not defined");
}
if (!DB_PASSWORD) {
throw new Error("DB_PASSWORD is not defined");
}
if (!JWT_SECRET_KEY) {
throw new Error("JWT_SECRET_KEY is not defined");
}
}
104 changes: 104 additions & 0 deletions src/database/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Client } from "pg";
import { Sequelize } from "sequelize";

import pgtools from "pgtools";

import { getConfig } from "../config/config";

const connectedDBs: Map<string, Sequelize> = new Map();

export async function connectDB(dbName?: string) {
const config = getConfig();
const name = dbName || config.DBName;
const db = connect({
Hostname: config.DBHostname,
Port: config.DBPort,
Name: name,
User: config.DBUsername,
Password: config.DBPassword,
});

try {
await db.authenticate();
connectedDBs.set(name, db);
console.log(`Connection to ${name} DB has been established successfully.`);
} catch (error) {
console.error(`Unable to connect to ${name} DB:`, error);
}

return db;
}

export function getDB(dbName?: string) {
const config = getConfig();
dbName = dbName || config.DBName;
const targetDb = connectedDBs.get(dbName);
if (connectedDBs.size === 0 || !targetDb) {
throw new Error("Database not connected");
}
return targetDb;
}

type Config = {
Hostname: string;
Port: number;
Name: string;
User: string;
Password: string;
};

function connect(config: Config) {
const { Hostname, Port, Name, User, Password } = config;
return new Sequelize(Name, User, Password, {
host: Hostname,
port: Port,
dialect: "postgres",
});
}
export async function createBackupDB() {
const config = getConfig();
const backupDBName = "gifty_backup";

try {
const client = new Client({
user: config.DBUsername,
password: config.DBPassword,
host: config.DBHostname,
port: config.DBPort,
database: "postgres",
});

await client.connect();
const res = await client.query(
`SELECT 1 FROM pg_database WHERE datname = $1`,
[backupDBName]
);
await client.end();

if (res.rows.length === 0) {
await pgtools.createdb(
{
user: config.DBUsername,
password: config.DBPassword,
host: config.DBHostname,
port: config.DBPort,
},
backupDBName
);
const backupDB = await connectDB(backupDBName);
await backupDB.authenticate();
console.log(`Created backup database: ${backupDBName}`);
return backupDB;
} else {
const backupDB = await connectDB(backupDBName);
await backupDB.authenticate();
console.log(`Backup database already exists: ${backupDBName}`);
return backupDB;
}
} catch (err) {
console.error(err);
throw err;
}
}

export default getDB;
4 changes: 4 additions & 0 deletions src/errors/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type ValidationError = {
error: boolean;
message: string;
};
31 changes: 31 additions & 0 deletions src/handlers/auth/deleteUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Request, Response } from "express";

import User from "../../models/user";
import { DeleteUserParams } from "../../params/auth/deleteUser";

const SUCCESS_DELETED_USER = "Deleted user successfully";

const ERROR_USER_DOES_NOT_EXIST = "User does not exist";
const ERROR_FAILED_TO_DELETE_USER = "Failed to delete user";

export default async function handleDeleteUser(
req: Request,
res: Response,
params: DeleteUserParams
) {
try {
const user: User = req.body.user;

if (!user) {
return res.status(404).json({ message: ERROR_USER_DOES_NOT_EXIST });
}

await user.destroy();

return res.status(201).json({
message: SUCCESS_DELETED_USER,
});
} catch (error) {
res.status(500).json({ message: ERROR_FAILED_TO_DELETE_USER, error });
}
}
49 changes: 49 additions & 0 deletions src/handlers/auth/getOTP.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Request, Response } from "express";

import ResetPasswordToken from "../../models/resetPasswordToken";
import User from "../../models/user";
import { sendEmailWithOTP } from "../../utilities/mail";
import { GetOTPParams } from "../../params/auth/getOTP";

const SUCCESS_OTP_GENERATED = "OTP token generated successfully";

const ERROR_USER_NOT_FOUND = "User not found";
const ERROR_FAILED_TO_GENERATE_OTP = "Failed to generate OTP token";

export default async function handleGenerateOTP(
req: Request,
res: Response,
params: GetOTPParams
) {
try {
const otp = Math.floor(100000 + Math.random() * 900000).toString();
const expirationTime = 900;
const expireAt = new Date(Date.now() + expirationTime * 1000);

const user = await User.findOne({
where: { email: params.email.toLowerCase() },
});
if (!user) {
return res.status(400).json({ message: ERROR_USER_NOT_FOUND });
}

let resetPasswordToken = await ResetPasswordToken.findOne({
where: { userId: user.id },
});

if (!resetPasswordToken) {
resetPasswordToken = await ResetPasswordToken.create({
userId: user.id,
otp,
expireAt,
});
} else {
await resetPasswordToken.update({ otp, expireAt });
}

await sendEmailWithOTP(user.email, otp);
res.status(201).json({ message: SUCCESS_OTP_GENERATED });
} catch (error) {
res.status(500).json({ message: ERROR_FAILED_TO_GENERATE_OTP, error });
}
}
Loading

0 comments on commit df2bc1b

Please sign in to comment.