Files
back-meditation/app/Http/Controllers/UserController.php
T
2026-06-23 17:36:20 +03:30

726 lines
25 KiB
PHP

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Helpers\MobileNumberHelper;
use App\Jobs\SendSMSJob;
use App\Models\PackageName;
use App\Models\Product;
use App\Models\Transaction;
use App\Models\User;
use App\Rules\MobileNumber;
use Carbon\Carbon;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule;
use Laravel\Socialite\Facades\Socialite;
use Illuminate\Support\Facades\Http;
use App\Models\BreathingExercise;
use App\Models\BreathingTemplate;
use App\Models\UserBreathingSession;
class UserController extends Controller
{
public function loginV2(Request $request)
{
$data = $request->validate([
'token' => 'required|string',
'package_name' => 'required|string',
]);
$response = Http::acceptJson()->withToken($data['token'])->get('https://api.approagency.ir/api/status', [
'package_name' => $request['package_name']
]);
if (!$response->successful()) {
return response()->json(['message' => 'invalid token'], 401);
}
$response = json_decode($response->body(), true);
// First, try to find user by identifier
$user = null;
if (!empty($response['uuid'])) {
$user = User::where('identifier', $response['uuid'])->first();
}
// Then by email — only when an email exists. (where('email', null) would
// match an arbitrary mobile-only user and cross-link the wrong account.)
if (!$user && !empty($response['email'])) {
$user = User::where('email', $response['email'])->first();
}
// Finally by mobile, so mobile-only users reconnect to their account
if (!$user && !empty($response['mobile'])) {
$user = User::where('mobile', $response['mobile'])->first();
}
$mobile = $response['mobile'] ?? null;
// If mobile already exists, set it null to avoid duplicate error
if ($mobile && User::where('mobile', $mobile)->where('id', '!=', $user?->id)->exists()) {
$mobile = null;
}
// If user still doesn't exist, create new
if (!$user) {
$user = User::create([
'email' => $response['email'],
'identifier' => $response['uuid'] ?? null,
'password' => config('app.default_password'),
// 'name' => trim(($response['first_name'] ?? '') . ' ' . ($response['last_name'] ?? '')),
'mobile' => $mobile,
'first_name' => $response['first_name'] ?? null,
'last_name' => $response['last_name'] ?? null,
'referral_code' => $response['referral_code'] ?? null,
]);
} else {
// Only update name and mobile for existing user
// Keep the local identifier aligned with the current approagency uuid
// (e.g. when the user was matched by mobile, not identifier).
$user->identifier = $response['uuid'] ?? $user->identifier;
$user->first_name = $response['first_name'] ?? $user->first_name;
$user->last_name = $response['last_name'] ?? $user->last_name;
// $user->name = trim(($response['first_name'] ?? '') . ' ' . ($response['last_name'] ?? ''));
$user->mobile = $mobile ?? $user->mobile;
$user->referral_code = $response['referral_code'] ?? $user->referral_code;
$user->save();
}
// Link the referral relationship and reward the referrer (کد معرف)
$this->linkReferral($user, $response);
$access_token = $user->createToken('user')->plainTextToken;
// Whether this user has answered any survey question.
$user->setAttribute(
'has_answered_survey',
\App\Models\SurveyAnswer::where('user_id', $user->id)->exists()
);
return [
'email' => $user->email,
'identifier' => $user->identifier,
'token' => $access_token,
'user' => $user,
'status' => $response,
];
}
private function syncUserFromStatus(array $response, ?User $user = null): User
{
// Try to find user if not passed
if (!$user) {
if (!empty($response['uuid'])) {
$user = User::where('identifier', $response['uuid'])->first();
}
if (!$user && !empty($response['email'])) {
$user = User::where('email', $response['email'])->first();
}
if (!$user && !empty($response['mobile'])) {
$user = User::where('mobile', $response['mobile'])->first();
}
}
$mobile = $response['mobile'] ?? null;
if ($mobile && User::where('mobile', $mobile)->where('id', '!=', $user?->id)->exists()) {
$mobile = null; // avoid duplicate
}
if (!$user) {
// create new
$user = User::create([
'email' => $response['email'],
'identifier' => $response['uuid'] ?? null,
'password' => config('app.default_password'),
'mobile' => $mobile,
'first_name' => $response['first_name'] ?? null,
'last_name' => $response['last_name'] ?? null,
'referral_code' => $response['referral_code'] ?? null,
'email_verified_at' => $response['email_verified_at'] ?? null
]);
} else {
// update existing
$user->identifier = $response['uuid'] ?? $user->identifier;
$user->first_name = $response['first_name'] ?? $user->first_name;
$user->last_name = $response['last_name'] ?? $user->last_name;
$user->mobile = $mobile ?? $user->mobile;
$user->referral_code = $response['referral_code'] ?? $user->referral_code;
$user->email_verified_at = $response['email_verified_at'] ?? $user->email_verified_at;
$user->save();
}
// Link the referral relationship and reward the referrer (کد معرف)
$this->linkReferral($user, $response);
return $user;
}
/**
* Resolve the approagency referrer (by uuid/identifier) to a local
* meditation user, record the relationship once, and grant the referrer
* their per-invite points. The free-month subscription milestone is
* handled in approagency, where products/plans/transactions are managed.
*/
private function linkReferral(User $user, array $response): void
{
if ($user->referred_by) {
return; // already linked — never reward twice
}
$referrerUuid = $response['referrer_uuid'] ?? null;
if (!$referrerUuid) {
return;
}
$referrer = User::where('identifier', $referrerUuid)->first();
if (!$referrer || $referrer->id === $user->id) {
return;
}
$user->referred_by = $referrer->id;
$user->save();
// 100 points per successful invite
$referrer->increment('referral_points', User::REFERRAL_POINTS_PER_INVITE);
}
/**
* Referral dashboard data (دعوت دوستان page).
*/
public function referral(Request $request)
{
$user = auth()->user();
$invites = $user->referrals()->count();
$target = User::REFERRAL_SUBSCRIPTION_TARGET;
return response()->json([
'referral_code' => $user->referral_code,
'referral_points' => (int) $user->referral_points,
'successful_invites' => $invites,
'subscription_target' => $target,
'remaining_to_subscription' => max(0, $target - $invites),
'rewards' => [
'points_per_invite' => User::REFERRAL_POINTS_PER_INVITE,
'friend_points_share_percent' => (int) (User::REFERRAL_SHARE * 100),
'subscription_invite_target' => $target,
],
]);
}
public function loginForeginer(Request $request)
{
$request->validate([
'user_token' => ['required'],
'package_name' => ['required', new PackageNameExist],
]);
$response = $this->repository->userTokenVerify($request['package_name'], $request['user_token']);
if ($response['ok']) {
$user = User::where('identifier', $response['data']['identifier'])->first();
if (empty($user)) {
$user = new User();
$user['identifier'] = $response['data']['identifier'];
$user['name'] = $response['data']['username'];
$user->save();
}
if (!empty($request['fcm_token'])) FcmToken::firstOrCreate(['token' => $request['fcm_token'], 'user_id' => $user['id']]);
$access_token = $user->createToken('user')->plainTextToken;
return ['identifier' => $user['identifier'], 'token' => $access_token];
} else abort(500);
}
public function login(Request $request)
{
$data = $request->validate([
'auth' => 'string|required',
'password' => 'string|required',
'package_name' => 'string|required',
'fcm_token' => 'string|nullable'
]);
if (!$packageName = PackageName::where('name', $data['package_name'])->first()) {
return response()->json(['message' => 'package name not found'], 404);
}
$authType = filter_var($data['auth'], FILTER_VALIDATE_EMAIL) ? 'email' : 'mobile';
if ($authType == 'mobile') {
$data['auth'] = MobileNumberHelper::formatMobile($data['auth']);
}
$user = User::where($authType, $data['auth'])->first();
if (!$user || !Hash::check(trim($data['password']), $user->password)) {
return response()->json(['message' => 'wrong credentials'], 404);
}
// Attach package if not exists
if (!$user->packageNames()->where('package_names.id', $packageName->id)->exists()) {
$user->packageNames()->attach($packageName->id, [
'tries' => $packageName->tries,
'fcm_token' => $data['fcm_token'] ?? null,
]);
} else {
$user->packageNames()->updateExistingPivot($packageName->id, [
'fcm_token' => $data['fcm_token'] ?? null,
]);
}
$token = $user->createToken('token', $user->is_admin ? ['admin'] : ['user'])->plainTextToken;
return response()->json([
'user' => $user,
'token' => $token,
]);
}
public function resendEmailVerification(Request $request)
{
$request->validate([
'email' => 'required|email',
]);
$user = User::where('email', $request->email)->first();
if (!$user) {
return response()->json(['message' => 'User not found'], 404);
}
if ($user->email_verified_at) {
return response()->json(['message' => 'Email is already verified'], 400);
}
// Generate new code
$code = rand(1000, 9999);
// Delete previous codes
\DB::table('email_verifications')->where('user_id', $user->id)->delete();
// Insert new verification code
\DB::table('email_verifications')->insert([
'user_id' => $user->id,
'code' => $code,
'expires_at' => now()->addMinutes(15),
'created_at' => now(),
'updated_at' => now(),
]);
// Send email
\Mail::to($user->email)->send(new \App\Mail\VerifyEmailCodeMail($code));
return response()->json([
'message' => 'Verification email resent successfully'
]);
}
public function register(Request $request)
{
$data = $request->validate([
'first_name' => 'string|nullable',
'last_name' => 'string|nullable',
'email' => 'email|required_without:mobile',
'password' => 'string|nullable|required_with:email',
'mobile' => ['string', new MobileNumber, 'nullable', 'required_without:email'],
'fcm_token' => 'string|nullable',
'package_name' => 'string|required'
]);
$packageNameValue = $data['package_name'];
unset($data['package_name']);
if (isset($data['mobile'])) {
$data['mobile'] = MobileNumberHelper::formatMobile($data['mobile']);
if (User::where('mobile', $data['mobile'])->exists()) {
return response()->json(['message' => 'Mobile number already exists'], 400);
}
}
if (isset($data['email']) && User::where('email', $data['email'])->exists()) {
return response()->json(['message' => 'Email already exists'], 400);
}
// ✅ Check if email can receive a message before creating the user
if (isset($data['email'])) {
$code = rand(1000, 9999);
try {
// Try sending email
\Mail::to($data['email'])->send(new \App\Mail\VerifyEmailCodeMail($code));
} catch (\Exception $e) {
return response()->json([
'message' => 'لطفا آدرس ایمیل خود را بررسی کنید و مجدد تلاش کنید',
], 400);
}
}
// Hash password
$data['password'] = isset($data['password'])
? Hash::make($data['password'])
: Hash::make(Str::random(8));
// Create user only if email sending didn't fail
$user = User::create($data);
// Attach package
if ($packageName = PackageName::where('name', $packageNameValue)->first()) {
$user->packageNames()->attach($packageName->id, [
'tries' => $packageName->tries,
'fcm_token' => $data['fcm_token'] ?? null,
]);
}
// Save email verification code after user is created
if (isset($data['email'])) {
\DB::table('email_verifications')->insert([
'user_id' => $user->id,
'code' => $code,
'expires_at' => now()->addMinutes(15),
'created_at' => now(),
'updated_at' => now(),
]);
}
// Create token
$token = $user->createToken('token', $user->is_admin ? ['admin'] : ['user'])->plainTextToken;
return response()->json([
'message' => 'User registered successfully.',
'user' => $user,
'token' => $token
]);
}
public function verifyEmailCode(Request $request)
{
$request->validate([
'email' => 'required|email',
'code' => 'required|string',
]);
$user = User::where('email', $request->email)->first();
if (!$user) {
return response()->json(['message' => 'User not found'], 404);
}
$record = \DB::table('email_verifications')
->where('user_id', $user->id)
->where('code', $request->code)
->where('expires_at', '>', now())
->first();
if (!$record) {
return response()->json(['message' => 'Invalid or expired code'], 400);
}
// Mark email as verified
$user->update(['email_verified_at' => now()]);
// Delete verification record
\DB::table('email_verifications')->where('id', $record->id)->delete();
$token = $user->createToken('token', $user->is_admin ? ['admin'] : ['user'])->plainTextToken;
return response()->json(['message' => 'Email verified successfully', 'token' => $token]);
}
public function loginOTP(Request $request)
{
$data = $request->validate([
'mobile' => ['string', new MobileNumber, 'required'],
'package_name' => 'string|required',
'fcm_token' => 'string',
]);
if (!$packageName = PackageName::where('name', $data['package_name'])->first()) {
return response()->json([
'message' => 'package name not found'
], 404);
}
if (!$user = User::where('mobile', $data['mobile'])->first()) {
$user = User::create([
'mobile' => MobileNumberHelper::formatMobile($data['mobile']),
'password' => config('app.default_password'),
]);
}
if ($data['mobile'] != config('approo.admin_mobile')) {
$otpToken = rand(pow(10, 4), pow(10, 5) - 1);
$user->otpTokens()->create([
'token' => $otpToken
]);
$verify = Str::random(8);
// SendSMSJob::dispatch([
// 'mobile' => $data['mobile'],
// 'message' => "کد ورود به برنامه: $otpToken
// @{$packageName->web_app_url} #$otpToken
// لغو 11"
// ]);
SendSMSJob::dispatch([
'mobile' => $data['mobile'],
'token' => "$otpToken"
]);
}
if (!$user->packageNames()->where('package_names.id', $packageName->id)->exists()) {
$user->packageNames()->attach($packageName->id, ['tries' => $packageName->tries,
'fcm_token' => $data['fcm_token'] ?? null,]);
} else {
$user->packageNames()->updateExistingPivot($packageName->id,[
'fcm_token' => $data['fcm_token'] ?? null,]);
}
return response()->json([
'message' => 'otp sent',
'verify_code' => $verify ?? null
]);
}
public function checkOTP(Request $request)
{
$data = $request->validate([
'mobile' => ['string', new MobileNumber, 'required'],
'token' => 'string|required',
]);
if (!$user = User::where('mobile', $data['mobile'])->first()) {
return response()->json([
'message' => 'user not found'
], 404);
}
$otpToken = $user->otpTokens()->where('token', $data['token'])->where('created_at', '>', now()->subMinutes(15))->first();
if (!$otpToken && !($data['mobile'] == config('approo.admin_mobile') && $data['token'] == config('approo.admin_otp'))) {
return response()->json([
'message' => 'token not valid'
], 400);
}
$otpToken?->delete();
$token = '';
if ($user->is_admin) {
$token = $user->createToken('token', ['admin'])->plainTextToken;
} else {
$token = $user->createToken('token', ['user'])->plainTextToken;
}
return response()->json([
'user' => $user,
'token' => $token,
]);
}
public function updateProfile(Request $request)
{
$data = $request->validate([
'first_name' => 'string|nullable',
'last_name' => 'string|nullable',
'birthday' => 'date|nullable',
'age' => 'integer|nullable|min:0|max:120',
'gender' => ['integer', 'nullable', Rule::in(User::GENDERS)],
'email' => 'email|nullable',
'mobile' => [new MobileNumber, 'string'],
'avatar' => 'image'
]);
if (isset($data['email'])) {
$user = User::where('email', $data['email'])->first();
if ($user && $data['email'] != auth()->user()->email) {
return response()->json([
'message' => 'user with given email exists'
], 400);
}
}
if (isset($data['mobile'])) {
$user = User::where('mobile', $data['mobile'])->first();
if ($user && $data['mobile'] != auth()->user()->mobile) {
return response()->json([
'message' => 'user with given email exists'
], 400);
}
}
$user = auth()->user();
if (isset($data['avatar'])) {
Storage::disk('public')->put("avatars/$user->uuid.png", file_get_contents($data['avatar']->path()));
$data['avatar'] = "storage/avatars/$user->uuid.png";
}
$user->update($data);
return response()->json([
'user' => $user,
'message' => 'user updated'
]);
}
public function profile(Request $request)
{
$data = $request->validate([
'token' => 'required|string',
'package_name' => 'required|string',
]);
$user = auth()->user();
$response = Http::acceptJson()->withToken($data['token'])->get('https://api.approagency.ir/api/status', [
'package_name' => $request['package_name']
]);
if ($response->successful()) {
$user = $this->syncUserFromStatus($response->json(), $user);
}
$remindersResponse = Http::acceptJson()
->withToken($data['token'])
->get('https://api.approagency.ir/api/reminders', [
'type' => 'reminder',
'package_name' => $data['package_name'],
]);
$reminders = $remindersResponse->successful()
? $remindersResponse->json()
: [];
$user->load(['breathingSessions.template']);
// Whether this user has answered any survey question.
$user->setAttribute(
'has_answered_survey',
\App\Models\SurveyAnswer::where('user_id', $user->id)->exists()
);
return response()->json([
'user' => $user,
'reminders' => $reminders,
]);
}
public function status(Request $request)
{
$data = $request->validate([
'package_name' => 'string|required'
]);
if (!$packageName = PackageName::with('cafeConfig')->where('name', $data['package_name'])->first()) {
return response()->json([
'message' => 'package name not found'
], 404);
}
$user = auth()->user();
if ($user->mobile == config('approo.admin_mobile')) {
$user['products'] = Product::where('package_name_id', $packageName->id)->get();
} else {
// if ($packageName->cafeConfig) {
// $user['products'] = $user->products()->where('package_name_id', $packageName->id)->get();
// foreach ($user['products'] as $key => $product) {
// $service = new CafeService($packageName->cafeConfig);
// if ($product->pivot->gateway == Transaction::GATEWAYS['cafe'] && $product->pivot->purchase_token && $product->type != Product::TYPES['permanent']) {
// $activeSubscriptions = $service->checkSubscription($product, $product->pivot->purchase_token);
// if ($activeSubscriptions) {
// $expire = Carbon::createFromTimestamp($activeSubscriptions[0]['validUntilTimestampMsec'] / 1000);
// $user->products()->updateExistingPivot($product, ['expire_at' => $expire->format('Y-m-d H:i:s')]);
// } else {
// $user->products()->updateExistingPivot($product, ['expire_at' => now()->format('Y-m-d H:i:s')]);
// }
// }
// }
// }
$user['products'] = $user->products()
->where('package_name_id', $packageName->id)
->where(fn($q) => $q->where('user_product.expire_at', '>', now()->addHour())->orWhere('user_product.expire_at', null))
->get();
$user['package_name'] = $user->packageNames()
->where('package_names.id', $packageName->id)
->first();
}
return $user;
}
public function try(Request $request, $name) {
$user = auth()->user();
if (!$packageName = $user->packageNames()->where('name', $name)->first()) {
return response()->json([
'message' => 'package name not found'
], 404);
}
if ($packageName->pivot->tries <= 0) {
return response()->json([
'message' => 'no more tries'
], 400);
}
$packageName->pivot->tries->decrement();
return response()->json([
'message' => 'success'
]);
}
public function oauthCallback(Request $request)
{
return dd(Socialite::driver('google')->stateless()->user());
}
public function googleLogin(Request $request)
{
$data = $request->validate([
'access_token' => 'string|required',
'package_name' => 'string|required',
]);
if (!$packageName = PackageName::where('name', $data['package_name'])->first()) {
return response()->json([
'message' => 'package name not found'
], 404);
}
$response = Http::get("https://www.googleapis.com/oauth2/v3/userinfo", [
'access_token' => $data['access_token']
])->json();
if (!isset($response['email'])) {
return response()->json([
'message' => 'invalid token'
], 400);
}
if (!$user = User::where('email', $response['email'])->first()) {
$user = User::create([
'email' => $response['email'],
'first_name' => $response['given_name'] ?? null,
'last_name' => $response['family_name'] ?? null,
'password' => config('app.default_password'),
'email_verified_at' => now(),
]);
if (!$user->packageNames()->where('package_names.id', $packageName->id)->exists()) {
$user->packageNames()->attach($packageName->id, [
'tries' => $packageName->tries,
'source' => $data['source'] ?? 0,
]);
}
}else {
if (!$user->email_verified_at) {
$user->update([
'email_verified_at' => now(),
]);
}
}
$token = $user->createToken('token', ['user'])->plainTextToken;
return response()->json([
'user' => $user,
'token' => $token,
]);
}
public function leaderBoard(){
$users = User::with(['breathingSessions.template'])->where('xp', '>', 0)
->orderBy('xp', 'desc')->limit(20)
->get();
return $users;
}
}