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:
2025-05-23 21:35:43 +02:00
parent 97908db3e0
commit 26e3b76b11
102 changed files with 14233 additions and 668 deletions
+208 -190
View File
@@ -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,
};
}
},
};
+405 -157
View File
@@ -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,
};
+43 -21
View File
@@ -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,
+102
View File
@@ -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 };
}
}
};