Merge pull request #387 from specialpointcentral/attendance

Add configurable attendance CAPTCHA
This commit is contained in:
xiaomlove
2025-10-14 15:04:51 +07:00
committed by GitHub
10 changed files with 165 additions and 73 deletions
+1
View File
@@ -99,6 +99,7 @@ FORCE_SCHEME=
# Captcha settings
# Available drivers: image, cloudflare_turnstile, google_recaptcha_v2
CAPTCHA_DRIVER=image
CAPTCHA_ATTENDANCE_ENABLED=true
# Cloudflare Turnstile keys (used when CAPTCHA_DRIVER=cloudflare_turnstile)
TURNSTILE_SITE_KEY=
@@ -13,6 +13,7 @@ use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\CheckboxList;
use Filament\Schemas\Components\Fieldset;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Section;
use App\Auth\Permission;
@@ -30,6 +31,7 @@ use Filament\Facades\Filament;
use Filament\Resources\Pages\Page;
use Filament\Forms;
use Illuminate\Support\HtmlString;
use Illuminate\Support\Arr;
use Meilisearch\Contracts\Index\Settings;
use Nexus\Database\NexusDB;
@@ -64,6 +66,16 @@ class EditSetting extends Page implements HasForms
private function fillForm()
{
$settings = Setting::getFromDb();
$fallbackEnabled = (bool) config('captcha.attendance.enabled', true);
$rawSetting = Arr::get($settings, 'captcha.attendance.enabled', $fallbackEnabled);
if (is_string($rawSetting)) {
$normalized = in_array(strtolower($rawSetting), ['1', 'true', 'yes'], true);
} else {
$normalized = (bool) $rawSetting;
}
Arr::set($settings, 'captcha.attendance.enabled', $normalized);
$this->form->fill($settings);
}
@@ -298,7 +310,20 @@ class EditSetting extends Page implements HasForms
Setting::get('captcha.recaptcha.size', nexus_env('RECAPTCHA_SIZE', 'normal'))
);
$attendanceCaptchaSetting = Setting::get('captcha.attendance.enabled', true);
if (is_string($attendanceCaptchaSetting)) {
$attendanceCaptchaEnabled = in_array(strtolower($attendanceCaptchaSetting), ['1', 'true', 'yes'], true);
} else {
$attendanceCaptchaEnabled = (bool) $attendanceCaptchaSetting;
}
$schema = [
Toggle::make("$captchaPrefix.attendance.enabled")
->label(__('label.setting.captcha.attendance.enabled'))
->helperText(__('label.setting.captcha.attendance.enabled_help'))
->default($attendanceCaptchaEnabled)
->columnSpanFull()
,
Select::make("$captchaPrefix.default")
->options($driverOptions)
->label(__('label.setting.captcha.driver'))
+4
View File
@@ -24,4 +24,8 @@ return [
'size' => nexus_env('RECAPTCHA_SIZE', 'normal'),
],
],
'attendance' => [
'enabled' => nexus_env('CAPTCHA_ATTENDANCE_ENABLED', true),
],
];
+1
View File
@@ -13,4 +13,5 @@ $lang_attendance = array
'retroactive_event_text' => '补',
'retroactive_confirm_tip' => '确定要补签: ',
'retroactive_description' => '点击白色背景的圆点进行补签。你目前拥有补签卡 <b>%d</b> 张。',
'attend_button' => '立即签到',
);
+1
View File
@@ -13,4 +13,5 @@ $lang_attendance = array
'retroactive_event_text' => '補',
'retroactive_confirm_tip' => '確定要補簽: ',
'retroactive_description' => '點擊白色背景的圓點進行補簽。你目前擁有補簽卡 <b>%d</b> 張。',
'attend_button' => '立即簽到',
);
+1
View File
@@ -13,4 +13,5 @@ $lang_attendance = array
'retroactive_event_text' => 'Re',
'retroactive_confirm_tip' => 'Confirm to attend: ',
'retroactive_description' => 'Click on the dot on the white background to do attend. You currently have a attendance card <b>%d</b>.',
'attend_button' => 'Check in now',
);
+120 -73
View File
@@ -1,32 +1,10 @@
<?php
require '../include/bittorrent.php';
dbconn();
require get_langfile_path();
loggedinorreturn();
parked();
//$desk = new Attendance($CURUSER['id']);
//
//if($result = $desk->attend($attendance_initial_bonus, $attendance_step_bonus, $attendance_max_bonus, $attendance_continuous_bonus)){
// list($count, $cdays, $points) = $result;
// stdhead($lang_attendance['title']);
// begin_main_frame();
// begin_frame($lang_attendance['success']);
// printf('<p>'.$lang_attendance['attend_info'].'</p>', $count, $cdays, $points);
// end_frame();
// echo '<ul>';
// printf('<li>'.$lang_attendance['initial'].'</li>', $attendance_initial_bonus);
// printf('<li>'.$lang_attendance['steps'].'</li>', $attendance_step_bonus, $attendance_max_bonus);
// echo '<li><ol>';
// foreach($attendance_continuous_bonus as $day => $value){
// printf('<li>'.$lang_attendance['continuous'].'</li>', $day, $value);
// }
// echo '</ol></li>';
// echo '</ul>';
// end_main_frame();
// stdfoot();
//}else{
// stderr($lang_attendance['sorry'], $lang_attendance['already_attended']);
//}
\Nexus\Nexus::css('vendor/fullcalendar-5.10.2/main.min.css', 'header', true);
\Nexus\Nexus::js('vendor/fullcalendar-5.10.2/main.min.js', 'footer', true);
@@ -44,38 +22,106 @@ $today = \Carbon\Carbon::today();
$tomorrow = \Carbon\Carbon::tomorrow();
$end = $today->clone()->endOfMonth();
$start = $today->clone()->subMonth(2);
$rep = new \App\Repositories\AttendanceRepository();
$attendance = $rep->attend($CURUSER['id']);
$logs = $attendance->logs()->where('date', '>=', $start->format('Y-m-d'))->get()->keyBy('date');
$interval = new \DateInterval('P1D');
$period = new \DatePeriod($start, $interval, $end);
$interval = \Carbon\CarbonInterval::make($interval);
$period = \Carbon\CarbonPeriod::make($period);
$events = [];
foreach ($period as $value) {
if ($value->gte($tomorrow)) {
continue;
}
$checkDate = $value->format('Y-m-d');
$eventBase = ['start' => $checkDate, 'end' => $checkDate];
if ($logs->has($checkDate)) {
$logValue = $logs->get($checkDate);
$events[] = array_merge($eventBase, ['display' => 'background']);
if ($logValue->points > 0) {
$events[] = array_merge($eventBase, ['title' => $logValue->points]);
}
if ($logValue->is_retroactive) {
$events[] = array_merge($eventBase, ['title' => $lang_attendance['retroactive_event_text'], 'display' => 'list-item']);
}
} elseif ($value->lte($today) && $value->diffInDays($today, true) <= \App\Models\Attendance::MAX_RETROACTIVE_DAYS) {
$events[] = array_merge($eventBase, ['groupId' => 'to_do', 'display' => 'list-item']);
}
$attendanceRepository = new \App\Repositories\AttendanceRepository();
$attendanceCaptchaSetting = \App\Models\Setting::get('captcha.attendance.enabled', config('captcha.attendance.enabled', true));
if (is_string($attendanceCaptchaSetting)) {
$attendanceCaptchaEnabled = in_array(strtolower($attendanceCaptchaSetting), ['1', 'true', 'yes'], true);
} else {
$attendanceCaptchaEnabled = (bool) $attendanceCaptchaSetting;
}
$eventStr = json_encode($events);
$validRangeStr = json_encode(['start' => $start->format('Y-m-d'), 'end' => $end->clone()->addDays(1)->format('Y-m-d')]);
$js = <<<EOP
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($attendanceCaptchaEnabled && $iv == 'yes') {
check_code($_POST['imagehash'] ?? null, $_POST['imagestring'] ?? null, 'attendance.php');
}
$attendance = $attendanceRepository->attend($CURUSER['id']);
if (!$attendance->is_updated) {
stderr($lang_attendance['sorry'], $lang_attendance['already_attended']);
}
} else {
$attendance = $attendanceRepository->getAttendance($CURUSER['id']);
}
$hasAttendedToday = $attendance && $attendance->added && $attendance->added->isSameDay($today);
if (!$attendanceCaptchaEnabled && !$hasAttendedToday) {
$attendance = $attendanceRepository->attend($CURUSER['id']);
$hasAttendedToday = $attendance && $attendance->added && $attendance->added->isSameDay($today);
}
if (!$attendance) {
$attendance = new \App\Models\Attendance([
'uid' => $CURUSER['id'],
'points' => 0,
'days' => 0,
'total_days' => 0,
]);
$attendance->added = null;
$hasAttendedToday = false;
}
stdhead($lang_attendance['title']);
begin_main_frame();
if ($hasAttendedToday) {
$todayDate = $today->format('Y-m-d');
$baseQuery = \App\Models\AttendanceLog::query()->where('date', $todayDate);
$todayCounts = $baseQuery->count();
$myLog = (clone $baseQuery)->where('uid', $CURUSER['id'])->first(['id']);
$myRanking = 0;
if ($myLog) {
$myRanking = (clone $baseQuery)->where('id', '<=', $myLog->id)->count();
}
$count = $attendance->total_days;
$cdays = $attendance->days;
$points = $attendance->points;
$headerLeft = sprintf($lang_attendance['attend_info'] . $lang_attendance['retroactive_description'], $count, $cdays, $points, $CURUSER['attendance_card']);
$headerRight = nexus_trans('attendance.ranking', ['ranking' => $myRanking, 'counts' => $todayCounts]);
begin_frame($lang_attendance['success']);
printf('<p>%s<span style="float:right">%s</span></p>', $headerLeft, $headerRight);
end_frame();
$logs = \App\Models\AttendanceLog::query()
->where('uid', $CURUSER['id'])
->where('date', '>=', $start->format('Y-m-d'))
->get()
->keyBy('date');
$interval = new \DateInterval('P1D');
$period = new \DatePeriod($start, $interval, $end);
$interval = \Carbon\CarbonInterval::make($interval);
$period = \Carbon\CarbonPeriod::make($period);
$events = [];
foreach ($period as $value) {
if ($value->gte($tomorrow)) {
continue;
}
$checkDate = $value->format('Y-m-d');
$eventBase = ['start' => $checkDate, 'end' => $checkDate];
if ($logs->has($checkDate)) {
$logValue = $logs->get($checkDate);
$events[] = array_merge($eventBase, ['display' => 'background']);
if ($logValue->points > 0) {
$events[] = array_merge($eventBase, ['title' => $logValue->points]);
}
if ($logValue->is_retroactive) {
$events[] = array_merge($eventBase, ['title' => $lang_attendance['retroactive_event_text'], 'display' => 'list-item']);
}
} elseif ($value->lte($today) && $value->diffInDays($today, true) <= \App\Models\Attendance::MAX_RETROACTIVE_DAYS) {
$events[] = array_merge($eventBase, ['groupId' => 'to_do', 'display' => 'list-item']);
}
}
$eventStr = json_encode($events);
$validRangeStr = json_encode(['start' => $start->format('Y-m-d'), 'end' => $end->clone()->addDays(1)->format('Y-m-d')]);
$calendarScript = <<<EOP
let events = JSON.parse('$eventStr')
let validRange = JSON.parse('$validRangeStr')
let confirmText = "{$lang_attendance['retroactive_confirm_tip']}"
@@ -87,7 +133,6 @@ document.addEventListener('DOMContentLoaded', function() {
events: events,
validRange: validRange,
eventClick: function(info) {
console.log(info.event);
if (info.event.groupId == 'to_do') {
retroactive(info.event.startStr)
}
@@ -98,11 +143,9 @@ document.addEventListener('DOMContentLoaded', function() {
function retroactive(dateStr) {
if (!window.confirm(confirmText + dateStr + ' ?')) {
console.log("cancel")
return
}
jQuery.post('ajax.php', {params: {date: dateStr}, action: 'attendanceRetroactive'}, function (response) {
console.log(response);
if (response.ret != 0) {
alert(response.msg)
} else {
@@ -112,33 +155,37 @@ function retroactive(dateStr) {
}
EOP;
\Nexus\Nexus::js($js, 'footer', false);
\Nexus\Nexus::js($calendarScript, 'footer', false);
if (1) {
$count = $attendance->total_days;
$cdays = $attendance->days;
$points = $attendance->points;
stdhead($lang_attendance['title']);
begin_main_frame();
begin_frame($lang_attendance['success']);
$headerLeft = sprintf($lang_attendance['attend_info'].$lang_attendance['retroactive_description'], $count, $cdays, $points, $CURUSER['attendance_card']);
$headerRight = nexus_trans('attendance.ranking', ['ranking' => $attendance->my_ranking, 'counts' => $attendance->today_counts]);
printf('<p>%s<span style="float:right">%s</span></p>', $headerLeft, $headerRight);
end_frame();
echo '<div style="display: flex;justify-content: center;padding: 20px 0"><div id="calendar" style="width: 60%"></div></div>';
echo '<ul>';
printf('<li>'.$lang_attendance['initial'].'</li>', $attendance_initial_bonus);
printf('<li>'.$lang_attendance['steps'].'</li>', $attendance_step_bonus, $attendance_max_bonus);
echo '<li><ol>';
foreach($attendance_continuous_bonus as $day => $value){
foreach ($attendance_continuous_bonus as $day => $value) {
printf('<li>'.$lang_attendance['continuous'].'</li>', $day, $value);
}
echo '</ol></li>';
echo '</ul>';
end_main_frame();
stdfoot();
} else {
stderr($lang_attendance['sorry'], $lang_attendance['already_attended']);
$buttonLabel = $lang_attendance['attend_button'] ?? 'Check in';
begin_frame($lang_attendance['title']);
echo '<table width="100%" border="1" cellspacing="0" cellpadding="10"><tbody>';
echo '<tr><td class="text">';
echo '<div style="margin-top: 20px; text-align: center;">';
echo '<form method="post" action="attendance.php" style="display: inline-block;">';
echo '<table border="0" cellpadding="5">';
if ($attendanceCaptchaEnabled && $iv == 'yes') {
show_image_code();
}
echo '<tr><td class="toolbox" colspan="2" align="center"><input type="submit" value="' . htmlspecialchars($buttonLabel, ENT_QUOTES, 'UTF-8') . '" class="btn" /></td></tr>';
echo '</table>';
echo '</form>';
echo '</div>';
echo '</td></tr>';
echo '</tbody></table>';
end_frame();
}
end_main_frame();
stdfoot();
+4
View File
@@ -144,6 +144,10 @@ return [
'size_normal' => 'Normal',
'size_compact' => 'Compact',
],
'attendance' => [
'enabled' => 'Require captcha for attendance check-in',
'enabled_help' => 'When enabled, members must solve the captcha before signing in.',
],
],
'meilisearch' => [
'tab_header' => 'Meilisearch',
+4
View File
@@ -185,6 +185,10 @@ return [
'size_normal' => '普通',
'size_compact' => '紧凑',
],
'attendance' => [
'enabled' => '启用签到验证码',
'enabled_help' => '开启后,用户每天签到前必须完成验证码验证。',
],
],
'meilisearch' => [
'tab_header' => 'Meilisearch',
+4
View File
@@ -144,6 +144,10 @@ return [
'size_normal' => '一般',
'size_compact' => '緊湊',
],
'attendance' => [
'enabled' => '啟用簽到驗證碼',
'enabled_help' => '啟用後,使用者簽到前必須完成驗證碼驗證。',
],
],
'meilisearch' => [
'tab_header' => 'Meilisearch',