middleware('auth:api')->except(['login', 'applyAccount']); $this->middleware(function (Request $request, \Closure $next) { /** @var User|null $user */ $user = Auth::guard('api')->user(); if (! $user || ! $user->force_password_change) { return $next($request); } $allowed = [ 'auth/me', 'auth/password', 'auth/logout', 'api/auth/me', 'api/auth/password', 'api/auth/logout', ]; if (in_array($request->path(), $allowed, true)) { return $next($request); } return response()->json([ 'code' => 423, 'message' => '请先修改密码后再继续操作', 'data' => ['force_password_change' => true], ], 423); })->except(['login', 'applyAccount']); } #[Apidoc\Title('登录'), Apidoc\Method('POST'), Apidoc\Url('/auth/login')] public function login(Request $request): JsonResponse { $validated = $request->validate([ 'email' => ['nullable', 'email', 'required_without:phone'], 'phone' => ['nullable', 'string', 'max:32', 'required_without:email'], 'password' => ['required', 'string'], ]); $credentials = ['password' => (string) $validated['password']]; $accountType = ''; if (! empty($validated['email'])) { $credentials['email'] = (string) $validated['email']; $accountType = 'email'; } if (! empty($validated['phone'])) { $credentials['phone'] = (string) $validated['phone']; $accountType = 'phone'; } $token = Auth::guard('api')->attempt($credentials); if (! $token) { return response()->json(['code' => 401, 'message' => '账号或密码错误', 'data' => null], 401); } /** @var User|null $user */ $user = Auth::guard('api')->user(); $permissionCount = (int) ($user?->getAllPermissions()->count() ?? 0); if ($permissionCount <= 0) { Auth::guard('api')->logout(); return response()->json([ 'code' => 403, 'message' => '账号申请已通过,但尚未开通任何权限,请联系管理员分配权限后再登录', 'data' => null, ], 403); } $this->auditLog($request, 'login', ['metadata' => ['account_type' => $accountType]]); return response()->json([ 'code' => 0, 'message' => 'ok', 'data' => [ 'token' => $token, 'type' => 'bearer', 'expires_in' => Auth::guard('api')->factory()->getTTL() * 60, ], ]); } #[Apidoc\Title('账号申请'), Apidoc\Method('POST'), Apidoc\Url('/auth/apply-account')] public function applyAccount(Request $request): JsonResponse { $validated = $request->validate([ 'nickname' => ['required', 'string', 'max:255'], 'email' => ['required', 'email', 'max:255'], 'phone' => ['required', 'string', 'regex:/^1[3-9]\d{9}$/'], 'password' => ['required', 'confirmed', Password::min(6)], 'application_note' => ['nullable', 'string', 'max:2000'], ]); $guestRole = Role::query()->firstOrCreate( ['name' => 'guest', 'guard_name' => 'api'], ['name' => 'guest', 'guard_name' => 'api'] ); $existingUsers = User::query() ->with('roles') ->where('email', $validated['email']) ->orWhere('phone', $validated['phone']) ->get(); if ($existingUsers->isNotEmpty()) { if ($existingUsers->contains(fn (User $user): bool => ! $user->hasRole('guest', 'api'))) { throw ValidationException::withMessages([ 'account' => ['邮箱或手机号对应的账号已开通,不可重复提交申请。'], ]); } if ($existingUsers->count() > 1) { throw ValidationException::withMessages([ 'account' => ['邮箱和手机号分别对应不同申请账号,请联系管理员处理。'], ]); } /** @var User $existingUser */ $existingUser = $existingUsers->first(); $existingUser->fill([ 'nickname' => $validated['nickname'], 'email' => $validated['email'], 'phone' => $validated['phone'], 'password' => $validated['password'], 'force_password_change' => false, 'application_note' => $validated['application_note'] ?? null, ]); $existingUser->save(); $existingUser->syncRoles([$guestRole->id]); $this->auditLog($request, 'account_apply_update', ['metadata' => ['target_user_id' => $existingUser->id]]); return response()->json([ 'code' => 0, 'message' => '申请信息已更新,请联系或等待管理员开通权限后登录控制台', 'data' => ['updated' => true], ]); } $user = User::query()->create([ 'nickname' => $validated['nickname'], 'email' => $validated['email'], 'phone' => $validated['phone'], 'password' => $validated['password'], 'force_password_change' => false, 'application_note' => $validated['application_note'] ?? null, ]); $user->syncRoles([$guestRole->id]); $this->auditLog($request, 'account_apply', ['metadata' => ['target_user_id' => $user->id]]); return response()->json([ 'code' => 0, 'message' => '申请提交成功,请联系或等待管理员开通权限后登录控制台', 'data' => ['updated' => false], ], 201); } #[Apidoc\Title('当前用户信息'), Apidoc\Method('GET'), Apidoc\Url('/auth/me')] public function me(): JsonResponse { /** @var User $user */ $user = Auth::guard('api')->user(); return response()->json([ 'code' => 0, 'message' => 'ok', 'data' => [ 'user' => $user?->load('roles'), 'permissions' => $user?->getAllPermissions()->pluck('name')->values(), 'force_password_change' => (bool) ($user?->force_password_change ?? false), ], ]); } #[Apidoc\Title('更新个人信息'), Apidoc\Method('PUT'), Apidoc\Url('/auth/profile')] public function updateProfile(Request $request): JsonResponse { /** @var User $user */ $user = Auth::guard('api')->user(); $validated = $request->validate([ 'nickname' => ['sometimes', 'required', 'string', 'max:255'], 'email' => ['sometimes', 'required', 'email', 'max:255', 'unique:users,email,'.$user->id], 'phone' => ['nullable', 'string', 'max:32', 'unique:users,phone,'.$user->id], ]); $user->update($validated); $this->auditLog($request, 'profile_update'); return response()->json(['code' => 0, 'message' => 'ok', 'data' => $user->fresh(['roles'])]); } #[Apidoc\Title('修改密码'), Apidoc\Method('PUT'), Apidoc\Url('/auth/password')] public function updatePassword(Request $request): JsonResponse { /** @var User $user */ $user = Auth::guard('api')->user(); $rules = [ 'password' => ['required', 'confirmed', Password::min(6)], ]; if (! $user->force_password_change) { $rules['current_password'] = ['required', 'current_password:api']; } $validated = $request->validate($rules); $user->password = $validated['password']; $user->force_password_change = false; $user->save(); $this->auditLog($request, 'password_update'); return response()->json(['code' => 0, 'message' => 'ok', 'data' => null]); } #[Apidoc\Title('登出'), Apidoc\Method('POST'), Apidoc\Url('/auth/logout')] public function logout(Request $request): JsonResponse { $this->auditLog($request, 'logout'); Auth::guard('api')->logout(); return response()->json(['code' => 0, 'message' => 'ok', 'data' => null]); } }