feat: add global styles and transitions for dark mode support
- Created style.css for base styles and color variables for light and dark themes. - Added tailwind.css for Tailwind CSS integration and custom theme variables. - Introduced transitions.css to manage global transitions and reduce motion preferences. - Updated tsconfig.app.json to include path aliases for easier imports. - Modified tsconfig.json to set baseUrl and paths for module resolution. - Added typed-router.d.ts for auto-generated route types. - Enhanced vite.config.ts with Vue Router plugin, Tailwind CSS integration, and server proxy settings.
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
"": {
|
||||
"name": "backend",
|
||||
"dependencies": {
|
||||
"@koa/cors": "^5.0.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"koa": "^3.0.0",
|
||||
@@ -19,6 +20,7 @@
|
||||
"@types/koa": "^2.15.0",
|
||||
"@types/koa-bodyparser": "^4.3.12",
|
||||
"@types/koa-router": "^7.4.8",
|
||||
"@types/koa__cors": "^5.0.0",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
@@ -28,6 +30,8 @@
|
||||
"packages": {
|
||||
"@hapi/bourne": ["@hapi/bourne@3.0.0", "", {}, "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w=="],
|
||||
|
||||
"@koa/cors": ["@koa/cors@5.0.0", "", { "dependencies": { "vary": "^1.1.2" } }, "sha512-x/iUDjcS90W69PryLDIMgFyV21YLTnG9zOpPXS7Bkt2b8AsY3zZsIpOLBkYr9fBcF3HbkKaER5hOBZLfpLgYNw=="],
|
||||
|
||||
"@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.2.2", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-EB0O3SCSNRUFk66iRCpI+cXzIjdswfCs7F6nOC3RAGJ7xr5YhaicvsRwJ9eyzYvYRlCSDUO/c7g4yNulxKC1WA=="],
|
||||
|
||||
"@types/accepts": ["@types/accepts@1.3.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ=="],
|
||||
@@ -64,6 +68,8 @@
|
||||
|
||||
"@types/koa-router": ["@types/koa-router@7.4.8", "", { "dependencies": { "@types/koa": "*" } }, "sha512-SkWlv4F9f+l3WqYNQHnWjYnyTxYthqt8W9az2RTdQW7Ay8bc00iRZcrb8MC75iEfPqnGcg2csEl8tTG1NQPD4A=="],
|
||||
|
||||
"@types/koa__cors": ["@types/koa__cors@5.0.0", "", { "dependencies": { "@types/koa": "*" } }, "sha512-LCk/n25Obq5qlernGOK/2LUwa/2YJb2lxHUkkvYFDOpLXlVI6tKcdfCHRBQnOY4LwH6el5WOLs6PD/a8Uzau6g=="],
|
||||
|
||||
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
|
||||
|
||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
// Import and run the Koa.js application
|
||||
import './src/app';
|
||||
import "./src/app";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"name": "budget-manager-pwa-backend",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
@@ -9,12 +9,14 @@
|
||||
"@types/jsonwebtoken": "^9.0.9",
|
||||
"@types/koa": "^2.15.0",
|
||||
"@types/koa-bodyparser": "^4.3.12",
|
||||
"@types/koa-router": "^7.4.8"
|
||||
"@types/koa-router": "^7.4.8",
|
||||
"@types/koa__cors": "^5.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@koa/cors": "^5.0.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"koa": "^3.0.0",
|
||||
|
||||
+40
-29
@@ -1,8 +1,9 @@
|
||||
import Koa from 'koa';
|
||||
import bodyParser from 'koa-bodyparser';
|
||||
import router from './routes';
|
||||
import { authenticate, handleJwtError } from './middleware/auth';
|
||||
import DatabaseService from './services/database';
|
||||
import Koa from "koa";
|
||||
import bodyParser from "koa-bodyparser";
|
||||
import router from "./routes";
|
||||
import { authenticate, handleJwtError } from "./middleware/auth";
|
||||
import DatabaseService from "./services/database";
|
||||
import cors from "@koa/cors";
|
||||
|
||||
const app = new Koa();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
@@ -10,15 +11,25 @@ const db = DatabaseService.getInstance();
|
||||
|
||||
// Connect to database
|
||||
db.connect()
|
||||
.then(() => {
|
||||
console.log('Database connection established');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Database connection failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
.then(() => {
|
||||
console.log("Database connection established");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Database connection failed:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Middlewares
|
||||
app.use(
|
||||
cors({
|
||||
origin: "*",
|
||||
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
allowHeaders: ["Content-Type", "Authorization", "Accept"],
|
||||
credentials: true,
|
||||
exposeHeaders: ["WWW-Authenticate", "Server-Authorization"],
|
||||
maxAge: 86400, // 1 day
|
||||
}),
|
||||
);
|
||||
app.use(bodyParser());
|
||||
app.use(handleJwtError);
|
||||
app.use(authenticate);
|
||||
@@ -26,30 +37,30 @@ app.use(router.routes());
|
||||
app.use(router.allowedMethods());
|
||||
|
||||
// Error handling
|
||||
app.on('error', (err, ctx) => {
|
||||
console.error('Server error:', err);
|
||||
ctx.status = err.status || 500;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: err.message || 'Internal server error'
|
||||
};
|
||||
app.on("error", (err, ctx) => {
|
||||
console.error("Server error:", err);
|
||||
ctx.status = err.status || 500;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: err.message || "Internal server error",
|
||||
};
|
||||
});
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on http://localhost:${PORT}`);
|
||||
console.log(`Server running on http://localhost:${PORT}`);
|
||||
});
|
||||
|
||||
// Handle graceful shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('Shutting down server...');
|
||||
try {
|
||||
await db.disconnect();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error during shutdown:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
process.on("SIGINT", async () => {
|
||||
console.log("Shutting down server...");
|
||||
try {
|
||||
await db.disconnect();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("Error during shutdown:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
export default app;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export default {
|
||||
adminKey: process.env.ADMIN_KEY || 'admin-secret-key-change-this-in-production'
|
||||
}
|
||||
adminKey:
|
||||
process.env.ADMIN_KEY || "admin-secret-key-change-this-in-production",
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
export default {
|
||||
uri: process.env.MONGODB_URI || 'mongodb://localhost:27017',
|
||||
dbName: process.env.MONGODB_DB_NAME || 'budget-manager',
|
||||
uri: process.env.MONGODB_URI || "mongodb://localhost:27017",
|
||||
dbName: process.env.MONGODB_DB_NAME || "budget-manager",
|
||||
collections: {
|
||||
users: 'users',
|
||||
transactions: 'transactions',
|
||||
categories: 'categories'
|
||||
}
|
||||
users: "users",
|
||||
transactions: "transactions",
|
||||
categories: "categories",
|
||||
pushSubscriptions: "push_subscriptions",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default {
|
||||
secret: process.env.JWT_SECRET || 'your-secret-key-change-this-in-production',
|
||||
expiresIn: '1d', // Token expires in 1 day
|
||||
secret: process.env.JWT_SECRET || "your-secret-key-change-this-in-production",
|
||||
expiresIn: "1d", // Token expires in 1 day
|
||||
};
|
||||
|
||||
+208
-190
@@ -1,197 +1,215 @@
|
||||
import bcrypt from 'bcrypt';
|
||||
import jwt, { type SignOptions } from 'jsonwebtoken';
|
||||
import type { Context } from 'koa';
|
||||
import appConfig from '../config/app';
|
||||
import dbConfig from '../config/database';
|
||||
import jwtConfig from '../config/jwt';
|
||||
import type { User } from '../models';
|
||||
import DatabaseService from '../services/database';
|
||||
import bcrypt from "bcrypt";
|
||||
import jwt, { type SignOptions } from "jsonwebtoken";
|
||||
import type { Context } from "koa";
|
||||
import appConfig from "../config/app";
|
||||
import dbConfig from "../config/database";
|
||||
import jwtConfig from "../config/jwt";
|
||||
import type { User } from "../models";
|
||||
import DatabaseService from "../services/database";
|
||||
|
||||
const db = DatabaseService.getInstance();
|
||||
|
||||
export default {
|
||||
// User registration
|
||||
async register(ctx: Context) {
|
||||
const { username, password, email } = ctx.request.body as {
|
||||
username: string,
|
||||
password: string,
|
||||
email?: string
|
||||
};
|
||||
// User registration
|
||||
async register(ctx: Context) {
|
||||
const { username, password, email } = ctx.request.body as {
|
||||
username: string;
|
||||
password: string;
|
||||
email?: string;
|
||||
};
|
||||
|
||||
// Validate input
|
||||
if (!username || !password) {
|
||||
ctx.status = 400;
|
||||
ctx.body = { success: false, message: 'Username and password are required' };
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const usersCollection = db.getCollection<User>(dbConfig.collections.users);
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await usersCollection.findOne({ username });
|
||||
if (existingUser) {
|
||||
ctx.status = 409;
|
||||
ctx.body = { success: false, message: 'Username already exists' };
|
||||
return;
|
||||
}
|
||||
|
||||
// Hash password before saving
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
const hashedPassword = await bcrypt.hash(password, salt);
|
||||
|
||||
// Create new user
|
||||
const now = new Date();
|
||||
const newUser: User = {
|
||||
username,
|
||||
password: hashedPassword,
|
||||
email,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
};
|
||||
|
||||
// Save user to database
|
||||
const result = await usersCollection.insertOne(newUser);
|
||||
|
||||
ctx.status = 201;
|
||||
ctx.body = {
|
||||
success: true,
|
||||
message: 'User registered successfully',
|
||||
userId: result.insertedId
|
||||
};
|
||||
} catch (error) {
|
||||
ctx.status = 500;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: 'Error registering user',
|
||||
error: (error as Error).message
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// Admin registration
|
||||
async registerAdmin(ctx: Context) {
|
||||
const { username, password, email, adminKey } = ctx.request.body as {
|
||||
username: string,
|
||||
password: string,
|
||||
email?: string,
|
||||
adminKey: string
|
||||
};
|
||||
|
||||
// Validate input
|
||||
if (!username || !password) {
|
||||
ctx.status = 400;
|
||||
ctx.body = { success: false, message: 'Username and password are required' };
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate admin key
|
||||
const validAdminKey = appConfig.adminKey;
|
||||
if (adminKey !== validAdminKey) {
|
||||
ctx.status = 403;
|
||||
ctx.body = { success: false, message: 'Invalid admin key' };
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const usersCollection = db.getCollection<User>(dbConfig.collections.users);
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await usersCollection.findOne({ username });
|
||||
if (existingUser) {
|
||||
ctx.status = 409;
|
||||
ctx.body = { success: false, message: 'Username already exists' };
|
||||
return;
|
||||
}
|
||||
|
||||
// Hash password before saving
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
const hashedPassword = await bcrypt.hash(password, salt);
|
||||
|
||||
// Create new admin user
|
||||
const now = new Date();
|
||||
const newUser: User = {
|
||||
username,
|
||||
password: hashedPassword,
|
||||
email,
|
||||
role: 'admin', // Add admin role
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
};
|
||||
|
||||
// Save user to database
|
||||
const result = await usersCollection.insertOne(newUser);
|
||||
|
||||
ctx.status = 201;
|
||||
ctx.body = {
|
||||
success: true,
|
||||
message: 'Admin registered successfully',
|
||||
userId: result.insertedId
|
||||
};
|
||||
} catch (error) {
|
||||
ctx.status = 500;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: 'Error registering admin',
|
||||
error: (error as Error).message
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// User login
|
||||
async login(ctx: Context) {
|
||||
const { username, password } = ctx.request.body as { username: string, password: string };
|
||||
|
||||
// Validate input
|
||||
if (!username || !password) {
|
||||
ctx.status = 400;
|
||||
ctx.body = { success: false, message: 'Username and password are required' };
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const usersCollection = db.getCollection<User>(dbConfig.collections.users);
|
||||
|
||||
// Check if user exists
|
||||
const user = await usersCollection.findOne({ username });
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
ctx.body = { success: false, message: 'Invalid credentials' };
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify password
|
||||
const isPasswordValid = await bcrypt.compare(password, user.password);
|
||||
if (!isPasswordValid) {
|
||||
ctx.status = 401;
|
||||
ctx.body = { success: false, message: 'Invalid credentials' };
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
const token = jwt.sign(
|
||||
{
|
||||
id: user._id?.toString(),
|
||||
username: user.username
|
||||
},
|
||||
jwtConfig.secret,
|
||||
{ expiresIn: jwtConfig.expiresIn } as SignOptions
|
||||
);
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
message: 'Login successful',
|
||||
userId: user._id,
|
||||
username: user.username,
|
||||
token
|
||||
};
|
||||
} catch (error) {
|
||||
ctx.status = 500;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: 'Error during login',
|
||||
error: (error as Error).message
|
||||
};
|
||||
}
|
||||
// Validate input
|
||||
if (!username || !password) {
|
||||
ctx.status = 400;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Username and password are required",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const usersCollection = db.getCollection<User>(
|
||||
dbConfig.collections.users,
|
||||
);
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await usersCollection.findOne({ username });
|
||||
if (existingUser) {
|
||||
ctx.status = 409;
|
||||
ctx.body = { success: false, message: "Username already exists" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Hash password before saving
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
const hashedPassword = await bcrypt.hash(password, salt);
|
||||
|
||||
// Create new user
|
||||
const now = new Date();
|
||||
const newUser: User = {
|
||||
username,
|
||||
password: hashedPassword,
|
||||
email,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
// Save user to database
|
||||
const result = await usersCollection.insertOne(newUser);
|
||||
|
||||
ctx.status = 201;
|
||||
ctx.body = {
|
||||
success: true,
|
||||
message: "User registered successfully",
|
||||
userId: result.insertedId,
|
||||
};
|
||||
} catch (error) {
|
||||
ctx.status = 500;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Error registering user",
|
||||
error: (error as Error).message,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// Admin registration
|
||||
async registerAdmin(ctx: Context) {
|
||||
const { username, password, email, adminKey } = ctx.request.body as {
|
||||
username: string;
|
||||
password: string;
|
||||
email?: string;
|
||||
adminKey: string;
|
||||
};
|
||||
|
||||
// Validate input
|
||||
if (!username || !password) {
|
||||
ctx.status = 400;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Username and password are required",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate admin key
|
||||
const validAdminKey = appConfig.adminKey;
|
||||
if (adminKey !== validAdminKey) {
|
||||
ctx.status = 403;
|
||||
ctx.body = { success: false, message: "Invalid admin key" };
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const usersCollection = db.getCollection<User>(
|
||||
dbConfig.collections.users,
|
||||
);
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await usersCollection.findOne({ username });
|
||||
if (existingUser) {
|
||||
ctx.status = 409;
|
||||
ctx.body = { success: false, message: "Username already exists" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Hash password before saving
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
const hashedPassword = await bcrypt.hash(password, salt);
|
||||
|
||||
// Create new admin user
|
||||
const now = new Date();
|
||||
const newUser: User = {
|
||||
username,
|
||||
password: hashedPassword,
|
||||
email,
|
||||
role: "admin", // Add admin role
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
// Save user to database
|
||||
const result = await usersCollection.insertOne(newUser);
|
||||
|
||||
ctx.status = 201;
|
||||
ctx.body = {
|
||||
success: true,
|
||||
message: "Admin registered successfully",
|
||||
userId: result.insertedId,
|
||||
};
|
||||
} catch (error) {
|
||||
ctx.status = 500;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Error registering admin",
|
||||
error: (error as Error).message,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// User login
|
||||
async login(ctx: Context) {
|
||||
const { username, password } = ctx.request.body as {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
// Validate input
|
||||
if (!username || !password) {
|
||||
ctx.status = 400;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Username and password are required",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const usersCollection = db.getCollection<User>(
|
||||
dbConfig.collections.users,
|
||||
);
|
||||
|
||||
// Check if user exists
|
||||
const user = await usersCollection.findOne({ username });
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
ctx.body = { success: false, message: "Invalid credentials" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify password
|
||||
const isPasswordValid = await bcrypt.compare(password, user.password);
|
||||
if (!isPasswordValid) {
|
||||
ctx.status = 401;
|
||||
ctx.body = { success: false, message: "Invalid credentials" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
const token = jwt.sign(
|
||||
{
|
||||
id: user._id?.toString(),
|
||||
username: user.username,
|
||||
},
|
||||
jwtConfig.secret,
|
||||
{ expiresIn: jwtConfig.expiresIn } as SignOptions,
|
||||
);
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
message: "Login successful",
|
||||
userId: user._id,
|
||||
username: user.username,
|
||||
token,
|
||||
};
|
||||
} catch (error) {
|
||||
ctx.status = 500;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Error during login",
|
||||
error: (error as Error).message,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,166 +1,414 @@
|
||||
import type { Context } from 'koa';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import dbConfig from '../config/database';
|
||||
import type { Category } from '../models';
|
||||
import DatabaseService from '../services/database';
|
||||
import type { Context } from "koa";
|
||||
import { ObjectId } from "mongodb";
|
||||
import dbConfig from "../config/database";
|
||||
import type { Category } from "../models";
|
||||
import DatabaseService from "../services/database";
|
||||
|
||||
const db = DatabaseService.getInstance();
|
||||
|
||||
// Middleware to check if user is admin
|
||||
function requireAdmin(ctx: Context, next: Function) {
|
||||
const user = (ctx.state as any).user;
|
||||
if (user.role !== "admin") {
|
||||
ctx.status = 403;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Admin access required",
|
||||
};
|
||||
return;
|
||||
}
|
||||
return next();
|
||||
}
|
||||
|
||||
export default {
|
||||
// Get categories for a user
|
||||
async getUserCategories(ctx: Context) {
|
||||
const user = (ctx.state as any).user;
|
||||
const userId = user.id;
|
||||
// Get categories for a user
|
||||
async getUserCategories(ctx: Context) {
|
||||
const user = (ctx.state as any).user;
|
||||
const userId = user.id;
|
||||
|
||||
try {
|
||||
const categoriesCollection = db.getCollection<Category>(dbConfig.collections.categories);
|
||||
try {
|
||||
const categoriesCollection = db.getCollection<Category>(
|
||||
dbConfig.collections.categories,
|
||||
);
|
||||
|
||||
const categories = await categoriesCollection.find({
|
||||
userId: new ObjectId(userId)
|
||||
}).toArray();
|
||||
const categories = await categoriesCollection
|
||||
.find({
|
||||
userId: new ObjectId(String(userId)),
|
||||
})
|
||||
.toArray();
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
data: categories
|
||||
};
|
||||
} catch (error) {
|
||||
ctx.status = 500;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: 'Error retrieving categories',
|
||||
error: (error as Error).message
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// Add new category
|
||||
async addCategory(ctx: Context) {
|
||||
const user = (ctx.state as any).user;
|
||||
const userId = user.id;
|
||||
const { name, icon, color } = ctx.request.body as {
|
||||
name: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
if (!name) {
|
||||
ctx.status = 400;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: 'Category name is required'
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const categoriesCollection = db.getCollection<Category>(dbConfig.collections.categories);
|
||||
|
||||
// Check if category already exists
|
||||
const existingCategory = await categoriesCollection.findOne({
|
||||
userId: new ObjectId(userId),
|
||||
name
|
||||
});
|
||||
|
||||
if (existingCategory) {
|
||||
ctx.status = 409;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: 'Category already exists'
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const newCategory: Category = {
|
||||
userId: new ObjectId(userId),
|
||||
name,
|
||||
icon,
|
||||
color,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
};
|
||||
|
||||
const result = await categoriesCollection.insertOne(newCategory);
|
||||
|
||||
ctx.status = 201;
|
||||
ctx.body = {
|
||||
success: true,
|
||||
message: 'Category added successfully',
|
||||
categoryId: result.insertedId
|
||||
};
|
||||
} catch (error) {
|
||||
ctx.status = 500;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: 'Error adding category',
|
||||
error: (error as Error).message
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// Update category
|
||||
async updateCategory(ctx: Context) {
|
||||
const user = (ctx.state as any).user;
|
||||
const userId = user.id;
|
||||
const categoryId = ctx.params.id;
|
||||
|
||||
const { name, icon, color } = ctx.request.body as {
|
||||
name?: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
if (!categoryId || !ObjectId.isValid(categoryId)) {
|
||||
ctx.status = 400;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: 'Valid category ID is required'
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const categoriesCollection = db.getCollection<Category>(dbConfig.collections.categories);
|
||||
|
||||
// Make sure category belongs to user
|
||||
const category = await categoriesCollection.findOne({
|
||||
_id: new ObjectId(categoryId),
|
||||
userId: new ObjectId(userId)
|
||||
});
|
||||
|
||||
if (!category) {
|
||||
ctx.status = 404;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: 'Category not found'
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const updateData: Partial<Category> = {
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
if (name) updateData.name = name;
|
||||
if (icon) updateData.icon = icon;
|
||||
if (color) updateData.color = color;
|
||||
|
||||
await categoriesCollection.updateOne(
|
||||
{ _id: new ObjectId(categoryId) },
|
||||
{ $set: updateData }
|
||||
);
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
message: 'Category updated successfully'
|
||||
};
|
||||
} catch (error) {
|
||||
ctx.status = 500;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: 'Error updating category',
|
||||
error: (error as Error).message
|
||||
};
|
||||
}
|
||||
ctx.body = {
|
||||
success: true,
|
||||
data: categories,
|
||||
};
|
||||
} catch (error) {
|
||||
ctx.status = 500;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Error retrieving categories",
|
||||
error: (error as Error).message,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// Add new category
|
||||
async addCategory(ctx: Context) {
|
||||
const user = (ctx.state as any).user;
|
||||
const userId = user.id;
|
||||
const { name, icon, color } = ctx.request.body as {
|
||||
name: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
if (!name) {
|
||||
ctx.status = 400;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Category name is required",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const categoriesCollection = db.getCollection<Category>(
|
||||
dbConfig.collections.categories,
|
||||
);
|
||||
|
||||
// Check if category already exists
|
||||
const existingCategory = await categoriesCollection.findOne({
|
||||
userId: new ObjectId(userId),
|
||||
name,
|
||||
});
|
||||
|
||||
if (existingCategory) {
|
||||
ctx.status = 409;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Category already exists",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const newCategory: Category = {
|
||||
userId: new ObjectId(userId),
|
||||
name,
|
||||
icon,
|
||||
color,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
const result = await categoriesCollection.insertOne(newCategory);
|
||||
|
||||
ctx.status = 201;
|
||||
ctx.body = {
|
||||
success: true,
|
||||
message: "Category added successfully",
|
||||
categoryId: result.insertedId,
|
||||
};
|
||||
} catch (error) {
|
||||
ctx.status = 500;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Error adding category",
|
||||
error: (error as Error).message,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// Update category
|
||||
async updateCategory(ctx: Context) {
|
||||
const user = (ctx.state as any).user;
|
||||
const userId = user.id;
|
||||
const categoryId = ctx.params.id;
|
||||
|
||||
const { name, icon, color } = ctx.request.body as {
|
||||
name?: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
if (!categoryId || !ObjectId.isValid(categoryId)) {
|
||||
ctx.status = 400;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Valid category ID is required",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const categoriesCollection = db.getCollection<Category>(
|
||||
dbConfig.collections.categories,
|
||||
);
|
||||
|
||||
// Make sure category belongs to user
|
||||
const category = await categoriesCollection.findOne({
|
||||
_id: new ObjectId(categoryId),
|
||||
userId: new ObjectId(userId),
|
||||
});
|
||||
|
||||
if (!category) {
|
||||
ctx.status = 404;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Category not found",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const updateData: Partial<Category> = {
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
if (name) updateData.name = name;
|
||||
if (icon) updateData.icon = icon;
|
||||
if (color) updateData.color = color;
|
||||
|
||||
await categoriesCollection.updateOne(
|
||||
{ _id: new ObjectId(categoryId) },
|
||||
{ $set: updateData },
|
||||
);
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
message: "Category updated successfully",
|
||||
};
|
||||
} catch (error) {
|
||||
ctx.status = 500;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Error updating category",
|
||||
error: (error as Error).message,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// Delete category (Admin only)
|
||||
async deleteCategory(ctx: Context) {
|
||||
const user = (ctx.state as any).user;
|
||||
const categoryId = ctx.params.id;
|
||||
|
||||
if (!categoryId || !ObjectId.isValid(categoryId)) {
|
||||
ctx.status = 400;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Valid category ID is required",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const categoriesCollection = db.getCollection<Category>(
|
||||
dbConfig.collections.categories,
|
||||
);
|
||||
|
||||
// Check if category exists
|
||||
const category = await categoriesCollection.findOne({
|
||||
_id: new ObjectId(categoryId),
|
||||
});
|
||||
|
||||
if (!category) {
|
||||
ctx.status = 404;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Category not found",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if category is being used in transactions
|
||||
const transactionsCollection = db.getCollection(
|
||||
dbConfig.collections.transactions,
|
||||
);
|
||||
|
||||
const transactionCount = await transactionsCollection.countDocuments({
|
||||
categoryId: new ObjectId(categoryId),
|
||||
});
|
||||
|
||||
if (transactionCount > 0) {
|
||||
ctx.status = 409;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: `Cannot delete category. It is used in ${transactionCount} transaction(s).`,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
await categoriesCollection.deleteOne({
|
||||
_id: new ObjectId(categoryId),
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
message: "Category deleted successfully",
|
||||
};
|
||||
} catch (error) {
|
||||
ctx.status = 500;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Error deleting category",
|
||||
error: (error as Error).message,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// Get all categories (Admin only)
|
||||
async getAllCategories(ctx: Context) {
|
||||
try {
|
||||
const categoriesCollection = db.getCollection<Category>(
|
||||
dbConfig.collections.categories,
|
||||
);
|
||||
|
||||
const categories = await categoriesCollection
|
||||
.find({})
|
||||
.sort({ createdAt: -1 })
|
||||
.toArray();
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
data: categories,
|
||||
};
|
||||
} catch (error) {
|
||||
ctx.status = 500;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Error retrieving categories",
|
||||
error: (error as Error).message,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// Create global category (Admin only)
|
||||
async createGlobalCategory(ctx: Context) {
|
||||
const { name, icon, color } = ctx.request.body as {
|
||||
name: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
if (!name) {
|
||||
ctx.status = 400;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Category name is required",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const categoriesCollection = db.getCollection<Category>(
|
||||
dbConfig.collections.categories,
|
||||
);
|
||||
|
||||
// Check if global category already exists
|
||||
const existingCategory = await categoriesCollection.findOne({
|
||||
name,
|
||||
userId: null, // Global categories have no userId
|
||||
});
|
||||
|
||||
if (existingCategory) {
|
||||
ctx.status = 409;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Global category already exists",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const newCategory: any = {
|
||||
userId: null, // Global category
|
||||
name,
|
||||
icon,
|
||||
color,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
const result = await categoriesCollection.insertOne(newCategory);
|
||||
|
||||
ctx.status = 201;
|
||||
ctx.body = {
|
||||
success: true,
|
||||
message: "Global category created successfully",
|
||||
categoryId: result.insertedId,
|
||||
};
|
||||
} catch (error) {
|
||||
ctx.status = 500;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Error creating global category",
|
||||
error: (error as Error).message,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// Update any category (Admin only)
|
||||
async updateAnyCategory(ctx: Context) {
|
||||
const categoryId = ctx.params.id;
|
||||
const { name, icon, color } = ctx.request.body as {
|
||||
name?: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
if (!categoryId || !ObjectId.isValid(categoryId)) {
|
||||
ctx.status = 400;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Valid category ID is required",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const categoriesCollection = db.getCollection<Category>(
|
||||
dbConfig.collections.categories,
|
||||
);
|
||||
|
||||
const category = await categoriesCollection.findOne({
|
||||
_id: new ObjectId(categoryId),
|
||||
});
|
||||
|
||||
if (!category) {
|
||||
ctx.status = 404;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Category not found",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const updateData: Partial<Category> = {
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
if (name) updateData.name = name;
|
||||
if (icon) updateData.icon = icon;
|
||||
if (color) updateData.color = color;
|
||||
|
||||
await categoriesCollection.updateOne(
|
||||
{ _id: new ObjectId(categoryId) },
|
||||
{ $set: updateData },
|
||||
);
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
message: "Category updated successfully",
|
||||
};
|
||||
} catch (error) {
|
||||
ctx.status = 500;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Error updating category",
|
||||
error: (error as Error).message,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// Middleware function for admin routes
|
||||
requireAdmin,
|
||||
};
|
||||
|
||||
@@ -31,7 +31,7 @@ export default {
|
||||
|
||||
try {
|
||||
const transactionsCollection = db.getCollection<Transaction>(
|
||||
dbConfig.collections.transactions
|
||||
dbConfig.collections.transactions,
|
||||
);
|
||||
|
||||
// Get the latest 10 transactions for the user
|
||||
@@ -61,40 +61,61 @@ export default {
|
||||
async addTransaction(ctx: Context) {
|
||||
const user = (ctx.state as any).user;
|
||||
const userId = user.id;
|
||||
const { amount, type, categoryId, description } = ctx.request.body as {
|
||||
amount: number;
|
||||
type: "income" | "expense";
|
||||
categoryId: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
// Validate input
|
||||
if (!amount || !type || !categoryId) {
|
||||
ctx.status = 400;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Amount, type and category are required",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { amount, type, categoryId, description, date } = ctx.request
|
||||
.body as {
|
||||
amount: number;
|
||||
type: "income" | "expense";
|
||||
categoryId: string;
|
||||
description: string;
|
||||
date?: string;
|
||||
};
|
||||
|
||||
console.log("Received transaction data:", ctx.request.body);
|
||||
|
||||
// Validate input
|
||||
if (!amount || !type || !categoryId) {
|
||||
ctx.status = 400;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Amount, type and category are required",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const transactionsCollection = db.getCollection<Transaction>(
|
||||
dbConfig.collections.transactions
|
||||
dbConfig.collections.transactions,
|
||||
);
|
||||
|
||||
const now = new Date();
|
||||
const transactionDate = date ? new Date(date) : now;
|
||||
|
||||
// Check if categoryId is a valid ObjectId
|
||||
let categoryObjectId;
|
||||
try {
|
||||
categoryObjectId = new ObjectId(categoryId);
|
||||
} catch (err) {
|
||||
ctx.status = 400;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Invalid category ID format",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const newTransaction: Transaction = {
|
||||
userId: new ObjectId(userId),
|
||||
amount,
|
||||
amount: Number(amount),
|
||||
type,
|
||||
categoryId: new ObjectId(categoryId),
|
||||
categoryId: categoryObjectId,
|
||||
description: description || "",
|
||||
date: now,
|
||||
date: transactionDate,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
console.log("Inserting transaction:", newTransaction);
|
||||
const result = await transactionsCollection.insertOne(newTransaction);
|
||||
|
||||
ctx.status = 201;
|
||||
@@ -104,6 +125,7 @@ export default {
|
||||
transactionId: result.insertedId,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Transaction error:", error);
|
||||
ctx.status = 500;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import type { Context } from "koa";
|
||||
import { ObjectId } from "mongodb";
|
||||
import dbConfig from "../config/database";
|
||||
import DatabaseService from "../services/database";
|
||||
import webpush from "web-push";
|
||||
|
||||
// Configure web-push with VAPID keys
|
||||
const vapidKeys = {
|
||||
publicKey: process.env.VAPID_PUBLIC_KEY || 'your-public-key',
|
||||
privateKey: process.env.VAPID_PRIVATE_KEY || 'your-private-key'
|
||||
};
|
||||
|
||||
webpush.setVapidDetails(
|
||||
'mailto:support@budgetmanager.com',
|
||||
vapidKeys.publicKey,
|
||||
vapidKeys.privateKey
|
||||
);
|
||||
|
||||
const db = DatabaseService.getInstance();
|
||||
|
||||
export default {
|
||||
// Get VAPID public key
|
||||
async getVapidPublicKey(ctx: Context) {
|
||||
ctx.body = vapidKeys.publicKey;
|
||||
},
|
||||
|
||||
// Register a new push subscription
|
||||
async registerPushSubscription(ctx: Context) {
|
||||
const user = (ctx.state as any).user;
|
||||
const userId = user.id;
|
||||
const { subscription } = ctx.request.body;
|
||||
|
||||
if (!subscription) {
|
||||
ctx.status = 400;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Subscription data is required"
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const subscriptionsCollection = db.getCollection(
|
||||
dbConfig.collections.pushSubscriptions
|
||||
);
|
||||
|
||||
// Store subscription with user ID
|
||||
await subscriptionsCollection.updateOne(
|
||||
{ userId: new ObjectId(String(userId)) },
|
||||
{ $set: { subscription, updatedAt: new Date() } },
|
||||
{ upsert: true }
|
||||
);
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
message: "Push subscription registered successfully"
|
||||
};
|
||||
} catch (error) {
|
||||
ctx.status = 500;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Error registering push subscription",
|
||||
error: (error as Error).message
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// Send notification for a new transaction
|
||||
async sendTransactionNotification(userId: string, transaction: any) {
|
||||
try {
|
||||
const subscriptionsCollection = db.getCollection(
|
||||
dbConfig.collections.pushSubscriptions
|
||||
);
|
||||
|
||||
// Find user's subscription
|
||||
const record = await subscriptionsCollection.findOne({
|
||||
userId: new ObjectId(String(userId))
|
||||
});
|
||||
|
||||
if (!record || !record.subscription) {
|
||||
return { success: false, message: "No subscription found" };
|
||||
}
|
||||
|
||||
// Prepare notification payload
|
||||
const payload = JSON.stringify({
|
||||
message: `New ${transaction.type}: ${transaction.description || 'Transaction'} - ${transaction.amount}`,
|
||||
additionalData: {
|
||||
transactionId: transaction._id.toString(),
|
||||
type: transaction.type
|
||||
}
|
||||
});
|
||||
|
||||
// Send push notification
|
||||
await webpush.sendNotification(record.subscription, payload);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Error sending push notification:", error);
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,33 +1,29 @@
|
||||
import type { Middleware } from 'koa';
|
||||
import jwt from 'koa-jwt';
|
||||
import jwtConfig from '../config/jwt';
|
||||
import type { Middleware } from "koa";
|
||||
import jwt from "koa-jwt";
|
||||
import jwtConfig from "../config/jwt";
|
||||
|
||||
// JWT authentication middleware
|
||||
export const authenticate: Middleware = jwt({
|
||||
secret: jwtConfig.secret
|
||||
secret: jwtConfig.secret,
|
||||
}).unless({
|
||||
// Paths that don't require authentication
|
||||
path: [
|
||||
'/api/auth/login',
|
||||
'/api/auth/register',
|
||||
'/api/auth/register-admin'
|
||||
]
|
||||
// Paths that don't require authentication
|
||||
path: ["/api/auth/login", "/api/auth/register", "/api/auth/register-admin"],
|
||||
});
|
||||
|
||||
// Error handling middleware for JWT authentication
|
||||
export const handleJwtError: Middleware = async (ctx, next) => {
|
||||
try {
|
||||
// Next middleware have to be jwt call.
|
||||
await next();
|
||||
} catch (err: any) {
|
||||
if (err.status === 401) {
|
||||
ctx.status = 401;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: 'Unauthorized - Invalid or expired token'
|
||||
};
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
try {
|
||||
// Next middleware have to be jwt call.
|
||||
await next();
|
||||
} catch (err: any) {
|
||||
if (err.status === 401) {
|
||||
ctx.status = 401;
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: "Unauthorized - Invalid or expired token",
|
||||
};
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { ObjectId } from "mongodb";
|
||||
|
||||
export interface User {
|
||||
_id?: ObjectId;
|
||||
username: string;
|
||||
password: string;
|
||||
email?: string;
|
||||
role?: 'user' | 'admin';
|
||||
role?: "user" | "admin";
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -14,7 +14,7 @@ export interface Transaction {
|
||||
_id?: ObjectId;
|
||||
userId: ObjectId;
|
||||
amount: number;
|
||||
type: 'income' | 'expense';
|
||||
type: "income" | "expense";
|
||||
categoryId: ObjectId;
|
||||
description: string;
|
||||
date: Date;
|
||||
|
||||
+37
-14
@@ -1,9 +1,10 @@
|
||||
import Router from 'koa-router';
|
||||
import authController from '../controllers/auth';
|
||||
import protectedController from '../controllers/protected';
|
||||
import categoriesController from '../controllers/categories';
|
||||
import Router from "koa-router";
|
||||
import authController from "../controllers/auth";
|
||||
import protectedController from "../controllers/protected";
|
||||
import categoriesController from "../controllers/categories";
|
||||
import pushController from "../controllers/push";
|
||||
|
||||
const router = new Router({ prefix: '/api' });
|
||||
const router = new Router({ prefix: "/api" });
|
||||
|
||||
/*
|
||||
Middleware requires authentication for all routes.
|
||||
@@ -13,21 +14,43 @@ const router = new Router({ prefix: '/api' });
|
||||
|
||||
// --- Public routes ---
|
||||
// Auth routes
|
||||
router.post('/auth/register', authController.register);
|
||||
router.post('/auth/register-admin', authController.registerAdmin);
|
||||
router.post('/auth/login', authController.login);
|
||||
router.post("/auth/register", authController.register);
|
||||
router.post("/auth/register-admin", authController.registerAdmin);
|
||||
router.post("/auth/login", authController.login);
|
||||
|
||||
// --- All below routes require authentication ----
|
||||
// Protected routes
|
||||
router.get('/protected', protectedController.getProtectedData);
|
||||
router.get("/protected", protectedController.getProtectedData);
|
||||
|
||||
// Transaction routes
|
||||
router.get('/transactions', protectedController.getUserTransactions);
|
||||
router.post('/transactions', protectedController.addTransaction);
|
||||
router.get("/transactions", protectedController.getUserTransactions);
|
||||
router.post("/transactions", protectedController.addTransaction);
|
||||
|
||||
// Category routes
|
||||
router.get('/categories', categoriesController.getUserCategories);
|
||||
router.post('/categories', categoriesController.addCategory);
|
||||
router.put('/categories/:id', categoriesController.updateCategory);
|
||||
router.get("/categories", categoriesController.getUserCategories);
|
||||
router.post("/categories", categoriesController.addCategory);
|
||||
router.put("/categories/:id", categoriesController.updateCategory);
|
||||
|
||||
// Admin-only category routes
|
||||
router.get(
|
||||
"/admin/categories",
|
||||
categoriesController.requireAdmin,
|
||||
categoriesController.getAllCategories,
|
||||
);
|
||||
router.post(
|
||||
"/admin/categories",
|
||||
categoriesController.requireAdmin,
|
||||
categoriesController.createGlobalCategory,
|
||||
);
|
||||
router.put(
|
||||
"/admin/categories/:id",
|
||||
categoriesController.requireAdmin,
|
||||
categoriesController.updateAnyCategory,
|
||||
);
|
||||
router.delete(
|
||||
"/admin/categories/:id",
|
||||
categoriesController.requireAdmin,
|
||||
categoriesController.deleteCategory,
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,46 +1,48 @@
|
||||
import { Collection, Db, MongoClient, type Document } from 'mongodb';
|
||||
import dbConfig from '../config/database';
|
||||
import { Collection, Db, MongoClient, type Document } from "mongodb";
|
||||
import dbConfig from "../config/database";
|
||||
|
||||
class DatabaseService {
|
||||
private client: MongoClient;
|
||||
private db: Db | null = null;
|
||||
private static instance: DatabaseService;
|
||||
private client: MongoClient;
|
||||
private db: Db | null = null;
|
||||
private static instance: DatabaseService;
|
||||
|
||||
private constructor() {
|
||||
this.client = new MongoClient(dbConfig.uri);
|
||||
}
|
||||
private constructor() {
|
||||
this.client = new MongoClient(dbConfig.uri);
|
||||
}
|
||||
|
||||
static getInstance(): DatabaseService {
|
||||
if (!DatabaseService.instance) {
|
||||
DatabaseService.instance = new DatabaseService();
|
||||
}
|
||||
return DatabaseService.instance;
|
||||
static getInstance(): DatabaseService {
|
||||
if (!DatabaseService.instance) {
|
||||
DatabaseService.instance = new DatabaseService();
|
||||
}
|
||||
return DatabaseService.instance;
|
||||
}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
try {
|
||||
await this.client.connect();
|
||||
this.db = this.client.db(dbConfig.dbName);
|
||||
console.log('Connected to MongoDB successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to MongoDB', error);
|
||||
throw error;
|
||||
}
|
||||
async connect(): Promise<void> {
|
||||
try {
|
||||
await this.client.connect();
|
||||
this.db = this.client.db(dbConfig.dbName);
|
||||
console.log("Connected to MongoDB successfully");
|
||||
} catch (error) {
|
||||
console.error("Failed to connect to MongoDB", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
getCollection<T extends Document>(collectionName: string): Collection<T> {
|
||||
if (!this.db) {
|
||||
throw new Error('Database connection not established. Call connect() first.');
|
||||
}
|
||||
return this.db.collection<T>(collectionName);
|
||||
getCollection<T extends Document>(collectionName: string): Collection<T> {
|
||||
if (!this.db) {
|
||||
throw new Error(
|
||||
"Database connection not established. Call connect() first.",
|
||||
);
|
||||
}
|
||||
return this.db.collection<T>(collectionName);
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
if (this.client) {
|
||||
await this.client.close();
|
||||
console.log('Disconnected from MongoDB');
|
||||
}
|
||||
async disconnect(): Promise<void> {
|
||||
if (this.client) {
|
||||
await this.client.close();
|
||||
console.log("Disconnected from MongoDB");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default DatabaseService;
|
||||
|
||||
Reference in New Issue
Block a user