543 lines
21 KiB
PHP
543 lines
21 KiB
PHP
<?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\ClientChangePasswordRequest;
|
|
use App\Http\Requests\Api\Client\ClientRegisterRequest;
|
|
use App\Http\Requests\Api\Client\ClientUpdateCccdRequest;
|
|
use App\Http\Requests\Api\Client\ClientUpdateRequest;
|
|
use App\Http\Resources\Api\Client\ClientResource;
|
|
use App\Services\ActivityLogger;
|
|
use App\Services\Api\Client\ClientAuthService;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Tymon\JWTAuth\Facades\JWTAuth;
|
|
use Tymon\JWTAuth\Exceptions\JWTException;
|
|
|
|
/**
|
|
* @OA\Tag(
|
|
* name="Client Authentication",
|
|
* description="API Endpoints for Client Authentication"
|
|
* )
|
|
*/
|
|
class ClientController extends Controller
|
|
{
|
|
protected $authService;
|
|
protected ActivityLogger $activityLogger;
|
|
|
|
public function __construct(ClientAuthService $authService, ActivityLogger $activityLogger)
|
|
{
|
|
$this->authService = $authService;
|
|
$this->activityLogger = $activityLogger;
|
|
}
|
|
|
|
/**
|
|
* @OA\Get(
|
|
* path="/client/me",
|
|
* operationId="getClientMe",
|
|
* tags={"Client Authentication"},
|
|
* summary="Get current client information",
|
|
* description="Returns the currently authenticated client's details",
|
|
* security={{"bearerAuth":{}}},
|
|
* @OA\Response(
|
|
* response=200,
|
|
* description="Successful operation",
|
|
* @OA\JsonContent(
|
|
* @OA\Property(property="customer", type="object",
|
|
* @OA\Property(property="fullname", type="string", example="Nguyen Van A"),
|
|
* @OA\Property(property="email", type="string", example="client@example.com"),
|
|
* @OA\Property(property="phone", type="string", example="0987654321"),
|
|
* @OA\Property(property="address", type="string", example="123 Street"),
|
|
* @OA\Property(property="city", type="string", example="Hanoi"),
|
|
* @OA\Property(property="ward", type="string", example="Ward 1"),
|
|
* @OA\Property(property="province", type="string", example="Hanoi"),
|
|
* @OA\Property(property="gender", type="string", example="Nam"),
|
|
* @OA\Property(property="date", type="string", format="date-time", example="2024-01-01 12:00:00"),
|
|
* @OA\Property(property="company", type="string", example="Company Ltd"),
|
|
* @OA\Property(property="avatar", type="string", example="/uploads/avatar.jpg"),
|
|
* @OA\Property(property="cmnd_after", type="string", example="cmnd_back.jpg"),
|
|
* @OA\Property(property="cmnd_before", type="string", example="cmnd_front.jpg"),
|
|
* @OA\Property(property="cmnd", type="string", example="123456789"),
|
|
* @OA\Property(property="fb_link", type="string", nullable=true),
|
|
* @OA\Property(property="active_2fa", type="integer", example=0),
|
|
* @OA\Property(property="off_mail_marketing", type="integer", example=0),
|
|
* @OA\Property(property="is_recharge_zalo", type="integer", example=0),
|
|
* @OA\Property(property="is_verify_telegram", type="integer", example=0)
|
|
* ),
|
|
* @OA\Property(property="credit", type="object",
|
|
* @OA\Property(property="credit", type="number", example=1000000),
|
|
* @OA\Property(property="total", type="number", example=5000000),
|
|
* @OA\Property(property="point", type="number", example=100),
|
|
* @OA\Property(property="point_used", type="integer", example=0),
|
|
* @OA\Property(property="point_total", type="number", example=100)
|
|
* ),
|
|
* @OA\Property(property="service", type="object",
|
|
* @OA\Property(property="vps", type="integer", example=2),
|
|
* @OA\Property(property="proxy", type="integer", example=0),
|
|
* @OA\Property(property="server", type="integer", example=0),
|
|
* @OA\Property(property="hosting", type="integer", example=1),
|
|
* @OA\Property(property="domain", type="integer", example=3)
|
|
* )
|
|
* )
|
|
* ),
|
|
* @OA\Response(
|
|
* response=401,
|
|
* description="Unauthenticated"
|
|
* )
|
|
* )
|
|
*/
|
|
public function me()
|
|
{
|
|
return response()->json([
|
|
'error' => false,
|
|
'data' => [
|
|
'client' => new ClientResource(auth('client')->user()->loadCount(['vps', 'hosting', 'domain'])),
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @OA\Post(
|
|
* path="/client/login",
|
|
* operationId="clientLogin",
|
|
* tags={"Client Authentication"},
|
|
* summary="Client login",
|
|
* description="Authenticate a client and return access token + refresh token",
|
|
* @OA\RequestBody(
|
|
* required=true,
|
|
* @OA\JsonContent(
|
|
* required={"email","password"},
|
|
* @OA\Property(property="email", type="string", format="email", example="client@example.com"),
|
|
* @OA\Property(property="password", type="string", format="password", example="password123")
|
|
* )
|
|
* ),
|
|
* @OA\Response(
|
|
* response=200,
|
|
* description="Login successful",
|
|
* @OA\JsonContent(
|
|
* @OA\Property(property="error", type="boolean", example=false),
|
|
* @OA\Property(property="data", type="object",
|
|
* @OA\Property(property="client", type="object"),
|
|
* @OA\Property(property="token", type="string", example="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."),
|
|
* @OA\Property(property="refresh_token", type="string", example="random64charstring..."),
|
|
* @OA\Property(property="expires_in", type="integer", example=900)
|
|
* )
|
|
* )
|
|
* ),
|
|
* @OA\Response(
|
|
* response=422,
|
|
* description="Validation error"
|
|
* )
|
|
* )
|
|
*/
|
|
public function login(ClientAuthRequest $request)
|
|
{
|
|
$result = $this->authService->authenticateClient($request->validated());
|
|
|
|
$client = $result['client'];
|
|
$this->activityLogger->logAuth('login', 'customer', $client->id, $client->email);
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'data' => [
|
|
'client' => new ClientResource($client->loadCount(['vps', 'hosting', 'domain'])),
|
|
'token' => $result['token'],
|
|
'refresh_token' => $result['refresh_token'],
|
|
'expires_in' => config('jwt.ttl', 60) * 60, // seconds
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @OA\Post(
|
|
* path="/client/register",
|
|
* operationId="clientRegister",
|
|
* tags={"Client Authentication"},
|
|
* summary="Client registration",
|
|
* description="Register a new client account",
|
|
* @OA\RequestBody(
|
|
* required=true,
|
|
* @OA\JsonContent(
|
|
* required={"fullname","phone","email","password","password_confirmation"},
|
|
* @OA\Property(property="fullname", type="string", example="Nguyen Van A"),
|
|
* @OA\Property(property="phone", type="string", example="0987654321"),
|
|
* @OA\Property(property="email", type="string", format="email", example="client@example.com"),
|
|
* @OA\Property(property="password", type="string", format="password", example="password123"),
|
|
* @OA\Property(property="password_confirmation", type="string", format="password", example="password123")
|
|
* )
|
|
* ),
|
|
* @OA\Response(
|
|
* response=200,
|
|
* description="Registration successful",
|
|
* @OA\JsonContent(
|
|
* @OA\Property(property="error", type="boolean", example=false),
|
|
* @OA\Property(property="data", type="object",
|
|
* @OA\Property(property="client", type="object"),
|
|
* @OA\Property(property="token", type="string", example="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."),
|
|
* @OA\Property(property="refresh_token", type="string", example="random64charstring..."),
|
|
* @OA\Property(property="expires_in", type="integer", example=900)
|
|
* )
|
|
* )
|
|
* ),
|
|
* @OA\Response(
|
|
* response=422,
|
|
* description="Validation error"
|
|
* )
|
|
* )
|
|
*/
|
|
public function register(ClientRegisterRequest $request)
|
|
{
|
|
$data = $request->validated();
|
|
|
|
if (!empty($data['trial_config'])) {
|
|
$decoded = json_decode((string) $data['trial_config'], true);
|
|
$data['trial_config'] = is_array($decoded) ? $decoded : null;
|
|
}
|
|
|
|
$result = $this->authService->registerClient($data);
|
|
|
|
$client = $result['client'];
|
|
$this->activityLogger->logAuth('register', 'customer', $client->id, $client->email);
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'message' => 'Đăng ký thành công. Vui lòng kiểm tra email và nhập mã xác thực để kích hoạt tài khoản.',
|
|
]);
|
|
}
|
|
|
|
public function verifyEmail(Request $request)
|
|
{
|
|
$request->validate([
|
|
'email' => ['required', 'email', 'max:255'],
|
|
'code' => ['required', 'string', 'size:6'],
|
|
]);
|
|
|
|
$result = $this->authService->verifyEmail(
|
|
$request->input('email'),
|
|
$request->input('code'),
|
|
);
|
|
|
|
$client = $result['client'];
|
|
$this->activityLogger->logAuth('email_verified', 'customer', $client->id, $client->email);
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'data' => [
|
|
'client' => new ClientResource($client->loadCount(['vps', 'hosting', 'domain'])),
|
|
'token' => $result['token'],
|
|
'refresh_token' => $result['refresh_token'],
|
|
'expires_in' => config('jwt.ttl', 60) * 60,
|
|
],
|
|
]);
|
|
}
|
|
|
|
public function resendVerifyCode(Request $request)
|
|
{
|
|
$request->validate([
|
|
'email' => ['required', 'email', 'max:255'],
|
|
]);
|
|
|
|
$this->authService->resendVerifyCode($request->input('email'));
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'message' => 'Nếu email tồn tại và chưa xác thực, mã xác thực mới đã được gửi.',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @OA\Post(
|
|
* path="/client/logout",
|
|
* operationId="clientLogout",
|
|
* tags={"Client Authentication"},
|
|
* summary="Client logout",
|
|
* description="Logout the currently authenticated client and revoke refresh token",
|
|
* security={{"bearerAuth":{}}},
|
|
* @OA\RequestBody(
|
|
* @OA\JsonContent(
|
|
* @OA\Property(property="refresh_token", type="string", example="random64charstring...")
|
|
* )
|
|
* ),
|
|
* @OA\Response(
|
|
* response=200,
|
|
* description="Logout successful",
|
|
* @OA\JsonContent(
|
|
* @OA\Property(property="error", type="boolean", example=false),
|
|
* @OA\Property(property="message", type="string", example="Đã đăng xuất thành công.")
|
|
* )
|
|
* ),
|
|
* @OA\Response(
|
|
* response=401,
|
|
* description="Unauthenticated"
|
|
* )
|
|
* )
|
|
*/
|
|
public function logout(Request $request)
|
|
{
|
|
$customer = auth('client')->user();
|
|
if ($customer) {
|
|
$this->activityLogger->logAuth('logout', 'customer', $customer->id, $customer->email);
|
|
}
|
|
|
|
if ($rawToken = $request->input('refresh_token')) {
|
|
$this->authService->revokeRefreshToken($rawToken);
|
|
}
|
|
|
|
auth('client')->logout();
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'message' => 'Đã đăng xuất thành công.',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @OA\Post(
|
|
* path="/client/refresh",
|
|
* operationId="clientRefresh",
|
|
* tags={"Client Authentication"},
|
|
* summary="Refresh access token",
|
|
* description="Exchange a valid refresh token for a new access token + rotated refresh token",
|
|
* @OA\RequestBody(
|
|
* required=true,
|
|
* @OA\JsonContent(
|
|
* required={"refresh_token"},
|
|
* @OA\Property(property="refresh_token", type="string", example="random64charstring...")
|
|
* )
|
|
* ),
|
|
* @OA\Response(
|
|
* response=200,
|
|
* description="Token refreshed",
|
|
* @OA\JsonContent(
|
|
* @OA\Property(property="error", type="boolean", example=false),
|
|
* @OA\Property(property="data", type="object",
|
|
* @OA\Property(property="token", type="string"),
|
|
* @OA\Property(property="refresh_token", type="string"),
|
|
* @OA\Property(property="expires_in", type="integer", example=900),
|
|
* @OA\Property(property="client", type="object")
|
|
* )
|
|
* )
|
|
* ),
|
|
* @OA\Response(
|
|
* response=401,
|
|
* description="Invalid or expired refresh token"
|
|
* )
|
|
* )
|
|
*/
|
|
public function refresh(Request $request)
|
|
{
|
|
$request->validate([
|
|
'refresh_token' => ['required', 'string'],
|
|
]);
|
|
|
|
try {
|
|
$result = $this->authService->rotateRefreshToken($request->input('refresh_token'));
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'data' => [
|
|
'token' => $result['token'],
|
|
'refresh_token' => $result['refresh_token'],
|
|
'expires_in' => config('jwt.ttl', 60) * 60,
|
|
'client' => new ClientResource($result['client']->loadCount(['vps', 'hosting', 'domain'])),
|
|
],
|
|
]);
|
|
} catch (\Illuminate\Validation\ValidationException $e) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => collect($e->errors())->flatten()->first(),
|
|
], 401);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Đổi mã impersonate (do admin cấp) lấy access token + refresh token.
|
|
* Endpoint public — code chỉ dùng được 1 lần và hết hạn sau 120s.
|
|
*/
|
|
public function impersonateExchange(Request $request)
|
|
{
|
|
$request->validate([
|
|
'code' => ['required', 'string', 'max:128'],
|
|
]);
|
|
|
|
$code = (string) $request->input('code');
|
|
$payload = \Illuminate\Support\Facades\Cache::pull("impersonate:{$code}");
|
|
|
|
if (!$payload) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => 'Mã đăng nhập không hợp lệ hoặc đã hết hạn.',
|
|
], 401);
|
|
}
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'data' => [
|
|
'token' => $payload['token'],
|
|
'refresh_token' => $payload['refresh_token'],
|
|
'client' => $payload['client'],
|
|
'expires_in' => config('jwt.ttl', 60) * 60,
|
|
],
|
|
]);
|
|
}
|
|
|
|
public function forgotPassword(Request $request)
|
|
{
|
|
$request->validate([
|
|
'email' => ['required', 'email', 'max:255'],
|
|
]);
|
|
|
|
$email = strtolower(trim((string) $request->input('email')));
|
|
|
|
$customer = \App\Models\Customer::whereRaw('LOWER(email) = ?', [$email])->first();
|
|
|
|
if (! $customer) {
|
|
// Do not disclose whether the email exists.
|
|
return response()->json([
|
|
'error' => false,
|
|
'message' => 'Nếu email tồn tại, mật khẩu mới đã được gửi đến hộp thư của bạn.',
|
|
]);
|
|
}
|
|
|
|
$alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%^&*';
|
|
$newPassword = '';
|
|
$max = strlen($alphabet) - 1;
|
|
for ($i = 0; $i < 16; $i++) {
|
|
$newPassword .= $alphabet[random_int(0, $max)];
|
|
}
|
|
|
|
try {
|
|
\Illuminate\Support\Facades\DB::beginTransaction();
|
|
|
|
$customer->password = Hash::make($newPassword);
|
|
$customer->save();
|
|
|
|
\Illuminate\Support\Facades\Mail::to($customer->email)->later(
|
|
now()->addSeconds(5),
|
|
new \App\Mail\Customer\PasswordResetByAdmin([
|
|
'fullname' => $customer->fullname,
|
|
'email' => $customer->email,
|
|
'password' => $newPassword,
|
|
])
|
|
);
|
|
|
|
$this->activityLogger->log(
|
|
event: 'password_forgot',
|
|
logName: 'Customer',
|
|
description: "Customer {$customer->email} yêu cầu cấp lại mật khẩu",
|
|
subject: $customer,
|
|
);
|
|
|
|
\Illuminate\Support\Facades\DB::commit();
|
|
} catch (\Throwable) {
|
|
\Illuminate\Support\Facades\DB::rollBack();
|
|
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => 'Không thể gửi email, vui lòng thử lại sau.',
|
|
], 500);
|
|
}
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'message' => 'Mật khẩu mới đã được gửi đến email của bạn.',
|
|
]);
|
|
}
|
|
|
|
public function update(ClientUpdateRequest $request)
|
|
{
|
|
$user = auth('client')->user();
|
|
|
|
$oldValues = array_intersect_key($user->toArray(), $request->validated());
|
|
$user->fill($request->validated());
|
|
$dirty = $user->getDirty();
|
|
$user->save();
|
|
|
|
if (!empty($dirty)) {
|
|
$this->activityLogger->log(
|
|
event: 'profile_update',
|
|
logName: 'Customer',
|
|
description: "Customer {$user->email} cập nhật thông tin cá nhân: " . implode(', ', array_keys($dirty)),
|
|
subject: $user,
|
|
oldValues: array_intersect_key($oldValues, $dirty),
|
|
newValues: array_diff_key($dirty, array_flip(['password', 'remember_token'])),
|
|
);
|
|
}
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'message' => 'Cập nhật thông tin thành công',
|
|
'data' => [
|
|
'client' => new ClientResource($user->loadCount(['vps', 'hosting', 'domain'])),
|
|
],
|
|
]);
|
|
}
|
|
|
|
public function updateCccd(ClientUpdateCccdRequest $request)
|
|
{
|
|
$user = auth('client')->user();
|
|
|
|
$oldValues = array_intersect_key($user->toArray(), $request->validated());
|
|
|
|
$user->identity_number = $request->validated('cmnd');
|
|
$user->front_identity_number = Storage::disk('public')->url(
|
|
$request->file('cmnd_before')->store('cccd', 'public')
|
|
);
|
|
$user->back_identity_number = Storage::disk('public')->url(
|
|
$request->file('cmnd_after')->store('cccd', 'public')
|
|
);
|
|
|
|
$dirty = $user->getDirty();
|
|
$user->save();
|
|
|
|
if (!empty($dirty)) {
|
|
$this->activityLogger->log(
|
|
event: 'cccd_update',
|
|
logName: 'Customer',
|
|
description: "Customer {$user->email} cập nhật chứng minh thư",
|
|
subject: $user,
|
|
oldValues: array_intersect_key($oldValues, $dirty),
|
|
newValues: $dirty,
|
|
);
|
|
}
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'message' => 'Cập nhật chứng minh thư thành công',
|
|
'data' => [
|
|
'client' => new ClientResource($user->loadCount(['vps', 'hosting', 'domain'])),
|
|
],
|
|
]);
|
|
}
|
|
|
|
public function changePassword(ClientChangePasswordRequest $request)
|
|
{
|
|
$user = auth('client')->user();
|
|
|
|
if (!Hash::check($request->input('current_password'), $user->password)) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => 'Mật khẩu hiện tại không chính xác',
|
|
], 422);
|
|
}
|
|
|
|
$user->password = bcrypt($request->input('password'));
|
|
$user->save();
|
|
|
|
$this->activityLogger->log(
|
|
event: 'password_change',
|
|
logName: 'Customer',
|
|
description: "Customer {$user->email} đã đổi mật khẩu",
|
|
subject: $user,
|
|
);
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'message' => 'Đổi mật khẩu thành công',
|
|
]);
|
|
}
|
|
}
|