@@ -0,0 +1,2 @@ | |||
PORT=9000 | |||
MONGO_URI=mongodb://localhost:27017/CompanyAuth |
@@ -0,0 +1,6 @@ | |||
/node_modules | |||
/dist | |||
/build | |||
/coverage | |||
/logs | |||
/temp |
@@ -0,0 +1,35 @@ | |||
{ | |||
"name": "authserver", | |||
"version": "1.0.0", | |||
"description": "", | |||
"main": "index.js", | |||
"scripts": { | |||
"start": "node dist/index.js", | |||
"dev": "nodemon src/index.ts", | |||
"build": "tsc", | |||
"test": "echo \"Error: no test specified\" && exit 1" | |||
}, | |||
"keywords": [], | |||
"author": "", | |||
"license": "ISC", | |||
"dependencies": { | |||
"bcrypt": "^5.1.1", | |||
"cors": "^2.8.5", | |||
"dotenv": "^16.5.0", | |||
"express": "^5.1.0", | |||
"jsonwebtoken": "^9.0.2", | |||
"mongoose": "^8.14.1", | |||
"uuid": "^11.1.0" | |||
}, | |||
"devDependencies": { | |||
"@types/bcrypt": "^5.0.2", | |||
"@types/cors": "^2.8.18", | |||
"@types/express": "^5.0.1", | |||
"@types/jsonwebtoken": "^9.0.9", | |||
"@types/mongoose": "^5.11.97", | |||
"@types/node": "^22.15.16", | |||
"nodemon": "^3.1.10", | |||
"ts-node": "^10.9.2", | |||
"typescript": "^5.8.3" | |||
} | |||
} |
@@ -0,0 +1,21 @@ | |||
import { NextFunction, Request, Response } from "express"; | |||
import { CompanyType } from "../models/company.modal"; | |||
import { createCompany } from "../services/company.service"; | |||
import { successResponse } from "../utils/response"; | |||
export const createCompanyController = async ( | |||
req: Request, | |||
res: Response, | |||
next: NextFunction | |||
) => { | |||
try { | |||
const { name } = req.body; | |||
if (!name) throw new Error("400:Name is required"); | |||
const company = await createCompany(name); | |||
successResponse<CompanyType>(res, company); | |||
} catch (error) { | |||
next(error); | |||
} | |||
}; |
@@ -0,0 +1,45 @@ | |||
import { NextFunction, Request, Response } from "express"; | |||
import { CreateJwtTokenRequest, UserType } from "../models/user.modal"; | |||
import { createJWTToken, createUserToken } from "../services/user.service"; | |||
import { TypedRequestBody } from "../type/api"; | |||
import { checkBodyAndThrowError } from "../utils/checkBodyAndThrowError"; | |||
import { successResponse } from "../utils/response"; | |||
export const getUserTokenController = async ( | |||
req: Request, | |||
res: Response, | |||
next: NextFunction | |||
) => { | |||
try { | |||
const token = req.headers.authorization?.split(" ")[1]; | |||
const { companyName } = req.params; | |||
if (!token) throw new Error("401:Token is required"); | |||
if (!companyName) throw new Error("400:companyName is required"); | |||
const user = await createUserToken(token, companyName); | |||
successResponse<UserType>(res, user); | |||
} catch (error) { | |||
next(error); | |||
} | |||
}; | |||
export const createJwtTokenController = async ( | |||
req: TypedRequestBody<CreateJwtTokenRequest>, | |||
res: Response, | |||
next: NextFunction | |||
) => { | |||
try { | |||
checkBodyAndThrowError(req.body, [ | |||
"name", | |||
"amount", | |||
"lobby", | |||
"companyName", | |||
]); | |||
const token = await createJWTToken(req.body); | |||
successResponse<string>(res, token); | |||
} catch (error) { | |||
next(error); | |||
} | |||
}; |
@@ -0,0 +1,30 @@ | |||
import cors from "cors"; | |||
import dotenv from "dotenv"; | |||
import express, { Express } from "express"; | |||
import connectDB from "./lib/database"; | |||
import { errorHandler } from "./middleware/error"; | |||
import companyRoutes from "./routes/company"; | |||
import userRoutes from "./routes/user"; | |||
dotenv.config(); | |||
const app: Express = express(); | |||
const port = process.env.PORT || 3000; | |||
connectDB(); | |||
app.use(cors()); | |||
app.use(express.json()); | |||
app.use("/api/company", companyRoutes); | |||
app.use("/api/user", userRoutes); | |||
app.use(errorHandler); | |||
app.get("/health", (req, res) => { | |||
res.status(200).json({ status: "ok" }); | |||
}); | |||
app.listen(port, () => { | |||
console.log(`⚡️ Server running at http://localhost:${port}`); | |||
}); |
@@ -0,0 +1,18 @@ | |||
import dotenv from "dotenv"; | |||
import mongoose from "mongoose"; | |||
dotenv.config(); | |||
const connectDB = async (): Promise<void> => { | |||
try { | |||
const mongoUri = | |||
process.env.MONGO_URI || "mongodb://localhost:27017/companyAuth"; | |||
await mongoose.connect(mongoUri); | |||
console.log("✅ MongoDB Connected"); | |||
} catch (error) { | |||
console.error("❌ MongoDB Connection Error:", error); | |||
process.exit(1); | |||
} | |||
}; | |||
export default connectDB; |
@@ -0,0 +1,19 @@ | |||
import { NextFunction, Request, Response } from "express"; | |||
import { errorResponse } from "../utils/response"; | |||
export const errorHandler = ( | |||
err: Error, | |||
req: Request, | |||
res: Response, | |||
next: NextFunction | |||
) => { | |||
if (err instanceof Error) { | |||
console.log(err); | |||
const errorCode = err.message.split(":")[0]; | |||
const errorMessage = err.message.split(":")[1]; | |||
errorResponse(res, errorCode, errorMessage); | |||
} else { | |||
console.log(err); | |||
errorResponse(res, "9999", "Internal server error"); | |||
} | |||
}; |
@@ -0,0 +1,28 @@ | |||
import mongoose from "mongoose"; | |||
export interface CreateCompanyRequest { | |||
name: string; | |||
} | |||
export interface CompanyOptions { | |||
name?: string; | |||
secretKey?: string; | |||
} | |||
export interface CompanyType extends mongoose.Document { | |||
name: string; | |||
secretKey: string; | |||
createdAt: Date; | |||
updatedAt: Date; | |||
} | |||
const companySchema = new mongoose.Schema({ | |||
name: { type: String, required: true, unique: true }, | |||
secretKey: { type: String, required: true, unique: true }, | |||
createdAt: { type: Date, default: Date.now }, | |||
updatedAt: { type: Date, default: Date.now }, | |||
}); | |||
const Company = mongoose.model("Company", companySchema); | |||
export default Company; |
@@ -0,0 +1,39 @@ | |||
import mongoose from "mongoose"; | |||
export interface UserPayloadRequest { | |||
name: string; | |||
amount: number; | |||
lobby: string; | |||
} | |||
export interface UserPayload extends UserPayloadRequest { | |||
iat: number; | |||
exp: number; | |||
} | |||
export interface CreateJwtTokenRequest extends UserPayload { | |||
companyName: string; | |||
} | |||
export interface UserType extends mongoose.Document { | |||
name: string; | |||
amount: number; | |||
lobby: string; | |||
token: string; | |||
count: number; | |||
} | |||
const userSchema = new mongoose.Schema({ | |||
name: { type: String, required: true }, | |||
amount: { type: Number, required: true }, | |||
lobby: { type: String, required: true }, | |||
token: { type: String, required: true }, | |||
count: { type: Number, default: 0 }, | |||
record: { type: Array, default: [] }, | |||
createdAt: { type: Date, default: Date.now }, | |||
updatedAt: { type: Date, default: Date.now }, | |||
}); | |||
const User = mongoose.model("User", userSchema); | |||
export default User; |
@@ -0,0 +1,8 @@ | |||
import { Router } from "express"; | |||
import { createCompanyController } from "../controllers/company.controller"; | |||
const router = Router(); | |||
router.post("/create", createCompanyController); | |||
export default router; |
@@ -0,0 +1,12 @@ | |||
import { Router } from "express"; | |||
import { | |||
createJwtTokenController, | |||
getUserTokenController, | |||
} from "../controllers/user.controller"; | |||
const router = Router(); | |||
router.post("/createJwtToken", createJwtTokenController); | |||
router.get("/getUserToken/:companyName", getUserTokenController); | |||
export default router; |
@@ -0,0 +1,46 @@ | |||
import { v4 as uuidv4 } from "uuid"; | |||
import Company, { CompanyOptions } from "../models/company.modal"; | |||
export const createCompany = async (name: string) => { | |||
try { | |||
const existingCompanyName = await getCompanyByOptions({ name }); | |||
if (existingCompanyName) throw "409:Company already exists"; | |||
const secretKey = uuidv4(); | |||
const existingCompanySecretKey = await getCompanyByOptions({ secretKey }); | |||
if (existingCompanySecretKey) throw "409:Company secret key already exists"; | |||
const newCompany = await Company.create({ | |||
name, | |||
secretKey, | |||
}); | |||
return newCompany; | |||
} catch (error) { | |||
throw new Error((error as string) || "500:Failed to create company"); | |||
} | |||
}; | |||
export const getCompanyById = async (id: string) => { | |||
try { | |||
const company = await Company.findById(id); | |||
return company; | |||
} catch (error) { | |||
throw new Error( | |||
((error as string) || "500:Failed to get company by") + `id: ${id}` | |||
); | |||
} | |||
}; | |||
export const getCompanyByOptions = async (options: CompanyOptions) => { | |||
try { | |||
const company = await Company.findOne(options); | |||
return company; | |||
} catch (error) { | |||
throw new Error( | |||
((error as string) || "500:Failed to get company by") + | |||
`options: ${JSON.stringify(options)}` | |||
); | |||
} | |||
}; |
@@ -0,0 +1,25 @@ | |||
import jwt from "jsonwebtoken"; | |||
import { UserPayload, UserPayloadRequest } from "../models/user.modal"; | |||
export const verifyToken = (token: string, secret: string) => { | |||
try { | |||
const decoded = jwt.verify(token, secret); | |||
if (!decoded) throw "401:Invalid token"; | |||
return decoded as UserPayload; | |||
} catch (error) { | |||
if (error instanceof jwt.TokenExpiredError) { | |||
throw new Error("401:Token expired"); | |||
} | |||
throw new Error((error as string) || "500:Failed to verify token"); | |||
} | |||
}; | |||
export const generateToken = (payload: UserPayloadRequest, secret: string) => { | |||
try { | |||
const token = jwt.sign(payload, secret, { expiresIn: "1h" }); | |||
return token; | |||
} catch (error) { | |||
throw new Error((error as string) || "500:Failed to generate token"); | |||
} | |||
}; |
@@ -0,0 +1,56 @@ | |||
import { Request } from "express"; | |||
import { v4 as uuidv4 } from "uuid"; | |||
import User, { UserPayload } from "../models/user.modal"; | |||
import { getCompanyByOptions } from "./company.service"; | |||
import { generateToken, verifyToken } from "./jwtToken"; | |||
export const createUserToken = async (token: string, companyName: string) => { | |||
try { | |||
const company = await getCompanyByOptions({ name: companyName }); | |||
if (!company) throw "404:Company not found"; | |||
const user = verifyToken(token, company.secretKey); | |||
if (!user) throw "0001:User not found"; | |||
const userModel = await createUserModel(user); | |||
return userModel; | |||
} catch (error) { | |||
if (error instanceof Error) throw error.message; | |||
throw new Error((error as string) || "500:Failed to create user token"); | |||
} | |||
}; | |||
export const createJWTToken = async (body: Request["body"]) => { | |||
try { | |||
const { name, amount, lobby, companyName } = body; | |||
const company = await getCompanyByOptions({ name: companyName }); | |||
if (!company) throw "404:Company not found"; | |||
const token = generateToken({ name, amount, lobby }, company.secretKey); | |||
return token; | |||
} catch (error) { | |||
if (error instanceof Error) throw error.message; | |||
throw new Error((error as string) || "500:Failed to create JWT token"); | |||
} | |||
}; | |||
export const createUserModel = async (user: UserPayload) => { | |||
try { | |||
const { iat, exp, ...userPayload } = user; | |||
const token = uuidv4(); | |||
const newUser = { | |||
...userPayload, | |||
token: token, | |||
count: 0, | |||
}; | |||
const userModel = await User.create(newUser); | |||
return userModel; | |||
} catch (error) { | |||
throw new Error((error as string) || "500:Failed to create user model"); | |||
} | |||
}; |
@@ -0,0 +1,5 @@ | |||
import { Request } from "express"; | |||
export interface TypedRequestBody<T> extends Request { | |||
body: T; | |||
} |
@@ -0,0 +1,10 @@ | |||
import { Request } from "express"; | |||
export const checkBodyAndThrowError = ( | |||
body: Request["body"], | |||
requiredFields: string[] | |||
) => { | |||
requiredFields.forEach((field) => { | |||
if (!body[field]) throw new Error(`400:${field} is required`); | |||
}); | |||
}; |
@@ -0,0 +1,52 @@ | |||
import { Response } from "express"; | |||
export const successResponse = <T>(res: Response, data: T) => { | |||
res.status(200).json({ | |||
message: "Success", | |||
data, | |||
}); | |||
}; | |||
export const errorResponse = ( | |||
res: Response, | |||
errorCode: string, | |||
message: string | |||
) => { | |||
switch (errorCode) { | |||
case "400": | |||
res.status(400).json({ | |||
message, | |||
}); | |||
break; | |||
case "401": | |||
res.status(401).json({ | |||
message, | |||
}); | |||
break; | |||
case "403": | |||
res.status(403).json({ | |||
message, | |||
}); | |||
break; | |||
case "404": | |||
res.status(404).json({ | |||
message, | |||
}); | |||
break; | |||
case "409": | |||
res.status(409).json({ | |||
message, | |||
}); | |||
break; | |||
case "9999": | |||
res.status(500).json({ | |||
message, | |||
}); | |||
break; | |||
default: | |||
res.status(500).json({ | |||
message, | |||
}); | |||
break; | |||
} | |||
}; |
@@ -0,0 +1,14 @@ | |||
{ | |||
"compilerOptions": { | |||
"target": "es2016", | |||
"module": "commonjs", | |||
"outDir": "./dist", | |||
"rootDir": "./src", | |||
"strict": true, | |||
"esModuleInterop": true, | |||
"skipLibCheck": true, | |||
"forceConsistentCasingInFileNames": true | |||
}, | |||
"include": ["src/**/*"], | |||
"exclude": ["node_modules"] | |||
} |