Compare commits

..

8 Commits

Author SHA1 Message Date
4c3eb027ea basic CRUD for listings 2025-11-05 14:47:19 -08:00
8355d57fc4 Boilerplate for Listing resource 2025-11-05 02:18:59 -08:00
b20e2258b2 Fix user model 2025-11-05 01:45:28 -08:00
0a531dce16 Update setup instructions 2025-11-04 11:08:13 -08:00
b8fbbe633a Missing .env option made Sail break :/ 2025-11-03 22:19:42 -08:00
531e9f1347 Add sanctum + fortify 2025-10-30 16:53:46 -07:00
5a7f46b75c Revert database.php connections 2025-10-30 11:11:34 -07:00
283595485c Laravel Sail setup 2025-10-30 11:10:32 -07:00
32 changed files with 1451 additions and 119 deletions

View File

@@ -27,6 +27,9 @@ DB_DATABASE=ewaste_backend
DB_USERNAME=root
DB_PASSWORD=
WWWGROUP=1000
WWWUSER=1000
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false

View File

@@ -1,30 +0,0 @@
FROM php:8.3-apache
# Install dependencies and PHP extensions
RUN apt-get update && apt-get install -y \
libpq-dev \
curl \
git \
libzip-dev \
&& curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \
&& apt-get install -y nodejs \
&& docker-php-ext-install pdo pdo_pgsql pcntl zip
# Enable Apache mod_rewrite
RUN a2enmod rewrite
# Set working directory
WORKDIR /var/www/html
# Copy only package files, install node deps (faster caches)
COPY package*.json ./
RUN npm ci --unsafe-perm
# Make git accept repo path (prevents dubious ownership)
RUN git config --global --add safe.directory /var/www/html
# Now copy app code
COPY . .
# Install composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

132
README.md
View File

