[#40] BACKEND USER, Company

https://project.mdnd-it.cc/work_packages/40
This commit is contained in:
2025-06-15 01:12:46 +02:00
parent f68540f511
commit fa868e7c1d
32 changed files with 6337 additions and 860 deletions
+25
View File
@@ -0,0 +1,25 @@
import { DataSource, DataSourceOptions } from 'typeorm';
import { User } from './user.entity';
import { Company } from './company.entity';
import { QBank } from './qbank.entity';
// Get the database type from environment variables or default to MariaDB
const dbType = process.env.DB_TYPE || 'mariadb';
// Create the options object with correct typing
const options: DataSourceOptions = {
type: dbType as any, // Using 'as any' to bypass TypeScript's strict type checking
host: process.env.DB_HOST || 'localhost',
port: Number(process.env.DB_PORT) || 3306,
username: process.env.DB_USERNAME || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_DATABASE || 'serpent_race',
synchronize: process.env.NODE_ENV !== 'production',
logging: process.env.NODE_ENV !== 'production',
entities: [User, Company, QBank],
migrations: [],
subscribers: [],
};
// Create the DataSource with properly typed options
export const AppDataSourceOne = new DataSource(options);
@@ -0,0 +1,39 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
OneToMany
} from 'typeorm';
import { User } from './user.entity';
@Entity('companies')
export class Company {
@PrimaryGeneratedColumn()
CompanyId!: number;
@Column({ type: 'varchar', length: 255, nullable: false })
Name!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
ContactFirstName!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
ContactLastName!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
ContactEmail!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
FirstAPI!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
TokenAPI!: string;
@CreateDateColumn()
RegDate!: Date;
// Relation to User entity (optional)
@OneToMany(() => User, user => user.company)
users!: User[];
}
@@ -0,0 +1,24 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, OneToMany } from 'typeorm';
import { User } from './user.entity';
@Entity('q_bank')
export class QBank {
@PrimaryGeneratedColumn()
QBankId!: number;
@Column({ type: 'varchar', length: 255 })
Title!: string;
@Column()
Creator!: number;
@Column({ type: 'datetime' })
Creation_Date!: Date;
@Column({ type: 'int' })
no_question!: number;
@ManyToOne(() => User, user => user.questionBanks)
@JoinColumn({ name: 'Creator' })
creator!: User;
}
+52
View File
@@ -0,0 +1,52 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
OneToMany,
ManyToOne,
JoinColumn
} from 'typeorm';
import { Company } from './company.entity';
import { QBank } from './qbank.entity';
// User entity for the SerpentRace API
// This entity represents a user in the system, with fields for personal information,
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id!: number;
@Column({ type: 'varchar', length: 255, nullable: false, unique: true })
Username!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
FirstName!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
LastName!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
Password!: string; // Consider hashing this password before storing
@Column({ type: 'varchar', length: 255, nullable: false, unique: true })
Email!: string;
@CreateDateColumn()
RegDate!: Date;
@Column({ nullable: true })
CompanyId: number = 0;
@Column({ type: 'varchar', length: 255, nullable: true })
CompanyToken: string = '';
@ManyToOne(() => Company, (company) => company.users)
@JoinColumn({ name: 'CompanyId' })
company!: Company;
@OneToMany(() => QBank, qbank => qbank.creator)
questionBanks!: QBank[];
}
+116
View File
@@ -0,0 +1,116 @@
//User class for the all the user related operations in the SerpentRace Backend
import { comparePasswords, hashPassword } from '../middleware/secure';
import { FindOptionsWhere, Equal } from 'typeorm';
import { AppDataSourceOne } from '../DB/Datasource';
import { User } from '../DB/user.entity';
import { Company } from '../DB/company.entity';
import { Request, Response } from 'express';
export class CompanyService {
private companyRepository = AppDataSourceOne.getRepository(Company);
async createCompany(company: Company): Promise<Company> {
return this.companyRepository.save(company);
}
private async findCompanyById(id: number): Promise<Company | null> {
const where: FindOptionsWhere<Company> = { CompanyId: Equal(id) };
return this.companyRepository.findOneBy(where);
}
private async findCompanyByName(name: string): Promise<Company | null> {
const where: FindOptionsWhere<Company> = { Name: Equal(name) };
return this.companyRepository.findOneBy(where);
}
async getCompanyDetails(req: Request, res: Response): Promise<void> {
const companyId = parseInt(req.params.companyid);
if (isNaN(companyId)) {
res.status(400).json({ message: 'Invalid company ID' });
return;
}
const company = await this.findCompanyById(companyId);
if (!company) {
res.status(404).json({ message: 'Company not found' });
return;
}
res.status(200).json(company);
}
async updateCompany(req: Request, res: Response): Promise<void> {
const companyId = parseInt(req.params.companyid);
if (isNaN(companyId)) {
res.status(400).json({ message: 'Invalid company ID' });
return;
}
const company = await this.findCompanyById(companyId);
if (!company) {
res.status(404).json({ message: 'Company not found' });
return;
}
const updatedData = req.body;
Object.assign(company, updatedData);
const updatedCompany = await this.companyRepository.save(company);
res.status(200).json(updatedCompany);
}
async deleteCompany(req: Request, res: Response): Promise<void> {
const companyId = parseInt(req.params.companyid);
if (isNaN(companyId)) {
res.status(400).json({ message: 'Invalid company ID' });
return;
}
const company = await this.findCompanyById(companyId);
if (!company) {
res.status(404).json({ message: 'Company not found' });
return;
}
await this.companyRepository.remove(company);
res.status(204).send();
}
private async addUserToCompany(userId: number, companyId: number): Promise<void> {
const user = await AppDataSourceOne.getRepository(User).findOneBy({ id: userId });
if (!user) {
throw new Error('User not found');
}
const company = await this.findCompanyById(companyId);
if (!company) {
throw new Error('Company not found');
}
user.CompanyId = companyId;
await AppDataSourceOne.getRepository(User).save(user);
// Optionally, you can also add the user to the company's user list if needed
if (!company.users) {
company.users = [];
}
company.users.push(user);
}
private async removeUserFromCompany(userId: number, companyId: number): Promise<void> {
const user = await AppDataSourceOne.getRepository(User).findOneBy({ id: userId });
if (!user) {
throw new Error('User not found');
}
const company = await this.findCompanyById(companyId);
if (!company) {
throw new Error('Company not found');
}
user.CompanyId = 0; // Remove association
user.CompanyToken = ''; // Remove association
await AppDataSourceOne.getRepository(User).save(user);
// Optionally, remove the user from the company's user list
company.users = company.users?.filter(u => u.id !== userId) || [];
await this.companyRepository.save(company);
}
}
+113
View File
@@ -0,0 +1,113 @@
//User class for the all the user related operations in the SerpentRace Backend
import { comparePasswords, hashPassword } from '../middleware/secure';
import { FindOptionsWhere, Equal } from 'typeorm';
import { AppDataSourceOne } from '../DB/Datasource';
import { createToken } from '../middleware/auth';
import { User } from '../DB/user.entity';
import { Request, Response } from 'express';
export class UserService {
private userRepository = AppDataSourceOne.getRepository(User);
async createUser(user : User): Promise<User> {
// Hash the password before saving the user
user.Password = await hashPassword(user.Password);
return this.userRepository.save(user);
}
private async findUserByEmail(email: string): Promise<User | null> {
const where: FindOptionsWhere<User> = { Email: Equal(email) };
return this.userRepository.findOneBy(where);
}
private async findUserByUsername(username: string): Promise<User | null> {
const where: FindOptionsWhere<User> = { Username: Equal(username) };
return this.userRepository.findOneBy(where);
}
private async findUserById(id: number): Promise<User | null> {
const where: FindOptionsWhere<User> = { id: Equal(id) };
return this.userRepository.findOneBy(where);
}
async authenticateUser(req: Request, res: Response): Promise<void> {
const { String_data, Password } = req.body;
if (!String_data || !Password) {
res.status(400).json({ message: 'Email and password are required' });
return;
}
// Check if String_data is an email or username
let user: User | null = null;
if (String_data.includes('@')) {
user = await this.findUserByEmail(String_data.toLowerCase());
}
else {
user = await this.findUserByUsername(String_data);
}
// If user is not found or password does not match, return 401
if (!user || !(await comparePasswords(Password, user.Password))) {
res.status(401).json({ message: 'Invalid credentials' });
return;
}
const token = createToken(user);
// Set the token in the response header
res.cookie("jwt", token, {
httpOnly: true,
secure: true,
sameSite: "none",
maxAge: 60 * 60 * 1000
});
res.status(200).json({ message: 'Authentication successful' });
}
async getUserDetails(req: Request, res: Response): Promise<void> {
const userId = req.user?.id;
if (!userId) {
res.status(401).json({ message: 'Unauthorized' });
return;
}
const user = await this.userRepository.findOneBy({ id: userId });
if (!user) {
res.status(404).json({ message: 'User not found' });
return;
}
res.json(user);
}
async updateUser(req: Request, res: Response): Promise<void> {
const userId = req.user?.id;
if (!userId) {
res.status(401).json({ message: 'Unauthorized' });
return;
}
const user = await this.findUserById(userId);
if (!user) {
res.status(404).json({ message: 'User not found' });
return;
}
Object.assign(user, req.body);
if (req.body.Password) {
user.Password = await hashPassword(req.body.Password);
}
await this.userRepository.save(user);
res.json({ message: 'User details updated successfully' });
}
async deleteUser(req: Request, res: Response): Promise<void> {
const userId = req.user?.id;
if (!userId) {
res.status(401).json({ message: 'Unauthorized' });
return;
}
const user = await this.findUserById(userId);
if (!user) {
res.status(404).json({ message: 'User not found' });
return;
}
await this.userRepository.remove(user);
res.json({ message: 'User deleted successfully' });
}
}
@@ -0,0 +1,58 @@
//Router for user-related routes
import { Router } from 'express';
import { UserService } from './User';
import { auth } from '../middleware/auth';
import { User } from '../DB/user.entity';
const userRouter = Router();
const userService = new UserService();
// Route to create a new user
userRouter.post('/create', async (req, res) => {
try {
const user = await userService.createUser(req.body);
res.status(201).json({ message: 'User created successfully'});
} catch (error) {
res.status(500).json({ message: 'Error creating user', error });
}
});
// Route to authenticate a user
userRouter.post('/authenticate', async (req, res) => {
try {
await userService.authenticateUser(req, res);
} catch (error) {
res.status(500).json({ message: 'Error authenticating user', error });
}
});
// Route to get user details (protected route)
userRouter.get('/details', auth, async (req, res) => {
try {
await userService.getUserDetails(req, res);
} catch (error) {
res.status(500).json({ message: 'Error fetching user details', error });
}
});
// Route to update user details (protected route)
userRouter.put('/update', auth, async (req, res) =>{
try{
await userService.updateUser(req, res);
}
catch (error) {
res.status(500).json({ message: 'Error updating user', error });
}
});
// Route to delete a user (protected route)
userRouter.delete('/delete', auth, async (req, res) => {
try {
await userService.deleteUser(req, res);
} catch (error) {
res.status(500).json({ message: 'Error deleting user', error });
}
});
//export default userRouter;
export { userRouter };
@@ -0,0 +1,9 @@
import Express from 'express';
declare global {
namespace Express {
interface Request {
user?: any;
}
}
}
-14
View File
@@ -1,14 +0,0 @@
import express, { request } from 'express';
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json())
app.listen(PORT, '0.0.0.0', ()=>{
console.log(`Running on Port: ${PORT}`)
});
app.get("/api/", (req, res) => {
res.send("KÖRTE");
});
+23
View File
@@ -0,0 +1,23 @@
console.log(process.env.DATABASE_URL_1 as string);
import express from 'express';
import cors from "cors";
import cookieParser from "cookie-parser";
import { mainRouter } from './router';
import { AppDataSourceOne } from './DB/Datasource';
const app = express();
// Initialize the database connection
AppDataSourceOne.initialize();
app.use(cors({
origin: process.env.FRONTEND_URL,
credentials: true
}));
app.use(cookieParser());
app.use('/', mainRouter);
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
@@ -0,0 +1,47 @@
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
export function auth(req: Request, res: Response, next: NextFunction): void {
// Read token from cookie named 'jwt'
const token = req.cookies.jwt;
if (!token) {
res.status(401).json({ message: "No token provided" });
return;
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET || "secret");
if (typeof decoded === "string") {
res.status(401).json({ message: "Invalid token payload" });
return;
}
req.user = decoded;
// Check if expiring soon & refresh if needed
const now = Math.floor(Date.now() / 1000);
if (decoded.exp && decoded.exp - now < 60 * 5) {
const { iat, exp, ...payload } = decoded;
const newToken = createToken(payload);
res.cookie("jwt", newToken, {
httpOnly: true,
secure: true,
sameSite: "none",
maxAge: 60 * 60 * 1000, // 1 hour
});
}
next();
} catch (err) {
res.status(401).json({ message: "Invalid token" });
return;
}
}
export function createToken(user: any): string {
return jwt.sign({ id: user.id, username: user.username }, process.env.JWT_SECRET || 'secret', {
expiresIn: '1h',
});
}
@@ -0,0 +1,12 @@
//hash password
import { Request, Response, NextFunction } from 'express';
import bcrypt from 'bcryptjs';
export async function hashPassword(password: string): Promise<string> {
const salt = await bcrypt.genSalt(10);
return bcrypt.hash(password, salt);
}
export async function comparePasswords(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
+13
View File
@@ -0,0 +1,13 @@
//collection of all routes
import { Router } from 'express';
import { userRouter } from '../User/UserRouter';
// import { companyRouter } from '../User/CompanyRouter';
const router = Router();
// Use the user router for user-related routes
router.use('/user', userRouter);
// Use the company router for company-related routes
// router.use('/company', companyRouter);
// Add more routers as needed
// Export the main router
export { router as mainRouter };