Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Data is stored in In-memory storage, since small project. I dont see the need to use sql or nosql db.
Patterns used are:
Repository for Data
Service for Logic

Authentication used is jwt

external integration: openweathermap.org

main language is PHP so I am not yet familiar with typescript and decided not to use it
4 changes: 4 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
PORT=4000
JWT_SECRET=my-super-secret-key-change-this-in-production
JWT_EXPIRES_IN=1h
WEATHER_API_KEY=your-weather-api-key-here
2 changes: 2 additions & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.env
node_modules
54 changes: 48 additions & 6 deletions backend/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,55 @@
require("dotenv").config();

const express = require("express");
const config = require("./src/config");
const logger = require("./src/utils/logger");
const authRoutes = require("./src/routes/authRoutes");
const weatherRoutes = require("./src/routes/weatherRoutes");

const app = express();
const PORT = process.env.PORT || 4000;
app.use(express.json());
app.use((req, res, next) => {
logger.info(`${req.method} ${req.originalUrl}`, {
ip: req.ip,
});
next();
});

app.use("/api/auth", authRoutes);
app.use("/api/weather", weatherRoutes);

// Basic route
app.get("/", (req, res) => {
res.send("Hello from Express!");
res.json({ status: "ok", message: "API is running" });
});

// Start server
app.listen(PORT, () => {
console.log(`Backend is running on http://localhost:${PORT}`);
app.use((req, res) => {
res.status(404).json({
status: "error",
message: `Route ${req.method} ${req.originalUrl} not found`,
});
});

app.use((err, req, res, next) => {
logger.error(err.message, {
statusCode: err.statusCode || 500,
stack: err.stack,
url: req.originalUrl,
method: req.method,
});

const statusCode = err.statusCode || 500;

res.status(statusCode).json({
status: "error",
message: err.isOperational ? err.message : "An unexpected error occurred",
...(process.env.NODE_ENV === "development" && { stack: err.stack }),
});
});

app.listen(config.port, () => {
logger.info(`Server running on http://localhost:${config.port}`);
logger.info("Available endpoints:");
logger.info(" POST /api/auth/register - Register a new user");
logger.info(" POST /api/auth/login - Login");
logger.info(" GET /api/weather?city= - Get weather (requires auth)");
});
13 changes: 11 additions & 2 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,18 @@
"name": "backend",
"version": "1.0.0",
"scripts": {
"start": "node index.js"
"start": "node index.js",
"dev": "nodemon index.js"
},
"dependencies": {
"express": "^4.18.2"
"axios": "^1.13.5",
"bcryptjs": "^3.0.3",
"dotenv": "^17.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.3",
"winston": "^3.19.0"
},
"devDependencies": {
"nodemon": "^3.1.11"
}
}
10 changes: 10 additions & 0 deletions backend/src/config/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
require("dotenv").config();

const config = {
port: process.env.PORT || 4000,
jwtSecret: process.env.JWT_SECRET || "jwt-default-secret",
jwtExpiresIn: process.env.JWT_EXPIRES_IN || "1h",
weatherApiKey: process.env.WEATHER_API_KEY || "",
};

module.exports = config;
44 changes: 44 additions & 0 deletions backend/src/controllers/authController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const authService = require("../services/authService");
const AppError = require("../utils/AppError");

const authController = {

async register(req, res, next) {
try {
const { email, password, role } = req.body;
if (!email || !password) {
throw new AppError("Email and password are required", 400);
}

const response = await authService.register(email, password, role);

res.status(201).json({
status: "success",
data: response,
});
} catch (error) {
next(error);
}
},

async login(req, res, next) {
try {
const { email, password } = req.body;

if (!email || !password) {
throw new AppError("Email and password are required", 400);
}

const response = await authService.login(email, password);

res.status(200).json({
status: "success",
data: response,
});
} catch (error) {
next(error);
}
},
};

module.exports = authController;
20 changes: 20 additions & 0 deletions backend/src/controllers/weatherController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const weatherService = require("../services/weatherService");

const weatherController = {
async getWeather(req, res, next) {
try {
const city = req.query.city || "London";

const weather = await weatherService.getWeather(city);

res.status(200).json({
status: "success",
data: weather,
});
} catch (error) {
next(error);
}
},
};

module.exports = weatherController;
22 changes: 22 additions & 0 deletions backend/src/middleware/authMiddleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const jwt = require("jsonwebtoken");
const config = require("../config");
const AppError = require("../utils/AppError");

function authMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return next(new AppError("No token provided. Please log in.", 401));
}

const token = authHeader.split(" ")[1];

try {
const decoded = jwt.verify(token, config.jwtSecret);
req.user = decoded;
next();
} catch (error) {
return next(new AppError("Invalid or expired token. Please log in again.", 401));
}
}

module.exports = authMiddleware;
20 changes: 20 additions & 0 deletions backend/src/middleware/roleMiddleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const AppError = require("../utils/AppError");

function authorize(...roles) {
return (req, res, next) => {

if (!req.user) {
return next(new AppError("Authentication required", 401));
}

if (!roles.includes(req.user.role)) {
return next(
new AppError("You do not have permission to access this resource", 403)
);
}

next();
};
}

module.exports = authorize;
30 changes: 30 additions & 0 deletions backend/src/repositories/userRepository.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const users = [];

let nextId = 1;

const userRepository = {
findByEmail(email) {
return users.find((user) => user.email === email);
},


findById(id) {
return users.find((user) => user.id === id);
},

create(userData) {
const user = {
id: nextId++,
...userData,
createdAt: new Date().toISOString(),
};
users.push(user);
return user;
},

findAll() {
return users;
},
};

module.exports = userRepository;
8 changes: 8 additions & 0 deletions backend/src/routes/authRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const express = require("express");
const router = express.Router();
const authController = require("../controllers/authController");

router.post("/register", authController.register);
router.post("/login", authController.login);

module.exports = router;
9 changes: 9 additions & 0 deletions backend/src/routes/weatherRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const express = require("express");
const router = express.Router();
const weatherController = require("../controllers/weatherController");
const authMiddleware = require("../middleware/authMiddleware");
const authorize = require("../middleware/roleMiddleware");

router.get("/", authMiddleware, authorize("admin", "user"), weatherController.getWeather);

module.exports = router;
60 changes: 60 additions & 0 deletions backend/src/services/authService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const config = require("../config");
const userRepository = require("../repositories/userRepository");
const AppError = require("../utils/AppError");
const logger = require("../utils/logger");

const authService = {
async register(email, password, role = "user") {
const existingUser = userRepository.findByEmail(email);
if (existingUser) {
throw new AppError("Email already registered", 409);
}

const hashedPassword = await bcrypt.hash(password, 10);
const user = userRepository.create({
email,
password: hashedPassword,
role,
});

const token = jwt.sign(
{ id: user.id, email: user.email, role: user.role },
config.jwtSecret,
{ expiresIn: config.jwtExpiresIn }
);

logger.info("User registered successfully", { userId: user.id, email: user.email });

const { password: _, ...userWithoutPassword } = user;

return { user: userWithoutPassword, token };
},

async login(email, password) {
const user = userRepository.findByEmail(email);
if (!user) {
throw new AppError("Invalid email or password", 401);
}

const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
throw new AppError("Invalid email or password", 401);
}

const token = jwt.sign(
{ id: user.id, email: user.email, role: user.role },
config.jwtSecret,
{ expiresIn: config.jwtExpiresIn }
);

logger.info("User logged in successfully", { userId: user.id, email: user.email });

const { password: _, ...userWithoutPassword } = user;

return { user: userWithoutPassword, token };
},
};

module.exports = authService;
Loading