'use server' import fs from 'fs/promises' import path from 'path' import { HabitsData, CoinsData, CoinTransaction, TransactionType, WishlistItemType, WishlistData, Settings, DataType, DATA_DEFAULTS, getDefaultSettings, UserData, getDefaultUsersData, User, getDefaultWishlistData, getDefaultHabitsData, getDefaultCoinsData, Permission, ServerSettings } from '@/lib/types' import { d2t, deepMerge, getNow, checkPermission, uuid } from '@/lib/utils'; import { verifyPassword } from "@/lib/server-helpers"; import { saltAndHashPassword } from "@/lib/server-helpers"; import { signInSchema } from '@/lib/zod'; import { auth } from '@/auth'; import _ from 'lodash'; import { getCurrentUser, getCurrentUserId } from '@/lib/server-helpers' import { PermissionError } from '@/lib/exceptions' type ResourceType = 'habit' | 'wishlist' | 'coins' type ActionType = 'write' | 'interact' async function verifyPermission( resource: ResourceType, action: ActionType ): Promise { // const user = await getCurrentUser() // if (!user) throw new PermissionError('User not authenticated') // if (user.isAdmin) return // Admins bypass permission checks // if (!checkPermission(user.permissions, resource, action)) { // throw new PermissionError(`User does not have ${action} permission for ${resource}`) // } return } function getDefaultData(type: DataType): T { return DATA_DEFAULTS[type]() as T; } async function ensureDataDir() { const dataDir = path.join(process.cwd(), 'data') try { await fs.access(dataDir) } catch { await fs.mkdir(dataDir, { recursive: true }) } } async function loadData(type: DataType): Promise { try { await ensureDataDir() const filePath = path.join(process.cwd(), 'data', `${type}.json`) try { await fs.access(filePath) } catch { // File doesn't exist, create it with default data const initialData = getDefaultData(type) await fs.writeFile(filePath, JSON.stringify(initialData, null, 2)) return initialData as T } // File exists, read and return its contents const data = await fs.readFile(filePath, 'utf8') const jsonData = JSON.parse(data) as T return jsonData } catch (error) { console.error(`Error loading ${type} data:`, error) return getDefaultData(type) } } async function saveData(type: DataType, data: T): Promise { try { const user = await getCurrentUser() if (!user) throw new Error('User not authenticated') await ensureDataDir() const filePath = path.join(process.cwd(), 'data', `${type}.json`) const saveData = data await fs.writeFile(filePath, JSON.stringify(saveData, null, 2)) } catch (error) { console.error(`Error saving ${type} data:`, error) } } // Wishlist specific functions export async function loadWishlistData(): Promise { const user = await getCurrentUser() if (!user) return getDefaultWishlistData() const data = await loadData('wishlist') return { ...data, items: data.items.filter(x => user.isAdmin || x.userIds?.includes(user.id)) } } export async function loadWishlistItems(): Promise { const data = await loadWishlistData() return data.items } export async function saveWishlistItems(data: WishlistData): Promise { await verifyPermission('wishlist', 'write') const user = await getCurrentUser() data.items = data.items.map(wishlist => ({ ...wishlist, userIds: wishlist.userIds || (user ? [user.id] : undefined) })) if (!user?.isAdmin) { const existingData = await loadData('wishlist') existingData.items = existingData.items.filter(x => user?.id && !x.userIds?.includes(user?.id)) data.items = [ ...existingData.items, ...data.items ] } return saveData('wishlist', data) } // Habits specific functions export async function loadHabitsData(): Promise { const user = await getCurrentUser() if (!user) return getDefaultHabitsData() const data = await loadData('habits') return { ...data, habits: data.habits.filter(x => user.isAdmin || x.userIds?.includes(user.id)) } } export async function saveHabitsData(data: HabitsData): Promise { await verifyPermission('habit', 'write') const user = await getCurrentUser() // Create clone of input data const newData = _.cloneDeep(data) // Map habits with user IDs newData.habits = newData.habits.map(habit => ({ ...habit, userIds: habit.userIds || (user ? [user.id] : undefined) })) if (!user?.isAdmin) { const existingData = await loadData('habits') const existingHabits = existingData.habits.filter(x => user?.id && !x.userIds?.includes(user?.id)) newData.habits = [ ...existingHabits, ...newData.habits ] } return saveData('habits', newData) } // Coins specific functions export async function loadCoinsData(): Promise { try { const user = await getCurrentUser() if (!user) return getDefaultCoinsData() const data = await loadData('coins') return { ...data, transactions: user.isAdmin ? data.transactions : data.transactions.filter(x => x.userId === user.id) } } catch { return getDefaultCoinsData() } } export async function saveCoinsData(data: CoinsData): Promise { const user = await getCurrentUser() // Create clones of the data const newData = _.cloneDeep(data) newData.transactions = newData.transactions.map(transaction => ({ ...transaction, userId: transaction.userId || user?.id })) if (!user?.isAdmin) { const existingData = await loadData('coins') const existingTransactions = existingData.transactions.filter(x => user?.id && x.userId !== user.id) newData.transactions = [ ...newData.transactions, ...existingTransactions ] } return saveData('coins', newData) } export async function addCoins({ amount, description, type = 'MANUAL_ADJUSTMENT', relatedItemId, note, userId, }: { amount: number description: string type?: TransactionType relatedItemId?: string note?: string userId?: string }): Promise { await verifyPermission('coins', type === 'MANUAL_ADJUSTMENT' ? 'write' : 'interact') const data = await loadCoinsData() const newTransaction: CoinTransaction = { id: uuid(), amount, type, description, timestamp: d2t({ dateTime: getNow({}) }), ...(relatedItemId && { relatedItemId }), ...(note && note.trim() !== '' && { note }), userId: userId || await getCurrentUserId() } const newData: CoinsData = { balance: data.balance + amount, transactions: [newTransaction, ...data.transactions] } await saveCoinsData(newData) return newData } export async function loadSettings(): Promise { const defaultSettings = getDefaultSettings() try { const user = await getCurrentUser() if (!user) return defaultSettings const data = await loadData('settings') return { ...defaultSettings, ...data } } catch { return defaultSettings } } export async function saveSettings(settings: Settings): Promise { return saveData('settings', settings) } export async function removeCoins({ amount, description, type = 'MANUAL_ADJUSTMENT', relatedItemId, note, userId, }: { amount: number description: string type?: TransactionType relatedItemId?: string note?: string userId?: string }): Promise { await verifyPermission('coins', type === 'MANUAL_ADJUSTMENT' ? 'write' : 'interact') const data = await loadCoinsData() const newTransaction: CoinTransaction = { id: uuid(), amount: -amount, type, description, timestamp: d2t({ dateTime: getNow({}) }), ...(relatedItemId && { relatedItemId }), ...(note && note.trim() !== '' && { note }), userId: userId || await getCurrentUserId() } const newData: CoinsData = { balance: Math.max(0, data.balance - amount), transactions: [newTransaction, ...data.transactions] } await saveCoinsData(newData) return newData } export async function uploadAvatar(formData: FormData): Promise { const file = formData.get('avatar') as File if (!file) throw new Error('No file provided') if (file.size > 5 * 1024 * 1024) { // 5MB throw new Error('File size must be less than 5MB') } // Create avatars directory if it doesn't exist const avatarsDir = path.join(process.cwd(), 'data', 'avatars') await fs.mkdir(avatarsDir, { recursive: true }) // Generate unique filename const ext = file.name.split('.').pop() const filename = `${Date.now()}.${ext}` const filePath = path.join(avatarsDir, filename) // Save file const buffer = await file.arrayBuffer() await fs.writeFile(filePath, Buffer.from(buffer)) return `/data/avatars/${filename}` } export async function getChangelog(): Promise { try { const changelogPath = path.join(process.cwd(), 'CHANGELOG.md') return await fs.readFile(changelogPath, 'utf8') } catch (error) { console.error('Error loading changelog:', error) return '# Changelog\n\nNo changelog available.' } } // user logic export async function loadUsersData(): Promise { try { return await loadData('auth') } catch { return getDefaultUsersData() } } export async function saveUsersData(data: UserData): Promise { return saveData('auth', data) } export async function getUser(username: string, plainTextPassword?: string): Promise { const data = await loadUsersData() const user = data.users.find(user => user.username === username) if (!user) return null // Verify the plaintext password against the stored salt:hash const isValidPassword = verifyPassword(plainTextPassword, user.password) if (!isValidPassword) return null return user } export async function createUser(formData: FormData): Promise { const username = formData.get('username') as string; let password = formData.get('password') as string | undefined; const avatarPath = formData.get('avatarPath') as string; const permissions = formData.get('permissions') ? JSON.parse(formData.get('permissions') as string) as Permission[] : undefined; if (password === null) password = undefined // Validate username and password against schema await signInSchema.parseAsync({ username, password }); const data = await loadUsersData(); // Check if username already exists if (data.users.some(user => user.username === username)) { throw new Error('Username already exists'); } const hashedPassword = password ? saltAndHashPassword(password) : ''; const newUser: User = { id: uuid(), username, password: hashedPassword, permissions, isAdmin: false, ...(avatarPath && { avatarPath }) }; const newData: UserData = { users: [...data.users, newUser] }; await saveUsersData(newData); return newUser; } export async function updateUser(userId: string, updates: Partial>): Promise { const data = await loadUsersData() const userIndex = data.users.findIndex(user => user.id === userId) if (userIndex === -1) { throw new Error('User not found') } // If username is being updated, check for duplicates if (updates.username) { const isDuplicate = data.users.some( user => user.username === updates.username && user.id !== userId ) if (isDuplicate) { throw new Error('Username already exists') } } const updatedUser = { ...data.users[userIndex], ...updates } const newData: UserData = { users: [ ...data.users.slice(0, userIndex), updatedUser, ...data.users.slice(userIndex + 1) ] } await saveUsersData(newData) return updatedUser } export async function updateUserPassword(userId: string, newPassword?: string): Promise { const data = await loadUsersData() const userIndex = data.users.findIndex(user => user.id === userId) if (userIndex === -1) { throw new Error('User not found') } const hashedPassword = newPassword ? saltAndHashPassword(newPassword) : '' const updatedUser = { ...data.users[userIndex], password: hashedPassword } const newData: UserData = { users: [ ...data.users.slice(0, userIndex), updatedUser, ...data.users.slice(userIndex + 1) ] } await saveUsersData(newData) } export async function deleteUser(userId: string): Promise { const data = await loadUsersData() const userIndex = data.users.findIndex(user => user.id === userId) if (userIndex === -1) { throw new Error('User not found') } const newData: UserData = { users: [ ...data.users.slice(0, userIndex), ...data.users.slice(userIndex + 1) ] } await saveUsersData(newData) } export async function loadServerSettings(): Promise { return { isDemo: !!process.env.DEMO, } }