Compare commits

...
10 Commits
Author SHA1 Message Date
Amirmahdi ae6b32a0b2 feat: change user controller for iran access 2026-04-19 12:22:24 +00:00
Amirmahdi bc86c41578 feat: add verify mobile 2026-02-25 00:11:22 +03:30
Amirmahdi e6b0ad6cf9 feat 2026-02-25 00:03:37 +03:30
Amirmahdi acf2ed7f18 feat: add request verify 2026-02-25 00:02:39 +03:30
Amirmahdi 06cdf30f0a fix: emails fixed 2026-02-18 07:40:10 +00:00
Amirmahdi e98c0502db feat: add otp sms 2026-02-18 11:04:47 +03:30
Amirmahdi bbe70011dc fix 2025-12-18 18:29:06 +03:30
Amirmahdi 854bd4101f fix 2025-12-18 18:25:58 +03:30
Amirmahdi ffda368718 fix 2025-12-18 18:24:45 +03:30
Amirmahdi 0edece042f fix 2025-12-18 18:23:17 +03:30
25 changed files with 265 additions and 40 deletions
+7 -7
View File
@@ -156,13 +156,13 @@ public function subscribe(Request $request, $packageName, $productId)
} }
if ($data['gateway'] == 'myket') { if ($data['gateway'] == 'myket') {
$service = new MyketService($product->packageName->cafeConfig); // $service = new MyketService($product->packageName->cafeConfig);
$purchaseStatus = $service->checkPurchase($product, $data['purchase_token']); // $purchaseStatus = $service->checkPurchase($product, $data['purchase_token']);
if (!$purchaseStatus['status']) { // if (!$purchaseStatus['status']) {
return response()->json([ // return response()->json([
'message' => $purchaseStatus['message'], // 'message' => $purchaseStatus['message'],
], 400); // ], 400);
} // }
$user->transactions()->create([ $user->transactions()->create([
'amount' => $product->price, 'amount' => $product->price,
'authority' => $data['purchase_token'], 'authority' => $data['purchase_token'],
+22 -8
View File
@@ -6,19 +6,33 @@
use App\Models\Reminder; use App\Models\Reminder;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use App\Enums\ReminderType; use App\ReminderType;
use Illuminate\Validation\Rules\Enum; use Illuminate\Validation\Rules\Enum;
class ReminderController extends Controller class ReminderController extends Controller
{ {
public function index() public function index(Request $request)
{ {
$reminders = Reminder::where('user_id', Auth::id()) $request->validate([
->orderBy('time') 'type' => ['nullable', new Enum(ReminderType::class)],
->get(); 'package_name' => ['nullable', 'exists:package_names,name'],
]);
return response()->json($reminders); $reminders = Reminder::query()
} ->where('user_id', Auth::id())
->when($request->type, function ($q) use ($request) {
$q->where('type', $request->type);
})
->when($request->package_name, function ($q) use ($request) {
$q->whereHas('packageName', function ($q2) use ($request) {
$q2->where('name', $request->package_name);
});
})
->orderBy('time')
->get();
return response()->json($reminders);
}
public function store(Request $request) public function store(Request $request)
{ {
+88 -22
View File
@@ -4,6 +4,7 @@
use App\Helpers\MobileNumberHelper; use App\Helpers\MobileNumberHelper;
use App\Jobs\SendSMSJob; use App\Jobs\SendSMSJob;
use App\Jobs\SendOTPJob;
use App\Models\PackageName; use App\Models\PackageName;
use App\Models\Product; use App\Models\Product;
use App\Models\Transaction; use App\Models\Transaction;
@@ -230,21 +231,21 @@ public function register(Request $request)
$code = rand(1000, 9999); $code = rand(1000, 9999);
$token = Str::random(64); $token = Str::random(64);
// ✅ Check if email can receive a message before creating the user // ✅ Check if email can receive a message before creating the user
if (isset($data['email'])) { // if (isset($data['email'])) {
// $code = rand(1000, 9999); // $code = rand(1000, 9999);
try { // try {
if ($method === 'code') { // if ($method === 'code') {
\Mail::to($data['email'])->send(new \App\Mail\VerifyEmailCodeMail($code)); // \Mail::to($data['email'])->send(new \App\Mail\VerifyEmailCodeMail($code));
} else { // } else {
$verificationUrl = url("api/auth/verify-email-link/{$token}?package={$packageNameValue}"); // $verificationUrl = url("api/auth/verify-email-link/{$token}?package={$packageNameValue}");
\Mail::to($data['email'])->send(new \App\Mail\VerifyEmailLinkMail($verificationUrl)); // \Mail::to($data['email'])->send(new \App\Mail\VerifyEmailLinkMail($verificationUrl));
} // }
} catch (\Exception $e) { // } catch (\Exception $e) {
return response()->json([ // return response()->json([
'error' => $e->getMessage(), // 'error' => $e->getMessage(),
'message' => 'لطفا آدرس ایمیل خود را بررسی کنید و مجدد تلاش کنید' // 'message' => 'لطفا آدرس ایمیل خود را بررسی کنید و مجدد تلاش کنید'
], 400); // ], 400);
} // }
// try { // try {
// // Try sending email // // Try sending email
// \Mail::to($data['email'])->send(new \App\Mail\VerifyEmailCodeMail($code)); // \Mail::to($data['email'])->send(new \App\Mail\VerifyEmailCodeMail($code));
@@ -256,7 +257,7 @@ public function register(Request $request)
// ], 400); // ], 400);
// } // }
} // }
// Hash password // Hash password
$data['password'] = isset($data['password']) $data['password'] = isset($data['password'])
@@ -392,12 +393,13 @@ public function loginOTP(Request $request)
]); ]);
$verify = Str::random(8); $verify = Str::random(8);
SendSMSJob::dispatch([ SendOTPJob::dispatch($data['mobile'], (string)$otpToken);
'mobile' => $data['mobile'], // SendSMSJob::dispatch([
'message' => "کد ورود به برنامه: $otpToken // 'mobile' => $data['mobile'],
@{$packageName->web_app_url} #$otpToken // 'message' => "کد ورود به برنامه: $otpToken
لغو 11" // @{$packageName->web_app_url} #$otpToken
]); // لغو 11"
// ]);
} }
if (!$user->packageNames()->where('package_names.id', $packageName->id)->exists()) { if (!$user->packageNames()->where('package_names.id', $packageName->id)->exists()) {
@@ -451,7 +453,71 @@ public function checkOTP(Request $request)
'token' => $token, 'token' => $token,
]); ]);
} }
public function requestMobileVerification(Request $request)
{
$data = $request->validate([
'mobile' => ['required', 'string', new MobileNumber],
]);
$user = auth()->user();
$mobile = MobileNumberHelper::formatMobile($data['mobile']);
// چک اینکه موبایل مال یوزر دیگه نباشه
if (User::where('mobile', $mobile)
->where('id', '!=', $user->id)
->exists()) {
return response()->json([
'message' => 'mobile already taken'
], 400);
}
// حذف otp های قبلی
$user->otpTokens()->delete();
$otp = rand(10000, 99999);
$user->otpTokens()->create([
'token' => $otp
]);
SendOTPJob::dispatch($mobile, (string)$otp);
return response()->json([
'message' => 'otp sent successfully'
]);
}
public function confirmMobileVerification(Request $request)
{
$data = $request->validate([
'mobile' => ['required', 'string', new MobileNumber],
'token' => 'required|string'
]);
$user = auth()->user();
$mobile = MobileNumberHelper::formatMobile($data['mobile']);
$otp = $user->otpTokens()
->where('token', $data['token'])
->where('created_at', '>', now()->subMinutes(5))
->first();
if (!$otp) {
return response()->json([
'message' => 'invalid or expired token'
], 400);
}
$otp->delete();
$user->update([
'mobile' => $mobile,
'mobile_verified_at' => now(),
]);
return response()->json([
'message' => 'mobile verified successfully'
]);
}
public function updateProfile(Request $request) public function updateProfile(Request $request)
{ {
$data = $request->validate([ $data = $request->validate([
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Jobs;
use App\Services\KavenegarService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SendOTPJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
private string $mobile,
private string $token
) {}
public function handle(KavenegarService $service)
{
$response = $service->sendVerify($this->mobile, $this->token);
logger()->info('Kavenegar response', $response);
if (!isset($response['return']) || $response['return']['status'] != 200) {
logger()->error('Kavenegar failed', $response);
throw new \Exception(json_encode($response));
}
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use App\ReminderType;
class Reminder extends Model class Reminder extends Model
{ {
use HasFactory; use HasFactory;
+7 -1
View File
@@ -20,6 +20,9 @@ class User extends Authenticatable
'female' => 2, 'female' => 2,
]; ];
protected $guarded = []; protected $guarded = [];
protected $casts = [
'mobile_verified_at' => 'datetime',
];
protected $hidden = [ protected $hidden = [
'password', 'password',
]; ];
@@ -39,7 +42,10 @@ public function getFullNameAttribute()
{ {
return $this->first_name . ' ' . $this->last_name; return $this->first_name . ' ' . $this->last_name;
} }
public function hasVerifiedMobile(): bool
{
return !is_null($this->mobile_verified_at);
}
public function getAvatarAttribute() public function getAvatarAttribute()
{ {
return isset($this->attributes['avatar']) ? url($this->attributes['avatar']) : null; return isset($this->attributes['avatar']) ? url($this->attributes['avatar']) : null;
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
class KavenegarService
{
public function sendVerify(string $mobile, string $token)
{
$apiKey = config('kave.api_key');
if (!$apiKey) {
throw new \Exception('KAVE_API_KEY is null');
}
$url = "https://api.kavenegar.com/v1/{$apiKey}/verify/lookup.json";
$response = Http::timeout(5)->get($url, [
'receptor' => $mobile,
'token' => $token,
'template' => config('kave.template'),
]);
logger()->info('Kavenegar raw', [
'url' => $url,
'status' => $response->status(),
'body' => $response->body(),
]);
return $response->json();
}
}
Regular → Executable
View File
+10 -1
View File
@@ -1,5 +1,14 @@
<?php <?php
return [ return [
'url' => 'https://api.kavenegar.com/v1/' . env('KAVE_API_KEY') . '/sms/send.json' 'base_url' => 'https://api.kavenegar.com/v1/',
'api_key' => env('KAVE_API_KEY'),
'template' => 'loginotp'
]; ];
// return [
// 'url' => 'https://api.kavenegar.com/v1/' . env('KAVE_API_KEY') . '/verify/lookup.json'
// ];
@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('jobs', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
}
};
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->timestamp('mobile_verified_at')->nullable()->after('mobile');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('mobile_verified_at');
});
}
};
View File
+1
View File
@@ -0,0 +1 @@
User ID: . - . - FCM Token: .
+1
View File
@@ -0,0 +1 @@
Package Name: . - . \n
View File
+2
View File
@@ -55,6 +55,8 @@
Route::apiResource('reminders', ReminderController::class); Route::apiResource('reminders', ReminderController::class);
}); });
Route::middleware('auth:sanctum')->group(function () { Route::middleware('auth:sanctum')->group(function () {
Route::post('/verify-mobile/request', [UserController::class, 'requestMobileVerification']);
Route::post('/verify-mobile/confirm', [UserController::class, 'confirmMobileVerification']);
Route::post('profile', [UserController::class, 'updateProfile']); Route::post('profile', [UserController::class, 'updateProfile']);
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
View File
View File
View File
View File
View File
Regular → Executable
View File