Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

40 changes: 40 additions & 0 deletions app/Http/Controllers/Api/MutationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,31 @@ public function __construct(MutationHandlerRegistry $handlerRegistry, MutationRe
$this->mirrorService = $mirrorService;
}

/**
* 所有寫入端點(store/create/delete/batchStore/resubmit)的最外層守衛:
* 未登入回 401、已登入但帳號未啟用(is_active != 1,含被停用者)回 403。
*
* 各 handler 內部仍會依 direct/proposal 再做角色級授權(canWriteDirectly/canPropose,
* 見 AbstractMutationHandler),此處是縱深防禦——即使未來新增的 handler 漏掉授權,
* 未啟用帳號也無法從這裡進入任何寫入路徑。回傳非 null 即代表應直接以該回應中止。
*/
private function guardActiveUser(): ?JsonResponse {
$user = Auth::user();
if (!$user) {
return $this->errorResponse('Unauthenticated.', 401);
}
if (!$user->isActive()) {
return $this->errorResponse('該使用者沒有權限,請聯繫管理員', 403);
}

return null;
}

public function store(Request $request): JsonResponse {
if ($guard = $this->guardActiveUser()) {
return $guard;
}

$payload = $request->json()->all();
if (!is_array($payload) || empty($payload)) {
$payload = $request->all();
Expand Down Expand Up @@ -82,6 +106,10 @@ public function resubmit(Request $request, \App\Models\Operation $operation): Js
}

$user = Auth::user();
// 未啟用(含被停用)帳號一律不得重發提案,即使是提案本人。
if (!$user->isActive()) {
return $this->errorResponse('該使用者沒有權限,請聯繫管理員', 403);
}
$isOwner = (int) $operation->user_id === (int) Auth::id();
if (!$isOwner && !$user->canReviewProposals()) {
return $this->errorResponse('只有提案人或審核人可以修改提案', 403);
Expand Down Expand Up @@ -335,6 +363,10 @@ public function oppositeEdges(Request $request): JsonResponse {
}

public function create(Request $request): JsonResponse {
if ($guard = $this->guardActiveUser()) {
return $guard;
}

$payload = $request->json()->all();
if (!is_array($payload) || empty($payload)) {
$payload = $request->all();
Expand Down Expand Up @@ -368,6 +400,10 @@ public function create(Request $request): JsonResponse {
}

public function delete(Request $request): JsonResponse {
if ($guard = $this->guardActiveUser()) {
return $guard;
}

$payload = $request->json()->all();
if (!is_array($payload) || empty($payload)) {
$payload = $request->all();
Expand Down Expand Up @@ -411,6 +447,10 @@ public function delete(Request $request): JsonResponse {
* atomic=true:整批單一交易,任一筆失敗整批回滾,回 409 並帶 failed_index。
*/
public function batchStore(Request $request): JsonResponse {
if ($guard = $this->guardActiveUser()) {
return $guard;
}

$payload = $request->json()->all();
if (!is_array($payload) || empty($payload)) {
$payload = $request->all();
Expand Down
48 changes: 30 additions & 18 deletions app/Http/Controllers/Api/OperationsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,30 @@
use App\Models\OfficeCodeTypeRel;
use App\Models\OfficeTypeTree;
use App\Models\Operation;
use App\Models\User;
use App\Repositories\BiogMainRepository;
use Auth;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class OperationsController extends Controller {
/**
* 依 confirmation_token 解析呼叫者,並要求帳號為啟用狀態(is_active=1)。
* 找不到對應使用者、或帳號未啟用(含被停用者)一律回 null,寫入端據此拒絕,
* 避免未啟用帳號僅憑外洩/自查的 token 就能經此通道寫入 operations。
*/
private function resolveActiveUserByToken($token): ?User {
if (!is_string($token) || $token === '') {
return null;
}
$user = User::where('confirmation_token', $token)->first();
if (!$user || !$user->isActive()) {
return null;
}

return $user;
}

public function add(Request $request) {
//用來將json存入operations
$x = $this->add_operations($request);
Expand All @@ -36,13 +54,11 @@ public function del(Request $request) {
}

public function add_operations($keyword) {
$z = $keyword['token'];
$token = DB::table('users')->where('confirmation_token', $z)->get();
$token = json_decode($token, true);
$token = $token[0]['id'];
if (empty($token)) {
return '500';
$user = $this->resolveActiveUserByToken($keyword['token'] ?? null);
if (!$user) {
return response('403', 403);
}
$token = $user->id;
$x = $keyword['json'];
if (empty($x)) {
return '500';
Expand Down Expand Up @@ -72,13 +88,11 @@ public function add_operations($keyword) {
}

public function update_operations($keyword) {
$z = $keyword['token'];
$token = DB::table('users')->where('confirmation_token', $z)->get();
$token = json_decode($token, true);
$token = $token[0]['id'];
if (empty($token)) {
return '500';
$user = $this->resolveActiveUserByToken($keyword['token'] ?? null);
if (!$user) {
return response('403', 403);
}
$token = $user->id;
$x = $keyword['json'];
if (empty($x)) {
return '500';
Expand Down Expand Up @@ -136,13 +150,11 @@ public function update_operations($keyword) {
}

public function destroy_operations($keyword) {
$z = $keyword['token'];
$token = DB::table('users')->where('confirmation_token', $z)->get();
$token = json_decode($token, true);
$token = $token[0]['id'];
if (empty($token)) {
return '500';
$user = $this->resolveActiveUserByToken($keyword['token'] ?? null);
if (!$user) {
return response('403', 403);
}
$token = $user->id;
$y = $keyword['resource'];
if (empty($y)) {
return '500';
Expand Down
27 changes: 15 additions & 12 deletions app/Http/Controllers/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use App\Repositories\YearRangeRepository;
use App\Services\PinyinDictionary;
use App\Services\VariantCharNormalizer;
use App\Support\ExactCodeMatchGuard;
use App\Support\PinyinUmlaut;
use App\v1;
use Illuminate\Http\Request;
Expand Down Expand Up @@ -168,7 +169,7 @@ public function occasion() {

public function searchText(Request $request) {
//20190708依據需求修改輸出內容
$data = TextCode::where('c_title_chn', 'like', '%'.$request->q.'%')->orWhere('c_title', 'like', '%'.$request->q.'%')->orWhere('c_textid', $request->q)->paginate(20);
$data = TextCode::where('c_title_chn', 'like', '%'.$request->q.'%')->orWhere('c_title', 'like', '%'.$request->q.'%')->when(ExactCodeMatchGuard::isNumeric($request->q), fn ($q) => $q->orWhere('c_textid', $request->q))->paginate(20);
$data->appends(['q' => $request->q])->links();
foreach ($data as $item) {
$item['id'] = $item->c_textid;
Expand Down Expand Up @@ -241,7 +242,7 @@ public function searchOffice(Request $request) {
$baseQuery = OfficeCode::where(function ($q) use ($request) {
$q->where('c_office_chn', 'like', '%'.$request->q.'%')
->orWhere('c_office_pinyin', 'like', '%'.$request->q.'%')
->orWhere('c_office_id', $request->q);
->when(ExactCodeMatchGuard::isNumeric($request->q), fn ($q2) => $q2->orWhere('c_office_id', $request->q));
});

if ((int) $request->c_dy > 0) {
Expand Down Expand Up @@ -272,7 +273,7 @@ public function searchOffice(Request $request) {
}

public function socialinst(Request $request) {
$data = SocialInst::where('c_inst_name_hz', 'like', '%'.$request->q.'%')->orWhere('c_inst_name_py', 'like', '%'.$request->q.'%')->orWhere('c_inst_name_code', $request->q)->paginate(20);
$data = SocialInst::where('c_inst_name_hz', 'like', '%'.$request->q.'%')->orWhere('c_inst_name_py', 'like', '%'.$request->q.'%')->when(ExactCodeMatchGuard::isNumeric($request->q), fn ($q) => $q->orWhere('c_inst_name_code', $request->q))->paginate(20);
$data->appends(['q' => $request->q])->links();
foreach ($data as $item) {
$item['id'] = $item->c_inst_name_code;
Expand Down Expand Up @@ -422,7 +423,7 @@ public function socialinstcode(Request $request) {
}

public function searchEntry(Request $request) {
$data = EntryCode::where('c_entry_desc_chn', 'like', '%'.$request->q.'%')->orWhere('c_entry_desc', 'like', '%'.$request->q.'%')->orWhere('c_entry_code', $request->q)->paginate(20);
$data = EntryCode::where('c_entry_desc_chn', 'like', '%'.$request->q.'%')->orWhere('c_entry_desc', 'like', '%'.$request->q.'%')->when(ExactCodeMatchGuard::isNumeric($request->q), fn ($q) => $q->orWhere('c_entry_code', $request->q))->paginate(20);
$data->appends(['q' => $request->q])->links();
foreach ($data as $item) {
$item['id'] = $item->c_entry_code;
Expand All @@ -436,7 +437,7 @@ public function searchEntry(Request $request) {
}

public function searchKincode(Request $request) {
$data = KinshipCode::where('c_kinrel_chn', 'like', '%'.$request->q.'%')->orWhere('c_kinrel', 'like', '%'.$request->q.'%')->orWhere('c_kincode', $request->q)->paginate(20);
$data = KinshipCode::where('c_kinrel_chn', 'like', '%'.$request->q.'%')->orWhere('c_kinrel', 'like', '%'.$request->q.'%')->when(ExactCodeMatchGuard::isNumeric($request->q), fn ($q) => $q->orWhere('c_kincode', $request->q))->paginate(20);
$data->appends(['q' => $request->q])->links();
foreach ($data as $item) {
$item['id'] = $item->c_kincode;
Expand All @@ -450,7 +451,7 @@ public function searchKincode(Request $request) {
}

public function searchAssoccode(Request $request) {
$data = AssocCode::where('c_assoc_desc', 'like', '%'.$request->q.'%')->orWhere('c_assoc_desc_chn', 'like', '%'.$request->q.'%')->orWhere('c_assoc_code', $request->q)->paginate(20);
$data = AssocCode::where('c_assoc_desc', 'like', '%'.$request->q.'%')->orWhere('c_assoc_desc_chn', 'like', '%'.$request->q.'%')->when(ExactCodeMatchGuard::isNumeric($request->q), fn ($q) => $q->orWhere('c_assoc_code', $request->q))->paginate(20);
$data->appends(['q' => $request->q])->links();
foreach ($data as $item) {
$item['id'] = $item->c_assoc_code;
Expand All @@ -464,7 +465,7 @@ public function searchAssoccode(Request $request) {
}

public function searchStatuscode(Request $request) {
$data = StatusCode::where('c_status_desc', 'like', '%'.$request->q.'%')->orWhere('c_status_desc_chn', 'like', '%'.$request->q.'%')->orWhere('c_status_code', $request->q)->paginate(20);
$data = StatusCode::where('c_status_desc', 'like', '%'.$request->q.'%')->orWhere('c_status_desc_chn', 'like', '%'.$request->q.'%')->when(ExactCodeMatchGuard::isNumeric($request->q), fn ($q) => $q->orWhere('c_status_code', $request->q))->paginate(20);
$data->appends(['q' => $request->q])->links();
foreach ($data as $item) {
$item['id'] = $item->c_status_code;
Expand Down Expand Up @@ -522,10 +523,12 @@ public function searchBiog(Request $request) {

$data = $query->paginate($num);
} else {
// 回退方案:FTS 未找到結果時,使用原有的 LIKE 查詢(§D-8:c_name 以展開集 OR 同查 v/ü 形)
// 回退方案:FTS 未找到結果時,使用原有的 LIKE 查詢(§D-8:c_name 以展開集 OR 同查 v/ü 形)。
// 註:走到這個 else 分支時 $request->q 必為非純數字(純數字已在上方 ctype_digit 分支處理),
// 不再加 orWhere('c_personid', $request->q)——否則 MySQL/MariaDB 會把非數字字串寬鬆轉型成 0,
// 誤中 c_personid=0(「未詳」占位列)。
$data = BiogMain::where(function ($sub) use ($request, $qForms) {
$sub->where('c_name_chn', 'like', '%'.$request->q.'%')
->orWhere('c_personid', $request->q);
$sub->where('c_name_chn', 'like', '%'.$request->q.'%');
foreach ($qForms as $form) {
$sub->orWhere('c_name', 'like', '%'.$form.'%');
}
Expand All @@ -547,7 +550,7 @@ public function searchBiog(Request $request) {
}

public function searchEvent(Request $request) {
$data = EventCode::where('c_event_name_chn', 'like', '%'.$request->q.'%')->orWhere('c_event_name', 'like', '%'.$request->q.'%')->orWhere('c_event_code', $request->q)->paginate(20);
$data = EventCode::where('c_event_name_chn', 'like', '%'.$request->q.'%')->orWhere('c_event_name', 'like', '%'.$request->q.'%')->when(ExactCodeMatchGuard::isNumeric($request->q), fn ($q) => $q->orWhere('c_event_code', $request->q))->paginate(20);
$data->appends(['q' => $request->q])->links();
foreach ($data as $item) {
$item['id'] = $item->c_event_code;
Expand All @@ -562,7 +565,7 @@ public function searchEvent(Request $request) {

public function codeAddr(Request $request) {
$num = is_null($request->num) ? 20 : $request->num;
$data = AddressCode::where('c_name_chn', 'like', '%'.$request->q.'%')->orWhere('c_name', 'like', '%'.$request->q.'%')->orWhere('c_addr_id', $request->q)->paginate($num);
$data = AddressCode::where('c_name_chn', 'like', '%'.$request->q.'%')->orWhere('c_name', 'like', '%'.$request->q.'%')->when(ExactCodeMatchGuard::isNumeric($request->q), fn ($q) => $q->orWhere('c_addr_id', $request->q))->paginate($num);
$data->appends(['q' => $request->q])->links();

return $data;
Expand Down
Loading