Skip to content
Merged
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
4 changes: 4 additions & 0 deletions app/controllers/api/reports_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,9 @@ def index
@reports = @reports.where(user_id: params[:user_id]) if params[:user_id].present?
@reports = @reports.limit(params[:limit].to_i) if params[:limit].present?
@reports = @reports.joins(:user).where(users: { company_id: params[:company_id] }) if params[:company_id]
return unless params[:target] == 'unchecked_reports'
return head :forbidden unless current_user.admin_or_mentor?

@reports = @reports.includes(:checks).unchecked.not_wip
end
end
12 changes: 11 additions & 1 deletion app/controllers/users/reports_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

class Users::ReportsController < ApplicationController
before_action :set_user
before_action :set_target
before_action :require_admin_or_mentor_login, if: -> { params[:target] == 'unchecked_reports' }
before_action :set_reports
before_action :set_report
before_action :set_export
Expand Down Expand Up @@ -30,13 +32,21 @@ def set_user
end

def set_reports
@reports = user.reports.list.page(params[:page])
@reports = if @target == 'unchecked_reports'
@user.reports.unchecked.not_wip.list.page(params[:page])
else
@user.reports.list.page(params[:page])
end
end

def set_report
@report = @reports[0]
end

def set_target
@target = params[:target] || 'all_reports'
end

def user
@user ||= User.find(params[:user_id])
end
Expand Down
3 changes: 2 additions & 1 deletion app/helpers/page_tabs/users_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ module PageTabs
module UsersHelper
def user_page_tabs(user, active_tab:)
comment_count = user.comments.without_private_comment.length
unchecked_report_badge = current_user.admin_or_mentor? ? Cache.user_unchecked_report_count(user) : nil
tabs = []
tabs << { name: 'プロフィール', link: user_path(user) }
tabs << { name: 'ポートフォリオ', link: user_portfolio_path(user) }
tabs << { name: '日報', link: user_reports_path(user), count: user.reports.length }
tabs << { name: '日報', link: user_reports_path(user), count: user.reports.length, badge: unchecked_report_badge }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

管理者・メンターユーザでメンターモードOFF時に、未チェック数バッチが表示されているようです。
管理者・メンターユーザでメンターモードOFF時は、未チェック数バッチが表示されない仕様だったと認識していますが、認識合っていますでしょうか?
ご確認いただけると幸いです。😌
(最初のレビューの際に気づけなくて失礼いたしました。)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

提出物の部分もメンタモードオフでバッヂが表示になっていたので、同様の対応をしました💦
勝手な推測になってしまうのですが、メンターモードオフだと確認が遅れてしまうということがあるのかな?と思いました。しかし、チームリーダーに確認すべきだと思ったので、komagataさんに確認してみます。

image

@komagata さん
お疲れ様です。
こちらの件ですが、提出物のタブではメンターモードオフの場合、バッヂが表示されていますが、未確認の日報数の場合は非表示にすべきでしょうか?
お手隙でご確認よろしくお願いいたします。

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hirokiej

勝手な推測になってしまうのですが、メンターモードオフだと確認が遅れてしまうということがあるのかな?と思いました。

こちらどういった意味でしょうか?

質問に回答するのにこの情報が必要かもなので先に聞いておきたいと思いました。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@komagata さん

質問に回答するのにこの情報が必要かもなので先に聞いておきたいと思いました。

ユーザーとしては提出物や日報がチェックされていると安心感につながる部分だと感じています。
そのため、未チェックの日報数がadmin/mentorにはデフォルトで表示されている方が、未チェックに気づきやすく対応しやすいのではないか、という推測でした。

提出物のタブではメンターモードオフでも表示されているのに、日報では非表示にするのは統一感もないと感じ、提出物のタブに合わせる形で実装しています。
しかし、どちらが適切か判断がつかなかったため、改めて確認させていただきました🙇

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hirokiej 返信ありがとうございます。

すみません、ちょっと僕の聞き方がわかりづらかったです。
僕がわからなかった部分は、

メンターモードオフだと確認が遅れてしまうということがあるのかな?

という部分です。「確認が遅れてしまう」というのはどういう意味でしょうか。
(「誰が」、「何を」、「なぜ」、遅れてしまうと考えたのでしょうか)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@komagata さん

