Backend API (Current)
Base URL during local backend development:
http://127.0.0.1:8000/api
Public Routes
GET /app-configGET /coursesGET /courses/{course}POST /auth/registerPOST /auth/register/request-verification-codePOST /auth/register/verify-codePOST /auth/loginPOST /auth/forgot-passwordPOST /auth/reset-passwordGET /auth/email/verify/{id}/{hash}
Auth
POST /auth/register creates a student or instructor account, sends an email verification notification, and returns a Sanctum bearer token.
{
"name": "New Student",
"email": "student@example.com",
"password": "password123",
"password_confirmation": "password123",
"role": "student",
"token_name": "frontend"
}Allowed self-registration roles are student and instructor; admins are still managed by seed data or admin tools.
GET /app-config returns the runtime CAPTCHA state used by the frontend. The response currently exposes captcha_enabled and captcha_site_key.
When CAPTCHA_ENABLED=true in backend .env, registration, login, and the email-code signup request flow also require captcha_token.
When CAPTCHA_ENABLED=false, the backend accepts the auth requests without CAPTCHA and the frontend hides the CAPTCHA UI.
POST /auth/login accepts email, password, and optional token_name, then returns:
{
"token": "1|...",
"token_type": "Bearer",
"user": {
"id": 1,
"email": "student@example.com",
"role": "student"
}
}Banned users cannot log in.
Auth endpoints are rate-limited server-side to reduce brute-force abuse.
Password reset flow:
POST /auth/forgot-passwordwith{ "email": "student@example.com" }sends a reset email.POST /auth/reset-passwordwithemail,token,password, andpassword_confirmationupdates the password and revokes existing Sanctum tokens.
Email verification flow:
- Registration sends a signed verification URL.
GET /auth/email/verify/{id}/{hash}marks the address as verified when the URL signature is valid.POST /auth/email/resendresends the verification email for the authenticated user.
Alternative email verification with 6-digit code:
TechTutor also supports a two-step registration flow with email verification codes:
POST /auth/register/request-verification-code— Creates a verification record, generates a 6-digit code, and emails it to the provided address.
- Requires
name,email,password,password_confirmation, optionalrole, andcaptcha_tokenwhen CAPTCHA is enabled - Code expires in 5 minutes
- Returns the email address for reference
POST /auth/register/verify-code— Validates the 6-digit code and finalizes registration.- Requires
email,code,name,password,password_confirmation, optionalroleandtoken_name - Returns a Sanctum bearer token and user info on success
- Requires
Auth cURL / Postman Examples
Register with CAPTCHA token, when CAPTCHA is enabled:
curl -X POST "http://127.0.0.1:8000/api/auth/register" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"name": "New Student",
"email": "student@example.com",
"password": "password123",
"password_confirmation": "password123",
"role": "student",
"token_name": "frontend",
"captcha_token": "demo-captcha-token"
}'If CAPTCHA is disabled, omit captcha_token from these requests.
Login with CAPTCHA token, when CAPTCHA is enabled:
curl -X POST "http://127.0.0.1:8000/api/auth/login" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"email": "student@example.com",
"password": "password123",
"token_name": "frontend",
"captcha_token": "demo-captcha-token"
}'Forgot password:
curl -X POST "http://127.0.0.1:8000/api/auth/forgot-password" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"email": "student@example.com"
}'Reset password:
curl -X POST "http://127.0.0.1:8000/api/auth/reset-password" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"token": "RESET_TOKEN_HERE",
"email": "student@example.com",
"password": "new-password123",
"password_confirmation": "new-password123"
}'Email Verification Code Registration Flow
Request verification code (sends 6-digit code to email):
curl -X POST "http://127.0.0.1:8000/api/auth/register/request-verification-code" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"name": "New Student",
"email": "student@example.com",
"password": "password123",
"password_confirmation": "password123",
"role": "student",
"captcha_token": "demo-captcha-token"
}'Response:
{
"message": "Verification code sent to your email.",
"email": "student@example.com"
}Verify code and complete registration:
curl -X POST "http://127.0.0.1:8000/api/auth/register/verify-code" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"email": "student@example.com",
"code": "123456",
"name": "New Student",
"password": "password123",
"password_confirmation": "password123",
"role": "student",
"token_name": "frontend"
}'Response:
{
"token": "1|...",
"token_type": "Bearer",
"user": {
"id": 1,
"email": "student@example.com",
"name": "New Student",
"role": "student",
"email_verified_at": "2026-05-13T10:30:00Z"
}
}Course Catalog Query Parameters
GET /courses supports catalog filters and MeiliSearch-backed free-text search when Scout is configured for MeiliSearch:
q: search title, subtitle, description, category, and slugcategorylevellanguageinstructor_idprice_type:freeorpaidmin_pricemax_pricesort:newest,oldest,title,price_asc,price_desc, orratingper_page: 1-50
Example:
curl -X GET "$BASE_URL/courses?q=laravel&category=backend&price_type=paid&sort=price_desc"When SCOUT_DRIVER=meilisearch, the q parameter routes through MeiliSearch for free-text matching, while the remaining filters continue to apply to the catalog query.
Protected Routes (Sanctum)
Banned users are blocked from protected routes.
Authenticated User
GET /auth/mePATCH /auth/me— update authenticated user's profile (see details below)DELETE /auth/me— deletes the authenticated user's account (data cleanup applied)POST /auth/logoutPOST /auth/email/resend
POST /auth/logout deletes the current Sanctum access token.
Profile update (PATCH /auth/me)
Accepts multipart/form-data for avatar uploads and the following fields (all optional):
name(string) — full display namenickname(string) — unique lowercase nickname (alpha_dash)bio(string, nullable) — short user bio (up to 1000 chars)email_notifications_enabled(boolean) — toggle email notificationsavatar(file) — image upload (max 2048 KB); stored on the public disk underavatars/remove_avatar(boolean) — when truthy, removes the existing avatar
Response: returns the fresh user resource after update.
Account delete (DELETE /auth/me)
Deletes the authenticated user's account and performs configured cleanup (owned content may be anonymized or removed depending on retention rules). Response: { "message": "Account deleted." }.
Admin
GET /admin/usersPATCH /admin/users/{user}GET /admin/platform-dashboardGET /admin/moderation-queuePATCH /admin/moderation-queue/reviews/{review}PATCH /admin/moderation-queue/comments/{comment}
GET /admin/platform-dashboard returns live platform monitoring metrics. It is admin-only and does not store separate statistics rows.
Returned data includes:
- user/course/enrollment/certificate/quiz attempt totals
- moderation queue counts
- payment totals and paid revenue
- payment status breakdown
- paid revenue grouped by course
- recent activity feed
Revenue currently uses internal payments rows with status = paid. When Stripe/LiqPay checkout and webhooks are added, this aggregation should be based on verified provider-backed payment states.
Instructor Dashboard
GET /instructor/dashboard
Available to instructors and admins. The response is calculated live from current courses, enrollments, lesson progress, certificates, quiz attempts, and paid payment records.
Returned summary metrics include:
courses_countpublished_courses_countdraft_courses_countenrollments_countcertificates_countrevenue_totalaverage_progressaverage_quiz_score
Each course row includes lesson/module/quiz counts, enrollment count, certificate count, completion rate, average progress, average quiz score, paid payments count, and paid revenue total.
Revenue currently uses internal payments rows with status = paid. When Stripe/LiqPay checkout and webhooks are added, this aggregation should be pointed at the verified provider-backed payment states.
Courses
POST /coursesPUT /courses/{course}PATCH /courses/{course}DELETE /courses/{course}
Course create/update payloads support catalog metadata:
subtitlecategorylevellanguagethumbnail_pathduration_minutes
Enrollments
GET /courses/{course}/enrollmentsPOST /courses/{course}/enrollmentsDELETE /courses/{course}/enrollments/{enrollment}
Creating a new enrollment sends an email notification to the enrolled student through the configured Laravel mailer. Re-enrolling into an existing enrollment does not send a duplicate notification.
For paid courses, students must purchase the course before direct enrollment. If no paid payment exists, POST /courses/{course}/enrollments returns 402 with:
{
"message": "Purchase this course before enrolling."
}Admins and the course instructor may enroll without purchase checks.
Modules
GET /courses/{course}/modulesPOST /courses/{course}/modulesGET /courses/{course}/modules/{module}PUT /courses/{course}/modules/{module}PATCH /courses/{course}/modules/{module}DELETE /courses/{course}/modules/{module}
Lessons
GET /modules/{module}/lessonsPOST /modules/{module}/lessonsGET /modules/{module}/lessons/{lesson}PUT /modules/{module}/lessons/{lesson}PATCH /modules/{module}/lessons/{lesson}DELETE /modules/{module}/lessons/{lesson}
Progress
POST /lessons/{lesson}/progressPUT /lessons/{lesson}/progress
When progress reaches 100, the backend checks whether the student has completed the course. If yes, it issues or returns the existing course certificate in the progress response. The same certificate flow also runs after a passing quiz attempt when that attempt completes the course.
Progress response shape:
{
"progress": {
"id": 1,
"progress_percent": 100
},
"certificate": {
"id": 1,
"certificate_number": "TT-1-3-20260505-ABC123"
}
}certificate is null until the full course is complete.
Certificates
GET /certificatesGET /certificates/{certificate}POST /courses/{course}/certificate
Certificate access is role-aware:
- Students see their own certificates.
- Instructors see certificates issued for their courses.
- Admins see all certificates.
POST /courses/{course}/certificate manually checks completion eligibility for the authenticated student and returns the existing certificate if one was already issued. Certificates are stored because they are stable issued artifacts, not transient statistics.
When a certificate is first issued, the backend emails the student with the course title and certificate number.
Quizzes
GET /courses/{course}/quizzesPOST /courses/{course}/quizzesGET /courses/{course}/quizzes/{quiz}PUT /courses/{course}/quizzes/{quiz}PATCH /courses/{course}/quizzes/{quiz}DELETE /courses/{course}/quizzes/{quiz}
Quiz create/update payloads may include questions.
Supported question types:
single_choice: exactly one option must haveis_correct: truemultiple_choice: one or more options may haveis_correct: true
Example question payload:
{
"type": "multiple_choice",
"prompt": "Which pieces belong to the backend flow?",
"points": 2,
"options": [
{ "key": "policies", "text": "Policies", "is_correct": true },
{ "key": "middleware", "text": "Middleware", "is_correct": true },
{ "key": "tailwind", "text": "Tailwind utility classes" }
]
}Question responses hide correct_answers so students can view available options without receiving the answer key.
Quiz Attempts
GET /quizzes/{quiz}/attemptsPOST /quizzes/{quiz}/attempts
Quiz attempts accept answers only. The backend calculates score and passed.
Example:
{
"answers": {
"1": "sanctum",
"2": ["middleware", "policies"]
}
}After an attempt is created, the backend emails the student with the quiz title, calculated score, pass threshold, and pass/fail status.
Quiz Analytics
GET /quizzes/{quiz}/analytics
Only the course instructor and admins may access quiz analytics. The response is calculated live from quiz_attempts and quiz_questions; no separate analytics/statistics entity is stored.
Returned metrics include:
attempts_countunique_students_countaverage_scorehighest_scorelowest_scorepassed_countfailed_countpass_ratequestion_breakdownrecent_attempts
Reviews
GET /courses/{course}/reviewsPOST /courses/{course}/reviewsPUT /courses/{course}/reviews/{review}PATCH /courses/{course}/reviews/{review}DELETE /courses/{course}/reviews/{review}
Lesson Comments
GET /lessons/{lesson}/commentsPOST /lessons/{lesson}/commentsPUT /lessons/{lesson}/comments/{comment}PATCH /lessons/{lesson}/comments/{comment}DELETE /lessons/{lesson}/comments/{comment}
Payments
GET /paymentsGET /payments/{payment}POST /courses/{course}/paymentsPOST /courses/{course}/payments/stripe-checkoutPOST /stripe/webhook
POST /courses/{course}/payments is the current internal purchase endpoint. It validates that the submitted amount matches the current course price for non-admin users, creates a paid payment record, issues a receipt number, grants access, and creates/returns the active enrollment.
Example payload:
{
"provider": "manual_demo",
"amount": 49.99,
"currency": "USD",
"transaction_id": "txn_optional_unique_id",
"provider_payload": {
"source": "frontend_demo"
}
}Response shape:
{
"payment": {
"id": 1,
"status": "paid",
"receipt_number": "TT-RCPT-20260507-ABC12345",
"receipt_issued_at": "2026-05-07T12:00:00.000000Z",
"access_granted_at": "2026-05-07T12:00:00.000000Z"
},
"enrollment": {
"id": 1,
"status": "active"
}
}GET /payments/{payment} returns a receipt/payment record to the payment owner, the course instructor, or an admin.
Stripe Checkout session creation and webhook fulfillment are complete. LiqPay integration is planned. The payment flow is provider-agnostic: both Stripe webhooks and internal payments create the same payments rows with status = paid, so enrollment logic remains unified.
POST /courses/{course}/payments/stripe-checkout creates a Stripe Checkout Session for a paid course and stores a local pending Stripe payment tied to the returned Checkout Session ID.
Optional payload:
{
"success_url": "http://127.0.0.1:5173/payment/success",
"cancel_url": "http://127.0.0.1:5173/payment/cancel"
}Response shape:
{
"payment": {
"id": 2,
"provider": "stripe",
"status": "pending",
"transaction_id": "cs_test_..."
},
"checkout": {
"session_id": "cs_test_...",
"url": "https://checkout.stripe.com/c/pay/...",
"mode": "payment"
}
}Important: Stripe Checkout creation does not grant access immediately. Access is granted only after a verified checkout.session.completed webhook confirms payment_status = paid.
POST /stripe/webhook is public because Stripe calls it server-to-server. It verifies the Stripe-Signature header with STRIPE_WEBHOOK_SECRET. On a valid paid checkout.session.completed event, it finds the pending local Stripe payment by Checkout Session ID, validates amount/currency when present, marks the payment paid, issues a receipt, sets access timestamps, and creates the active enrollment idempotently.
For local testing, forward only the needed event:
stripe listen --forward-to http://127.0.0.1:8000/api/stripe/webhook --events checkout.session.completedCopy the printed whsec_... value into backend .env as STRIPE_WEBHOOK_SECRET, then clear cached config if needed:
php artisan config:clearWhen deployed, create a Workbench webhook endpoint pointing to:
https://your-domain.example/api/stripe/webhookGET /payments/status allows checking the current payment status for a Stripe Checkout Session. No authentication is required if a session_id query parameter is provided.
Query parameters:
session_id(required if not authenticated): Stripe Checkout Session IDcourse_id(optional): Course ID for filtering
Response shape:
{
"status": "paid",
"payment": {
"id": 2,
"amount": "49.99",
"currency": "EUR",
"transaction_id": "cs_test_..."
}
}When authenticated, the endpoint scopes results to the current user. When unauthenticated, only session_id results are returned.
POST /payments/stripe/confirm finalizes a pending Stripe payment by verifying the checkout session status with Stripe's API. Requires authentication and a valid session_id.
Payload:
{
"session_id": "cs_test_..."
}Response shape:
{
"payment": {
"id": 2,
"status": "paid",
"receipt_number": "TT-RCPT-20260510-ABC12345"
},
"enrollment": {
"id": 10,
"status": "active"
}
}This endpoint is called automatically by the frontend return handler after a successful Stripe redirect to ensure the payment is marked paid before checking enrollment state.
Select the checkout.session.completed event and use that endpoint's signing secret in production/staging env.
Publish-request workflow
Instructors may create courses as drafts and request publishing. The backend tracks these requests in a publish_requests table and exposes publish controls via the existing course endpoints.
- Instructor request (create/update course): include
request_publish: truein the JSON payload to create a pending publish request. - Admin accept: admin publishes the course using
PATCH /courses/{course}with{ "is_published": true }. Any pending request for the course is markedaccepted, and the requester is emailed. - Admin decline: admin may decline a pending request using
PATCH /courses/{course}with{ "decline_publish": true, "publish_request_declined_reason": "optional reason" }. The request is markeddeclined, the decline reason is stored, and the requester is emailed.
Data model & files:
- Migration:
database/migrations/2026_04_29_000000_create_publish_requests_table.php - Model:
app/Models/PublishRequest.php - Controller handling:
app/Http/Controllers/CourseController.php
Email notifications
Email delivery uses Laravel notifications and the mailer configured in backend .env.
Current triggers:
- Registration verification
- Password reset
- Enrollment confirmation after a new enrollment is created
- Quiz result after a quiz attempt is completed
- Certificate issued after full course completion
- Publish request approved or declined by an admin
For local demos with SMTP/Gmail configured, these emails are sent by the same API actions listed above. Automated tests fake notifications and use the array mailer from phpunit.xml, so the test suite does not send real emails.
Controller Implementation Details
AuthController
Public Methods:
register()- Creates new student/instructor, sends verification emaillogin()- Validates credentials, returns Sanctum tokenrequestVerificationCode()- Creates 6-digit verification code, emails to addressverifyEmailCode()- Validates code, completes registrationverifyEmail()- Marks email verified from signed URLforgotPassword()- Sends password reset emailresetPassword()- Updates password, revokes existing tokensredirectToGoogle()- Initiates Google OAuth flowhandleGoogleCallback()- Processes Google callback, creates/updates userresendVerification()- Resends verification email for authenticated user
Key Features:
- CAPTCHA integration for registration/login
- Multi-step email verification with 6-digit codes
- Google OAuth with popup handshake
- Rate limiting (60 requests/min)
- User banning checks
CourseController
Public Methods:
index()- List/search/filter courses with Meilisearch supportstore()- Create new course (draft)show()- Get course detailsupdate()- Full course updatedestroy()- Delete coursecatalogOptions()- Get available filter options
Features:
- Meilisearch integration for full-text search
- Filtering: category, level, language, price, instructor
- Sorting: newest, oldest, title, price_asc, price_desc, rating
- Catalog metadata: subtitle, category, level, language, thumbnail, duration
- Publish request workflow with admin approval
EnrollmentController
Public Methods:
index()- Get course roster (instructor/admin only)store()- Enroll user in coursedestroy()- Drop course
Key Checks:
- Paid courses require existing paid payment
- Admins/instructors can enroll without payment
- Duplicate prevention (re-enrolling doesn't create new record)
- Enrollment notifications sent
QuizAttemptController
Public Methods:
index()- Get user's attempts (limit 3 per student)store()- Submit quiz attempt with answers
Scoring:
- Backend calculates score from answers
- Supports multiple choice and true/false
- Pass threshold configurable per quiz
- Attempt limit: 3 per student (instructors unlimited)
PaymentController
Public Methods:
index()- List user's paymentsshow()- Get payment detailsstore()- Create internal payment (manual purchase)stripeCheckout()- Create Stripe checkout sessionconfirmStripeCheckout()- Confirm pending Stripe payment
Payment Flow:
- Create payment with
POST /courses/{course}/payments - Mark as paid, generate receipt number (TT-RCPT-YYYYMMDD-XXXXXXXX)
- Create enrollment automatically
- Send receipt email
Stripe Flow:
- Create session with
POST /courses/{course}/payments/stripe-checkout - Return checkout URL to client
- Client redirects to Stripe
- Stripe redirects back with session_id
- Confirm with
POST /payments/stripe/confirm - Webhook verifies payment and grants access
ProgressController
Public Methods:
store()- Create progress recordupdate()- Update progress percentage
Certificate Logic:
- When progress reaches 100% and all lessons completed
- Check eligibility and issue certificate idempotently
- Send certificate email
QuizController
Public Methods:
index()- List course quizzesstore()- Create quiz with questionsshow()- Get quiz detailsupdate()- Update quizdestroy()- Delete quiz
Questions:
- Types: multiple_choice, true_false
- Options with is_correct flags
- Points per question
- Answers hidden from students
ModuleController & LessonController
Features:
- Nested CRUD under courses/modules
- Positional ordering
- Draft/published states
- File attachment support
- Preview mode for lessons
ReviewController
Features:
- 1-5 star ratings
- Optional text reviews
- Moderation queue (unpublished by default)
- Instructor/admin can publish
- Average rating aggregation
CommentController
Features:
- Threaded comments (parent_comment_id)
- Moderation queue
- Instructor pending queue
- Per-lesson comments
- Reply notifications
AdminModerationQueueController
Features:
- Centralized queue for reviews, comments, publish requests
- Approve/decline with optional reason
- Status tracking (pending, accepted, declined)
- Notification system for decisions
AdminPlatformDashboardController
Response Data:
- User counts (total, students, instructors, admins, banned)
- Course counts (published, draft)
- Totals: enrollments, certificates, quiz attempts, payments
- Status breakdown for payments
- Revenue by course
- Recent activity feed
- Moderation queue counts
Request Validation Classes
StoreCourseRequest / UpdateCourseRequest
Validation Rules:
- title: required, string, max 255
- slug: required, alpha_dash, unique (on creation), max 100
- description: required, string
- subtitle: nullable, string, max 255
- category: nullable, in:backend,frontend,mobile,data-science,other
- level: nullable, in:beginner,intermediate,advanced
- language: nullable, string, max 50
- thumbnail_path: nullable, url
- duration_minutes: nullable, integer, min:1
- price: nullable, numeric, min:0, max:99999.99
- is_published: boolean
- request_publish: boolean (triggers publish request workflow)
StoreQuizRequest / UpdateQuizRequest
Validation Rules:
- title: required, string, max 255
- description: nullable, string
- pass_score: nullable, integer, min:0, max:100 (default 70)
- is_published: boolean
- position: nullable, integer
- questions: array of question objects
- type: required, in:multiple_choice,true_false
- prompt: required, string
- points: required, integer, min:1
- options: array with key, text, is_correct
StorePaymentRequest
Validation Rules:
- provider: required, string (e.g., stripe, manual_demo)
- amount: required, numeric, min:0
- currency: required, string, size:3 (ISO 4217)
- transaction_id: required, unique per course/user
- provider_payload: nullable, array
Auth Request Classes
LoginRequest:
- email: required, email
- password: required, string
- token_name: nullable, string
- captcha_token: required_if CAPTCHA enabled
RegisterRequest:
- name: required, string, max 255
- email: required, email, unique
- password: required, confirmed, min 8
- role: nullable, in:student,instructor (default student)
- captcha_token: required_if CAPTCHA enabled
RequestVerificationCodeRequest:
- name: required, string, max 255
- email: required, email, unique
- password: required, confirmed, min 8
- role: nullable, in:student,instructor
- captcha_token: required_if CAPTCHA enabled
VerifyEmailCodeRequest:
- email: required, email
- code: required, digits:6
- name: required, string, max 255
- password: required, confirmed, min 8
- role: nullable, in:student,instructor
- token_name: nullable, string
Response Shapes
Course Object
{
"id": 1,
"instructor_id": 2,
"title": "Learn Laravel",
"slug": "learn-laravel",
"description": "Full description...",
"subtitle": "Master modern web development",
"category": "backend",
"level": "intermediate",
"language": "en",
"price": 49.99,
"thumbnail_path": "https://...",
"duration_minutes": 240,
"is_published": true,
"published_at": "2026-05-01T10:00:00.000000Z",
"created_at": "2026-04-15T10:00:00.000000Z",
"updated_at": "2026-04-20T15:30:00.000000Z",
"instructor": {
"id": 2,
"name": "Jane Instructor",
"email": "jane@example.com",
"role": "instructor"
},
"enrollments_count": 42,
"published_reviews_count": 15,
"average_rating": 4.6
}Quiz Object
{
"id": 5,
"course_id": 1,
"module_id": 3,
"title": "Laravel Fundamentals Quiz",
"description": "Test your knowledge...",
"pass_score": 70,
"is_published": true,
"position": 1,
"questions": [
{
"id": 1,
"type": "multiple_choice",
"prompt": "What is dependency injection?",
"points": 2,
"options": [
{
"key": "option1",
"text": "Injecting code into dependencies",
"is_correct": false
},
{
"key": "option2",
"text": "Providing dependencies to a class",
"is_correct": true
}
]
}
],
"created_at": "2026-04-15T10:00:00Z"
}Enrollment Object
{
"id": 1,
"course_id": 1,
"user_id": 5,
"status": "active",
"enrolled_at": "2026-05-01T10:00:00Z",
"created_at": "2026-05-01T10:00:00Z",
"course": {
"id": 1,
"title": "Learn Laravel"
},
"user": {
"id": 5,
"name": "John Student",
"email": "john@example.com"
}
}Progress Object
{
"id": 1,
"lesson_id": 10,
"user_id": 5,
"progress_percent": 75,
"completed_at": null,
"created_at": "2026-05-01T10:00:00Z",
"updated_at": "2026-05-05T15:00:00Z"
}Certificate Object
{
"id": 1,
"course_id": 1,
"user_id": 5,
"certificate_number": "TT-1-5-20260505-ABC123",
"issued_at": "2026-05-05T10:00:00Z",
"course": {
"id": 1,
"title": "Learn Laravel"
},
"user": {
"id": 5,
"name": "John Student"
}
}Seeded demo accounts (local development)
The project includes a database seeder that creates several demo accounts for local testing. All seeded accounts use the password password (see backend/database/seeders/DatabaseSeeder.php).
admin@techtutor.test— admin (use for admin endpoints and moderation)backend@techtutor.test— instructor (backend course owner)frontend@techtutor.test— instructor (frontend course owner)ml@techtutor.test— instructor (ml course owner)devops@techtutor.test— instructor (devops course owner)student@techtutor.test— student (enrolled in seeded courses)student2@techtutor.test— student (additional student, has completed some courses)banned@techtutor.test— banned student (used to test ban enforcement)
Tip: run php artisan migrate:fresh --seed from the backend folder to recreate the database and seed these accounts.
Payment Object
{
"id": 1,
"course_id": 1,
"user_id": 5,
"provider": "stripe",
"amount": "49.99",
"currency": "USD",
"status": "paid",
"transaction_id": "ch_test_123",
"receipt_number": "TT-RCPT-20260505-ABC12345",
"receipt_issued_at": "2026-05-05T10:00:00.000000Z",
"access_granted_at": "2026-05-05T10:00:00.000000Z",
"provider_payload": {},
"created_at": "2026-05-05T09:00:00Z"
}QuizAttempt Object
{
"id": 1,
"quiz_id": 5,
"user_id": 5,
"score": 85,
"passed": true,
"answers": {
"1": "option2",
"2": ["option1", "option3"],
"3": "true"
},
"started_at": "2026-05-05T14:00:00Z",
"completed_at": "2026-05-05T14:15:00Z",
"created_at": "2026-05-05T14:15:00Z"
}Review Object
{
"id": 1,
"course_id": 1,
"user_id": 5,
"rating": 5,
"comment": "Great course! Very well structured.",
"is_published": true,
"moderated_at": "2026-05-05T15:00:00Z",
"created_at": "2026-05-05T10:00:00Z",
"updated_at": "2026-05-05T10:30:00Z",
"user": {
"id": 5,
"name": "John Student"
}
}Comment Object
{
"id": 1,
"lesson_id": 10,
"user_id": 5,
"parent_comment_id": null,
"body": "Question about this lesson...",
"is_published": true,
"moderated_at": "2026-05-05T15:00:00Z",
"created_at": "2026-05-05T10:00:00Z",
"replies": [
{
"id": 2,
"parent_comment_id": 1,
"user_id": 2,
"body": "Here's the answer...",
"user": {
"id": 2,
"name": "Jane Instructor",
"role": "instructor"
}
}
],
"user": {
"id": 5,
"name": "John Student"
}
}