first commit

This commit is contained in:
ct
2025-05-28 12:59:01 +08:00
commit 21526508b1
230 changed files with 60411 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Http\Controllers;
use Inertia\Inertia;
class AdminDashboardController extends Controller
{
public function index()
{
return Inertia::render('admin/dashboard');
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
use Inertia\Response;
class AuthenticatedSessionController extends Controller
{
/**
* Show the login page.
*/
public function create(Request $request): Response
{
return Inertia::render('auth/login', [
'canResetPassword' => Route::has('password.request'),
'status' => $request->session()->get('status'),
]);
}
/**
* Handle an incoming authentication request.
*/
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended(route('dashboard', absolute: false));
}
/**
* Destroy an authenticated session.
*/
public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
class ConfirmablePasswordController extends Controller
{
/**
* Show the confirm password page.
*/
public function show(): Response
{
return Inertia::render('auth/confirm-password');
}
/**
* Confirm the user's password.
*/
public function store(Request $request): RedirectResponse
{
if (! Auth::guard('web')->validate([
'email' => $request->user()->email,
'password' => $request->password,
])) {
throw ValidationException::withMessages([
'password' => __('auth.password'),
]);
}
$request->session()->put('auth.password_confirmed_at', time());
return redirect()->intended(route('dashboard', absolute: false));
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class EmailVerificationNotificationController extends Controller
{
/**
* Send a new email verification notification.
*/
public function store(Request $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false));
}
$request->user()->sendEmailVerificationNotification();
return back()->with('status', 'verification-link-sent');
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class EmailVerificationPromptController extends Controller
{
/**
* Show the email verification prompt page.
*/
public function __invoke(Request $request): Response|RedirectResponse
{
return $request->user()->hasVerifiedEmail()
? redirect()->intended(route('dashboard', absolute: false))
: Inertia::render('auth/verify-email', ['status' => $request->session()->get('status')]);
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
class NewPasswordController extends Controller
{
/**
* Show the password reset page.
*/
public function create(Request $request): Response
{
return Inertia::render('auth/reset-password', [
'email' => $request->email,
'token' => $request->route('token'),
]);
}
/**
* Handle an incoming new password request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'token' => 'required',
'email' => 'required|email',
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function ($user) use ($request) {
$user->forceFill([
'password' => Hash::make($request->password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
if ($status == Password::PasswordReset) {
return to_route('login')->with('status', __($status));
}
throw ValidationException::withMessages([
'email' => [__($status)],
]);
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Inertia\Inertia;
use Inertia\Response;
class PasswordResetLinkController extends Controller
{
/**
* Show the password reset link request page.
*/
public function create(Request $request): Response
{
return Inertia::render('auth/forgot-password', [
'status' => $request->session()->get('status'),
]);
}
/**
* Handle an incoming password reset link request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'email' => 'required|email',
]);
Password::sendResetLink(
$request->only('email')
);
return back()->with('status', __('A reset link will be sent if the account exists.'));
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Inertia\Inertia;
use Inertia\Response;
class RegisteredUserController extends Controller
{
/**
* Show the registration page.
*/
public function create(): Response
{
return Inertia::render('auth/register');
}
/**
* Handle an incoming registration request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|lowercase|email|max:255|unique:'.User::class,
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
event(new Registered($user));
Auth::login($user);
return to_route('dashboard');
}
}

View File

@@ -0,0 +1,137 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
use Illuminate\Validation\ValidationException;
class SanctumAuthController extends Controller
{
/**
* Register a new user and return a token
*
* @return \Illuminate\Http\JsonResponse
*/
public function register(Request $request)
{
try {
$request->validate([
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'confirmed', Password::defaults()],
]);
$user = User::create([
'email' => $request->email,
'password' => Hash::make($request->password),
]);
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json([
'success' => [
'data' => [
'token' => $token,
'user' => $user,
],
'message' => 'Registration completed successfully.',
],
], 201);
} catch (ValidationException $e) {
return response()->json([
'error' => [
'data' => $e->errors(),
'message' => 'Please review your inputs before submitting again.',
],
], 422);
} catch (\Exception $e) {
return response()->json([
'error' => [
'data' => [],
'message' => $e->getMessage(),
],
], 500);
}
}
/**
* Login a user and return a token
*
* @return \Illuminate\Http\JsonResponse
*/
public function login(Request $request)
{
try {
$request->validate([
'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'],
]);
if (! Auth::attempt($request->only('email', 'password'))) {
return response()->json([
'error' => [
'data' => [],
'message' => 'Invalid credentials provided.',
],
], 401);
}
$user = User::where('email', $request->email)->firstOrFail();
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json([
'success' => [
'data' => [
'token' => $token,
'user' => $user,
],
'message' => 'Authentication successful.',
],
]);
} catch (ValidationException $e) {
return response()->json([
'error' => [
'data' => $e->errors(),
'message' => 'Please review your inputs before submitting again.',
],
], 422);
} catch (\Exception $e) {
return response()->json([
'error' => [
'data' => [],
'message' => $e->getMessage(),
],
], 500);
}
}
/**
* Logout the user (revoke the token)
*
* @return \Illuminate\Http\JsonResponse
*/
public function logout(Request $request)
{
try {
$request->user()->currentAccessToken()->delete();
return response()->json([
'success' => [
'data' => [],
'message' => 'Successfully signed out.',
],
]);
} catch (\Exception $e) {
return response()->json([
'error' => [
'data' => [],
'message' => $e->getMessage(),
],
], 500);
}
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
class VerifyEmailController extends Controller
{
/**
* Mark the authenticated user's email address as verified.
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
/** @var \Illuminate\Contracts\Auth\MustVerifyEmail $user */
$user = $request->user();
event(new Verified($user));
}
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
}

View File

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

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Http\Controllers;
use App;
use Inertia\Inertia;
class FrontHomeController extends Controller
{
public function index()
{
if (App::environment('local')) {
return Inertia::render('welcome');
}
return Inertia::render('comingsoon');
}
}

View File

@@ -0,0 +1,377 @@
<?php
namespace App\Http\Controllers;
use App\Helpers\FirstParty\Render\RenderConstants;
use App\Models\User;
use App\Models\Video;
use App\Models\VideoCaption;
use App\Models\VideoElement;
use App\Models\VideoRender;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Validator;
use stdClass;
use Str;
class RenderController extends Controller
{
public function getVideoElements(Request $request, string $uuid)
{
if (! Str::isUuid($uuid)) {
return response()->json([
'error' => [
'message' => 'Invalid UUID format.',
],
], 400);
}
$video = Video::with('video_elements')->where('uuid', $uuid)->first();
if (! $video) {
return response()->json([
'error' => [
'message' => 'Video not found.',
],
]);
}
return response()->json((object) [
'success' => [
'data' => [
'video_elements' => $video->video_elements,
],
],
]);
if (! $video_render) {
return response()->json([
'error' => [
'message' => 'Video render not found.',
],
]);
}
$video = Video::where('id', $video_render->video_id)->first();
if (! $video) {
return response()->json([
'error' => [
'message' => 'Video not found.',
],
]);
}
}
public function startRender(Request $request)
{
$video_render_request = array_to_object_2025($request->all());
$video_render_action = $this->saveUserVideoRenderRequest(Auth::user(), $video_render_request);
if (! $video_render_action->success) {
$error_message = $video_render_action?->message ? $video_render_action->message : 'Unable to render, possibly because the video is already being rendered. Check external ID.';
return response()->json([
'error' => [
'message' => $error_message,
],
]);
}
// Create a video
return response()->json((object) [
'success' => [
'data' => [
'uuid' => $video_render_action->model->uuid,
'status' => $video_render_action->model->status,
],
],
]);
}
public function renderStatus(Request $request, string $uuid)
{
if (! Str::isUuid($uuid)) {
return response()->json([
'error' => [
'message' => 'Invalid UUID.',
],
]);
}
$video_render = VideoRender::where('uuid', $uuid)->first();
if (! $video_render) {
return response()->json([
'error' => [
'message' => 'Video render not found.',
],
]);
}
return response()->json((object) [
'success' => [
'data' => [
'uuid' => $video_render->uuid,
'status' => $video_render->status,
],
],
]);
}
public function allRenders(Request $request)
{
$user = Auth::user();
$video_renders = VideoRender::where('user_id', $user->id)
->orderBy('id', 'desc')->get();
return response()->json((object) [
'success' => [
'data' => [
'video_renders' => $video_renders,
],
],
]);
}
private function saveUserVideoRenderRequest(User $user, stdClass $video_render_request)
{
// check if there is an existing video render request with the same external id
$video_render_is_busy = VideoRender::where('user_id', $user->id)
->where('external_id', $video_render_request->external_id)
->whereIn('status', [
RenderConstants::STATUS_PLANNED,
RenderConstants::STATUS_WAITING,
RenderConstants::STATUS_TRANSCRIBING,
RenderConstants::STATUS_RENDERING,
])->first();
if ($video_render_is_busy) {
return (object) [
'success' => false,
'message' => 'Video is already in queue or rendering. Status: '.$video_render_is_busy->status,
];
}
// dd($video_render_request);
$video = $this->getUserVideoByExternalId($user, $video_render_request->external_id);
$video = $this->updateVideoWithRenderRequest($video, $video_render_request);
$video = $this->saveUserVideo($video);
$this->saveVideoCaptions($video);
try {
$this->saveVideoElements($video);
} catch (\Exception $e) {
return (object) [
'success' => false,
'message' => $e->getMessage(),
];
}
$new_video_render = VideoRender::create([
'user_id' => $user->id,
'video_id' => $video->id,
'external_id' => $video_render_request->external_id,
'payload' => $video_render_request,
'status' => RenderConstants::STATUS_PLANNED,
]);
return (object) [
'success' => true,
'model' => $new_video_render,
];
}
private function saveVideoCaptions(Video $video)
{
VideoCaption::where('video_id', $video->id)->delete();
if (isset($video->payload->captions)) {
foreach ($video->payload->captions as $caption) {
$video_caption = new VideoCaption;
$video_caption->video_id = $video->id;
$video_caption->time = $caption->time;
$video_caption->duration = $caption->duration;
$video_caption->text = $caption->text;
if (isset($caption->parameters)) {
$video_caption->parameters = $caption->parameters;
}
$video_caption->words = $caption->words;
$video_caption->save();
}
}
}
private function saveVideoElements(Video $video)
{
if (isset($video->payload->elements)) {
$existing_video_elements = VideoElement::where('video_id', $video->id)->get();
// Create a lookup array of existing elements by asset hash, but keep ALL matching elements
$existing_elements_by_hash = [];
foreach ($existing_video_elements as $existing_element) {
if (! isset($existing_elements_by_hash[$existing_element->asset_hash])) {
$existing_elements_by_hash[$existing_element->asset_hash] = [];
}
$existing_elements_by_hash[$existing_element->asset_hash][] = $existing_element;
}
// Track which elements we're keeping
$kept_element_ids = [];
// Track which hashes we've already processed to handle duplicates
$processed_hashes = [];
// Validate element URL if exist
foreach ($video->payload->elements as $element) {
if (isset($element->url)) {
if (! $this->validateElementUrl($element->url)) {
throw new \Exception('Invalid URL: '.$element->url);
}
}
}
// Save
foreach ($video->payload->elements as $element) {
$asset_hash = $this->getAssetHash($video, $element->url);
if (isset($existing_elements_by_hash[$asset_hash]) && count($existing_elements_by_hash[$asset_hash]) > 0) {
// Get the next unused element with this hash
$unused_elements = array_filter($existing_elements_by_hash[$asset_hash], function ($elem) use ($kept_element_ids) {
return ! in_array($elem->id, $kept_element_ids);
});
if (count($unused_elements) > 0) {
// Use the first unused element
$video_element = reset($unused_elements);
$kept_element_ids[] = $video_element->id;
} else {
// All elements with this hash are already used, create a new one
$video_element = new VideoElement;
$video_element->video_id = $video->id;
$video_element->asset_hash = $asset_hash;
$video_element->original_asset_url = $element->url;
}
} else {
// No elements with this hash, create a new one
$video_element = new VideoElement;
$video_element->video_id = $video->id;
$video_element->asset_hash = $asset_hash;
$video_element->original_asset_url = $element->url;
}
if (isset($element->external_reference) && ! is_empty($element->external_reference)) {
$video_element->external_reference = $element->external_reference;
}
$video_element->type = $element->type;
$video_element->time = $element->time;
$video_element->track = $element->track;
$video_element->duration = $element->duration;
if (isset($element->parameters)) {
$video_element->parameters = $element->parameters;
}
$video_element->save();
// Add newly created ID to kept list if needed
if (! in_array($video_element->id, $kept_element_ids)) {
$kept_element_ids[] = $video_element->id;
}
}
// Delete elements that weren't in the payload
if (count($existing_video_elements) > 0) {
VideoElement::where('video_id', $video->id)
->whereNotIn('id', $kept_element_ids)
->delete();
}
}
}
private function getAssetHash(Video $video, $url)
{
return hash('sha256', $video->id.'-'.$url);
}
private function validateElementUrl(string $url)
{
// First check if it's a valid URL format
$validator = Validator::make(['url' => $url], [
'url' => 'required|url',
]);
if ($validator->fails()) {
return false;
}
// validate url by making a http head request, return true | false boolean
try {
// Using Laravel's HTTP client to make a HEAD request
$response = Http::withOptions([
'timeout' => 10,
'connect_timeout' => 5,
'verify' => true,
'http_errors' => false, // Don't throw exceptions for 4xx/5xx responses
])->head($url);
// Check if the response is successful (2xx status code) or a redirect (3xx)
return $response->successful();
} catch (\Exception $e) {
// Catch any exceptions (connection issues, invalid URLs, etc.)
return false;
}
}
private function saveUserVideo(Video $video)
{
if ($video->isDirty()) {
$video->save();
}
return $video;
}
private function updateVideoWithRenderRequest(Video $video, stdClass $video_render_request)
{
// dd($video_render_request);
$video->content_type = $video_render_request->content_type;
$video->width = $video_render_request->width;
$video->height = $video_render_request->height;
$video->aspect_ratio = $video_render_request->aspect_ratio;
$video->payload = $video_render_request;
$video->render_settings = (object) [
'video_bitrate' => $video_render_request->video_bitrate,
'audio_bitrate' => $video_render_request->audio_bitrate,
'fps' => $video_render_request->fps,
];
return $video;
}
private function getUserVideoByExternalId(User $user, string $external_id)
{
$video = Video::where('user_id', $user->id)
->where('external_id', $external_id)
->first();
if (! $video) {
$video = new Video;
$video->user_id = $user->id;
$video->external_id = $external_id;
}
return $video;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers\Settings;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
use Inertia\Inertia;
use Inertia\Response;
class PasswordController extends Controller
{
/**
* Show the user's password settings page.
*/
public function edit(): Response
{
return Inertia::render('settings/password');
}
/**
* Update the user's password.
*/
public function update(Request $request): RedirectResponse
{
$validated = $request->validate([
'current_password' => ['required', 'current_password'],
'password' => ['required', Password::defaults(), 'confirmed'],
]);
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
return back();
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Http\Controllers\Settings;
use App\Http\Controllers\Controller;
use App\Http\Requests\Settings\ProfileUpdateRequest;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Inertia\Inertia;
use Inertia\Response;
class ProfileController extends Controller
{
/**
* Show the user's profile settings page.
*/
public function edit(Request $request): Response
{
return Inertia::render('settings/profile', [
'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail,
'status' => $request->session()->get('status'),
]);
}
/**
* Update the user's profile settings.
*/
public function update(ProfileUpdateRequest $request): RedirectResponse
{
$request->user()->fill($request->validated());
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
}
$request->user()->save();
return to_route('profile.edit');
}
/**
* Delete the user's account.
*/
public function destroy(Request $request): RedirectResponse
{
$request->validate([
'password' => ['required', 'current_password'],
]);
$user = $request->user();
Auth::logout();
$user->delete();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers;
use App\Jobs\RunVideoRenderPipelineJob;
use App\Models\Video;
class TestController extends Controller
{
public function RunVideoRenderPipelineJob()
{
$dispatch_job = true;
$video = Video::latest()->first();
if ($video) {
if ($dispatch_job) {
RunVideoRenderPipelineJob::dispatch($video->id)->onQueue('render');
} else {
$job = new RunVideoRenderPipelineJob($video->id);
$job->handle();
}
} else {
echo 'NO VIDEO';
}
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Http\Controllers;
use Inertia\Inertia;
class UserDashboardController extends Controller
{
public function index()
{
return Inertia::render('dashboard');
}
}