initial commit
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run

This commit is contained in:
Edward Tirado Jr 2025-12-12 23:07:04 -06:00
commit 0c42bef077
109 changed files with 16545 additions and 0 deletions

View file

@ -0,0 +1,38 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
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, [
'username' => ['required', 'string', 'max:255'],
'email' => [
'required',
'string',
'email',
'max:255',
Rule::unique(User::class),
],
//'password' => $this->passwordRules(),
])->validate();
return User::create([
'username' => $input['username'],
'email' => $input['email'],
]);
}
}

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,28 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
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' => $input['password'],
])->save();
}
}

View file

@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View file

@ -0,0 +1,22 @@
<?php
namespace App\Livewire\Actions;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;
class Logout
{
/**
* Log the current user out of the application.
*/
public function __invoke()
{
Auth::guard('web')->logout();
Session::invalidate();
Session::regenerateToken();
return redirect('/');
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace App\Livewire\Auth;
use App\Livewire\Forms\PasswordResetForm;
use App\Mail\PasswordResetNewUser;
use App\Models\User;
use App\Models\UserProfile;
use Exception;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Password;
use Livewire\Attributes\Layout;
use Livewire\Component;
use function Laravel\Prompts\error;
class PasswordReset extends Component
{
public PasswordResetForm $form;
public string $token;
public function mount($token)
{
$this->token = $token;
}
#[Layout('components.layouts.auth')]
public function resetPassword()
{
logger()->info("Validating password reset...");
logger()->info($this->form);
$validated = $this->form->validate();
logger()->info("Validated password reset", $validated);
$status = Password::reset(array_merge($validated, ['token' => $this->token]),
function (User $user, string $password) {
$user->forceFill([
'password' => Hash::make($password),
])->save();
});
if ($status === Password::PASSWORD_RESET) {
//Mail::to($user->email)->send(new PasswordResetNewUser($user));
return redirect()->route('login');
}
logger()->error("Password reset failed", $status);
$this->addError('email', 'The provided credentials do not match our records.');
}
public function render()
{
return view('pages.auth.reset-password');
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace App\Livewire\Auth;
use App\Livewire\Forms\RegisterUserForm;
use App\Mail\PasswordResetNewUser;
use App\Models\User;
use App\Models\UserProfile;
use Illuminate\Http\RedirectResponse;
use Illuminate\Log\Logger;
use Illuminate\Support\Facades\Mail;
use Illuminate\View\View;
use Livewire\Component;
class RegisterUser extends Component
{
public RegisterUserForm $form;
public function register()
{
logger()->info("Validating...");
logger()->info($this->form->toArray());
$validated = $this->form->validate();
logger()->info("Validated", $validated);
$user = User::create($this->form->all());
UserProfile::create(["user_id" => $user->id]);
Mail::to($user->email)->send(new PasswordResetNewUser($user));
logger()->info("New user registered: " . $user->email);
return redirect()->route('login');
}
public function render()
{
return view('pages.auth.register');
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace App\Livewire\Forms;
use Livewire\Form;
class MovieListForm extends Form
{
public string $name = "";
public bool $is_public = false;
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'is_public' => ['required', 'boolean'],
];
}
}

View file

@ -0,0 +1,11 @@
<?php
namespace App\Livewire\Forms;
use Livewire\Attributes\Validate;
use Livewire\Form;
class MovieSearchForm extends Form
{
//
}

View file

@ -0,0 +1,21 @@
<?php
namespace App\Livewire\Forms;
use Livewire\Attributes\Validate;
use Livewire\Form;
class PasswordResetForm extends Form
{
public string $email = "";
public string $password = "";
public string $password_confirmation = "";
public function rules(): array
{
return [
'email' => ['required', 'email', 'exists:users,email'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
];
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace App\Livewire\Forms;
use Livewire\Attributes\Validate;
use Livewire\Form;
class RegisterUserForm extends Form
{
public string $email = "";
public string $username = "";
public function rules(): array
{
return [
"email" => ["required", "email", "unique:users,email"],
"username" => ["required", "unique:users,username"],
];
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace App\Livewire;
use App\Livewire\Forms\MovieListForm;
use App\Models\Interfaces\MovieDbInterface;
use App\Models\MovieList as MovieListModel;
use Livewire\Component;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class MovieList extends Component
{
public int $id;
public string $query;
public MovieListModel $list;
public $movies = [];
public MovieListForm $form;
public function mount($id): void
{
$this->id = $id;
$this->getList();
}
public function getList()
{
$list = MovieListModel::with('movies')
->find($this->id);
if ($list) {
$this->list = $list;
$this->movies = $list->movies;
} else {
abort(404);
}
}
public function addMovie(MovieDbInterface $movie_db)
{
$this->resetErrorBag();
try {
$movie = $movie_db->search($this->query);
} catch (NotFoundHttpException $e) {
$this->addError('query', 'Movie not found');
return;
}
$this->list->movies()->syncWithoutDetaching($movie->id);
$this->getList();
$this->form->reset();
}
public function render()
{
return view('livewire.movie-list');
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace App\Livewire;
use App\Livewire\Forms\MovieListForm;
use App\Models\MovieList;
use Livewire\Component;
class MovieLists extends Component
{
public MovieListForm $form;
public $lists = [];
public function addList(): void
{
if (!auth()->check()) {
$this->redirectRoute('login');
return;
}
$user = auth()->user();
$validated = $this->form->validate();
MovieList::create(array_merge($validated, ["user_id" => $user->id]));
$this->getLists();
$this->form->reset();
}
public function getLists()
{
$this->lists = MovieList::all();
}
public function render()
{
$this->getLists();
return view('livewire.lists');
}
}

View file

@ -0,0 +1,53 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class PasswordResetExistingUser extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*/
public function __construct()
{
//
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: 'Password Reset Existing User',
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
view: 'view.name',
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Mail\Mailables\Attachment;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Password;
use Laravel\Fortify\Fortify;
class PasswordResetNewUser extends Mailable
{
use Queueable, SerializesModels;
public string $token;
/**
* Create a new message instance.
*/
public function __construct(private User $user)
{
logger()->debug("\n==============================\n User email is $user->email. Username is $user->username \n=====================================");
$this->token = Password::createToken($this->user);
logger()->debug("Password reset token is $this->token");
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: 'Welcome to Movie Night!',
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
view: 'mail.password-reset-new-user',
);
}
/**
* Get the attachments for the message.
*
* @return array<int, Attachment>
*/
public function attachments(): array
{
return [];
}
}

View file

@ -0,0 +1,8 @@
<?php
namespace App\Models\Interfaces;
interface MovieDbInterface
{
public function search(string $query, array $options);
}

47
app/Models/Movie.php Normal file
View file

@ -0,0 +1,47 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Movie extends Model
{
protected $fillable = [
'title',
'imdb_id',
'year',
'director',
'actors',
'plot',
'genre',
'mpaa_rating',
'critic_scores',
'poster',
'added_by',
];
public static function fromOmdb(array $omdb): self
{
$attributes = [
'title' => $omdb['Title'],
'imdb_id' => $omdb['imdbID'],
'year' => $omdb['Year'],
'director' => $omdb['Director'],
'actors' => $omdb['Actors'],
'plot' => $omdb['Plot'],
'genre' => $omdb['Genre'],
'mpaa_rating' => $omdb['Rated'],
'critic_scores' => json_encode($omdb['Ratings']),
'poster' => $omdb['Poster'],
'added_by' => $omdb['added_by'],
];
return static::create($attributes);
}
public function movieLists(): belongsToMany
{
return $this->belongsToMany(MovieList::class);
}
}

21
app/Models/MovieList.php Normal file
View file

@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
class MovieList extends Model
{
protected $fillable = [
'user_id',
'name',
'is_public'
];
public function movies(): BelongsToMany
{
return $this->belongsToMany(Movie::class);
}
}

10
app/Models/Schedule.php Normal file
View file

@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Schedule extends Model
{
//
}

10
app/Models/Showing.php Normal file
View file

@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Showing extends Model
{
//
}

71
app/Models/User.php Normal file
View file

@ -0,0 +1,71 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Str;
use Laravel\Fortify\TwoFactorAuthenticatable;
class User extends Authenticatable
{
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable, TwoFactorAuthenticatable;
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'username',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'password',
'two_factor_secret',
'two_factor_recovery_codes',
'remember_token',
];
public function profile(): HasOne
{
return $this->hasOne(UserProfile::class);
}
/**
* Get the user's initials
*/
//public function initials(): string
//{
// return Str::of($this->name)
// ->explode(' ')
// ->take(2)
// ->map(fn($word) => Str::substr($word, 0, 1))
// ->implode('');
//}
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class UserProfile extends Model
{
protected $fillable = [
"user_id",
"first_name",
"last_name",
"location",
"date_of_birth",
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace App\Providers;
use App\Models\Interfaces\MovieDbInterface;
use App\Services\OmdbService;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
$this->app->bind(MovieDbInterface::class, OmdbService::class);
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}

View file

@ -0,0 +1,76 @@
<?php
namespace App\Providers;
use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\ResetUserPassword;
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\Fortify;
class FortifyServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
$this->configureActions();
$this->configureViews();
$this->configureRateLimiting();
Fortify::requestPasswordResetLinkView(function () {
return view('pages.auth.reset-password');
});
}
/**
* Configure Fortify actions.
*/
private function configureActions(): void
{
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
Fortify::createUsersUsing(CreateNewUser::class);
}
/**
* Configure Fortify views.
*/
private function configureViews(): void
{
Fortify::loginView(fn() => view('pages.auth.login'));
//Fortify::verifyEmailView(fn () => view('livewire.auth.verify-email'));
//Fortify::twoFactorChallengeView(fn () => view('livewire.auth.two-factor-challenge'));
//Fortify::confirmPasswordView(fn () => view('livewire.auth.confirm-password'));
//Fortify::registerView(fn() => view('pages.auth.register'));
//Fortify::resetPasswordView(fn() => view('pages.auth.reset-password'));
//Fortify::requestPasswordResetLinkView(fn () => view('livewire.auth.forgot-password'));
}
/**
* Configure rate limiting.
*/
private function configureRateLimiting(): void
{
RateLimiter::for('two-factor', function (Request $request) {
return Limit::perMinute(5)->by($request->session()->get('login.id'));
});
RateLimiter::for('login', function (Request $request) {
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())) . '|' . $request->ip());
return Limit::perMinute(5)->by($throttleKey);
});
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace App\Services;
use App\Models\Interfaces\MovieDbInterface;
use App\Models\Movie;
use App\Models\User;
use Exception;
use Illuminate\Support\Facades\Http;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class OmdbService implements MovieDbInterface
{
private string $url;
private User $user;
public function __construct(private Movie $movie)
{
$this->url = config('services.omdb.api_url') . config('services.omdb.api_key');
$this->user = auth()->user();
}
public function search(string $query, array $options = [])
{
$movie = Http::get($this->url . "&t=" . $query)->json();
if ($movie['Response'] !== 'True') {
throw new NotFoundHttpException("Movie not found");
}
logger()->debug("Movie from OMDB: " . json_encode($movie));
try {
$existing_movie = $this->movie->where('imdb_id', $movie['imdbID'])->firstOrFail();
return $existing_movie;
} catch (Exception $e) {
return Movie::fromOmdb(array_merge(
$movie,
["added_by" => $this->user->id])
);
}
}
}