diff --git a/.env.dev b/.env.dev new file mode 100644 index 00000000..cc0a4cb8 --- /dev/null +++ b/.env.dev @@ -0,0 +1,131 @@ +APP_NAME=Alrahma_API +APP_ENV=local +APP_KEY=base64:RfIZKqgXC9seghl2Jqs2MgLh1X1Z7APRsnhUK8CgWx8= +APP_DEBUG=true +APP_TIMEZONE=America/New_York +APP_URL=http://localhost:8080 + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file + +LOG_CHANNEL=stack +LOG_LEVEL=debug +LOG_DEPRECATIONS_CHANNEL=null + +# ---------------------------- +# DATABASE +# ---------------------------- +DB_CONNECTION=mysql +DB_HOST=mysql_db +DB_PORT=3306 +DB_DATABASE=school_api +DB_USERNAME=root +DB_PASSWORD=root + +# ---------------------------- +# SESSION +# ---------------------------- +SESSION_DRIVER=file +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +# ---------------------------- +# CACHE & QUEUE +# ---------------------------- +CACHE_STORE=file +QUEUE_CONNECTION=sync + +# ---------------------------- +# REDIS +# ---------------------------- +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +# ---------------------------- +# MAIL +# ---------------------------- +MAIL_MAILER=log +MAIL_PROFILE_DEFAULT=default +MAIL_DEFAULT_HOST=smtp.gmail.com +MAIL_DEFAULT_PORT=587 +MAIL_DEFAULT_USER=alrahma.sunday.school@gmail.com +MAIL_DEFAULT_PASS="psnp emdq dykw ypul" +MAIL_DEFAULT_ENCRYPTION=tls +MAIL_DEFAULT_FROM_EMAIL="alrahma.sunday.school@gmail.com" +MAIL_DEFAULT_FROM_NAME="Alrahma API" +MAIL_DEFAULT_REPLY_TO="alrahma.isgl@gmail.com" +MAIL_DEFAULT_REPLY_TO_NAME="Al Rahma Sunday School" +MAIL_COMMUNICATION_REPLY_TO="alrahma.isgl@gmail.com" +MAIL_COMMUNICATION_REPLY_TO_NAME="School Communications" +MAIL_PAYMENT_REPLY_TO="alrahma.isgl@gmail.com" +MAIL_PAYMENT_REPLY_TO_NAME="School Payments" +MAIL_SENDERS='{"general":{"name":"Alrahma API","email":"alrahma.sunday.school@gmail.com"},"communication":{"name":"School Communications","email":"alrahma.sunday.school@gmail.com"},"payment":{"name":"School Payments","email":"alrahma.sunday.school@gmail.com"}}' + +# Laravel mailer compatibility +MAIL_HOST="${MAIL_DEFAULT_HOST}" +MAIL_PORT="${MAIL_DEFAULT_PORT}" +MAIL_USERNAME="${MAIL_DEFAULT_USER}" +MAIL_PASSWORD="${MAIL_DEFAULT_PASS}" +MAIL_ENCRYPTION="${MAIL_DEFAULT_ENCRYPTION}" +MAIL_FROM_ADDRESS="${MAIL_DEFAULT_FROM_EMAIL}" +MAIL_FROM_NAME="${MAIL_DEFAULT_FROM_NAME}" + +# Legacy fallback keys still read by some services +SMTP_HOST="${MAIL_DEFAULT_HOST}" +SMTP_USER="${MAIL_DEFAULT_USER}" +SMTP_PASS="${MAIL_DEFAULT_PASS}" +SMTP_PORT="${MAIL_DEFAULT_PORT}" +SMTP_ENCRYPTION="${MAIL_DEFAULT_ENCRYPTION}" + +# ---------------------------- +# FILESYSTEM +# ---------------------------- +FILESYSTEM_DISK=local + +# ---------------------------- +# AWS STORAGE +# ---------------------------- +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +# ---------------------------- +# JWT +# ---------------------------- +JWT_SECRET=Uj8rGnYcXMgeRc5qCIn9Wn03tYo1pCsBz1Biou8T9zWtaNxBYi3P4NP5vuFiXHmd +JWT_ALGO=HS256 +JWT_TTL=60 +JWT_REFRESH_TTL=20160 +JWT_BLACKLIST_ENABLED=true +JWT_BLACKLIST_GRACE_PERIOD=0 + +# ---------------------------- +# DOCS +# ---------------------------- +DOCS_CLIENT_URL=http://localhost:3000 +DOCS_INDEX_TITLE=Alrahma API Documentation +DOCS_INDEX_DESCRIPTION= + +# ---------------------------- +# BADGE +# ---------------------------- +BADGE_SCHOOL_NAME=Al Rahma Sunday School +BADGE_HEADER_SUBTITLE= +BADGE_MOTTO= +BADGE_MOTTO_FALLBACK= +BADGE_ROLE_LABEL=Student +BADGE_FOOTER_ADDRESS=5 Courthouse Lane, Chelmsford, MA 01824, USA + +# ---------------------------- +# FRONT-END +# ---------------------------- +VITE_APP_NAME="${APP_NAME}" diff --git a/.env.prod.example b/.env.prod.example new file mode 100644 index 00000000..69a42eee --- /dev/null +++ b/.env.prod.example @@ -0,0 +1,133 @@ +APP_NAME=Alrahma_API +APP_ENV=production +APP_KEY=base64:CHANGE_ME_RUN_php_artisan_key_generate +APP_DEBUG=false +APP_TIMEZONE=America/New_York +APP_URL=https://api.alrahmaisgl.org + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file + +LOG_CHANNEL=stderr +LOG_LEVEL=info +LOG_DEPRECATIONS_CHANNEL=null + +# ---------------------------- +# DATABASE +# ---------------------------- +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=CHANGE_ME +DB_USERNAME=CHANGE_ME +DB_PASSWORD=CHANGE_ME + +# ---------------------------- +# SESSION +# ---------------------------- +SESSION_DRIVER=cookie +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null +SESSION_SECURE_COOKIE=true +SESSION_SAME_SITE=strict + +# ---------------------------- +# CACHE & QUEUE +# ---------------------------- +CACHE_STORE=file +QUEUE_CONNECTION=sync + +# ---------------------------- +# REDIS +# ---------------------------- +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +# ---------------------------- +# MAIL +# ---------------------------- +MAIL_MAILER=smtp +MAIL_PROFILE_DEFAULT=default +MAIL_DEFAULT_HOST=CHANGE_ME +MAIL_DEFAULT_PORT=587 +MAIL_DEFAULT_USER=CHANGE_ME +MAIL_DEFAULT_PASS=CHANGE_ME +MAIL_DEFAULT_ENCRYPTION=tls +MAIL_DEFAULT_FROM_EMAIL="no-reply@alrahmaisgl.org" +MAIL_DEFAULT_FROM_NAME="Alrahma API" +MAIL_DEFAULT_REPLY_TO="no-reply@alrahmaisgl.org" +MAIL_DEFAULT_REPLY_TO_NAME="Alrahma API" +MAIL_COMMUNICATION_REPLY_TO="no-reply@alrahmaisgl.org" +MAIL_COMMUNICATION_REPLY_TO_NAME="School Communications" +MAIL_PAYMENT_REPLY_TO="no-reply@alrahmaisgl.org" +MAIL_PAYMENT_REPLY_TO_NAME="School Payments" +MAIL_SENDERS='{"general":{"name":"Alrahma API","email":"no-reply@alrahmaisgl.org"},"communication":{"name":"School Communications","email":"no-reply@alrahmaisgl.org"},"payment":{"name":"School Payments","email":"no-reply@alrahmaisgl.org"}}' + +# Laravel mailer compatibility +MAIL_HOST="${MAIL_DEFAULT_HOST}" +MAIL_PORT="${MAIL_DEFAULT_PORT}" +MAIL_USERNAME="${MAIL_DEFAULT_USER}" +MAIL_PASSWORD="${MAIL_DEFAULT_PASS}" +MAIL_ENCRYPTION="${MAIL_DEFAULT_ENCRYPTION}" +MAIL_FROM_ADDRESS="${MAIL_DEFAULT_FROM_EMAIL}" +MAIL_FROM_NAME="${MAIL_DEFAULT_FROM_NAME}" + +# Legacy fallback keys still read by some services +SMTP_HOST="${MAIL_DEFAULT_HOST}" +SMTP_USER="${MAIL_DEFAULT_USER}" +SMTP_PASS="${MAIL_DEFAULT_PASS}" +SMTP_PORT="${MAIL_DEFAULT_PORT}" +SMTP_ENCRYPTION="${MAIL_DEFAULT_ENCRYPTION}" + +# ---------------------------- +# FILESYSTEM +# ---------------------------- +FILESYSTEM_DISK=local + +# ---------------------------- +# AWS STORAGE (optional) +# ---------------------------- +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +# ---------------------------- +# JWT +# ---------------------------- +JWT_SECRET=CHANGE_ME_USE_STRONG_RANDOM_SECRET +JWT_ALGO=HS256 +JWT_TTL=60 +JWT_REFRESH_TTL=20160 +JWT_BLACKLIST_ENABLED=true +JWT_BLACKLIST_GRACE_PERIOD=0 + +# ---------------------------- +# DOCS +# ---------------------------- +DOCS_CLIENT_URL=https://CHANGE_ME +DOCS_INDEX_TITLE=Alrahma API Documentation +DOCS_INDEX_DESCRIPTION= + +# ---------------------------- +# BADGE +# ---------------------------- +BADGE_SCHOOL_NAME=Al Rahma Sunday School +BADGE_HEADER_SUBTITLE= +BADGE_MOTTO= +BADGE_MOTTO_FALLBACK= +BADGE_ROLE_LABEL=Student +BADGE_FOOTER_ADDRESS=5 Courthouse Lane, Chelmsford, MA 01824, USA + +# ---------------------------- +# FRONT-END +# ---------------------------- +VITE_APP_NAME="${APP_NAME}" diff --git a/Dockerfile b/Dockerfile index 903671f5..ac974654 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM composer:2 AS vendor WORKDIR /app COPY composer.json composer.lock ./ COPY app/Helpers/ci_helpers.php app/Helpers/ -RUN composer install --no-dev --prefer-dist --no-interaction --no-progress --no-scripts +RUN composer install --no-dev --prefer-dist --no-interaction --no-progress --no-scripts --ignore-platform-req=ext-gd COPY . . RUN composer dump-autoload --optimize \ && php artisan package:discover --ansi @@ -30,7 +30,10 @@ RUN apt-get update \ libonig-dev \ libxml2-dev \ libzip-dev \ - && docker-php-ext-install pdo_mysql mbstring xml zip \ + libjpeg-dev \ + libfreetype6-dev \ + && docker-php-ext-configure gd --with-freetype --with-jpeg \ + && docker-php-ext-install pdo_mysql mbstring xml zip gd \ && rm -rf /var/lib/apt/lists/* WORKDIR /var/www/html diff --git a/app/Helpers/ci_helpers.php b/app/Helpers/ci_helpers.php index 38a6a045..f2efdbf1 100644 --- a/app/Helpers/ci_helpers.php +++ b/app/Helpers/ci_helpers.php @@ -111,7 +111,13 @@ if (!function_exists('ci_password_verify')) { return false; } - if (strpos($storedHash, ':') !== false || strpos($storedHash, '$') !== false) { + $hashInfo = password_get_info($storedHash); + if (($hashInfo['algo'] ?? null) !== null) { + return Hash::check($password, $storedHash); + } + + $parts = preg_split('/[:$]/', $storedHash); + if (is_array($parts) && count($parts) === 4) { return pbkdf2_verify($password, $storedHash); } diff --git a/app/Http/Controllers/Api/Certificates/CertificateController.php b/app/Http/Controllers/Api/Certificates/CertificateController.php new file mode 100644 index 00000000..dc2f8458 --- /dev/null +++ b/app/Http/Controllers/Api/Certificates/CertificateController.php @@ -0,0 +1,153 @@ +query('school_year'); + if ($schoolYearParam !== null) { + $schoolYear = trim((string) $schoolYearParam); + } else { + try { + $schoolYear = trim((string) (Configuration::getConfig('school_year') ?? '')); + } catch (\Throwable) { + $schoolYear = ''; + } + } + $classSectionId = $request->query('class_section_id'); + + try { + $classSections = DB::table('classSection') + ->select('class_section_id', 'class_section_name') + ->orderBy('class_section_name', 'ASC') + ->get() + ->map(fn ($r) => (array) $r) + ->all(); + } catch (\Throwable $e) { + Log::error('Certificate formOptions class query failed: ' . $e->getMessage()); + return $this->error('Failed to load classes: ' . $e->getMessage(), 500); + } + + $students = []; + if ($classSectionId) { + try { + $studentQuery = DB::table('student_class as sc') + ->select('s.id', 's.firstname', 's.lastname', 'cs.class_section_name as grade') + ->join('students as s', 's.id', '=', 'sc.student_id') + ->join('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id') + ->where('sc.class_section_id', (int) $classSectionId) + ->where('s.is_active', 1) + ->orderBy('s.lastname') + ->orderBy('s.firstname'); + + if ($schoolYear !== '') { + $studentQuery->where('sc.school_year', $schoolYear); + } + + $students = $studentQuery->get()->map(fn ($r) => (array) $r)->all(); + } catch (\Throwable $e) { + Log::error('Certificate formOptions student query failed: ' . $e->getMessage()); + return $this->error('Failed to load students: ' . $e->getMessage(), 500); + } + } + + return $this->success([ + 'class_sections' => $classSections, + 'students' => $students, + 'school_year' => $schoolYear, + 'selected_class_id' => $classSectionId, + ]); + } + + /** + * POST /api/v1/administrator/certificates/generate + */ + public function generate(Request $request): Response|JsonResponse + { + $studentIds = $request->input('student_ids', []); + $certDate = trim((string) ($request->input('cert_date') ?? date('m/d/Y'))); + $classSectionId = $request->input('class_section_id'); + + if (empty($studentIds) || !is_array($studentIds)) { + return $this->error('Please select at least one student.', 422); + } + + $studentIds = array_values(array_filter(array_map('intval', $studentIds))); + $certDate = preg_replace('/[^0-9\/\-]/', '', $certDate) ?? date('m/d/Y'); + + if (empty($studentIds)) { + return $this->error('Invalid student selection.', 422); + } + + $students = []; + foreach ($studentIds as $id) { + $row = null; + + if ($classSectionId) { + $row = DB::table('student_class as sc') + ->select('s.id', 's.firstname', 's.lastname', 'cs.class_section_name as grade') + ->join('students as s', 's.id', '=', 'sc.student_id') + ->join('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id') + ->where('sc.class_section_id', (int) $classSectionId) + ->where('s.id', $id) + ->first(); + } + + if (!$row) { + $row = DB::table('students') + ->select('id', 'firstname', 'lastname', 'registration_grade as grade') + ->where('id', $id) + ->first(); + } + + if ($row) { + $students[] = (array) $row; + } + } + + if (empty($students)) { + return $this->error('No valid students found.', 422); + } + + try { + $pdfService = app(CertificatePdfService::class); + $pdfContent = $pdfService->generate($students, $certDate); + } catch (\Throwable $e) { + Log::error('Certificate PDF generation failed: ' . $e->getMessage(), ['exception' => $e]); + return $this->error('Failed to generate certificates.', 500); + } + + $filename = 'Certificates_' . date('Ymd_His') . '.pdf'; + + return response($pdfContent, 200, [ + 'Content-Type' => 'application/pdf', + 'Content-Disposition' => 'inline; filename="' . $filename . '"', + ]); + } + + private function currentSchoolYear(): string + { + return trim((string) (Configuration::getConfig('school_year') ?? '')); + } +} diff --git a/app/Services/Certificates/CertificatePdfService.php b/app/Services/Certificates/CertificatePdfService.php new file mode 100644 index 00000000..a7e00507 --- /dev/null +++ b/app/Services/Certificates/CertificatePdfService.php @@ -0,0 +1,143 @@ +fontDir = public_path('assets/certificates/fonts') . DIRECTORY_SEPARATOR; + $this->imgDir = public_path('assets/certificates/images') . DIRECTORY_SEPARATOR; + } + + /** + * @param array $students + */ + public function generate(array $students, string $certDate): string + { + $edwardianFont = \TCPDF_FONTS::addTTFfont($this->fontDir . 'Edwardian Script ITC Regular.ttf', 'TrueTypeUnicode', '', 32); + $garamondBold = \TCPDF_FONTS::addTTFfont($this->fontDir . 'Garamond Bold.ttf', 'TrueTypeUnicode', '', 32); + $ebGaramond = \TCPDF_FONTS::addTTFfont($this->fontDir . 'EBGaramond-Regular.ttf', 'TrueTypeUnicode', '', 32); + + $pdf = new \TCPDF('L', 'pt', 'A4', true, 'UTF-8', false); + $pdf->SetCreator('Al Rahma Sunday School'); + $pdf->SetTitle('Student Certificates'); + $pdf->SetMargins(0, 0, 0, true); + $pdf->SetAutoPageBreak(false, 0); + $pdf->setPrintHeader(false); + $pdf->setPrintFooter(false); + $pdf->SetHeaderMargin(0); + $pdf->SetFooterMargin(0); + + $W = $pdf->getPageWidth(); + $H = $pdf->getPageHeight(); + + foreach ($students as $student) { + $pdf->AddPage(); + $name = $student['firstname'] . ' ' . $student['lastname']; + $grade = $this->formatGrade((string) ($student['grade'] ?? '')); + $this->drawCertificate($pdf, $W, $H, $name, $grade, $certDate, $edwardianFont, $garamondBold, $ebGaramond); + } + + return (string) $pdf->Output('', 'S'); + } + + private function drawCertificate( + \TCPDF $pdf, + float $W, + float $H, + string $name, + string $grade, + string $certDate, + string $edwardianFont, + string $garamondBold, + string $ebGaramond + ): void { + $pdf->Image($this->imgDir . 'title.png', 126, 0, 600); + $pdf->Image($this->imgDir . 'background.png', 280, 176, 291, 340); + $pdf->Image($this->imgDir . 'signature.png', 140, 399, 90, 90); + + $pdf->SetFont('times', 'B', 24); + $pdf->SetTextColor(0, 0, 0); + $pdf->SetXY(0, 151); + $pdf->Cell($W, 24, 'Presented to:', 0, 0, 'C'); + + $pdf->SetFont($edwardianFont, '', 38); + $pdf->SetDrawColor(0, 0, 0); + $pdf->setTextRenderingMode(0.4, true, false); + $pdf->SetXY(0, 219); + $pdf->Cell($W, 38, $name, 0, 0, 'C'); + $pdf->setTextRenderingMode(0, true, false); + + $pdf->SetFont('times', '', 20); + $pdf->SetXY(0, 243); + $pdf->Cell(600, 20, '___________________________________', 0, 0, 'R'); + + $pdf->SetFont($ebGaramond, '', 20); + $pdf->SetXY(0, 273); + $pdf->Cell($W, 20, 'for successfully completing the requirements of', 0, 0, 'C'); + + $pdf->SetFont($garamondBold, '', 20); + $pdf->SetXY(0, 310); + $pdf->Cell($W, 20, $grade, 0, 0, 'C'); + + $pdf->SetFont('times', '', 20); + $pdf->SetXY(0, 343); + $pdf->Cell($W, 20, 'at', 0, 0, 'C'); + + $pdf->SetFont($garamondBold, '', 20); + $pdf->SetXY(0, 375); + $pdf->Cell($W, 20, 'Al Rahma Sunday School', 0, 0, 'C'); + + $this->drawGradientText($pdf, $edwardianFont, 26, $certDate, 598, 453); + + $pdf->SetFont('times', '', 20); + $pdf->SetXY(0, 463); + $pdf->Cell(742, 20, '_________________', 0, 0, 'R'); + $pdf->SetXY(650, 492); + $pdf->Cell(80, 20, 'Date', 0, 0, 'C'); + + $pdf->SetXY(106, 463); + $pdf->Cell(200, 20, '_________________', 0, 0, 'L'); + $pdf->SetXY(106, 492); + $pdf->Cell(168, 20, 'Signature', 0, 0, 'C'); + } + + private function drawGradientText(\TCPDF $pdf, string $fontName, float $fontSize, string $text, float $x, float $y): void + { + $pdf->SetFont($fontName, '', $fontSize); + + for ($i = 0; $i < 6; $i++) { + $pdf->setAlpha(($i + 1) * 25 / 255); + $pdf->SetTextColor(0, 0, 0); + $pdf->Text($x + $i / 5, $y + $i / 5, $text); + } + + $pdf->setAlpha(1); + $pdf->SetTextColor(0, 0, 0); + $pdf->Text($x, $y, $text); + } + + private function formatGrade(string $raw): string + { + $clean = trim($raw); + $lower = strtolower($clean); + + if (preg_match('/^\d+$/', $clean) && (int) $clean >= 1 && (int) $clean <= 9) { + return 'Grade ' . $clean; + } + if ($lower === 'youth') { + return 'Youth'; + } + if ($lower === 'kg') { + return 'Kindergarten'; + } + + return $clean; + } +} diff --git a/composer.json b/composer.json index e4410a6e..c7c333e7 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ "laravel/sanctum": "^4.3", "laravel/tinker": "^2.10.1", "php-open-source-saver/jwt-auth": "^2.8", - "setasign/fpdf": "*" + "setasign/fpdf": "*", + "tecnickcom/tcpdf": "^6.11" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/composer.lock b/composer.lock index dc398f12..6c6c3357 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "661306f5e5cfc18e168ca8a287b7097a", + "content-hash": "5b18bd0e376f1f5e5670692ccc52165c", "packages": [ { "name": "brick/math", @@ -6673,6 +6673,83 @@ ], "time": "2026-02-15T10:53:20+00:00" }, + { + "name": "tecnickcom/tcpdf", + "version": "6.11.3", + "source": { + "type": "git", + "url": "https://github.com/tecnickcom/TCPDF.git", + "reference": "b18f6119161019916c5bb07cb8da5205ae5c1b63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/b18f6119161019916c5bb07cb8da5205ae5c1b63", + "reference": "b18f6119161019916c5bb07cb8da5205ae5c1b63", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=7.1.0" + }, + "suggest": { + "ext-gd": "Enables additional image handling in some workflows.", + "ext-imagick": "Enables additional image format support when available.", + "ext-zlib": "Recommended for compressed streams and related features.", + "tecnickcom/tc-lib-pdf": "Modern replacement for TCPDF for new projects." + }, + "type": "library", + "autoload": { + "classmap": [ + "config", + "include", + "tcpdf.php", + "tcpdf_barcodes_1d.php", + "tcpdf_barcodes_2d.php", + "include/tcpdf_colors.php", + "include/tcpdf_filters.php", + "include/tcpdf_font_data.php", + "include/tcpdf_fonts.php", + "include/tcpdf_images.php", + "include/tcpdf_static.php", + "include/barcodes/datamatrix.php", + "include/barcodes/pdf417.php", + "include/barcodes/qrcode.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Nicola Asuni", + "email": "info@tecnick.com", + "role": "lead" + } + ], + "description": "Deprecated legacy PDF engine for PHP. For new projects use tecnickcom/tc-lib-pdf.", + "homepage": "https://tcpdf.org", + "keywords": [ + "PDFD32000-2008", + "TCPDF", + "barcodes", + "datamatrix", + "pdf", + "pdf417", + "qrcode" + ], + "support": { + "issues": "https://github.com/tecnickcom/TCPDF/issues", + "source": "https://github.com/tecnickcom/TCPDF" + }, + "funding": [ + { + "url": "https://www.paypal.com/donate/?hosted_button_id=NZUEC5XS8MFBJ", + "type": "paypal" + } + ], + "time": "2026-04-21T17:00:18+00:00" + }, { "name": "thecodingmachine/safe", "version": "v3.4.0", @@ -9415,5 +9492,5 @@ "php": "^8.2" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 00000000..5e1d7eb0 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,30 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + container_name: alrahma_dev_app + env_file: + - .env.dev + volumes: + - ./:/var/www/html + - /var/www/html/vendor + networks: + - alrahma_sunday_school_default + + web: + image: nginx:1.25-alpine + container_name: alrahma_dev_web + ports: + - "8080:80" + volumes: + - ./:/var/www/html:ro + - ./docker/nginx/app.conf:/etc/nginx/conf.d/default.conf:ro + depends_on: + - app + networks: + - alrahma_sunday_school_default + +networks: + alrahma_sunday_school_default: + external: true diff --git a/envfile.txt b/envfile.txt index 7e663015..c693b9ec 100644 --- a/envfile.txt +++ b/envfile.txt @@ -49,16 +49,36 @@ REDIS_PASSWORD=null REDIS_PORT=6379 # ---------------------------- -# MAIL (logs only) +# MAIL # ---------------------------- MAIL_MAILER=log -MAIL_HOST=127.0.0.1 -MAIL_PORT=2525 -MAIL_USERNAME=null -MAIL_PASSWORD=null -MAIL_ENCRYPTION=null -MAIL_FROM_ADDRESS="no-reply@alrahmaisgl.org" -MAIL_FROM_NAME="Alrahma API" +MAIL_PROFILE_DEFAULT=default +MAIL_DEFAULT_HOST=127.0.0.1 +MAIL_DEFAULT_PORT=2525 +MAIL_DEFAULT_USER= +MAIL_DEFAULT_PASS= +MAIL_DEFAULT_ENCRYPTION=tls +MAIL_DEFAULT_FROM_EMAIL="no-reply@alrahmaisgl.org" +MAIL_DEFAULT_FROM_NAME="Alrahma API" +MAIL_DEFAULT_REPLY_TO="no-reply@alrahmaisgl.org" +MAIL_DEFAULT_REPLY_TO_NAME="Alrahma API" +MAIL_SENDERS='{"general":{"name":"Alrahma API","email":"no-reply@alrahmaisgl.org"},"communication":{"name":"School Communications","email":"no-reply@alrahmaisgl.org"},"payment":{"name":"School Payments","email":"no-reply@alrahmaisgl.org"}}' + +# Laravel mailer compatibility +MAIL_HOST="${MAIL_DEFAULT_HOST}" +MAIL_PORT="${MAIL_DEFAULT_PORT}" +MAIL_USERNAME="${MAIL_DEFAULT_USER}" +MAIL_PASSWORD="${MAIL_DEFAULT_PASS}" +MAIL_ENCRYPTION="${MAIL_DEFAULT_ENCRYPTION}" +MAIL_FROM_ADDRESS="${MAIL_DEFAULT_FROM_EMAIL}" +MAIL_FROM_NAME="${MAIL_DEFAULT_FROM_NAME}" + +# Legacy fallback keys still read by some services +SMTP_HOST="${MAIL_DEFAULT_HOST}" +SMTP_USER="${MAIL_DEFAULT_USER}" +SMTP_PASS="${MAIL_DEFAULT_PASS}" +SMTP_PORT="${MAIL_DEFAULT_PORT}" +SMTP_ENCRYPTION="${MAIL_DEFAULT_ENCRYPTION}" # ---------------------------- # AWS STORAGE (optional) diff --git a/public/assets/certificates/fonts/EBGaramond-Regular.ttf b/public/assets/certificates/fonts/EBGaramond-Regular.ttf new file mode 100644 index 00000000..c86defbb Binary files /dev/null and b/public/assets/certificates/fonts/EBGaramond-Regular.ttf differ diff --git a/public/assets/certificates/fonts/Edwardian Script ITC Regular.ttf b/public/assets/certificates/fonts/Edwardian Script ITC Regular.ttf new file mode 100644 index 00000000..dcc21fdd Binary files /dev/null and b/public/assets/certificates/fonts/Edwardian Script ITC Regular.ttf differ diff --git a/public/assets/certificates/fonts/Garamond Bold.ttf b/public/assets/certificates/fonts/Garamond Bold.ttf new file mode 100644 index 00000000..30fb512b Binary files /dev/null and b/public/assets/certificates/fonts/Garamond Bold.ttf differ diff --git a/public/assets/certificates/images/background.png b/public/assets/certificates/images/background.png new file mode 100644 index 00000000..6cf7233d Binary files /dev/null and b/public/assets/certificates/images/background.png differ diff --git a/public/assets/certificates/images/signature.png b/public/assets/certificates/images/signature.png new file mode 100644 index 00000000..3ed1e942 Binary files /dev/null and b/public/assets/certificates/images/signature.png differ diff --git a/public/assets/certificates/images/title.png b/public/assets/certificates/images/title.png new file mode 100644 index 00000000..01ee2824 Binary files /dev/null and b/public/assets/certificates/images/title.png differ diff --git a/routes/api.php b/routes/api.php index b48cca9e..30066186 100644 --- a/routes/api.php +++ b/routes/api.php @@ -122,6 +122,7 @@ use App\Http\Controllers\Api\Public\PublicWinnersController; use App\Http\Controllers\Api\ScannerController; use App\Http\Controllers\Api\Staff\TimeOffNotificationController; use App\Http\Controllers\Api\System\AccessDeniedController; +use App\Http\Controllers\Api\Certificates\CertificateController; // CodeIgniter also registers POST /api/login and POST /api/register (public, no /v1). Route::post('login', [AuthController::class, 'login']); Route::post('register', [RegisterController::class, 'store']); @@ -390,6 +391,11 @@ Route::prefix('v1')->group(function () { }); }); + Route::prefix('certificates')->group(function () { + Route::get('form-options', [CertificateController::class, 'formOptions']); + Route::post('generate', [CertificateController::class, 'generate']); + }); + Route::prefix('role-permission')->group(function () { Route::get('roles', [RolePermissionController::class, 'roles']); Route::post('roles', [RolePermissionController::class, 'storeRole']); diff --git a/school_api b/school_api.md similarity index 100% rename from school_api rename to school_api.md