client api

This commit is contained in:
pbao9
2026-02-11 10:08:39 +07:00
parent 458ad0a4cb
commit d59b2a0840
27 changed files with 4369 additions and 28 deletions
Vendored
BIN
View File
Binary file not shown.
@@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers\Api\Client;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\Client\ClientAuthRequest;
use App\Http\Requests\Api\Client\ClientRegisterRequest;
use App\Http\Resources\Api\Client\ClientResource;
use App\Services\Api\Client\ClientAuthService;
use Illuminate\Support\Facades\Auth;
class ClientController extends Controller
{
protected $authService;
public function __construct(ClientAuthService $authService)
{
$this->authService = $authService;
}
/**
* Lấy thông tin khách hàng hiện tại
*/
public function me()
{
return new ClientResource(auth('sanctum')->user());
}
/**
* Đăng nhập khách hàng
*/
public function login(ClientAuthRequest $request)
{
$result = $this->authService->authenticateClient($request->validated());
return response()->json([
'error' => false,
'data' => [
'client' => new ClientResource($result['client']),
'token' => $result['token'],
],
]);
}
/**
* Đăng ký khách hàng mới
*/
public function register(ClientRegisterRequest $request)
{
$result = $this->authService->registerClient($request->validated());
return response()->json([
'error' => false,
'data' => [
'client' => new ClientResource($result['client']),
'token' => $result['token'],
],
]);
}
/**
* Đăng xuất khách hàng
*/
public function logout()
{
auth('sanctum')->user()->currentAccessToken()->delete();
return response()->json([
'error' => false,
'message' => 'Đã đăng xuất thành công.',
]);
}
}
@@ -0,0 +1,51 @@
<?php
namespace App\Http\Requests\Api\Client;
use Illuminate\Foundation\Http\FormRequest;
class ClientAuthRequest extends FormRequest
{
/**
* Allow request
*/
public function authorize(): bool
{
return true;
}
/**
* Validation rules
*/
public function rules(): array
{
return [
'email' => [
'required',
'email',
'exists:customers,email'
],
'password' => [
'required',
'string',
'min:6'
],
];
}
/**
* Custom messages (optional)
*/
public function messages(): array
{
return [
'email.required' => 'Email là bắt buộc',
'email.email' => 'Email không đúng định dạng',
'email.exists' => 'Email không tồn tại',
'password.required' => 'Mật khẩu là bắt buộc',
'password.min' => 'Mật khẩu tối thiểu 6 ký tự',
];
}
}
@@ -0,0 +1,69 @@
<?php
namespace App\Http\Requests\Api\Client;
use Illuminate\Foundation\Http\FormRequest;
class ClientRegisterRequest extends FormRequest
{
/**
* Allow request
*/
public function authorize(): bool
{
return true;
}
/**
* Validation rules
*/
public function rules(): array
{
return [
'fullname' => [
'required',
'string',
'max:255'
],
'phone' => [
'required',
'digits:10',
'unique:customers,phone'
],
'email' => [
'required',
'email',
'max:255',
'unique:customers,email'
],
'password' => [
'required',
'string',
'min:6',
'confirmed'
],
];
}
/**
* Custom messages
*/
public function messages(): array
{
return [
'fullname.required' => 'Tên là bắt buộc',
'phone.required' => 'Số điện thoại là bắt buộc',
'phone.digits' => 'Số điện thoại phải có 10 chữ số',
'phone.unique' => 'Số điện thoại đã tồn tại',
'email.required' => 'Email là bắt buộc',
'email.email' => 'Email không đúng định dạng',
'email.unique' => 'Email đã tồn tại',
'password.required' => 'Mật khẩu là bắt buộc',
'password.min' => 'Mật khẩu tối thiểu 6 ký tự',
'password.confirmed' => 'Xác nhận mật khẩu không khớp',
];
}
}
@@ -22,11 +22,11 @@ class UserRegisterRequest extends FormRequest
public function rules(): array
{
return [
'email' => 'required|email|unique:customer',
'email' => 'required|email|unique:customers',
'firstname' => 'required',
'lastname' => 'required',
'password' => 'required|min:8',
'phone' => 'required|unique:customer'
'phone' => 'required|unique:customers'
];
}
@@ -24,7 +24,7 @@ class UserStoreInformation extends FormRequest
return [
'firstname' => 'required',
'lastname' => 'required',
'email' => 'nullable|email|unique:customer,email',
'email' => 'nullable|email|unique:customers,email',
'address' => 'nullable',
'phone' => 'required',
'gender' => 'nullable',
@@ -0,0 +1,63 @@
<?php
namespace App\Http\Resources\Api\Client;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ClientResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
if (!$this->resource) {
return [];
}
return [
'customer' => [
'name' => $this->fullname,
'email' => $this->email,
'phone' => $this->phone,
'address' => $this->address,
'city' => $this->city,
'ward' => $this->ward,
'province' => $this->province,
'active_phone' => 1,
'active_zalo' => 1,
'gender' => $this->gender,
'date' => $this->created_at ? $this->created_at->format('Y-m-d H:i:s') : null,
'company' => $this->company ?? '',
'mst' => null,
'avatar' => $this->avatar,
'cmnd_after' => $this->cmnd_after,
'cmnd_before' => $this->cmnd_before,
'cmnd_active' => (bool) $this->identity_number,
'cmnd' => $this->cmnd,
'fb_link' => null,
'active_2fa' => 0,
'off_mail_marketing' => $this->off_mail_marketing,
'is_recharge_zalo' => 0,
'is_verify_telegram' => 0,
],
'credit' => [
'credit' => $this->credit,
'total' => $this->total,
'point' => $this->point,
'point_used' => 0,
'point_total' => $this->point,
],
'service' => [
'vps' => $this->vps()->count(),
'proxy' => 0,
'server' => 0,
'hosting' => $this->hosting()->count(),
'domain' => $this->domain()->count(),
]
];
}
}
+63 -7
View File
@@ -2,19 +2,28 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Sanctum\HasApiTokens;
class Customer extends Authenticatable
{
use HasFactory, HasApiTokens;
use HasFactory, HasApiTokens, HasUuids;
protected $table = 'customer';
protected $table = 'customers';
/**
* Chỉ định cột sẽ nhận UUID.
* Mặc định HasUuids dùng cột 'id', mình đổi sang 'uuid'.
*/
public function uniqueIds()
{
return ['uuid'];
}
protected $fillable = [
'firstname',
'lastname',
'fullname',
'avatar',
'address',
'state',
@@ -28,15 +37,26 @@ class Customer extends Authenticatable
'email_hash',
'email_marketing',
'receive_deposit',
'cccd',
'front_cccd',
'back_cccd',
'identity_number',
'front_identity_number',
'back_identity_number',
'password',
'credit',
'total',
'remember_token',
'status',
'gender',
'province_code',
'ward_code',
];
/**
* Appends for API
*/
protected $appends = [
'first_name',
'last_name',
'point',
];
/**
@@ -163,4 +183,40 @@ class Customer extends Authenticatable
{
return (bool) $this->receive_deposit;
}
public function getPointAttribute()
{
return (string) floor($this->total / 100);
}
public function getCmndBeforeAttribute()
{
return $this->front_identity_number ?? '';
}
public function getCmndAfterAttribute()
{
return $this->back_identity_number ?? '';
}
public function getCmndAttribute()
{
return $this->identity_number;
}
public function getOffMailMarketingAttribute()
{
return $this->email_marketing;
}
public function province()
{
return $this->belongsTo(Province::class, 'province_code', 'code');
}
public function ward()
{
return $this->belongsTo(Ward::class, 'ward_code', 'code');
}
}
+78
View File
@@ -0,0 +1,78 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Province extends Model
{
use HasFactory;
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'provinces';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = false;
/**
* The primary key for the model.
*
* @var string
*/
protected $primaryKey = 'code';
/**
* The "type" of the primary key ID.
*
* @var string
*/
protected $keyType = 'string';
/**
* Indicates if the IDs are auto-incrementing.
*
* @var bool
*/
public $incrementing = false;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'code',
'name',
'name_en',
'full_name',
'full_name_en',
'code_name',
'administrative_unit_id',
];
/**
* Get the administrative unit that owns the province.
*/
public function administrativeUnit()
{
return $this->belongsTo(AdministrativeUnit::class, 'administrative_unit_id');
}
/**
* Get the wards for the province.
*/
public function wards()
{
return $this->hasMany(Ward::class, 'province_code', 'code');
}
}
+79
View File
@@ -0,0 +1,79 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Ward extends Model
{
use HasFactory;
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'wards';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = false;
/**
* The primary key for the model.
*
* @var string
*/
protected $primaryKey = 'code';
/**
* The "type" of the primary key ID.
*
* @var string
*/
protected $keyType = 'string';
/**
* Indicates if the IDs are auto-incrementing.
*
* @var bool
*/
public $incrementing = false;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'code',
'name',
'name_en',
'full_name',
'full_name_en',
'code_name',
'province_code',
'administrative_unit_id',
];
/**
* Get the province that owns the ward.
*/
public function province()
{
return $this->belongsTo(Province::class, 'province_code', 'code');
}
/**
* Get the administrative unit that owns the ward.
*/
public function administrativeUnit()
{
return $this->belongsTo(AdministrativeUnit::class, 'administrative_unit_id');
}
}
+6
View File
@@ -359,6 +359,12 @@ class RepositoryProvider extends ServiceProvider
ProductCategoryRepositoryInterface::class,
ProductCategoryRepository::class
);
// Client API
$this->app->bind(
\App\Repositories\Api\Client\ClientRepositoryInterface::class,
\App\Repositories\Api\Client\ClientRepository::class
);
}
/**
@@ -0,0 +1,71 @@
<?php
namespace App\Repositories\Api;
use App\Repositories\BaseRepositoryInterface;
abstract class BaseApiRepository implements BaseRepositoryInterface
{
protected $model;
public function __construct()
{
$this->setModel();
}
abstract public function getModel();
protected function setModel()
{
$this->model = app()->make($this->getModel());
return $this;
}
public function create(array $data = [])
{
return $this->model->create($data);
}
public function getAll()
{
return $this->model->all();
}
public function find($id)
{
return $this->model->find($id);
}
public function insert(array $data = [])
{
return $this->model->insert($data);
}
public function update(int $id = 0, array $data = [])
{
$result = $this->find($id);
if ($result) {
$result->update($data);
return $result;
}
return false;
}
public function delete($id)
{
$result = $this->find($id);
if ($result) {
$result->delete();
return true;
}
return false;
}
public function getCount()
{
return $this->model->count('id');
}
}
@@ -0,0 +1,22 @@
<?php
namespace App\Repositories\Api\Client;
use App\Models\Customer;
use App\Repositories\Api\BaseApiRepository;
class ClientRepository extends BaseApiRepository implements ClientRepositoryInterface
{
public function getModel()
{
return Customer::class;
}
/**
* Tìm khách hàng theo email
*/
public function findByEmail(string $email)
{
return $this->model->where('email', $email)->first();
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Repositories\Api\Client;
use App\Repositories\BaseRepositoryInterface;
interface ClientRepositoryInterface extends BaseRepositoryInterface
{
/**
* Tìm khách hàng theo email
*/
public function findByEmail(string $email);
}
@@ -0,0 +1,57 @@
<?php
namespace App\Services\Api\Client;
use App\Repositories\Api\Client\ClientRepositoryInterface;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
class ClientAuthService
{
protected $clientRepository;
public function __construct(ClientRepositoryInterface $clientRepository)
{
$this->clientRepository = $clientRepository;
}
/**
* Xác thực khách hàng và tạo token
*/
public function authenticateClient(array $credentials): array
{
if (!Auth::guard('client')->attempt($credentials)) {
throw ValidationException::withMessages([
'email' => ['Thông tin đăng nhập không chính xác.'],
]);
}
$client = Auth::guard('client')->user();
$token = $client->createToken('client-token')->plainTextToken;
return [
'client' => $client,
'token' => $token,
];
}
/**
* Đăng ký khách hàng mới và tạo token
*/
public function registerClient(array $data): array
{
$client = $this->clientRepository->create([
'fullname' => $data['fullname'],
'phone' => $data['phone'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
]);
$token = $client->createToken('client-token')->plainTextToken;
return [
'client' => $client,
'token' => $token,
];
}
}
@@ -0,0 +1,7 @@
<?php
namespace app\Services\Api\Client;
class ClientService{
}
+5 -1
View File
@@ -51,7 +51,11 @@ return [
'users-api' => [
'driver' => 'passport',
'provider' => 'users',
]
],
'client' => [
'driver' => 'session',
'provider' => 'users',
],
],
/*
@@ -0,0 +1,30 @@
<?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('administrative_regions', function (Blueprint $table) {
$table->integer('id')->primary();
$table->string('name');
$table->string('name_en');
$table->string('code_name')->nullable();
$table->string('code_name_en')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('administrative_regions');
}
};
@@ -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('administrative_units', function (Blueprint $table) {
$table->integer('id')->primary();
$table->string('full_name')->nullable();
$table->string('full_name_en')->nullable();
$table->string('short_name')->nullable();
$table->string('short_name_en')->nullable();
$table->string('code_name')->nullable();
$table->string('code_name_en')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('administrative_units');
}
};
@@ -0,0 +1,39 @@
<?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('provinces', function (Blueprint $table) {
$table->string('code', 20)->primary();
$table->string('name');
$table->string('name_en')->nullable();
$table->string('full_name');
$table->string('full_name_en')->nullable();
$table->string('code_name')->nullable();
$table->integer('administrative_unit_id')->nullable();
$table->foreign('administrative_unit_id')
->references('id')
->on('administrative_units')
->onDelete('set null');
$table->index('administrative_unit_id', 'idx_provinces_unit');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('provinces');
}
};
@@ -0,0 +1,46 @@
<?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('wards', function (Blueprint $table) {
$table->string('code', 20)->primary();
$table->string('name');
$table->string('name_en')->nullable();
$table->string('full_name')->nullable();
$table->string('full_name_en')->nullable();
$table->string('code_name')->nullable();
$table->string('province_code', 20)->nullable();
$table->integer('administrative_unit_id')->nullable();
$table->foreign('administrative_unit_id')
->references('id')
->on('administrative_units')
->onDelete('set null');
$table->foreign('province_code')
->references('code')
->on('provinces')
->onDelete('set null');
$table->index('province_code', 'idx_wards_province');
$table->index('administrative_unit_id', 'idx_wards_unit');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('wards');
}
};
@@ -13,10 +13,10 @@ return new class extends Migration {
public function up()
{
//
Schema::create('customer', function (Blueprint $table) {
Schema::create('customers', function (Blueprint $table) {
$table->id();
$table->string('firstname', 100)->nullable();
$table->string('lastname', 100)->nullable();
$table->uuid('uuid')->unique();
$table->string('fullname', 100)->nullable();
$table->string('avatar', 100)->nullable();
$table->integer('gender')->default(0);
$table->string('address', 100)->nullable();
@@ -34,12 +34,27 @@ return new class extends Migration {
$table->string('password')->nullable();
$table->integer('credit')->default(0);
$table->integer('total')->default(0);
$table->string('cccd')->nullable();
$table->string('front_cccd')->nullable();
$table->string('back_cccd')->nullable();
$table->string('identity_number')->nullable();
$table->string('front_identity_number')->nullable();
$table->string('back_identity_number')->nullable();
$table->string('province_code', 20)->nullable();
$table->string('ward_code', 20)->nullable();
$table->string('remember_token', 100)->nullable();
$table->tinyInteger('status')->default(1);
$table->timestamps();
$table->foreign('province_code')
->references('code')
->on('provinces')
->onDelete('set null');
$table->foreign('ward_code')
->references('code')
->on('wards')
->onDelete('set null');
$table->index('province_code', 'idx_users_province');
$table->index('ward_code', 'idx_users_ward');
});
}
@@ -28,7 +28,7 @@ return new class extends Migration
// 3. Add foreign key constraints
Schema::table('order', function (Blueprint $table) {
$table->foreign('customer_id')->references('id')->on('customer')->onDelete('set null');
$table->foreign('customer_id')->references('id')->on('customers')->onDelete('set null');
$table->foreign('user_id')->references('id')->on('user')->onDelete('set null');
$table->foreign('discount_id')->references('id')->on('discount')->onDelete('set null');
$table->foreign('invoice_id')->references('id')->on('invoice')->onDelete('set null');
File diff suppressed because it is too large Load Diff
-1
View File
@@ -18,7 +18,6 @@ class DatabaseSeeder extends Seeder
User::factory(1)->create();
$this->call(TypeSeeder::class);
$this->call(DomainTypeSeeder::class);
Customer::factory(10)->create();
$this->call(GroupSeeder::class);
$this->call(Payment::class);
$this->call(AddonSeeder::class);
+3 -2
View File
@@ -1,5 +1,6 @@
<?php
use App\Http\Controllers\Api\Client\ClientController;
use App\Http\Controllers\rest\ModuleController;
use App\Http\Services\Media;
use App\Http\Services\vCenter as ServiceVCenter;
@@ -187,7 +188,7 @@ Route::prefix('v1')
})->name('api.GetVersionOs');
Route::post('get-all-image', function (Media $media) {
$files = $media->getAllMedia();
$data = view('administrator.pages.component.media')->with('media', $files)->render();
$data = view('administrator.pages.component.media')->with('media', $files)->render();
return response()->json(['data' => $data]);
})->name('api.GetAllImage');
Route::get('get-list-addon', function () {
@@ -197,7 +198,7 @@ Route::prefix('v1')
}
$allAddon = Addon::get();
$formAddon = view('administrator.pages.component.addon')->with([
$formAddon = view('administrator.pages.component.addon')->with([
'addon' => $allAddon
])->render();
+16 -7
View File
@@ -1,18 +1,27 @@
<?php
use App\Models\Product;
use App\Http\Controllers\Api\Client\ClientController;
Route::prefix('v1')->group(function () {
Route::get('get-list-product', function () {
return response()->json([
'error' => false,
'data' => Product::all()
]);
})->name('api.user.GetListProduct');
// Auth
Route::prefix('client')->as('client.')
->group(function () {
Route::controller(ClientController::class)->group(function () {
Route::post('login', 'login')->name('login');
Route::post('register', 'register')->name('register');
Route::group(['middleware' => ['api.auth']], function () {
Route::get('me', 'me')->name('me');
Route::post('logout', 'logout')->name('logout');
});
});
});
});
Route::any('{any}', function () {
return response()->json([
'error' => true,