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:
+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 };
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user