ご確認ありがとうございます。

「確認が遅れてしまう」というのはどういう意味でしょうか。
(「誰が」、「何を」、「なぜ」、遅れてしまうと考えたのでしょうか)

「メンターモードをオフにしているメンター」が「未チェックの日報の確認」を「メンターモードをオフにしている結果、バッヂが非表示になり認識しづらくなる」ため確認が遅れやすくなってしまうかも、という仮説として示したつもりでした。

こちらこそ言葉足らずで申し訳ございませんでした🙇💦

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hirokiej なるほどですね。説明ありがとうございます。

こちらの件ですが、提出物のタブではメンターモードオフの場合、バッヂが表示されていますが、未確認の日報数の場合は非表示にすべきでしょうか?

メンターモードがオフでも未確認の日報数は非表示にしなくてOKです。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@komagata さん
ご回答ありがとうございます🙇

@sjabcdefin さん

メンターモードがオフでも未確認の日報数は非表示にしなくてOKです。

確認したところ、表示した状態で大丈夫とのことでした🙇

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hirokiej
ご確認ありがとうございます。😌🙏
私の方での確認は以上ですので、approveさせていただきます。🙆‍♀️

ご対応ありがとうございました!🎉

Copy link
Copy Markdown
Contributor Author

@hirokiej hirokiej Aug 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sjabcdefin さん 
ご確認、ご対応ありがとうございました🙇!

tabs << { name: 'コメント', link: user_comments_path(user), count: comment_count }
tabs << { name: '提出物', link: user_products_path(user), count: user.products.length }
tabs << { name: '質問', link: user_questions_path(user), count: user.questions.length }
Expand Down
29 changes: 8 additions & 21 deletions app/javascript/components/Reports.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'
import React, { useState } from 'react'
import useSWR from 'swr'
import fetcher from '../fetcher'
import LoadingListPlaceholder from './LoadingListPlaceholder'
Expand All @@ -9,7 +9,6 @@ import UnconfirmedLink from './UnconfirmedLink'
import usePage from './hooks/usePage'

