- Removido README.md padrão do Laravel (backend) - Removidos scripts de deploy (não mais necessários) - Atualizado copilot-instructions.md para novo fluxo - Adicionada documentação de auditoria do servidor - Sincronizado código de produção com repositório Novo workflow: - Trabalhamos diretamente em /root/webmoney (symlink para /var/www/webmoney) - Mudanças PHP são instantâneas - Mudanças React requerem 'npm run build' - Commit após validação funcional
414 lines
15 KiB
PHP
Executable File
414 lines
15 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\User;
|
|
use App\Models\Plan;
|
|
use App\Models\Subscription;
|
|
use App\Mail\WelcomeNewUser;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Validation\Rules\Password;
|
|
use Carbon\Carbon;
|
|
|
|
class UserManagementController extends Controller
|
|
{
|
|
/**
|
|
* Create a new user with specified role/plan
|
|
* user_type: 'free' | 'pro' | 'admin'
|
|
*/
|
|
public function store(Request $request)
|
|
{
|
|
$validated = $request->validate([
|
|
'name' => 'required|string|max:255',
|
|
'email' => 'required|email|unique:users,email',
|
|
'password' => 'nullable|string|min:8',
|
|
'language' => 'sometimes|string|in:es,pt-BR,en',
|
|
'currency' => 'sometimes|string|size:3',
|
|
'user_type' => 'sometimes|string|in:free,pro,admin',
|
|
'send_welcome_email' => 'sometimes|boolean',
|
|
]);
|
|
|
|
// Generate random password if not provided or empty
|
|
$password = !empty($validated['password']) ? $validated['password'] : bin2hex(random_bytes(8));
|
|
$userType = $validated['user_type'] ?? 'free';
|
|
$sendWelcomeEmail = $validated['send_welcome_email'] ?? true;
|
|
|
|
$user = User::create([
|
|
'name' => $validated['name'],
|
|
'email' => $validated['email'],
|
|
'password' => Hash::make($password),
|
|
'language' => $validated['language'] ?? 'es',
|
|
'currency' => $validated['currency'] ?? 'EUR',
|
|
'email_verified_at' => now(), // Auto-verify admin-created users
|
|
'is_admin' => $userType === 'admin',
|
|
]);
|
|
|
|
$subscriptionInfo = null;
|
|
|
|
// Create Pro subscription if user_type is 'pro' or 'admin'
|
|
if (in_array($userType, ['pro', 'admin'])) {
|
|
$proPlan = Plan::where('slug', 'pro-annual')->first();
|
|
|
|
if ($proPlan) {
|
|
$subscription = Subscription::create([
|
|
'user_id' => $user->id,
|
|
'plan_id' => $proPlan->id,
|
|
'status' => Subscription::STATUS_ACTIVE,
|
|
'current_period_start' => now(),
|
|
'current_period_end' => Carbon::create(2037, 12, 31, 23, 59, 59), // "Lifetime" subscription (max timestamp)
|
|
'paypal_subscription_id' => 'ADMIN_GRANTED_' . strtoupper(bin2hex(random_bytes(8))),
|
|
'paypal_status' => 'ACTIVE',
|
|
'price_paid' => 0,
|
|
'currency' => 'EUR',
|
|
]);
|
|
|
|
$subscriptionInfo = [
|
|
'plan' => $proPlan->name,
|
|
'status' => 'active',
|
|
'expires' => 'Nunca (otorgado por admin)',
|
|
];
|
|
}
|
|
}
|
|
|
|
// Send welcome email with temporary password
|
|
$emailSent = false;
|
|
if ($sendWelcomeEmail) {
|
|
try {
|
|
Mail::to($user->email)->send(new WelcomeNewUser($user, $password));
|
|
$emailSent = true;
|
|
} catch (\Exception $e) {
|
|
\Log::error('Failed to send welcome email: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Usuario creado correctamente',
|
|
'data' => [
|
|
'user' => [
|
|
'id' => $user->id,
|
|
'name' => $user->name,
|
|
'email' => $user->email,
|
|
'is_admin' => $user->is_admin,
|
|
'language' => $user->language,
|
|
],
|
|
'user_type' => $userType,
|
|
'subscription' => $subscriptionInfo,
|
|
'temporary_password' => isset($validated['password']) ? null : $password,
|
|
'welcome_email_sent' => $emailSent,
|
|
],
|
|
], 201);
|
|
}
|
|
|
|
/**
|
|
* List all users with their subscription info
|
|
*/
|
|
public function index(Request $request)
|
|
{
|
|
$query = User::with(['subscription.plan'])
|
|
->withCount(['accounts', 'categories', 'budgets', 'transactions']);
|
|
|
|
// Search
|
|
if ($request->has('search') && $request->search) {
|
|
$search = $request->search;
|
|
$query->where(function ($q) use ($search) {
|
|
$q->where('name', 'like', "%{$search}%")
|
|
->orWhere('email', 'like', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
// Filter by subscription status
|
|
if ($request->has('subscription_status')) {
|
|
if ($request->subscription_status === 'active') {
|
|
$query->whereHas('subscription');
|
|
} elseif ($request->subscription_status === 'free') {
|
|
$query->whereDoesntHave('subscription');
|
|
}
|
|
}
|
|
|
|
// Pagination
|
|
$perPage = $request->get('per_page', 20);
|
|
$users = $query->orderBy('created_at', 'desc')->paginate($perPage);
|
|
|
|
// Transform data
|
|
$users->getCollection()->transform(function ($user) {
|
|
return [
|
|
'id' => $user->id,
|
|
'name' => $user->name,
|
|
'email' => $user->email,
|
|
'language' => $user->language,
|
|
'currency' => $user->currency,
|
|
'is_admin' => (bool) $user->is_admin,
|
|
'created_at' => $user->created_at,
|
|
'last_login_at' => $user->last_login_at,
|
|
'email_verified_at' => $user->email_verified_at,
|
|
'subscription' => $user->subscription ? [
|
|
'plan_name' => $user->subscription->plan->name ?? 'Unknown',
|
|
'plan_slug' => $user->subscription->plan->slug ?? 'unknown',
|
|
'status' => $user->subscription->status,
|
|
'current_period_end' => $user->subscription->current_period_end,
|
|
] : null,
|
|
'usage' => [
|
|
'accounts' => $user->accounts_count,
|
|
'categories' => $user->categories_count,
|
|
'budgets' => $user->budgets_count,
|
|
'transactions' => $user->transactions_count,
|
|
],
|
|
];
|
|
});
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => $users->items(),
|
|
'pagination' => [
|
|
'current_page' => $users->currentPage(),
|
|
'last_page' => $users->lastPage(),
|
|
'per_page' => $users->perPage(),
|
|
'total' => $users->total(),
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get single user details
|
|
*/
|
|
public function show($id)
|
|
{
|
|
$user = User::with(['subscription.plan', 'subscriptions.plan'])
|
|
->withCount(['accounts', 'categories', 'budgets', 'transactions', 'goals'])
|
|
->findOrFail($id);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'id' => $user->id,
|
|
'name' => $user->name,
|
|
'email' => $user->email,
|
|
'language' => $user->language,
|
|
'currency' => $user->currency,
|
|
'created_at' => $user->created_at,
|
|
'last_login_at' => $user->last_login_at,
|
|
'email_verified_at' => $user->email_verified_at,
|
|
'subscription' => $user->subscription ? [
|
|
'id' => $user->subscription->id,
|
|
'plan' => $user->subscription->plan,
|
|
'status' => $user->subscription->status,
|
|
'paypal_subscription_id' => $user->subscription->paypal_subscription_id,
|
|
'current_period_start' => $user->subscription->current_period_start,
|
|
'current_period_end' => $user->subscription->current_period_end,
|
|
'canceled_at' => $user->subscription->canceled_at,
|
|
] : null,
|
|
'subscription_history' => $user->subscriptions->map(function ($sub) {
|
|
return [
|
|
'plan_name' => $sub->plan->name ?? 'Unknown',
|
|
'status' => $sub->status,
|
|
'created_at' => $sub->created_at,
|
|
'canceled_at' => $sub->canceled_at,
|
|
];
|
|
}),
|
|
'usage' => [
|
|
'accounts' => $user->accounts_count,
|
|
'categories' => $user->categories_count,
|
|
'budgets' => $user->budgets_count,
|
|
'transactions' => $user->transactions_count,
|
|
'goals' => $user->goals_count,
|
|
],
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Update user
|
|
*/
|
|
public function update(Request $request, $id)
|
|
{
|
|
$user = User::findOrFail($id);
|
|
|
|
// Don't allow changing main admin's admin status
|
|
if ($user->email === 'marco@cnxifly.com' && $request->has('is_admin') && !$request->is_admin) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'No se puede remover permisos del administrador principal',
|
|
], 403);
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'name' => 'sometimes|string|max:255',
|
|
'email' => 'sometimes|email|unique:users,email,' . $id,
|
|
'language' => 'sometimes|string|in:es,pt-BR,en',
|
|
'currency' => 'sometimes|string|size:3',
|
|
'is_admin' => 'sometimes|boolean',
|
|
]);
|
|
|
|
$user->update($validated);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Usuario actualizado correctamente',
|
|
'data' => [
|
|
'id' => $user->id,
|
|
'name' => $user->name,
|
|
'email' => $user->email,
|
|
'language' => $user->language,
|
|
'currency' => $user->currency,
|
|
'is_admin' => $user->is_admin,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Reset user password (generate random)
|
|
*/
|
|
public function resetPassword($id)
|
|
{
|
|
$user = User::findOrFail($id);
|
|
|
|
// Generate random password
|
|
$newPassword = bin2hex(random_bytes(8)); // 16 chars
|
|
$user->password = Hash::make($newPassword);
|
|
$user->save();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Contraseña restablecida correctamente',
|
|
'data' => [
|
|
'temporary_password' => $newPassword,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Delete user and all their data
|
|
*/
|
|
public function destroy($id)
|
|
{
|
|
$user = User::findOrFail($id);
|
|
|
|
// Don't allow deleting admin
|
|
if ($user->email === 'marco@cnxifly.com') {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'No se puede eliminar el usuario administrador',
|
|
], 403);
|
|
}
|
|
|
|
// Delete user (cascade will handle related data)
|
|
$user->delete();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Usuario eliminado correctamente',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Change user subscription plan
|
|
*/
|
|
public function changePlan(Request $request, $id)
|
|
{
|
|
$user = User::findOrFail($id);
|
|
|
|
$validated = $request->validate([
|
|
'plan' => 'required|string|in:free,pro',
|
|
]);
|
|
|
|
// If changing to free, cancel any existing subscription
|
|
if ($validated['plan'] === 'free') {
|
|
$subscription = $user->subscription;
|
|
if ($subscription) {
|
|
$subscription->update([
|
|
'status' => Subscription::STATUS_CANCELED,
|
|
'canceled_at' => now(),
|
|
]);
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Usuario cambiado a plan Free',
|
|
'data' => [
|
|
'plan' => 'free',
|
|
'subscription' => null,
|
|
],
|
|
]);
|
|
}
|
|
|
|
// If changing to pro, create or reactivate subscription
|
|
if ($validated['plan'] === 'pro') {
|
|
$proPlan = Plan::where('slug', 'pro-annual')->first();
|
|
|
|
if (!$proPlan) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Plan Pro no encontrado en el sistema',
|
|
], 404);
|
|
}
|
|
|
|
// Check if user has existing subscription
|
|
$subscription = $user->subscription;
|
|
|
|
if ($subscription) {
|
|
// Reactivate existing subscription
|
|
$subscription->update([
|
|
'status' => Subscription::STATUS_ACTIVE,
|
|
'canceled_at' => null,
|
|
'current_period_start' => now(),
|
|
'current_period_end' => Carbon::create(2037, 12, 31, 23, 59, 59),
|
|
]);
|
|
} else {
|
|
// Create new subscription
|
|
$subscription = Subscription::create([
|
|
'user_id' => $user->id,
|
|
'plan_id' => $proPlan->id,
|
|
'status' => Subscription::STATUS_ACTIVE,
|
|
'current_period_start' => now(),
|
|
'current_period_end' => Carbon::create(2037, 12, 31, 23, 59, 59),
|
|
'paypal_subscription_id' => 'ADMIN_GRANTED_' . strtoupper(bin2hex(random_bytes(8))),
|
|
'paypal_status' => 'ACTIVE',
|
|
'price_paid' => 0,
|
|
'currency' => 'EUR',
|
|
]);
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Usuario cambiado a plan Pro',
|
|
'data' => [
|
|
'plan' => 'pro',
|
|
'subscription' => [
|
|
'id' => $subscription->id,
|
|
'plan_name' => $proPlan->name,
|
|
'status' => $subscription->status,
|
|
'current_period_end' => $subscription->current_period_end,
|
|
],
|
|
],
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get summary statistics
|
|
*/
|
|
public function summary()
|
|
{
|
|
$totalUsers = User::count();
|
|
$activeSubscribers = Subscription::where('status', 'active')->count();
|
|
$freeUsers = $totalUsers - $activeSubscribers;
|
|
$newUsersThisMonth = User::whereMonth('created_at', now()->month)
|
|
->whereYear('created_at', now()->year)
|
|
->count();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'total_users' => $totalUsers,
|
|
'active_subscribers' => $activeSubscribers,
|
|
'free_users' => $freeUsers,
|
|
'new_users_this_month' => $newUsersThisMonth,
|
|
],
|
|
]);
|
|
}
|
|
}
|