|
|
|
|
|
"use server"; |
|
|
|
|
|
import { getDb } from "@/lib/mongodb"; |
|
|
import { getLoggedInUser, logoutUser } from "@/lib/actions/auth"; |
|
|
import bcrypt from 'bcryptjs'; |
|
|
import { differenceInHours } from 'date-fns'; |
|
|
import type { User, Deployment } from "@/lib/types"; |
|
|
import type { TransferCoinsInput, ChangePasswordInput, UpdateProfileInput, DeleteAccountInput } from "@/lib/schemas"; |
|
|
import { revalidatePath } from 'next/cache'; |
|
|
|
|
|
const DAILY_COIN_AMOUNT = 10; |
|
|
const CLAIM_COOLDOWN_HOURS = 24; |
|
|
|
|
|
export async function claimDailyCoins(): Promise<{ success: boolean; message: string; newBalance?: number; nextClaimAvailableInMs?: number }> { |
|
|
const user = await getLoggedInUser(); |
|
|
if (!user) { |
|
|
return { success: false, message: "You must be logged in to claim coins." }; |
|
|
} |
|
|
|
|
|
const now = new Date(); |
|
|
if (user.lastCoinClaim) { |
|
|
const hoursSinceLastClaim = differenceInHours(now, new Date(user.lastCoinClaim)); |
|
|
if (hoursSinceLastClaim < CLAIM_COOLDOWN_HOURS) { |
|
|
const nextClaimTime = new Date(user.lastCoinClaim).getTime() + CLAIM_COOLDOWN_HOURS * 60 * 60 * 1000; |
|
|
const nextClaimAvailableInMs = Math.max(0, nextClaimTime - now.getTime()); |
|
|
return { |
|
|
success: false, |
|
|
message: `You can claim again in ${CLAIM_COOLDOWN_HOURS - hoursSinceLastClaim} hour(s).`, |
|
|
nextClaimAvailableInMs |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
try { |
|
|
const db = await getDb(); |
|
|
const usersCollection = db.collection<User>("users"); |
|
|
|
|
|
|
|
|
const updatedUserResult = await usersCollection.findOneAndUpdate( |
|
|
{ _id: user._id }, |
|
|
{ |
|
|
$inc: { coins: DAILY_COIN_AMOUNT }, |
|
|
$set: { lastCoinClaim: now } |
|
|
}, |
|
|
{ returnDocument: "after" } |
|
|
); |
|
|
|
|
|
if (!updatedUserResult) { |
|
|
return { success: false, message: "Failed to update user balance. User not found or update failed." }; |
|
|
} |
|
|
revalidatePath("/dashboard"); |
|
|
return { |
|
|
success: true, |
|
|
message: `Successfully claimed ${DAILY_COIN_AMOUNT} coins!`, |
|
|
newBalance: updatedUserResult.coins, |
|
|
nextClaimAvailableInMs: CLAIM_COOLDOWN_HOURS * 60 * 60 * 1000 |
|
|
}; |
|
|
} catch (error) { |
|
|
console.error("Error claiming daily coins:", error); |
|
|
return { success: false, message: "An unexpected error occurred." }; |
|
|
} |
|
|
} |
|
|
|
|
|
export async function transferCoins(data: TransferCoinsInput): Promise<{ success: boolean; message: string; newSenderBalance?: number }> { |
|
|
const sender = await getLoggedInUser(); |
|
|
if (!sender) { |
|
|
return { success: false, message: "You must be logged in to transfer coins." }; |
|
|
} |
|
|
|
|
|
if (data.amount <= 0) { |
|
|
return { success: false, message: "Transfer amount must be positive." }; |
|
|
} |
|
|
if (sender.coins < data.amount && sender.role !== 'admin') { |
|
|
return { success: false, message: "Insufficient coin balance." }; |
|
|
} |
|
|
if (sender.email === data.recipientEmail) { |
|
|
return { success: false, message: "You cannot transfer coins to yourself." }; |
|
|
} |
|
|
|
|
|
try { |
|
|
const db = await getDb(); |
|
|
const usersCollection = db.collection<User>("users"); |
|
|
|
|
|
const recipient = await usersCollection.findOne({ email: data.recipientEmail }); |
|
|
if (!recipient) { |
|
|
return { success: false, message: "Recipient user not found." }; |
|
|
} |
|
|
|
|
|
if (sender.role !== 'admin') { |
|
|
|
|
|
const freshSenderData = await usersCollection.findOne({ _id: sender._id }); |
|
|
if (!freshSenderData || freshSenderData.coins < data.amount) { |
|
|
return { success: false, message: "Insufficient coin balance. Please refresh and try again." }; |
|
|
} |
|
|
} |
|
|
|
|
|
const senderUpdateResult = await usersCollection.updateOne( |
|
|
{ _id: sender._id }, |
|
|
{ $inc: { coins: -data.amount } } |
|
|
); |
|
|
|
|
|
if (senderUpdateResult.modifiedCount === 0) { |
|
|
if (sender.role !== 'admin') { |
|
|
return { success: false, message: "Failed to update your balance. Please try again." }; |
|
|
} |
|
|
} |
|
|
|
|
|
const recipientUpdateResult = await usersCollection.updateOne( |
|
|
{ _id: recipient._id }, |
|
|
{ $inc: { coins: data.amount } } |
|
|
); |
|
|
|
|
|
if (recipientUpdateResult.modifiedCount === 0) { |
|
|
|
|
|
await usersCollection.updateOne( |
|
|
{ _id: sender._id }, |
|
|
{ $inc: { coins: data.amount } } |
|
|
); |
|
|
return { success: false, message: "Failed to transfer coins to recipient. Your balance has been restored." }; |
|
|
} |
|
|
|
|
|
const newSenderData = await usersCollection.findOne({ _id: sender._id }); |
|
|
revalidatePath("/dashboard"); |
|
|
return { |
|
|
success: true, |
|
|
message: `Successfully transferred ${data.amount} coins to ${recipient.name}.`, |
|
|
newSenderBalance: newSenderData?.coins ?? sender.coins - data.amount, |
|
|
}; |
|
|
|
|
|
} catch (error) { |
|
|
console.error("Error transferring coins:", error); |
|
|
return { success: false, message: "An unexpected error occurred during the transfer." }; |
|
|
} |
|
|
} |
|
|
|
|
|
export async function updateProfile(data: UpdateProfileInput): Promise<{ success: boolean; message: string }> { |
|
|
const loggedInUser = await getLoggedInUser(); |
|
|
if (!loggedInUser) { |
|
|
return { success: false, message: "You must be logged in to update your profile." }; |
|
|
} |
|
|
|
|
|
try { |
|
|
const db = await getDb(); |
|
|
const usersCollection = db.collection<User>("users"); |
|
|
|
|
|
const result = await usersCollection.updateOne( |
|
|
{ _id: loggedInUser._id }, |
|
|
{ $set: { name: data.name } } |
|
|
); |
|
|
|
|
|
if (result.modifiedCount === 0 && result.matchedCount > 0) { |
|
|
return { success: true, message: "No changes detected in profile information." }; |
|
|
} |
|
|
if (result.modifiedCount === 0) { |
|
|
return { success: false, message: "Failed to update profile. User not found." }; |
|
|
} |
|
|
|
|
|
revalidatePath('/dashboard/profile'); |
|
|
revalidatePath('/dashboard'); |
|
|
return { success: true, message: "Profile updated successfully." }; |
|
|
} catch (error) { |
|
|
console.error("Error updating profile:", error); |
|
|
return { success: false, message: "An unexpected error occurred while updating profile." }; |
|
|
} |
|
|
} |
|
|
|
|
|
export async function changePassword(data: ChangePasswordInput): Promise<{ success: boolean; message: string }> { |
|
|
const loggedInUser = await getLoggedInUser(); |
|
|
if (!loggedInUser) { |
|
|
return { success: false, message: "You must be logged in to change your password." }; |
|
|
} |
|
|
|
|
|
try { |
|
|
const db = await getDb(); |
|
|
const usersCollection = db.collection<User>("users"); |
|
|
|
|
|
const userFromDb = await usersCollection.findOne({ _id: loggedInUser._id }); |
|
|
if (!userFromDb) { |
|
|
return { success: false, message: "User not found." }; |
|
|
} |
|
|
|
|
|
const isCurrentPasswordValid = await bcrypt.compare(data.currentPassword, userFromDb.passwordHash); |
|
|
if (!isCurrentPasswordValid) { |
|
|
return { success: false, message: "Incorrect current password." }; |
|
|
} |
|
|
|
|
|
const newPasswordHash = await bcrypt.hash(data.newPassword, 10); |
|
|
await usersCollection.updateOne( |
|
|
{ _id: loggedInUser._id }, |
|
|
{ $set: { passwordHash: newPasswordHash } } |
|
|
); |
|
|
|
|
|
return { success: true, message: "Password changed successfully." }; |
|
|
} catch (error) { |
|
|
console.error("Error changing password:", error); |
|
|
return { success: false, message: "An unexpected error occurred while changing password." }; |
|
|
} |
|
|
} |
|
|
|
|
|
export async function deleteUserAccount(data: DeleteAccountInput): Promise<{ success: boolean; message: string }> { |
|
|
const loggedInUser = await getLoggedInUser(); |
|
|
if (!loggedInUser) { |
|
|
return { success: false, message: "You must be logged in to delete your account." }; |
|
|
} |
|
|
|
|
|
try { |
|
|
const db = await getDb(); |
|
|
const usersCollection = db.collection<User>("users"); |
|
|
const deploymentsCollection = db.collection<Deployment>("deployments"); |
|
|
|
|
|
const userFromDb = await usersCollection.findOne({ _id: loggedInUser._id }); |
|
|
if (!userFromDb) { |
|
|
return { success: false, message: "User not found." }; |
|
|
} |
|
|
|
|
|
const isCurrentPasswordValid = await bcrypt.compare(data.currentPassword, userFromDb.passwordHash); |
|
|
if (!isCurrentPasswordValid) { |
|
|
return { success: false, message: "Incorrect password. Account deletion failed." }; |
|
|
} |
|
|
|
|
|
if (userFromDb.role === 'admin') { |
|
|
const adminCount = await usersCollection.countDocuments({ role: 'admin' }); |
|
|
if (adminCount <= 1) { |
|
|
return { success: false, message: "Cannot delete the last admin account. Promote another user to admin first." }; |
|
|
} |
|
|
} |
|
|
|
|
|
await deploymentsCollection.deleteMany({ userId: loggedInUser._id }); |
|
|
|
|
|
const deleteResult = await usersCollection.deleteOne({ _id: loggedInUser._id }); |
|
|
if (deleteResult.deletedCount === 0) { |
|
|
return { success: false, message: "Failed to delete user account from the database." }; |
|
|
} |
|
|
|
|
|
await logoutUser(); |
|
|
|
|
|
return { success: true, message: "Account and associated deployments deleted successfully. You have been logged out." }; |
|
|
|
|
|
} catch (error) { |
|
|
console.error("Error deleting account:", error); |
|
|
if (error instanceof Error && error.message.includes('NEXT_REDIRECT')) { |
|
|
throw error; |
|
|
} |
|
|
return { success: false, message: "An unexpected error occurred while deleting your account." }; |
|
|
} |
|
|
} |
|
|
|