export default function Reports({
all = false,
userId = '',
practices = false,
unchecked = false,
Expand All @@ -22,26 +21,14 @@ export default function Reports({
const { page, setPage } = usePage()
const [userPracticeId, setUserPracticeId] = useState('')

useEffect(() => {
setUserPracticeId(userPracticeId)
}, [userPracticeId])
let reportsUrl = `/api/reports.json?page=${page}`
if (userId) reportsUrl += `&user_id=${userId}`
if (companyId) reportsUrl += `&company_id=${companyId}`
const pid = userPracticeId || practiceId
if (pid) reportsUrl += `&practice_id=${pid}`
if (unchecked) reportsUrl += `&target=unchecked_reports`

const { data, error } = useSWR(
practices
? `/api/reports.json?user_id=${userId}&page=${page}&practice_id=${userPracticeId}`
: unchecked
? `/api/reports/unchecked.json?page=${page}&user_id=${userId}`
: userId !== ''
? `/api/reports.json?page=${page}&user_id=${userId}`
: practiceId !== ''
? `/api/reports.json?page=${page}&practice_id=${practiceId}`
: companyId !== ''
? `/api/reports.json?page=${page}&company_id=${companyId}`
: all === true
? `/api/reports.json?page=${page}&practice_id=${userPracticeId}`
: console.log('data_fetched!'),
fetcher
)
const { data, error } = useSWR(reportsUrl, fetcher)

if (error) return <>エラーが発生しました。</>
if (!data) {
Expand Down
2 changes: 1 addition & 1 deletion app/views/reports/_report_body.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
= report.user.unchecked_report_message(count, report.user)
ul.card-body-main-actions__items
li.card-body-main-actions__item
= link_to user_reports_path(report.user), class: "card-body-main-actions__action a-button is-sm #{report.user.user_report_count_class(count)} is-block", target: '_blank', rel: 'noopener' do
= link_to user_reports_path(report.user, target: 'unchecked_reports'), class: "card-body-main-actions__action a-button is-sm #{report.user.user_report_count_class(count)} is-block", target: '_blank', rel: 'noopener' do
| #{report.user.login_name}さんの日報一覧へ
- else
.card-body-main-actions.p-0
Expand Down
11 changes: 10 additions & 1 deletion app/views/users/reports/index.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,13 @@
= render 'users/page_title', user: @user
= user_page_tabs(@user, active_tab: '日報')

= react_component('Reports', userId: @user.id, practices: @current_user_practice.sorted_practices)
- if admin_or_mentor_login?
nav.tab-nav(class="#{mentor_login? ? 'is-only-mentor' : ''}")
.container
ul.tab-nav__items
- targets = %w[all_reports unchecked_reports]
- targets.each do |target|
li.tab-nav__item
= link_to t("target.#{target}"), user_reports_path(@user, target:), class: (@target == target ? ['is-active'] : []) << 'tab-nav__item-link'

= react_component('Reports', userId: @user.id, practices: @current_user_practice.sorted_practices, unchecked: (@target == 'unchecked_reports'))
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Aug 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

未チェック一覧のアクセス制御(メンター限定)を確認してください

URL 直叩きで一般ユーザーが target=unchecked_reports を指定した場合の挙動(無視 or 403 など)が API/コントローラ側で適切にガードされているか確認をお願いします。メンター UI は非表示でも、パラメータで閲覧できると情報露出になります。

以下スクリプトで関連箇所の有無とガード条件を確認できます(リポジトリルートで実行)。


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== unchecked_reports を扱うコントローラ周辺の定義確認 =='
rg -n -C5 -g 'app/controllers/**/*.rb' -P '(unchecked_reports|target\s*=>?\s*[\'"]unchecked_reports[\'"]|params\[:target\]\s*==\s*[\'"]unchecked_reports[\'"])'

echo
echo '== メンター/管理者ガードらしきメソッドの出現箇所確認 =='
rg -n -C3 -g 'app/controllers/**/*.rb' -P '(admin_or_mentor\?|mentor_login\?|mentor_mode)'

echo
echo '== users/reports_controller.rb と api/reports_controller.rb の set_target / フィルタ分岐の確認 =='
fd -t f reports_controller.rb app/controllers | xargs -I{} sh -c 'echo "--- {} ---"; sed -n "1,200p" {} | nl -ba | sed -n "1,200p" | rg -n -C3 -P "(before_action|set_target|unchecked|params\\[:target\\])" || true'

Length of output: 220


シェルスクリプト内の正規表現のクオート部分でパースエラーが発生しているようです。以下の修正版をリポジトリルートで再度実行し、結果をご共有ください。


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== unchecked_reports を扱うコントローラ周辺の定義確認 =='
rg -n -C5 -g 'app/controllers/**/*.rb' -P "unchecked_reports|target\\s*=>?\\s*['\"]unchecked_reports['\"]|params\\[:target\\]\\s*==\\s*['\"]unchecked_reports['\"]"

echo
echo '== メンター/管理者ガードらしきメソッドの出現箇所確認 =='
rg -n -C3 -g 'app/controllers/**/*.rb' -P "admin_or_mentor\\?|mentor_login\\?|mentor_mode"

echo
echo '== users/reports_controller.rb と api/reports_controller.rb の set_target / フィルタ分岐の確認 =='
fd -t f reports_controller.rb app/controllers | while read file; do
  echo "--- $file ---"
  rg -n -C3 -e "before_action" -e "set_target" -e "params\\[:target\\]" -e "unchecked" "$file" || true
done

Length of output: 14405


以下のスクリプトで API 側と UI 側それぞれのベースコントローラにメンター制御(before_action 等)が仕込まれているか確認してください。


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== app/controllers/api/base_controller.rb (1-200行) =='
sed -n '1,200p' app/controllers/api/base_controller.rb | nl -ba

echo
echo '== API::BaseController の before_action 一覧 =='
rg -n --color=never 'before_action' -g 'app/controllers/api/base_controller.rb' || echo 'no matches'

echo
echo '== app/controllers/application_controller.rb (1-200行) =='
sed -n '1,200p' app/controllers/application_controller.rb | nl -ba

echo
echo '== ApplicationController の before_action 一覧 =='
rg -n --color=never 'before_action' -g 'app/controllers/application_controller.rb' || echo 'no matches'

Length of output: 293


以下のスクリプトで API 側と UI 側それぞれのベースコントローラにメンター権限制御が設定されているか確認し、結果を共有してください。


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== API::BaseController (app/controllers/api/base_controller.rb lines 1-200) =='
if [[ -f app/controllers/api/base_controller.rb ]]; then
  sed -n '1,200p' app/controllers/api/base_controller.rb
  echo
  echo '== before_action in API::BaseController =='
  grep -n 'before_action' app/controllers/api/base_controller.rb || echo '== no before_action =='
  echo
  echo '== require_mentor_login_for_api in API::BaseController =='
  grep -n 'require_mentor_login_for_api' app/controllers/api/base_controller.rb || echo '== no require_mentor_login_for_api =='
  echo
  echo '== require_admin_or_mentor_login in API::BaseController =='
  grep -n 'require_admin_or_mentor_login' app/controllers/api/base_controller.rb || echo '== no require_admin_or_mentor_login =='
else
  echo 'app/controllers/api/base_controller.rb not found'
fi

echo
echo '== ApplicationController (app/controllers/application_controller.rb lines 1-200) =='
if [[ -f app/controllers/application_controller.rb ]]; then
  sed -n '1,200p' app/controllers/application_controller.rb
  echo
  echo '== before_action in ApplicationController =='
  grep -n 'before_action' app/controllers/application_controller.rb || echo '== no before_action =='
  echo
  echo '== require_mentor_login in ApplicationController =='
  grep -n 'require_mentor_login' app/controllers/application_controller.rb || echo '== no require_mentor_login =='
  echo
  echo '== require_admin_or_mentor_login in ApplicationController =='
  grep -n 'require_admin_or_mentor_login' app/controllers/application_controller.rb || echo '== no require_admin_or_mentor_login =='
else
  echo 'app/controllers/application_controller.rb not found'
fi

Length of output: 4874


未チェック一覧へのアクセス制御を強化してください
現状、Users::ReportsController(UI側)と API::ReportsController(API側)いずれも target=unchecked_reports を指定すると誰でも未チェック一覧を取得できてしまいます。メンター以外のアクセスを禁止するため、以下の修正をお願いします。

  • app/controllers/users/reports_controller.rb

    • クラス定義直後に
      before_action :require_admin_or_mentor_login
      を追加し、Web UI 側の /users/:user_id/reports アクションをメンター限定にする
  • app/controllers/api/reports_controller.rb

    • コントローラ先頭に
      before_action :require_mentor_login_for_api, only: :index
      を追加し、API 経由で target=unchecked_reports を扱う際には JWT 認証+メンター権限を必須化する
    • もしくは index 内の
      if params[:target] == 'unchecked_reports'
      の前後で head :forbidden unless current_user.admin_or_mentor? のような明示的ガードを入れる

これにより、パラメータ直接指定による情報漏洩を防止できます。

🤖 Prompt for AI Agents
In app/views/users/reports/index.html.slim around line 16: the view passes
target=unchecked_reports which currently allows anyone to fetch unchecked
reports; to enforce mentor-only access add controller guards: in
app/controllers/users/reports_controller.rb (class start) add before_action
:require_admin_or_mentor_login to restrict the web UI /users/:user_id/reports to
mentors/admins; in app/controllers/api/reports_controller.rb (top of controller)
add before_action :require_mentor_login_for_api, only: :index to require
JWT+mentor for API index, or alternatively inside index add an explicit guard
(e.g. head :forbidden unless current_user&.admin_or_mentor?) immediately before
handling if params[:target] == 'unchecked_reports' so parameter-based access to
unchecked reports is forbidden for non-mentors.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

それぞれ、head :forbiddenで対応。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hirokiej head :forbidden での対応で問題ありません。これにより一般ユーザーが target=unchecked_reports パラメータを直接指定してもアクセスが拒否され、メンター限定機能として適切に保護されます。

2 changes: 2 additions & 0 deletions config/locales/ja.yml
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,8 @@ ja:
rails_course: Rails
front_end_course: フロントエンド
other_courses: その他
all_reports: 全て
unchecked_reports: 未チェック
invitation_role:
adviser: アドバイザー
trainee: 研修生(%{payment_method})
Expand Down