@@ -2,58 +2,58 @@
## Installation
Clone the repo.
Clone the repo, and copy the `.env.example` file to `.env`.
### 1a. Docker (Recommended)
### 1a. Laravel Sail / Docker (Recommended)
It's recommended to use Docker to set up the development environment. It's literally one command:
It's recommended to use Docker to set up the development environment, so you don't need PHP or Composer on your system. Run this for the first-time setup:
```bash
docker compose up -d
docker run --rm \
-v $(pwd):/var/www/html \
-w /var/www/html \
laravelsail/php84-composer:latest \
bash -c "composer install --ignore-platform-reqs && php artisan sail:install --with=pgsql"
```
From this point onward, run all shell commands inside the `ewaste-backend-laravel-1` container. The easiest way to do this is through your IDE's Docker extension, but the manual way is:
From this point onward, run all shell commands with the `./vendor/bin/sail` prefix so they get run inside the container. You might want to set up a shell alias in `~/.bashrc` to remove the relative path:
```bash
docker exec -it ewaste-backend-laravel-1 /bin/bash
alias sail='./vendor/bin/sail '
```
Then, it's just one command every time you want to spin up your development environment:
```bash
sail up -d
```
### 1b. Manual
Alternatively, you can install PHP 8 (with the relevant extensions), PHP Composer, and Postgres on your system, then go into this directory and run:
Alternatively, you can install PHP 8 (with the [relevant extensions](https://stackoverflow.com/questions/40815984/how-to-install-all-required-php-extensions-for-laravel)), PHP Composer, and Postgres on your system, then go into this directory and run:
```bash
composer install
```
Don't use the `sail` prefix if you install it this way.
### 2. Migrations
Apply all migrations to the database:
```bash
php artisan migrate
sail php artisan migrate
```
**Note:** this step has to be redone every time a new database migration is written.
### 3. Generate key
Your .env should look something like this prior to running the command:
```
DB_CONNECTION=pgsql
DB_HOST=db
DB_PORT=5432
DB_DATABASE=laravel
DB_USERNAME=laravel
DB_PASSWORD=secret
APP_KEY=
```
Generate Artisan key necessary for accessing endpoint:
Generate the application encryption key:
```bash
exec laravel php artisan key:generate
sail php artisan key:generate
```
## Development
@@ -61,24 +61,92 @@ exec laravel php artisan key:generate
### Dev Server
To start the Laravel dev server, run this *inside* the Docker container:
Start the Laravel dev server with this command:
```bash
composer run dev
sail php artisan serve
```
Then you can go to http://localhost:8000 in your browser, like it says. The site will live update any time you make changes.
Then you can go to http://localhost/ in your browser (if using Sail) or http://localhost:8000/ (for manual installs). The site will live update any time you make changes.
### Changing Docker Configs
If you change the Dockerfile, run this *outside* the Docker container:
### Create Model
```bash
docker compose up --build -d
sail php artisan make:model
```
If you change the docker-compose.yml file, run this *outside* the Docker container:
Check out the flags you can pass into this command first, since it can save you from writing boilerplate (controllers, factories, migrations, tests, etc).
### PHP Console
```bash
docker compose restart
```
sail php artisan tinker
```
Really useful for doing database operations and such by interacting with model objects (no SQL required).
### Rebuilding Sail Containers
Only do this if you totally break something in the container or need a fresh start after changing something in compose.yaml. It may take a while.
```bash
sail build --no-cache
```
For troubleshooting purposes, note that the containers still can access real paths on your system (like PHP packages going in `./vendor`, Postgres databases in the `sail-pgsql` volume), so you may want to try clearing those first.
## Testing
You should install an HTTP request client like Insomnia. Make sure you are setting the `Accept: application/json` and `Referer: localhost` headers in all requests (the Insomnia project already does this) -- this application is not designed to generate any HTML views, aside from email messages and the password reset page.
### List all Routes
```bash
sail php artisan route:list
```
### Authenticating
Authentication is handled by Laravel Sanctum and Laravel Fortify. Instead of using JWTs or non-expiring API tokens, which come with a host of security issues, we use good old fashioned cookies to maintain the logged-in user's session.
#### Sanctum Routes
`GET /sanctum/csrf-cookie`: must be ran first to get the `XSRF-TOKEN` cookie. Then, the client is responsible for pasting this token into the `X-XSRF-TOKEN` request header of all subsequent authenticated requests. Make sure to URL-decode it first (basically, if there's a `%3D` at the end, change it to `=`). The Insomnia project takes care of this for you, as long as you hit run in the first place.
#### Fortify Routes
`POST /login`
`POST /register`
`POST /logout`
### Application Routes
`GET /api/user`
#### Listings Resource
A group of [resource routes](https://laravel.com/docs/12.x/controllers#resource-controllers) created by Laravel for standard CRUD operations.
`GET /api/listing`
`POST /api/listing`
`GET /api/listing/{listing}`
`PUT/PATCH /api/listing/{listing}`
`DELETE /api/listing/{listing}`
### Verifying your Email
New accounts need to have their emails marked as verified in order to post on the site. For testing purposes, you don't need to send an actual email, you can just update the database:
```bash
sail php artisan serve
> $u = User::find(1);
> $u->email_verified_at = now();
> $u->save();
```

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Laravel\Fortify\Contracts\CreatesNewUsers;
class CreateNewUser implements CreatesNewUsers
{
use PasswordValidationRules;
/**
* Validate and create a newly registered user.
*
* @param array<string, string> $input
*/
public function create(array $input): User
{
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'email' => [
'required',
'string',
'email',
'max:255',
Rule::unique(User::class),
],
'password' => $this->passwordRules(),
])->validate();
return User::create([
'name' => $input['name'],
'email' => $input['email'],
'password' => Hash::make($input['password']),
]);
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Actions\Fortify;
use Illuminate\Validation\Rules\Password;
trait PasswordValidationRules
{
/**
* Get the validation rules used to validate passwords.
*
* @return array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string>
*/
protected function passwordRules(): array
{
return ['required', 'string', Password::default(), 'confirmed'];
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\ResetsUserPasswords;
class ResetUserPassword implements ResetsUserPasswords
{
use PasswordValidationRules;
/**
* Validate and reset the user's forgotten password.
*
* @param array<string, string> $input
*/
public function reset(User $user, array $input): void
{
Validator::make($input, [
'password' => $this->passwordRules(),
])->validate();
$user->forceFill([
'password' => Hash::make($input['password']),
])->save();
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\UpdatesUserPasswords;
class UpdateUserPassword implements UpdatesUserPasswords
{
use PasswordValidationRules;
/**
* Validate and update the user's password.
*
* @param array<string, string> $input
*/
public function update(User $user, array $input): void
{
Validator::make($input, [
'current_password' => ['required', 'string', 'current_password:web'],
'password' => $this->passwordRules(),
], [
'current_password.current_password' => __('The provided password does not match your current password.'),
])->validateWithBag('updatePassword');
$user->forceFill([
'password' => Hash::make($input['password']),
])->save();
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
class UpdateUserProfileInformation implements UpdatesUserProfileInformation
{
/**
* Validate and update the given user's profile information.
*
* @param array<string, string> $input
*/
public function update(User $user, array $input): void
{
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'email' => [
'required',
'string',
'email',
'max:255',
Rule::unique('users')->ignore($user->id),
],
])->validateWithBag('updateProfileInformation');
if ($input['email'] !== $user->email &&
$user instanceof MustVerifyEmail) {
$this->updateVerifiedUser($user, $input);
} else {
$user->forceFill([
'name' => $input['name'],
'email' => $input['email'],
])->save();
}
}
/**
* Update the given verified user's profile information.
*
* @param array<string, string> $input
*/
protected function updateVerifiedUser(User $user, array $input): void
{
$user->forceFill([
'name' => $input['name'],
'email' => $input['email'],
'email_verified_at' => null,
])->save();
$user->sendEmailVerificationNotification();
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreListingRequest;
use App\Http\Requests\UpdateListingRequest;
use App\Models\Listing;
class ListingController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
return Listing::with('poster')->get()->toJson();
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(StoreListingRequest $request)
{
$attrs = $request->safe()->merge([
'user_id' => $request->user()->id
]);
return Listing::create($attrs->all())->toJson();
}
/**
* Display the specified resource.
*/
public function show(Listing $listing)
{
return $listing->with('poster')->get()->toJson();
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Listing $listing)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(UpdateListingRequest $request, Listing $listing)
{
return $listing->update($request->validated());
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Listing $listing)
{
if (request()->user()->can('delete', $listing)) {
$listing->delete();
return response()->json([
'deleted_at' => $listing->deleted_at,
], 200);
} else {
return response()->json([
'error' => 'You are not authorized to delete this listing.',
], 403);
}
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use App\Models\Listing;
class StoreListingRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return $this->user()->can('create', Listing::class);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'title' => 'string|required|max:100',
'description' => 'string|required|max:1000',
'condition' => ['nullable', Rule::in(['parts only', 'poor', 'fair', 'good', 'excellent'])],
'price' => 'decimal:2|gte:0',
'location' => 'string|nullable|max:50',
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use App\Models\Listing;
class UpdateListingRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
$listing = Listing::find($this->route('listing'));
return $listing && $this->user()->can('update', $listing);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'title' => 'string|required|max:100',
'description' => 'string|required|max:1000',
'condition' => ['nullable', Rule::in(['parts only', 'poor', 'fair', 'good', 'excellent'])],
'price' => 'decimal:2|gte:0',
'location' => 'string|nullable|max:50',
];
}
}

46
app/Models/Listing.php Normal file
View File

@@ -0,0 +1,46 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class Listing extends Model
{
/** @use HasFactory<\Database\Factories\ListingFactory> */
use HasFactory, SoftDeletes;
/**
* Get the user that made this listing.
*/
public function poster(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'title',
'description',
'price',
'location',
'condition',
'user_id',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'user_id',
'deleted_at',
];
}

View File

@@ -31,6 +31,8 @@ class User extends Authenticatable
protected $hidden = [
'password',
'remember_token',
'two_factor_secret',
'two_factor_recovery_codes',
];
/**

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Policies;
use App\Models\Listing;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class ListingPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return true;
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Listing $listing): bool
{
return true;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->email_verified_at !== null;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Listing $listing): bool
{
return $user->id === $listing->user_id;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Listing $listing): bool
{
return $user->id === $listing->user_id;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Listing $listing): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Listing $listing): bool
{
return false;
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Providers;
use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\ResetUserPassword;
use App\Actions\Fortify\UpdateUserPassword;
use App\Actions\Fortify\UpdateUserProfileInformation;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use Laravel\Fortify\Actions\RedirectIfTwoFactorAuthenticatable;
use Laravel\Fortify\Fortify;
class FortifyServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Fortify::createUsersUsing(CreateNewUser::class);
Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
Fortify::redirectUserForTwoFactorAuthenticationUsing(RedirectIfTwoFactorAuthenticatable::class);
RateLimiter::for('login', function (Request $request) {
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip());
return Limit::perMinute(5)->by($throttleKey);
});
RateLimiter::for('two-factor', function (Request $request) {
return Limit::perMinute(5)->by($request->session()->get('login.id'));
});
}
}

View File

@@ -7,12 +7,14 @@ use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
//
$middleware->statefulApi();
})
->withExceptions(function (Exceptions $exceptions): void {
//
})->create();
})
->create();

View File

@@ -2,4 +2,5 @@
return [
App\Providers\AppServiceProvider::class,
App\Providers\FortifyServiceProvider::class,
];

56
compose.yaml Normal file
View File

@@ -0,0 +1,56 @@
services:
laravel.test:
build:
context: ./vendor/laravel/sail/runtimes/8.4
dockerfile: Dockerfile
args:
WWWGROUP: '${WWWGROUP}'
image: sail-8.4/app
extra_hosts:
- 'host.docker.internal:host-gateway'
ports:
- '${APP_PORT:-80}:80'
- '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
environment:
WWWUSER: '${WWWUSER}'
LARAVEL_SAIL: 1
XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
IGNITION_LOCAL_SITES_PATH: '${PWD}'
volumes:
- '.:/var/www/html'
networks:
- sail
depends_on:
- pgsql
pgsql:
image: 'postgres:18-alpine'
ports:
- '${FORWARD_DB_PORT:-5432}:5432'
environment:
PGPASSWORD: '${DB_PASSWORD:-secret}'
POSTGRES_DB: '${DB_DATABASE}'
POSTGRES_USER: '${DB_USERNAME}'
POSTGRES_PASSWORD: '${DB_PASSWORD:-secret}'
volumes:
- 'sail-pgsql:/var/lib/postgresql/data'
- './vendor/laravel/sail/database/pgsql/create-testing-database.sql:/docker-entrypoint-initdb.d/10-create-testing-database.sql'
networks:
- sail
healthcheck:
test:
- CMD
- pg_isready
- '-q'
- '-d'
- '${DB_DATABASE}'
- '-U'
- '${DB_USERNAME}'
retries: 3
timeout: 5s
networks:
sail:
driver: bridge
volumes:
sail-pgsql:
driver: local

View File

@@ -10,7 +10,9 @@
"license": "MIT",
"require": {
"php": "^8.2",
"laravel/fortify": "^1.31",
"laravel/framework": "^12.0",
"laravel/sanctum": "^4.0",
"laravel/tinker": "^2.10.1"
},
"require-dev": {
@@ -18,7 +20,7 @@
"laravel/boost": "^1.6",
"laravel/pail": "^1.2.2",
"laravel/pint": "^1.24",
"laravel/sail": "^1.41",
"laravel/sail": "*",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"pestphp/pest": "^4.1",
@@ -89,4 +91,4 @@
},
"minimum-stability": "stable",
"prefer-stable": true
}
}

356
composer.lock generated
View File

@@ -4,8 +4,62 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f57f42baa39a474eb39dad5682f0748c",
"content-hash": "d153d28fb93ee5adb2f22f23f24d4e34",
"packages": [
{
"name": "bacon/bacon-qr-code",
"version": "v3.0.1",
"source": {
"type": "git",
"url": "https://github.com/Bacon/BaconQrCode.git",
"reference": "f9cc1f52b5a463062251d666761178dbdb6b544f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/f9cc1f52b5a463062251d666761178dbdb6b544f",
"reference": "f9cc1f52b5a463062251d666761178dbdb6b544f",
"shasum": ""
},
"require": {
"dasprid/enum": "^1.0.3",
"ext-iconv": "*",
"php": "^8.1"
},
"require-dev": {
"phly/keep-a-changelog": "^2.12",
"phpunit/phpunit": "^10.5.11 || 11.0.4",
"spatie/phpunit-snapshot-assertions": "^5.1.5",
"squizlabs/php_codesniffer": "^3.9"
},
"suggest": {
"ext-imagick": "to generate QR code images"
},
"type": "library",
"autoload": {
"psr-4": {
"BaconQrCode\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "https://dasprids.de/",
"role": "Developer"
}
],
"description": "BaconQrCode is a QR code generator for PHP.",
"homepage": "https://github.com/Bacon/BaconQrCode",
"support": {
"issues": "https://github.com/Bacon/BaconQrCode/issues",
"source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.1"
},
"time": "2024-10-01T13:55:55+00:00"
},
{
"name": "brick/math",
"version": "0.14.0",
@@ -135,6 +189,56 @@
],
"time": "2024-02-09T16:56:22+00:00"
},
{
"name": "dasprid/enum",
"version": "1.0.7",
"source": {
"type": "git",
"url": "https://github.com/DASPRiD/Enum.git",
"reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce",
"reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce",
"shasum": ""
},
"require": {
"php": ">=7.1 <9.0"
},
"require-dev": {
"phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11",
"squizlabs/php_codesniffer": "*"
},
"type": "library",
"autoload": {
"psr-4": {
"DASPRiD\\Enum\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "https://dasprids.de/",
"role": "Developer"
}
],
"description": "PHP 7.1 enum implementation",
"keywords": [
"enum",
"map"
],
"support": {
"issues": "https://github.com/DASPRiD/Enum/issues",
"source": "https://github.com/DASPRiD/Enum/tree/1.0.7"
},
"time": "2025-09-16T12:23:56+00:00"
},
{
"name": "dflydev/dot-access-data",
"version": "v3.0.3",
@@ -1053,6 +1157,71 @@
],
"time": "2025-08-22T14:27:06+00:00"
},
{
"name": "laravel/fortify",
"version": "v1.31.2",
"source": {
"type": "git",
"url": "https://github.com/laravel/fortify.git",
"reference": "a046d52ee087ee52c9852b840cf4bbad19f10934"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/fortify/zipball/a046d52ee087ee52c9852b840cf4bbad19f10934",
"reference": "a046d52ee087ee52c9852b840cf4bbad19f10934",
"shasum": ""
},
"require": {
"bacon/bacon-qr-code": "^3.0",
"ext-json": "*",
"illuminate/support": "^10.0|^11.0|^12.0",
"php": "^8.1",
"pragmarx/google2fa": "^8.0",
"symfony/console": "^6.0|^7.0"
},
"require-dev": {
"mockery/mockery": "^1.0",
"orchestra/testbench": "^8.16|^9.0|^10.0",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^10.4|^11.3"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Fortify\\FortifyServiceProvider"
]
},
"branch-alias": {
"dev-master": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"Laravel\\Fortify\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Backend controllers and scaffolding for Laravel authentication.",
"keywords": [
"auth",
"laravel"
],
"support": {
"issues": "https://github.com/laravel/fortify/issues",
"source": "https://github.com/laravel/fortify"
},
"time": "2025-10-21T14:47:38+00:00"
},
{
"name": "laravel/framework",
"version": "v12.36.0",
@@ -1331,6 +1500,70 @@
},
"time": "2025-09-19T13:47:56+00:00"
},
{
"name": "laravel/sanctum",
"version": "v4.2.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/sanctum.git",
"reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/sanctum/zipball/fd6df4f79f48a72992e8d29a9c0ee25422a0d677",
"reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677",
"shasum": ""
},
"require": {
"ext-json": "*",
"illuminate/console": "^11.0|^12.0",
"illuminate/contracts": "^11.0|^12.0",
"illuminate/database": "^11.0|^12.0",
"illuminate/support": "^11.0|^12.0",
"php": "^8.2",
"symfony/console": "^7.0"
},
"require-dev": {
"mockery/mockery": "^1.6",
"orchestra/testbench": "^9.0|^10.0",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^11.3"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Sanctum\\SanctumServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Sanctum\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.",
"keywords": [
"auth",
"laravel",
"sanctum"
],
"support": {
"issues": "https://github.com/laravel/sanctum/issues",
"source": "https://github.com/laravel/sanctum"
},
"time": "2025-07-09T19:45:24+00:00"
},
{
"name": "laravel/serializable-closure",
"version": "v2.0.6",
@@ -2513,6 +2746,75 @@
],
"time": "2025-10-18T11:10:27+00:00"
},
{
"name": "paragonie/constant_time_encoding",
"version": "v3.1.3",
"source": {
"type": "git",
"url": "https://github.com/paragonie/constant_time_encoding.git",
"reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77",
"reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77",
"shasum": ""
},
"require": {
"php": "^8"
},
"require-dev": {
"infection/infection": "^0",
"nikic/php-fuzzer": "^0",
"phpunit/phpunit": "^9|^10|^11",
"vimeo/psalm": "^4|^5|^6"
},
"type": "library",
"autoload": {
"psr-4": {
"ParagonIE\\ConstantTime\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paragon Initiative Enterprises",
"email": "security@paragonie.com",
"homepage": "https://paragonie.com",
"role": "Maintainer"
},
{
"name": "Steve 'Sc00bz' Thomas",
"email": "steve@tobtu.com",
"homepage": "https://www.tobtu.com",
"role": "Original Developer"
}
],
"description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)",
"keywords": [
"base16",
"base32",
"base32_decode",
"base32_encode",
"base64",
"base64_decode",
"base64_encode",
"bin2hex",
"encoding",
"hex",
"hex2bin",
"rfc4648"
],
"support": {
"email": "info@paragonie.com",
"issues": "https://github.com/paragonie/constant_time_encoding/issues",
"source": "https://github.com/paragonie/constant_time_encoding"
},
"time": "2025-09-24T15:06:41+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.4",
@@ -2588,6 +2890,58 @@
],
"time": "2025-08-21T11:53:16+00:00"
},
{
"name": "pragmarx/google2fa",
"version": "v8.0.3",
"source": {
"type": "git",
"url": "https://github.com/antonioribeiro/google2fa.git",
"reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad",
"reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad",
"shasum": ""
},
"require": {
"paragonie/constant_time_encoding": "^1.0|^2.0|^3.0",
"php": "^7.1|^8.0"
},
"require-dev": {
"phpstan/phpstan": "^1.9",
"phpunit/phpunit": "^7.5.15|^8.5|^9.0"
},
"type": "library",
"autoload": {
"psr-4": {
"PragmaRX\\Google2FA\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Antonio Carlos Ribeiro",
"email": "acr@antoniocarlosribeiro.com",
"role": "Creator & Designer"
}
],
"description": "A One Time Password Authentication package, compatible with Google Authenticator.",
"keywords": [
"2fa",
"Authentication",
"Two Factor Authentication",
"google2fa"
],
"support": {
"issues": "https://github.com/antonioribeiro/google2fa/issues",
"source": "https://github.com/antonioribeiro/google2fa/tree/v8.0.3"
},
"time": "2024-09-05T11:56:40+00:00"
},
{
"name": "psr/clock",
"version": "1.0.0",

34
config/cors.php Normal file
View File

@@ -0,0 +1,34 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Cross-Origin Resource Sharing (CORS) Configuration
|--------------------------------------------------------------------------
|
| Here you may configure your settings for cross-origin resource sharing
| or "CORS". This determines what cross-origin operations may execute
| in web browsers. You are free to adjust these settings as needed.
|
| To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
*/
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => ['*'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => false,
];

View File

@@ -31,10 +31,10 @@ return [
'connections' => [
'pgsql' => [
'driver' => 'pgsql',
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DB_URL'),
'database' => env('DB_DATABASE', database_path('database.pgsql')),
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
'busy_timeout' => null,

159
config/fortify.php Normal file
View File

@@ -0,0 +1,159 @@
<?php
use Laravel\Fortify\Features;
return [
/*
|--------------------------------------------------------------------------
| Fortify Guard
|--------------------------------------------------------------------------
|
| Here you may specify which authentication guard Fortify will use while
| authenticating users. This value should correspond with one of your
| guards that is already present in your "auth" configuration file.
|
*/
'guard' => 'web',
/*
|--------------------------------------------------------------------------
| Fortify Password Broker
|--------------------------------------------------------------------------
|
| Here you may specify which password broker Fortify can use when a user
| is resetting their password. This configured value should match one
| of your password brokers setup in your "auth" configuration file.
|
*/
'passwords' => 'users',
/*
|--------------------------------------------------------------------------
| Username / Email
|--------------------------------------------------------------------------
|
| This value defines which model attribute should be considered as your
| application's "username" field. Typically, this might be the email
| address of the users but you are free to change this value here.
|
| Out of the box, Fortify expects forgot password and reset password
| requests to have a field named 'email'. If the application uses
| another name for the field you may define it below as needed.
|
*/
'username' => 'email',
'email' => 'email',
/*
|--------------------------------------------------------------------------
| Lowercase Usernames
|--------------------------------------------------------------------------
|
| This value defines whether usernames should be lowercased before saving
| them in the database, as some database system string fields are case
| sensitive. You may disable this for your application if necessary.
|
*/
'lowercase_usernames' => true,
/*
|--------------------------------------------------------------------------
| Home Path
|--------------------------------------------------------------------------
|
| Here you may configure the path where users will get redirected during
| authentication or password reset when the operations are successful
| and the user is authenticated. You are free to change this value.
|
*/
'home' => '/home',
/*
|--------------------------------------------------------------------------
| Fortify Routes Prefix / Subdomain
|--------------------------------------------------------------------------
|
| Here you may specify which prefix Fortify will assign to all the routes
| that it registers with the application. If necessary, you may change
| subdomain under which all of the Fortify routes will be available.
|
*/
'prefix' => '',
'domain' => null,
/*
|--------------------------------------------------------------------------
| Fortify Routes Middleware
|--------------------------------------------------------------------------
|
| Here you may specify which middleware Fortify will assign to the routes
| that it registers with the application. If necessary, you may change
| these middleware but typically this provided default is preferred.
|
*/
'middleware' => ['web'],
/*
|--------------------------------------------------------------------------
| Rate Limiting
|--------------------------------------------------------------------------
|
| By default, Fortify will throttle logins to five requests per minute for
| every email and IP address combination. However, if you would like to
| specify a custom rate limiter to call then you may specify it here.
|
*/
'limiters' => [
'login' => 'login',
'two-factor' => 'two-factor',
],
/*
|--------------------------------------------------------------------------
| Register View Routes
|--------------------------------------------------------------------------
|
| Here you may specify if the routes returning views should be disabled as
| you may not need them when building your own application. This may be
| especially true if you're writing a custom single-page application.
|
*/
'views' => false,
/*
|--------------------------------------------------------------------------
| Features
|--------------------------------------------------------------------------
|
| Some of the Fortify features are optional. You may disable the features
| by removing them from this array. You're free to only remove some of
| these features or you can even remove all of these if you need to.
|
*/
'features' => [
Features::registration(),
Features::resetPasswords(),
// Features::emailVerification(),
Features::updateProfileInformation(),
Features::updatePasswords(),
/*Features::twoFactorAuthentication([
'confirm' => true,
'confirmPassword' => true,
// 'window' => 0,
]),*/
],
];

84
config/sanctum.php Normal file
View File

@@ -0,0 +1,84 @@
<?php
use Laravel\Sanctum\Sanctum;
return [
/*
|--------------------------------------------------------------------------
| Stateful Domains
|--------------------------------------------------------------------------
|
| Requests from the following domains / hosts will receive stateful API
| authentication cookies. Typically, these should include your local
| and production domains which access your API via a frontend SPA.
|
*/
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
Sanctum::currentApplicationUrlWithPort(),
// Sanctum::currentRequestHost(),
))),
/*
|--------------------------------------------------------------------------
| Sanctum Guards
|--------------------------------------------------------------------------
|
| This array contains the authentication guards that will be checked when
| Sanctum is trying to authenticate a request. If none of these guards
| are able to authenticate the request, Sanctum will use the bearer
| token that's present on an incoming request for authentication.
|
*/
'guard' => ['web'],
/*
|--------------------------------------------------------------------------
| Expiration Minutes
|--------------------------------------------------------------------------
|
| This value controls the number of minutes until an issued token will be
| considered expired. This will override any values set in the token's
| "expires_at" attribute, but first-party sessions are not affected.
|
*/
'expiration' => null,
/*
|--------------------------------------------------------------------------
| Token Prefix
|--------------------------------------------------------------------------
|
| Sanctum can prefix new tokens in order to take advantage of numerous
| security scanning initiatives maintained by open source platforms
| that notify developers if they commit tokens into repositories.
|
| See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
|
*/
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
/*
|--------------------------------------------------------------------------
| Sanctum Middleware
|--------------------------------------------------------------------------
|
| When authenticating your first-party SPA with Sanctum you may need to
| customize some of the middleware Sanctum uses while processing the
| request. You may change the middleware listed below as required.
|
*/
'middleware' => [
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
],
];

View File

@@ -0,0 +1,23 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Listing>
*/
class ListingFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
//
];
}
}

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->id();
$table->morphs('tokenable');
$table->text('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamp('expires_at')->nullable()->index();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('personal_access_tokens');
}
};

View File

@@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->text('two_factor_secret')
->after('password')
->nullable();
$table->text('two_factor_recovery_codes')
->after('two_factor_secret')
->nullable();
$table->timestamp('two_factor_confirmed_at')
->after('two_factor_recovery_codes')
->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn([
'two_factor_secret',
'two_factor_recovery_codes',
'two_factor_confirmed_at',
]);
});
}
};

View File

@@ -0,0 +1,35 @@
<?php
use App\Models\User;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('listings', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->softDeletes();
$table->string('title')->nullable(false);
$table->text('description')->nullable(false);
$table->enum('condition', ['parts only', 'poor', 'fair', 'good', 'excellent'])->nullable();
$table->text('location')->nullable(false);
$table->decimal('price')->default(0.0);
$table->foreignIdFor(User::class)->nullable(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('listings');
}
};

View File

@@ -0,0 +1,17 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class ListingSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
}
}

View File

@@ -1,47 +0,0 @@
version: '3.8'
services:
laravel:
build:
context: .
dockerfile: Dockerfile
volumes:
- .:/var/www/html
environment:
DB_CONNECTION: pgsql
DB_HOST: db
DB_PORT: 5432
DB_DATABASE: laravel
DB_USERNAME: laravel
DB_PASSWORD: secret
ports:
- "5173:5173"
- "8000:8000"
networks:
- laravel-network
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_USER: laravel
POSTGRES_PASSWORD: secret
POSTGRES_DB: laravel
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 5s
timeout: 5s
retries: 5
networks:
- laravel-network
volumes:
postgres_data:
networks:
laravel-network:
driver: bridge

View File

@@ -23,8 +23,7 @@
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="BROADCAST_CONNECTION" value="null"/>
<env name="CACHE_STORE" value="array"/>
<env name="DB_CONNECTION" value="pgsql"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="DB_DATABASE" value="testing"/>
<env name="MAIL_MAILER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>

11
routes/api.php Normal file
View File

@@ -0,0 +1,11 @@
<?php
use App\Http\Controllers\ListingController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::get('/user', function (Request $request) {
return $request->user();
})->middleware('auth:sanctum');
Route::resource('/listing', ListingController::class);