diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ad83b35 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,69 @@ +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/go/build-context-dockerignore/ + +# Database +database/database.sqlite + +**/.DS_Store +**/__pycache__ +**/.venv +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +# **/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +/.phpunit.cache +/node_modules + +# Is built later on +/public/build + +/public/hot +/public/storage +/storage/*.key +/storage/pail +/vendor +.env +.env.backup +.env.production +.env.example +.phpactor.json +.phpunit.result.cache +Homestead.json +Homestead.yaml +npm-debug.log +yarn-error.log +/auth.json +/.fleet +/.idea +/.nova +/.vscode +/.zed + +# Browser +app/Browser/console/** +app/Browser/screenshots/** +app/Browser/source/** diff --git a/.env.docker b/.env.docker new file mode 100644 index 0000000..4f8b1c7 --- /dev/null +++ b/.env.docker @@ -0,0 +1,69 @@ +APP_NAME=DatBrowser +APP_ENV=production +APP_KEY=base64:O3G9GpqYI7ZkAvkffIiS+5oodFGe/yAQ/uIcSIqU3NU= +APP_DEBUG=true +APP_TIMEZONE=UTC +APP_URL=http://127.0.0.1 + +APP_LOCALE=fr +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=mysql +DB_HOST=db +DB_PORT=3306 +DB_DATABASE=datbrowser +DB_USERNAME=datboi +DB_PASSWORD=IMDATBOI + +SESSION_DRIVER=file +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_STORE=database +CACHE_PREFIX= + +MEMCACHED_HOST=127.0.0.1 + +# REDIS_CLIENT=phpredis +# REDIS_HOST=127.0.0.1 +# REDIS_PASSWORD=null +# REDIS_PORT=6379 + +# MAIL_MAILER=log +# MAIL_SCHEME=null +# MAIL_HOST=127.0.0.1 +# MAIL_PORT=2525 +# MAIL_USERNAME=null +# MAIL_PASSWORD=null +# MAIL_FROM_ADDRESS="hello@example.com" +# MAIL_FROM_NAME="${APP_NAME}" + +# AWS_ACCESS_KEY_ID= +# AWS_SECRET_ACCESS_KEY= +# AWS_DEFAULT_REGION=us-east-1 +# AWS_BUCKET= +# AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" +VITE_APP_URL="${APP_URL}" + +DUSK_DRIVER_URL="http://undetected-chromedriver:4444" diff --git a/.env.example b/.env.example index 1d73228..7502c8f 100644 --- a/.env.example +++ b/.env.example @@ -3,9 +3,9 @@ APP_ENV=local APP_KEY= APP_DEBUG=true APP_TIMEZONE=UTC -APP_URL=http://localhost:8000 +APP_URL=http://127.0.0.1:8000 -APP_LOCALE=en +APP_LOCALE=fr APP_FALLBACK_LOCALE=en APP_FAKER_LOCALE=en_US @@ -43,24 +43,26 @@ CACHE_PREFIX= MEMCACHED_HOST=127.0.0.1 -REDIS_CLIENT=phpredis -REDIS_HOST=127.0.0.1 -REDIS_PASSWORD=null -REDIS_PORT=6379 +# REDIS_CLIENT=phpredis +# REDIS_HOST=127.0.0.1 +# REDIS_PASSWORD=null +# REDIS_PORT=6379 -MAIL_MAILER=log -MAIL_SCHEME=null -MAIL_HOST=127.0.0.1 -MAIL_PORT=2525 -MAIL_USERNAME=null -MAIL_PASSWORD=null -MAIL_FROM_ADDRESS="hello@example.com" -MAIL_FROM_NAME="${APP_NAME}" +# MAIL_MAILER=log +# MAIL_SCHEME=null +# MAIL_HOST=127.0.0.1 +# MAIL_PORT=2525 +# MAIL_USERNAME=null +# MAIL_PASSWORD=null +# MAIL_FROM_ADDRESS="hello@example.com" +# MAIL_FROM_NAME="${APP_NAME}" -AWS_ACCESS_KEY_ID= -AWS_SECRET_ACCESS_KEY= -AWS_DEFAULT_REGION=us-east-1 -AWS_BUCKET= -AWS_USE_PATH_STYLE_ENDPOINT=false +# AWS_ACCESS_KEY_ID= +# AWS_SECRET_ACCESS_KEY= +# AWS_DEFAULT_REGION=us-east-1 +# AWS_BUCKET= +# AWS_USE_PATH_STYLE_ENDPOINT=false VITE_APP_NAME="${APP_NAME}" + +DUSK_DRIVER_URL="http://127.0.0.1:4444" diff --git a/.gitea/workflows/pushImage.yaml b/.gitea/workflows/pushImage.yaml new file mode 100644 index 0000000..503c606 --- /dev/null +++ b/.gitea/workflows/pushImage.yaml @@ -0,0 +1,23 @@ +name: Push Image to registry +on: + push: + branches: + - master + + +jobs: + build-image: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + registry: ${REGISTRY} + username: ${{ gitea.actor }} + password: ${{ secrets.REGISTRY_TOKEN }} + - name: Build and push + run: | + docker build -t ${REGISTRY}/${REPO_OWNER}/${IMAGE_NAME}:latest . + docker push ${REGISTRY}/${REPO_OWNER}/${IMAGE_NAME}:latest diff --git a/.gitignore b/.gitignore index c7cf1fa..8c7dfe1 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,7 @@ yarn-error.log /.nova /.vscode /.zed +# Browser +app/Browser/console +app/Browser/screenshots +app/Browser/source diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e89e536 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,86 @@ + +# INSTALL PHP COMPOSER DEPENDENCIES +FROM composer:lts AS composer-deps + +WORKDIR / + +# If your composer.json file defines scripts that run during dependency installation and +# reference your application source files, uncomment the line below to copy all the files +# into this layer. +# COPY composer.json composer.lock ./ +COPY . . + +# Download dependencies as a separate step to take advantage of Docker's caching. +# Leverage a bind mounts to composer.json and composer.lock to avoid having to copy them +# into this layer. +# Leverage a cache mount to /tmp/cache so that subsequent builds don't have to re-download packages. +RUN --mount=type=bind,source=composer.json,target=composer.json \ +--mount=type=bind,source=composer.lock,target=composer.lock \ +--mount=type=cache,target=/tmp/cache \ +composer install --no-interaction --prefer-dist + +# ======================================== + +# BUILD VUE APP +FROM node:20 AS build-vue + +WORKDIR /usr/app + +# COPY package.json tsconfig.json tailwind.config.js vite.config.js ./ +# COPY resources/ ./resources/ +COPY . . +RUN mv .env.docker .env +COPY --from=composer-deps /vendor/ ./vendor +RUN mkdir -p public/build/ && npm i && npm run build + +# ======================================== + +# RUN +FROM php:8.2-alpine AS final + +# Install system dependencies +RUN apk update && apk add --no-cache \ + git \ + curl \ + libpng-dev \ + libxml2-dev \ + libzip-dev \ + zip \ + unzip \ + supervisor \ + nginx \ + openssl \ + linux-headers + +RUN docker-php-ext-configure zip && docker-php-ext-install zip +RUN docker-php-ext-install gd pdo pdo_mysql zip + + +# Get latest Composer +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer + +WORKDIR /var/www + +COPY . . +COPY --from=build-vue /usr/app/public/build ./public/build +RUN mv .env.docker .env + +# DUSK +# RUN php artisan dusk:install && php artisan dusk:chrome-driver && mv ./undetectedChromedriver/chromedriver-linux ./vendor/laravel/dusk/bin/chromedriver-linux +# RUN php artisan dusk:install && php artisan dusk:chrome-driver +# RUN php artisan dusk:install + +RUN composer install --no-interaction --prefer-dist + +# Link the storage directory to the public directory. +RUN php artisan storage:link + +# Laravel optimization commands +# RUN php artisan cache:clear +RUN php artisan config:cache && php artisan route:cache + +RUN chmod +x ./dockerEntryPoint.sh + +EXPOSE 80 + +CMD ["./dockerEntryPoint.sh"] diff --git a/app/Browser/BrowserJob.php b/app/Browser/BrowserJob.php new file mode 100644 index 0000000..06462dd --- /dev/null +++ b/app/Browser/BrowserJob.php @@ -0,0 +1,272 @@ +jobId = $jobId; + } + + /** + * Execute the callback in a browser + * @param Closure $callback function with a Browser as parameter + * @return void + */ + private function executeInBrowser(Closure $callback): ?JobRun + { + + $this->prepare(); + $this->setUp(); + + $this->browse(function (Browser $browser) use ($callback, &$log) { + try { + $log = $callback($browser); + // } catch (Exception $e) { + // $browser->screenshot("failure-{$this->jobId}"); + // dump($e); + // throw $e; + } + catch (Throwable $e) { + $browser->screenshot("failure-{$this->jobId}"); + dump($e); + throw $e; + } finally { + $browser->quit(); + return []; + } + }); + + return $log; + } + + /** + * Execute the job + * @return void + */ + public function execute(): ?JobRun { + return $this->executeInBrowser(function (Browser $browser): ?JobRun { + return $this->run($browser); + }); + } + + /** + * Execute the job test + * @return void + */ + public function executeTest(): ?JobRun { + return $this->executeInBrowser(function (Browser $browser): ?JobRun { + return $this->runTest($browser); + }); + } + + /** + * Steps that run in the browser + * @param \Laravel\Dusk\Browser $browser + * @return void + */ + abstract public function run(Browser $browser): ?JobRun; + + abstract public function runTest(Browser $browser): ?JobRun; + + /** + * Prepare for Dusk test execution. + * @unused + */ + #[BeforeClass] + public static function prepare(): void + { + if (config("dusk.driver.url") == null && !(isset($_ENV['LARAVEL_SAIL']) && $_ENV['LARAVEL_SAIL'] == '1')) { + static::startChromeDriver(['--port=9515']); + } + } + + /** + * Register the base URL with Dusk. + * + * @return void + */ + protected function setUp(): void + { + Browser::$baseUrl = "https://pdftools.matthiasg.dev/"; + + Browser::$storeScreenshotsAt = base_path('app/Browser/screenshots'); + + Browser::$storeConsoleLogAt = base_path('app/Browser/console'); + + Browser::$storeSourceAt = base_path('app/Browser/source'); + + /*Browser::$userResolver = function () { + return $this->user(); + }; */ + } + + protected function makeSimpleJobRun(bool $success, string $name, string $content): JobRun { + $artifact = new JobRun([ + "job_id" => $this->jobId, + "success" => $success + ]); + $artifact->save(); + $artifact->artifacts()->save(new JobArtifact([ + "name" => $name, + "content" => $content, + ])); + return $artifact; + } + + /** + * Create the RemoteWebDriver instance. + */ + protected function driver(): RemoteWebDriver + { + $options = (new ChromeOptions)->addArguments(collect([ + $this->shouldStartMaximized() ? '--start-maximized' : '--window-size=1360,1020', + '--disable-search-engine-choice-screen', + '--disable-gpu', + '--no-sandbox', + '--disable-setuid-sandbox', + '--whitelisted-ips=""', + '--disable-dev-shm-usage', + '--user-data-dir=/home/seluser/profile/', + ])->all()); + + return RemoteWebDriver::create( + config("dusk.driver.url", 'http://localhost:9515'), + DesiredCapabilities::chrome()->setCapability( + ChromeOptions::CAPABILITY, + $options + ) + ); + } + + public function terminate() { + $this->browse(function (Browser $browser) { + $browser->quit(); + }); + } + + /** + * Determine whether the Dusk command has disabled headless mode. + */ + protected function hasHeadlessDisabled(): bool + { + return isset($_SERVER['DUSK_HEADLESS_DISABLED']) || + isset($_ENV['DUSK_HEADLESS_DISABLED']); + } + + /** + * Determine if the browser window should start maximized. + */ + protected function shouldStartMaximized(): bool + { + return isset($_SERVER['DUSK_START_MAXIMIZED']) || + isset($_ENV['DUSK_START_MAXIMIZED']); + } + + /** + * Register an "after class" tear down callback. + * + * @param \Closure $callback + * @return void + */ + public static function afterClass(Closure $callback) + { + static::$afterClassCallbacks[] = $callback; + } + + public static function name() + { + return "test"; + } + + public static function dataName() + { + return "dataTest"; + } + + + // BROWSER MACROS + protected function waitForAndClickText(Browser $browser, string $text, int $timeout = 30, bool $ignoreCase = true) { + $browser->waitForText($text, $timeout, $ignoreCase); + $this->findElementContainingText($browser, $text, $ignoreCase)?->click(); + } + + protected function waitForElementContainingTextAndGetIt(Browser $browser, string $text, int $timeout = 30, bool $ignoreCase = true): RemoteWebElement|null { + try { + $browser->waitForText($text, $timeout, $ignoreCase); + return $this->findElementContainingText($browser, $text, $ignoreCase); + } catch (Exception $e) { + return null; + } + } + + protected function findElementContainingText(Browser $browser, string $text, bool $ignoreCase = true): RemoteWebElement|null { + try { + if ($ignoreCase) { + return $browser->driver->findElement(WebDriverBy::xpath("//*[{$this->xpathContainsIgnoreCase($text)}]")); + } else { + return $browser->driver->findElement(WebDriverBy::xpath("//*[contains(text(), \"{$text}\")]")); + } + } catch (Exception $e) { + return null; + } + } + + private function xpathContainsIgnoreCase(string $needle, string $haystack = "text()") { + $needle = strtolower($needle); + return "contains(translate({$haystack}, \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\", \"abcdefghijklmnopqrstuvwxyz\"), \"{$needle}\")"; + } + + protected function waitForAndClickElementContainingText(Browser $browser, string $elementXpath, string $text, int $timeout = 30, bool $ignoreCase = true) { + $browser->waitForText($text, $timeout, $ignoreCase); + $this->findElementContainingElementWithText($browser, $elementXpath, $text, $ignoreCase)?->click(); + } + + protected function waitForElementContainingElementWithTextAndGetIt(Browser $browser, string $elementXpath, string $text, int $timeout = 30, bool $ignoreCase = true): RemoteWebElement|null { + try { + $browser->waitForText($text, $timeout, $ignoreCase); + sleep(2); + return $this->findElementContainingElementWithText($browser, $elementXpath, $text, $ignoreCase); + } catch (Exception $e) { + return null; + } + } + + protected function findElementContainingElementWithText(Browser $browser, string $elementXpath, string $text, bool $ignoreCase = true): RemoteWebElement|null { + try { + if ($ignoreCase) { + dump("{$elementXpath}[.//*[{$this->xpathContainsIgnoreCase($text)}]]"); + return $browser->driver->findElement(WebDriverBy::xpath("{$elementXpath}[.//*[{$this->xpathContainsIgnoreCase($text)}]]")); + } else { + return $browser->driver->findElement(WebDriverBy::xpath("{$elementXpath}[.//*[contains(text(), \"{$text}\")]]")); + } + } catch (Exception $e) { + return null; + } + } +} diff --git a/app/Browser/Components/Hellcase/MainNav.php b/app/Browser/Components/Hellcase/MainNav.php new file mode 100644 index 0000000..00addd0 --- /dev/null +++ b/app/Browser/Components/Hellcase/MainNav.php @@ -0,0 +1,43 @@ +assertVisible($this->selector()); + } + + /** + * Get the element shortcuts for the component. + * + * @return array + */ + public function elements(): array + { + return [ + '@logo' => 'a.header-logo', + '@daily-free-link' => 'a[href="/dailyfree"]', + ]; + } + + public function goToHome(Browser $browser) { + $browser->scrollIntoView('@logo'); + $browser->click('@logo'); + } +} diff --git a/app/Browser/Jobs/Hellcase/HellcaseDailyFreeScreenshot.php b/app/Browser/Jobs/Hellcase/HellcaseDailyFreeScreenshot.php new file mode 100644 index 0000000..376294e --- /dev/null +++ b/app/Browser/Jobs/Hellcase/HellcaseDailyFreeScreenshot.php @@ -0,0 +1,29 @@ +jobRun = new JobRun([ + "job_id" => $this->jobId, + "success" => false, + ]); + $this->jobRun->save(); + + $browser->visit('https://hellcase.com'); + sleep(5); + $this->removePopups($browser); + sleep(5); + $this->signin($browser); + $this->joinFreeGiveaways($browser); + $this->getDailyFree($browser); + + $this->jobRun->success = true; + $this->jobRun->save(); + + return $this->jobRun; + } + + /** + * @inheritDoc + */ + public function runTest(Browser $browser): ?JobRun + { + try { + $browser->visit('https://hellcase.com'); + sleep(5); + $this->removePopups($browser); + $this->signin($browser); + return $this->makeSimpleJobRun( + true, + "Connexion réussie", + "Datboi a réussi à se connecter sur Hellcase" + ); + } catch (\Exception $e) { + return $this->makeSimpleJobRun( + true, + "Connexion échouée", + "Datboi n'a pas réussi à se connecter sur Hellcase :\n" . $e->getMessage() + ); + } + } + + private function signin(Browser $browser) + { + try { + $browser->clickAtXPath('//button[.//span[text() = "Sign in"]]'); + } catch (\Exception $e) { + return; + } + + sleep(3); + $browser->clickAtXPath('//button[.//span[contains(text(), "Sign in through Steam")]]'); + sleep(5); + + // QR CODE SCANNING + try { + $browser->waitForTextIn("div", "Or sign in with QR", 30, true); + $qrCode = $browser->driver->findElement(WebDriverBy::xpath('//div[./*[contains(text(), "Or sign in with QR")]]')); + + // Wait to be redirected to the Steam login page, while waiting take a new screenshot every 30 seconds + $isBackOnHellcase = false; + $secondsCounter = 0; + while (!$isBackOnHellcase && $secondsCounter < self::STEAM_LOGIN_THRESHOLD) { + // Take a screenshot of the QR code and send it + $qrCode->takeElementScreenshot(HellcaseLoginQrCode::getImgFileAbsolutePath()); + AllNotification::send( + new HellcaseNotificationLogin( + $this->jobId, + new HellcaseNotificationLoginBody() + ) + ); + + try { + $browser->waitForLocation("https://hellcase.com", HellcaseLoginQrCode::QR_CODE_VALIDITY); // The QR code is only valid for 20 seconds + } catch (\Exception $e) { + $secondsCounter += HellcaseLoginQrCode::QR_CODE_VALIDITY; // we've waited for QR_CODE_VALIDITY seconds + continue; + } + $isBackOnHellcase = true; + } + } catch (\Exception $e) { + // If the QR code is not found, we are not on the QR code page + $isBackOnHellcase = true; + } catch (\Throwable $e) { + // If the QR code is not found, we are not on the QR code page + $isBackOnHellcase = true; + } + + if ($isBackOnHellcase) { + // Click a button tjat says "sign in" + $browser->waitForText("By signing into steam.loginhell.com through Steam", 30, true); + $browser->clickAtXPath('//input[@id = "imageLogin"]'); + sleep(30); + } + } + + private function joinFreeGiveaways(Browser $browser) + { + try { + $buttons = $browser->driver->findElements(WebDriverBy::xpath('//a[text() = "Join for free"]')); + } catch (\Exception $e) { + return; + } + + if (sizeof($buttons) == 0) { + $this->jobRun->addArtifact(new JobArtifact([ + "name" => "Pas de concours joignable", + "content" => "" + ])); + } + foreach ($buttons as $button) { + $button->click(); + sleep(5); + $this->joinGiveaway($browser); + $browser->within(new MainNav, function (Browser $browser) { + $browser->goToHome(); + }); + } + } + + private function joinGiveaway(Browser $browser) + { + $joinButton = $browser->driver->findElement(WebDriverBy::xpath('//button[span[contains(text(), "Join for free")]]')); + $joinButton->click(); + } + + private function getDailyFree(Browser $browser) + { + $browser->visit('https://hellcase.com/dailyfree'); + $browser->waitForText("Get Daily free loot", 30, true); + + // Do we fill the conditions ? + if (sizeof(value: $browser->driver->findElements(WebDriverBy::xpath('//p[contains(text(), "Fulfill the conditions below")]'))) > 0) { + $this->fillDailyFreeConditions($browser); + } + + // If we see "availible in 20 HR 49 MIN", parse the hours and minute and reschedule + $availibleInButton = $this->waitForElementContainingTextAndGetIt($browser, "available", 30); + if ($availibleInButton != null) { + $hours = $availibleInButton->getText(); + $hours = explode(" ", $hours); + $minutes = $hours[2]; + $hours = $hours[0]; + // $this->reschedule($hours); + + $this->jobRun->addArtifact(new JobArtifact([ + "name" => "Cadeau gratuit pas encore disponible", + "content" => "Le cadeau gratuit journalier sera disponible dans $hours heures et $minutes minutes.\nDatboi se fera un plaisir d'aller le chercher pour vous." + ])); + + return; + } + + $this->waitForAndClickText($browser, "Get Free Bonus", 30, true); + + $lootElement = $browser->driver->findElement(WebDriverBy::xpath('//div[contains(@class, "daily-free-win-bonus")]')); + $lootElement->takeElementScreenshot(HellcaseDailyFreeScreenshot::getImgFileAbsolutePath()); + + AllNotification::send( + new HellcaseNotificationDailyFree($this->jobId, new HellcaseNotificationDailyFreeBody()) + ); + + sleep(5000); + } + + /** + * Must be on the dailyfree page + * @param \Laravel\Dusk\Browser $browser + * @throws \Exception + * @return void + */ + private function fillDailyFreeConditions(Browser $browser) { + // 1. See what conditions we need to fullfill + $conditions = []; + $conditionsDivs = $browser->driver->findElements(WebDriverBy::xpath('//*[@class = "daily-free-requirement__heading-left"]')); + for($i = 0; $i < sizeof($conditionsDivs); $i++) { + $conditionDiv = $conditionsDivs[$i]; + // See if the element has the completed class + $conditions[$i] = [ + "isFilled" => str_contains($conditionDiv->getAttribute("class"), "completed"), + "text" => $conditionDiv->getText() + ]; + } + + if (sizeof($conditions) == 0) { + throw new \Exception("No dailyfree conditions found"); + } + + if (!$conditions[0]["isFilled"]) { + $this->changeSteamProfilePicture($browser); + } + if (!$conditions[1]["isFilled"]) { + $this->changeSteamProfileToPublic($browser); + } + } + + /** + * + * Must be on the dailyfree page + * @param \Laravel\Dusk\Browser $browser + * @return void + */ + private function changeSteamProfilePicture(Browser $browser) { + // Get all of the availible image link + $images = $browser->driver->findElements(WebDriverBy::xpath('//a[@class = "daily-free-user-requirement-avatar-item"]')); + // Download the image from the second link in a special folder + $imageLink = $images[1]->getAttribute("href"); + // Download the image in app/Browser/downloads/ + $imagePath = base_path("app/Browser/downloads/Hellcase/pp.jpg"); + file_put_contents($imagePath, file_get_contents($imageLink)); + + $this->goToSteamProfileSettings($browser); + + // Wait for and click "Avatar" + $this->waitForAndClickText($browser, "Avatar"); + + // Wait for and click "Upload your avatar" + $browser->waitForText("Upload your avatar", 30, true); + // $browser->clickAtXPath('//*[contains(text(), "Upload your avatar")]'); + + // Upload the downloaded image + $browser->attach('input[type="file"]', $imagePath); + + // Wait for and click save + $this->waitForAndClickText($browser, "Save"); + + // Go back to dailyfree + $browser->visit('https://hellcase.com/dailyfree'); + + sleep(10); + + try { + // wait and click "Check the condition" + $this->waitForAndClickText($browser, "Check the condition"); + $browser->waitForText("Your Steam profile avatar does not match any of the ones specified in the", 30, true); + } catch (\Exception $e) { + // If the text is not found, the condition is filled + return; + } + } + + private function changeSteamProfileToPublic(Browser $browser) { + $this->goToSteamProfileSettings($browser); + + // Wait for and click "Privacy Settings" + $this->waitForAndClickText($browser, "Privacy Settings"); + + $dropdownButton = $browser->driver->findElement(WebDriverBy::xpath('//div[text() = "My profile"]/div')); + $dropdownButton->click(); + + sleep(2); + + // Div that contains the visible class ath the body root an element with public text + $publicOption = $browser->driver->findElement(WebDriverBy::xpath('/body/div[contains(@class, "visible")]/div[contains(text(), "Public")]')); + $publicOption->click(); + + // Go back to dailyfree + $browser->visit('https://hellcase.com/dailyfree'); + } + + /** + * From the dailyfree page + * @param \Laravel\Dusk\Browser $browser + * @return void + */ + private function goToSteamProfileSettings(Browser $browser) { + // Get the link that has text "Steam profile" + $steamProfileLink = $browser->driver->findElement(WebDriverBy::xpath('//a[contains(text(), "Steam profile")]')); + $browser->visit($steamProfileLink->getAttribute("href")); + $browser->waitForText("Level"); + + // Click "Edit Profile + $browser->clickAtXPath('//*[contains(text(), "Edit Profile")]'); + } + + private function removePopups(Browser $browser) + { + // $browser->script('document.querySelector("div.app-modal")[0].remove();'); + // $browser->driver->executeScript('document.querySelector("div.app-modal")[0].remove();'); + } +} diff --git a/app/Browser/Jobs/Hellcase/HellcaseLoginQrCode.php b/app/Browser/Jobs/Hellcase/HellcaseLoginQrCode.php new file mode 100644 index 0000000..a17841f --- /dev/null +++ b/app/Browser/Jobs/Hellcase/HellcaseLoginQrCode.php @@ -0,0 +1,29 @@ +jobId, "Test", "Test des notifications")); + AllNotification::send(new SimpleNotification($this->jobId, "Test", "Test des notifications d'erreur", true)); + return $this->makeSimpleJobRun(true, "Envoi de notification réussi", "Datboi a réussi à envoyer des notifications"); + } catch (\Throwable $e) { + return $this->makeSimpleJobRun(false, "Envoi de notification échoué", "Datboi n'a pas réussi à envoyer des notifications :\n" . $e->getMessage()); + } + } +} diff --git a/app/Browser/downloads/.gitignore b/app/Browser/downloads/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/app/Browser/downloads/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/app/Http/Controllers/JobController.php b/app/Http/Controllers/JobController.php new file mode 100644 index 0000000..5d9af6b --- /dev/null +++ b/app/Http/Controllers/JobController.php @@ -0,0 +1,69 @@ + Job::where('id', $jobId)->with('jobInfos')->first(), + 'error' => $request->input('error'), + ]); + } + + public function update($jobId, Request $request) + { + $job = Job::where('id', $jobId)->first(); + $job->is_active = false; // Disable the job + + $errors = []; + + foreach ($job->jobInfos()->get() as $jobInfo) { + $value = $request->input($jobInfo->id); + if (!isset($value) && $jobInfo->is_required) { + $errors[] = 'Le champ ' . $jobInfo->name . ' est requis.'; + continue; + } + + Cache::forget($jobInfo->key); + + $jobInfo->value = $value; + $jobInfo->save(); + } + + $job->save(); + + if (count($errors) > 0) { + return redirect()->route('jobs.show', ['job' => $job, 'error' => implode('
', $errors)]); + } + + $job->is_active = $request->input('is_active'); + $job->save(); + + return redirect()->route('jobs.show', ['job' => $job]); + } + + public function test($jobId, Request $request) + { + $log = $this->jobInstances->getJobInstance($jobId)->executeTest(); + if (!empty($log)) { + return response()->json(['artifact' => $log->load('artifacts')]); + } + return response()->json([]); + } +} diff --git a/app/Models/Job.php b/app/Models/Job.php index b2107e5..bf0515b 100644 --- a/app/Models/Job.php +++ b/app/Models/Job.php @@ -7,6 +7,22 @@ use Illuminate\Database\Eloquent\Model; class Job extends Model { - /** @use HasFactory<\Database\Factories\JobFactory> */ - use HasFactory; + + protected $fillable = [ + "is_active", + ]; + + protected $casts = [ + "is_active" => "boolean", + ]; + + public function jobInfos() + { + return $this->hasMany(JobInfo::class)->with("jobInfoType")->orderBy("created_at"); + } + + public function jobRuns() + { + return $this->hasMany(JobRun::class)->orderBy("created_at"); + } } diff --git a/app/Models/JobArtifact.php b/app/Models/JobArtifact.php index 1d2418c..7e29e03 100644 --- a/app/Models/JobArtifact.php +++ b/app/Models/JobArtifact.php @@ -4,9 +4,17 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; class JobArtifact extends Model { - /** @use HasFactory<\Database\Factories\JobArtifactFactory> */ - use HasFactory; + protected $fillable = [ + "name", + "content", + ]; + + public function jobRun(): BelongsTo + { + return $this->belongsTo(JobRun::class); + } } diff --git a/app/Models/JobInfo.php b/app/Models/JobInfo.php index c799487..a3d0b8b 100644 --- a/app/Models/JobInfo.php +++ b/app/Models/JobInfo.php @@ -7,6 +7,19 @@ use Illuminate\Database\Eloquent\Model; class JobInfo extends Model { - /** @use HasFactory<\Database\Factories\JobInfoFactory> */ - use HasFactory; + + protected $fillable = [ + "value", + ]; + + public function job() + { + return $this->belongsTo(Job::class); + } + + public function jobInfoType() + { + return $this->belongsTo(JobInfoType::class)->select("id", "name"); + } + } diff --git a/app/Models/JobInfoType.php b/app/Models/JobInfoType.php index 9120611..6f49540 100644 --- a/app/Models/JobInfoType.php +++ b/app/Models/JobInfoType.php @@ -7,6 +7,12 @@ use Illuminate\Database\Eloquent\Model; class JobInfoType extends Model { - /** @use HasFactory<\Database\Factories\JobInfoTypeFactory> */ - use HasFactory; + protected $fillable = [ + "name", + ]; + + public function jobInfos() + { + return $this->hasMany(JobInfo::class); + } } diff --git a/app/Models/JobRun.php b/app/Models/JobRun.php index 691160b..edc5d57 100644 --- a/app/Models/JobRun.php +++ b/app/Models/JobRun.php @@ -4,9 +4,34 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; class JobRun extends Model { - /** @use HasFactory<\Database\Factories\JobRunFactory> */ - use HasFactory; + protected $fillable = [ + "job_id", + "success", + ]; + + protected $casts = [ + "success" => "boolean", + ]; + + protected $with = ['artifacts']; + + public function job(): BelongsTo + { + return $this->belongsTo(Job::class); + } + + public function artifacts(): HasMany + { + return $this->hasMany(JobArtifact::class); + } + + public function addArtifact(JobArtifact $artifact): void + { + $this->artifacts()->save($artifact); + } } diff --git a/app/Notification/Notification.php b/app/Notification/Notification.php new file mode 100644 index 0000000..e98a905 --- /dev/null +++ b/app/Notification/Notification.php @@ -0,0 +1,40 @@ +job = Job::find($jobId); + $this->body = $body; + $this->isError = $isError; + } + + public function getTitle(): Stringifiable { + return new StringifiableSimpleText($this->job->name); + } + + public function getBody(): Stringifiable { + return $this->body; + } + + abstract public function getLinkURL(): ?string; + + public function getImageURL(): ?string { + $imageProjectPath = $this->getImageProjectPath(); + if ($imageProjectPath === null) { + return null; + } + return url($imageProjectPath); + } + + abstract public function getImageProjectPath(): ?string; +} diff --git a/app/Notification/NotificationBody.php b/app/Notification/NotificationBody.php new file mode 100644 index 0000000..ed98b15 --- /dev/null +++ b/app/Notification/NotificationBody.php @@ -0,0 +1,8 @@ +content; + } + + /** + * @inheritDoc + */ + public function toString(): string { + return $this->content; + } +} diff --git a/app/Notification/NotificationBody/Hellcase/HellcaseNotificationLoginBody.php b/app/Notification/NotificationBody/Hellcase/HellcaseNotificationLoginBody.php new file mode 100644 index 0000000..221fa7e --- /dev/null +++ b/app/Notification/NotificationBody/Hellcase/HellcaseNotificationLoginBody.php @@ -0,0 +1,25 @@ +content; + } + + /** + * @inheritDoc + */ + public function toString(): string { + return $this->content; + } +} diff --git a/app/Notification/NotificationBody/ListNotificationBody.php b/app/Notification/NotificationBody/ListNotificationBody.php new file mode 100644 index 0000000..7336fb6 --- /dev/null +++ b/app/Notification/NotificationBody/ListNotificationBody.php @@ -0,0 +1,44 @@ +content = $content; + } + + /** + * @inheritDoc + */ + public function toMarkdownString(): string { + $string = ""; + foreach ($this->content as $item) { + $string .= "- ". $this->getTextFromContent($item) . "\n"; + } + return $string; + } + + /** + * @inheritDoc + */ + public function toString(): string { + $string = ""; + foreach ($this->content as $item) { + $string .= $this->getTextFromContent($item) . "\n"; + } + return $string; + } + + public function getTextFromContent(string|Stringifiable $content): string { + if ($content instanceof Stringifiable) { + return $content->toString(); + } + return $content; + } +} diff --git a/app/Notification/NotificationBody/SimpleNotificationBody.php b/app/Notification/NotificationBody/SimpleNotificationBody.php new file mode 100644 index 0000000..13595e8 --- /dev/null +++ b/app/Notification/NotificationBody/SimpleNotificationBody.php @@ -0,0 +1,32 @@ +body = $body; + } + /** + * @inheritDoc + */ + public function toMarkdownString(): string { + return $this->body; + } + + public function toHTMLString(): string { + return $this->body; + } + + /** + * @inheritDoc + */ + public function toString(): string { + return $this->body; + } +} diff --git a/app/Notification/NotificationProvider.php b/app/Notification/NotificationProvider.php new file mode 100644 index 0000000..6ccbdd0 --- /dev/null +++ b/app/Notification/NotificationProvider.php @@ -0,0 +1,7 @@ + $this->job->id]); + } +} diff --git a/app/Notification/Notifications/Hellcase/HellcaseNotificationLogin.php b/app/Notification/Notifications/Hellcase/HellcaseNotificationLogin.php new file mode 100644 index 0000000..a742c5b --- /dev/null +++ b/app/Notification/Notifications/Hellcase/HellcaseNotificationLogin.php @@ -0,0 +1,29 @@ + $this->job->id]); + } +} diff --git a/app/Notification/Notifications/NotificationLogin.php b/app/Notification/Notifications/NotificationLogin.php new file mode 100644 index 0000000..6985486 --- /dev/null +++ b/app/Notification/Notifications/NotificationLogin.php @@ -0,0 +1,11 @@ +title = new StringifiableSimpleText($title); + parent::__construct($jobId, new SimpleNotificationBody($body), $isError); + } + + public function getTitle(): Stringifiable { + return $this->title; + } + + /** + * @inheritDoc + */ + public function getImageProjectPath(): string|null { + return null; + } + + /** + * @inheritDoc + */ + public function getLinkURL(): string|null { + return route('home'); + } +} diff --git a/app/Notification/Providers/AllNotification.php b/app/Notification/Providers/AllNotification.php new file mode 100644 index 0000000..c9d15cc --- /dev/null +++ b/app/Notification/Providers/AllNotification.php @@ -0,0 +1,23 @@ +isError); + $body = [ + "content"=> "", + "tts"=> false, + "embeds" => [ + [ + "id" => 652627557, + "title" => $notification->getTitle()->toString(), + "description" => $notification->getBody()->toMarkdownString(), + "color" => self::EMBED_COLOR[(int)$notification->isError], + "url" => $notification->getLinkURL(), + ], + ], + "username" => "Datboi", + "avatar_url" => "https://media1.giphy.com/media/yDTWAecZcB2Jq/200w.gif?cid=6c09b952f68kz3wnkqsmyha8e7xrpe8n2kx0nkf2b8cir6am&rid=200w.gif&ct=g", + ]; + + if ($notification->getImageURL() !== null) { + $body["embeds"][0]["image"] = [ + "url" => "attachment://image.png" + ]; + } + + $payloadJson = json_encode($body); + + $formData = [ + 'payload_json' => $payloadJson, + ]; + + if ($notification->getImageURL() !== null) { + $formData['file'] = curl_file_create($notification->getImageProjectPath(), 'image/png', 'image.png'); + } + + $ch = curl_init($webHookUrl); + curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-type: multipart/form-data')); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $formData); + // curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + $response = curl_exec($ch); + curl_close($ch); + } + + private static function getDiscordWebHookUrl(bool $isError): string { + $generalWebHookUrlKey = 'discord_webhook_url'; + $generalWebHookUrl = Cache::rememberForever($generalWebHookUrlKey, function () use ($generalWebHookUrlKey) { + return JobInfo::where('key', $generalWebHookUrlKey)->first()->value; + }); + if ($generalWebHookUrl === null) { + throw new \Exception("Le webhook discord n'a pas été configuré"); + } + + if ($isError) { + $errorWebHookUrlKey = 'discord_error_webhook_url'; + $errorWebHookUrl = Cache::rememberForever($errorWebHookUrlKey, function () use ($errorWebHookUrlKey) { + return JobInfo::where('key', $errorWebHookUrlKey)->first()->value; + }); + return $errorWebHookUrl ?? $generalWebHookUrl; + } + else { + return $generalWebHookUrl; + } + } +} diff --git a/app/Notification/Stringifiable.php b/app/Notification/Stringifiable.php new file mode 100644 index 0000000..dd4951d --- /dev/null +++ b/app/Notification/Stringifiable.php @@ -0,0 +1,21 @@ +parsedown = new Parsedown(); + } + + abstract public function toString(): string; + + public function toHTMLString(): string { + return $this->parsedown->text($this->toMarkdownString()); + } + + abstract public function toMarkdownString(): string; +} diff --git a/app/Notification/Stringifiable/StringifiableSimpleText.php b/app/Notification/Stringifiable/StringifiableSimpleText.php new file mode 100644 index 0000000..13c0b85 --- /dev/null +++ b/app/Notification/Stringifiable/StringifiableSimpleText.php @@ -0,0 +1,34 @@ +text = $text; + } + + /** + * @inheritDoc + */ + public function toHTMLString(): string { + return $this->text; + } + + /** + * @inheritDoc + */ + public function toMarkdownString(): string { + return $this->text; + } + + /** + * @inheritDoc + */ + public function toString(): string { + return $this->text; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 96e9f6c..eba7cef 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use App\Browser\BrowserJob; use Illuminate\Support\Facades\Vite; use Illuminate\Support\ServiceProvider; diff --git a/app/Providers/BrowserJobsServiceProvider.php b/app/Providers/BrowserJobsServiceProvider.php new file mode 100644 index 0000000..3d2e8dc --- /dev/null +++ b/app/Providers/BrowserJobsServiceProvider.php @@ -0,0 +1,37 @@ +service = new BrowserJobsInstances(); + } + + /** + * Register services. + */ + public function register(): void + { + $this->app->instance(BrowserJobsInstances::class, $this->service); + } + + /** + * Bootstrap services. + */ + public function boot(): void + { + $this->app->terminating(function (BrowserJobsInstances $instances) { + $instances->terminateAll(); + }); + } +} diff --git a/app/Services/BrowserJobsInstances.php b/app/Services/BrowserJobsInstances.php new file mode 100644 index 0000000..80b629d --- /dev/null +++ b/app/Services/BrowserJobsInstances.php @@ -0,0 +1,92 @@ +jobId; + Cache::put('jobClass' . $jobId, $fullClassName); // Met le nom de la classe en cache + $this->jobInstancesByJobId[$jobId] = $jobInstance; + } + } + + /** + * Get an instance of a job by it's jobId + * @param mixed $jobId The ID of the job in the database + * @return BrowserJob + */ + public function getJobInstance($jobId): BrowserJob + { + try { + return $this->jobInstancesByJobId[$jobId]; + } catch (Exception $e) { + return $this->getNewJobInstance($jobId); + } + } + + /** + * Create a new instance of a job class + * @param int $jobId + * @return object + */ + private function getNewJobInstance(int $jobId): BrowserJob { + $jobClass = Cache::get('jobClass' . $jobId); + if ($jobClass == null) { // If we don't have the class in cache, we put all of the job in cache + $this->indexJobsClassesById(); + return $this->jobInstancesByJobId[$jobId]; // indexJobsClassesById() already created an instance of the job + } + // If we have the class in cache, we create a new instance of it + $instance = new $jobClass(); + $this->jobInstancesByJobId[$jobId] = $instance; + return $instance; + } + + /** + * Terminate all jobs + * @return void + */ + public function terminateAll() { + foreach ($this->jobInstancesByJobId as $jobInstance) { + $jobInstance->terminate(); + } + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index 113955b..461aafd 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -7,6 +7,7 @@ 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', ) diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 38b258d..8ddee42 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -2,4 +2,5 @@ return [ App\Providers\AppServiceProvider::class, + App\Providers\BrowserJobsServiceProvider::class, ]; diff --git a/compose.prod.yaml b/compose.prod.yaml new file mode 100644 index 0000000..157a9de --- /dev/null +++ b/compose.prod.yaml @@ -0,0 +1,54 @@ +# Comments are provided throughout this file to help you get started. +# If you need more help, visit the Docker Compose reference guide at +# https://docs.docker.com/go/compose-spec-reference/ + +# Here the instructions define your application as a service called "app". +# This service is built from the Dockerfile in the current directory. +# You can add other services your application may depend on here, such as a +# database or a cache. For examples, see the Awesome Compose repository: +# https://github.com/docker/awesome-compose +services: + app: + image: git.matthiasg.dev/ninluc/datbrowser:latest + restart: unless-stopped + ports: + - 80:80 + depends_on: + db: + condition: service_healthy + undetected-chromedriver: + condition: service_healthy + + db: + image: mysql:5.7 + restart: unless-stopped + healthcheck: + test: ["CMD", 'mysqladmin', 'ping', '-h', 'db', '-u', '${DB_USERNAME}', '-p${DB_PASSWORD}' ] + interval: 2s + timeout: 20s + retries: 10 + environment: + MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} + MYSQL_DATABASE: ${DB_DATABASE} + MYSQL_USER: ${DB_USERNAME} + MYSQL_PASSWORD: ${DB_PASSWORD} + volumes: + - dbdata:/var/lib/mysql + + undetected-chromedriver: + image: git.matthiasg.dev/ninluc/selenium/standalone-uc:latest + restart: unless-stopped + volumes: + - /tmp:/tmp + shm_size: 2gb + ports: + - "4444:4444" + - "7900:7900" + + watchtower: + image: containrrr/watchtower + volumes: + - /var/run/docker.sock:/var/run/docker.sock + +volumes: + dbdata: diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..d553ed1 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,53 @@ +# Comments are provided throughout this file to help you get started. +# If you need more help, visit the Docker Compose reference guide at +# https://docs.docker.com/go/compose-spec-reference/ + +# Here the instructions define your application as a service called "app". +# This service is built from the Dockerfile in the current directory. +# You can add other services your application may depend on here, such as a +# database or a cache. For examples, see the Awesome Compose repository: +# https://github.com/docker/awesome-compose +services: + app: + build: + context: . + target: final + restart: unless-stopped + ports: + - 80:80 + depends_on: + db: + condition: service_healthy + undetected-chromedriver: + condition: service_healthy + + db: + image: mysql:5.7 + restart: unless-stopped + healthcheck: + test: ["CMD", 'mysqladmin', 'ping', '-h', 'db', '-u', '${DB_USERNAME}', '-p${DB_PASSWORD}' ] + interval: 2s + timeout: 20s + retries: 10 + environment: + MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} + MYSQL_DATABASE: ${DB_DATABASE} + MYSQL_USER: ${DB_USERNAME} + MYSQL_PASSWORD: ${DB_PASSWORD} + volumes: + - dbdata:/var/lib/mysql + + undetected-chromedriver: + build: + context: . + dockerfile: undetectedChromedriver/seleniumChromedriverDockerfile + restart: unless-stopped + volumes: + - /tmp:/tmp + shm_size: 2gb + ports: + - "4444:4444" + - "7900:7900" + +volumes: + dbdata: diff --git a/composer.json b/composer.json index 8e6f12f..569aa4f 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,9 @@ "license": "MIT", "require": { "php": "^8.2", + "erusev/parsedown": "^1.7", "inertiajs/inertia-laravel": "^2.0", + "laravel/dusk": "^8.2", "laravel/framework": "^11.31", "laravel/sanctum": "^4.0", "laravel/tinker": "^2.9", diff --git a/composer.lock b/composer.lock index 350126f..7dc6cdd 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2f8fad98d414b6e76b3cad63a981e25e", + "content-hash": "fe9d04d1c63e92ba62edb93168567747", "packages": [ { "name": "brick/math", @@ -510,6 +510,56 @@ ], "time": "2024-12-27T00:36:43+00:00" }, + { + "name": "erusev/parsedown", + "version": "1.7.4", + "source": { + "type": "git", + "url": "https://github.com/erusev/parsedown.git", + "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/erusev/parsedown/zipball/cb17b6477dfff935958ba01325f2e8a2bfa6dab3", + "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35" + }, + "type": "library", + "autoload": { + "psr-0": { + "Parsedown": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Emanuil Rusev", + "email": "hello@erusev.com", + "homepage": "http://erusev.com" + } + ], + "description": "Parser for Markdown.", + "homepage": "http://parsedown.org", + "keywords": [ + "markdown", + "parser" + ], + "support": { + "issues": "https://github.com/erusev/parsedown/issues", + "source": "https://github.com/erusev/parsedown/tree/1.7.x" + }, + "time": "2019-12-30T22:54:17+00:00" + }, { "name": "fruitcake/php-cors", "version": "v1.3.0", @@ -970,16 +1020,16 @@ }, { "name": "guzzlehttp/uri-template", - "version": "v1.0.3", + "version": "v1.0.4", "source": { "type": "git", "url": "https://github.com/guzzle/uri-template.git", - "reference": "ecea8feef63bd4fef1f037ecb288386999ecc11c" + "reference": "30e286560c137526eccd4ce21b2de477ab0676d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/uri-template/zipball/ecea8feef63bd4fef1f037ecb288386999ecc11c", - "reference": "ecea8feef63bd4fef1f037ecb288386999ecc11c", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/30e286560c137526eccd4ce21b2de477ab0676d2", + "reference": "30e286560c137526eccd4ce21b2de477ab0676d2", "shasum": "" }, "require": { @@ -1036,7 +1086,7 @@ ], "support": { "issues": "https://github.com/guzzle/uri-template/issues", - "source": "https://github.com/guzzle/uri-template/tree/v1.0.3" + "source": "https://github.com/guzzle/uri-template/tree/v1.0.4" }, "funding": [ { @@ -1052,7 +1102,7 @@ "type": "tidelift" } ], - "time": "2023-12-03T19:50:20+00:00" + "time": "2025-02-03T10:55:03+00:00" }, { "name": "inertiajs/inertia-laravel", @@ -1129,17 +1179,89 @@ "time": "2024-12-13T02:48:29+00:00" }, { - "name": "laravel/framework", - "version": "v11.41.0", + "name": "laravel/dusk", + "version": "v8.2.14", "source": { "type": "git", - "url": "https://github.com/laravel/framework.git", - "reference": "42d6ae000c868c2abfa946da46702f2358493482" + "url": "https://github.com/laravel/dusk.git", + "reference": "28c9fce3900625522afc2468a9117cdf44f919c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/42d6ae000c868c2abfa946da46702f2358493482", - "reference": "42d6ae000c868c2abfa946da46702f2358493482", + "url": "https://api.github.com/repos/laravel/dusk/zipball/28c9fce3900625522afc2468a9117cdf44f919c1", + "reference": "28c9fce3900625522afc2468a9117cdf44f919c1", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-zip": "*", + "guzzlehttp/guzzle": "^7.5", + "illuminate/console": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0", + "php": "^8.1", + "php-webdriver/webdriver": "^1.15.2", + "symfony/console": "^6.2|^7.0", + "symfony/finder": "^6.2|^7.0", + "symfony/process": "^6.2|^7.0", + "vlucas/phpdotenv": "^5.2" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "orchestra/testbench": "^8.19|^9.0|^10.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.1|^11.0", + "psy/psysh": "^0.11.12|^0.12" + }, + "suggest": { + "ext-pcntl": "Used to gracefully terminate Dusk when tests are running." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Dusk\\DuskServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Dusk\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Dusk provides simple end-to-end testing and browser automation.", + "keywords": [ + "laravel", + "testing", + "webdriver" + ], + "support": { + "issues": "https://github.com/laravel/dusk/issues", + "source": "https://github.com/laravel/dusk/tree/v8.2.14" + }, + "time": "2025-01-26T19:36:00+00:00" + }, + { + "name": "laravel/framework", + "version": "v11.41.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "3ef433d5865f30a19b6b1be247586068399b59cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/3ef433d5865f30a19b6b1be247586068399b59cc", + "reference": "3ef433d5865f30a19b6b1be247586068399b59cc", "shasum": "" }, "require": { @@ -1341,7 +1463,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-01-28T15:22:55+00:00" + "time": "2025-01-30T13:25:22+00:00" }, { "name": "laravel/prompts", @@ -2646,6 +2768,72 @@ ], "time": "2024-11-21T10:39:51+00:00" }, + { + "name": "php-webdriver/webdriver", + "version": "1.15.2", + "source": { + "type": "git", + "url": "https://github.com/php-webdriver/php-webdriver.git", + "reference": "998e499b786805568deaf8cbf06f4044f05d91bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/998e499b786805568deaf8cbf06f4044f05d91bf", + "reference": "998e499b786805568deaf8cbf06f4044f05d91bf", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-zip": "*", + "php": "^7.3 || ^8.0", + "symfony/polyfill-mbstring": "^1.12", + "symfony/process": "^5.0 || ^6.0 || ^7.0" + }, + "replace": { + "facebook/webdriver": "*" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.20.0", + "ondram/ci-detector": "^4.0", + "php-coveralls/php-coveralls": "^2.4", + "php-mock/php-mock-phpunit": "^2.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpunit/phpunit": "^9.3", + "squizlabs/php_codesniffer": "^3.5", + "symfony/var-dumper": "^5.0 || ^6.0 || ^7.0" + }, + "suggest": { + "ext-SimpleXML": "For Firefox profile creation" + }, + "type": "library", + "autoload": { + "files": [ + "lib/Exception/TimeoutException.php" + ], + "psr-4": { + "Facebook\\WebDriver\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP client for Selenium WebDriver. Previously facebook/webdriver.", + "homepage": "https://github.com/php-webdriver/php-webdriver", + "keywords": [ + "Chromedriver", + "geckodriver", + "php", + "selenium", + "webdriver" + ], + "support": { + "issues": "https://github.com/php-webdriver/php-webdriver/issues", + "source": "https://github.com/php-webdriver/php-webdriver/tree/1.15.2" + }, + "time": "2024-11-21T15:12:59+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.3", diff --git a/config/dusk.php b/config/dusk.php new file mode 100644 index 0000000..da0c71b --- /dev/null +++ b/config/dusk.php @@ -0,0 +1,8 @@ + [ + "url" => $_ENV['DUSK_DRIVER_URL'] ?? env('DUSK_DRIVER_URL') ?? null + ] +]; diff --git a/config/sanctum.php b/config/sanctum.php new file mode 100644 index 0000000..764a82f --- /dev/null +++ b/config/sanctum.php @@ -0,0 +1,83 @@ + explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( + '%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + Sanctum::currentApplicationUrlWithPort() + ))), + + /* + |-------------------------------------------------------------------------- + | 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, + ], + +]; diff --git a/database/migrations/2025_00_00_000000_create_cache_table.php b/database/migrations/2025_00_00_000000_create_cache_table.php new file mode 100644 index 0000000..b9c106b --- /dev/null +++ b/database/migrations/2025_00_00_000000_create_cache_table.php @@ -0,0 +1,35 @@ +string('key')->primary(); + $table->mediumText('value'); + $table->integer('expiration'); + }); + + Schema::create('cache_locks', function (Blueprint $table) { + $table->string('key')->primary(); + $table->string('owner'); + $table->integer('expiration'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cache'); + Schema::dropIfExists('cache_locks'); + } +}; diff --git a/database/migrations/2025_01_23_190836_create_jobs_table.php b/database/migrations/2025_01_23_190836_create_jobs_table.php index 3193138..291ad60 100644 --- a/database/migrations/2025_01_23_190836_create_jobs_table.php +++ b/database/migrations/2025_01_23_190836_create_jobs_table.php @@ -17,7 +17,7 @@ return new class extends Migration $table->string('name'); $table->text('description')->nullable(); - $table->boolean('is_active')->default(true); + $table->boolean('is_active')->default(false); $table->timestamps(); }); @@ -38,21 +38,24 @@ return new class extends Migration $jobs = [ [ 'id' => 1, - 'name' => 'Hellcase', - 'description' => 'Prends le daily free et rentre dans les concours. Tourne toutes les 24h.', - 'is_active' => false, + 'name' => 'Paramètres généraux', + 'description' => "Les paramètres généraux de l'application.", + 'is_active' => true, ], [ 'id' => 2, - 'name' => 'Jeu gratuit Epic Games', - 'description' => 'Prends le jeu gratuit Epic games. Tourne tous les mois et tous les jours pendant la période de Noël.', - 'is_active' => false, + 'name' => 'Hellcase', + 'description' => 'Prends le daily free et rentre dans les concours. Tourne toutes les 24h.', ], [ 'id' => 3, + 'name' => 'Jeu gratuit Epic Games', + 'description' => 'Prends le jeu gratuit Epic games. Tourne tous les mois et tous les jours pendant la période de Noël.', + ], + [ + 'id' => 4, 'name' => 'Envoyer un post instagram', 'description' => "Envoye un post instagram avec l'image et le texte fourni. Tourne tous les jours.", - 'is_active' => false, ], ]; diff --git a/database/migrations/2025_01_23_190839_create_job_info_types_table.php b/database/migrations/2025_01_23_190839_create_job_info_types_table.php new file mode 100644 index 0000000..e8926b5 --- /dev/null +++ b/database/migrations/2025_01_23_190839_create_job_info_types_table.php @@ -0,0 +1,58 @@ +id(); + + $table->string('name')->unique()->index(); // html input type + + $table->timestamps(); + }); + + $this->seed(); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('job_info_types'); + } + + public function seed(): void + { + $jobInfosTypes = [ + [ + "id" => 1, + "name" => "text", + ], + [ + "id" => 2, + "name" => "email", + ], + [ + "id" => 3, + "name" => "password", + ], + [ + "id" => 4, + "name" => "url", + ], + ]; + + foreach ($jobInfosTypes as $jobInfoType) { + \App\Models\JobInfoType::forceCreate($jobInfoType); + } + } +}; diff --git a/database/migrations/2025_01_23_190849_create_job_infos_table.php b/database/migrations/2025_01_23_190849_create_job_infos_table.php new file mode 100644 index 0000000..a739fe2 --- /dev/null +++ b/database/migrations/2025_01_23_190849_create_job_infos_table.php @@ -0,0 +1,107 @@ +id(); + + $table->string('key')->unique(); + + $table->string('name'); + $table->text('description')->nullable(); + $table->string('placeholder')->nullable(); + + $table->text('value')->nullable(); + + $table->boolean('is_required')->default(true); + + $table->foreignIdFor(JobInfoType::class, "job_info_type_id")->constrained()->onDelete('cascade'); + + $table->foreignIdFor(Job::class,'job_id')->constrained()->onDelete('cascade'); + + $table->timestamps(); + }); + + $this->seed(); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('job_infos'); + } + + public function seed(): void + { + + $jobInfos = [ + /* GENERAL PARAMETERS */ + [ + "key" => "discord_webhook_url", + "name" => "Messages - Webhook Discord", + "description" => "Le lien discord webhook utilisé pour envoyer les messages des résultats des jobs.", + "placeholder" => "https://discord.com/api/webhooks/...", + "job_info_type_id" => 4, + "job_id" => 1, + ], + [ + "key" => "discord_error_webhook_url", + "name" => "Alertes - Webhook Discord", + "description" => "Le lien discord webhook utilisé pour envoyer les messages d'erreur et d'alerte. Laisser vide pour utiliser le même que les messages.", + "placeholder" => "https://discord.com/api/webhooks/...", + "is_required" => false, + "job_info_type_id" => 4, + "job_id" => 1, + ], + + /* EPIC GAMES */ + [ + "key" => "epicgames_account_email", + "name" => "E-mail", + "description" => "L'adresse e-mail utilisée pour votre compte Epic Games.", + "job_info_type_id" => 2, + "job_id" => 3, + ], + [ + "key" => "epicgames_account_password", + "name" => "Mot de passe", + "description" => "Le mot de passe utilisé pour votre compte Epic Games.", + "job_info_type_id" => 3, + "job_id" => 3, + ], + + /* INSTAGRAM */ + [ + "key" => "instagram_account_email", + "name" => "E-mail", + "description" => "L'adresse e-mail utilisée pour votre compte Instagram.", + "job_info_type_id" => 2, + "job_id" => 4, + ], + [ + "key" => "instagram_account_password", + "name" => "Mot de passe", + "description" => "Le mot de passe utilisé pour votre compte Instagram.", + "job_info_type_id" => 3, + "job_id" => 4, + ], + ]; + + foreach ($jobInfos as $jobInfo) { + \App\Models\JobInfo::forceCreate($jobInfo); + } + } +}; diff --git a/database/migrations/2025_01_23_190859_create_job_runs_table.php b/database/migrations/2025_01_23_190859_create_job_runs_table.php new file mode 100644 index 0000000..be303e5 --- /dev/null +++ b/database/migrations/2025_01_23_190859_create_job_runs_table.php @@ -0,0 +1,32 @@ +id(); + + $table->boolean('success')->default(true); + + $table->foreignIdFor(\App\Models\Job::class)->constrained()->cascadeOnDelete(); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('job_runs'); + } +}; diff --git a/database/migrations/2025_01_23_190925_create_job_artifacts_table.php b/database/migrations/2025_01_23_190925_create_job_artifacts_table.php new file mode 100644 index 0000000..47c0ac9 --- /dev/null +++ b/database/migrations/2025_01_23_190925_create_job_artifacts_table.php @@ -0,0 +1,33 @@ +id(); + + $table->string('name'); + $table->text('content')->nullable(); + + $table->foreignIdFor(\App\Models\JobRun::class)->constrained()->cascadeOnDelete(); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('job_artifacts'); + } +}; diff --git a/dockerEntryPoint.sh b/dockerEntryPoint.sh new file mode 100644 index 0000000..478995e --- /dev/null +++ b/dockerEntryPoint.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +# Migrate the database +php ./artisan migrate --force + +# Start all of the server sumulataneously +php ./artisan serve --no-interaction -vvv --port=80 --host=0.0.0.0 diff --git a/package-lock.json b/package-lock.json index 070998a..016807c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "packages": { "": { "dependencies": { + "@vueuse/core": "^12.5.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.474.0", @@ -1154,48 +1155,34 @@ "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==" }, "node_modules/@vueuse/core": { - "version": "10.11.1", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", - "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.5.0.tgz", + "integrity": "sha512-GVyH1iYqNANwcahAx8JBm6awaNgvR/SwZ1fjr10b8l1HIgDp82ngNbfzJUgOgWEoxjL+URAggnlilAEXwCOZtg==", "dependencies": { "@types/web-bluetooth": "^0.0.20", - "@vueuse/metadata": "10.11.1", - "@vueuse/shared": "10.11.1", - "vue-demi": ">=0.14.8" + "@vueuse/metadata": "12.5.0", + "@vueuse/shared": "12.5.0", + "vue": "^3.5.13" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, - "node_modules/@vueuse/core/node_modules/vue-demi": { - "version": "0.14.10", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", - "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", - "hasInstallScript": true, - "bin": { - "vue-demi-fix": "bin/vue-demi-fix.js", - "vue-demi-switch": "bin/vue-demi-switch.js" - }, - "engines": { - "node": ">=12" + "node_modules/@vueuse/core/node_modules/@vueuse/shared": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.5.0.tgz", + "integrity": "sha512-vMpcL1lStUU6O+kdj6YdHDixh0odjPAUM15uJ9f7MY781jcYkIwFA4iv2EfoIPO6vBmvutI1HxxAwmf0cx5ISQ==", + "dependencies": { + "vue": "^3.5.13" }, "funding": { "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "@vue/composition-api": "^1.0.0-rc.1", - "vue": "^3.0.0-0 || ^2.6.0" - }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - } } }, "node_modules/@vueuse/metadata": { - "version": "10.11.1", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", - "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.5.0.tgz", + "integrity": "sha512-Ui7Lo2a7AxrMAXRF+fAp9QsXuwTeeZ8fIB9wsLHqzq9MQk+2gMYE2IGJW48VMJ8ecvCB3z3GsGLKLbSasQ5Qlg==", "funding": { "url": "https://github.com/sponsors/antfu" } @@ -2768,6 +2755,53 @@ "vue": ">= 3.2.0" } }, + "node_modules/radix-vue/node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/radix-vue/node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/radix-vue/node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/radix-vue/node_modules/nanoid": { "version": "5.0.9", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz", diff --git a/package.json b/package.json index 1e73ae6..d067163 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "vue-tsc": "^2.0.24" }, "dependencies": { + "@vueuse/core": "^12.5.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.474.0", diff --git a/public/Hellcase-joinGiveawayButton b/public/Hellcase-joinGiveawayButton new file mode 100644 index 0000000..d1f7b0f Binary files /dev/null and b/public/Hellcase-joinGiveawayButton differ diff --git a/pushSeleniumStandaloneUcImage.sh b/pushSeleniumStandaloneUcImage.sh new file mode 100644 index 0000000..f7470db --- /dev/null +++ b/pushSeleniumStandaloneUcImage.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +sudo docker build -f undetectedChromedriver/seleniumChromedriverDockerfile -t git.matthiasg.dev/ninluc/selenium/standalone-uc:latest . +sudo docker push git.matthiasg.dev/ninluc/selenium/standalone-uc:latest diff --git a/resources/js/Components/Layout/AppLogo.vue b/resources/js/Components/Layout/AppLogo.vue index 9128955..d1c3981 100644 --- a/resources/js/Components/Layout/AppLogo.vue +++ b/resources/js/Components/Layout/AppLogo.vue @@ -3,8 +3,8 @@ import { appName } from '@/app.ts' diff --git a/resources/js/Components/Layout/Content/TitleDescription.vue b/resources/js/Components/Layout/Content/TitleDescription.vue index 56cc225..3658637 100644 --- a/resources/js/Components/Layout/Content/TitleDescription.vue +++ b/resources/js/Components/Layout/Content/TitleDescription.vue @@ -1,4 +1,8 @@ + + diff --git a/resources/js/Components/ui/job/JobCard.vue b/resources/js/Components/Layout/Job/JobCard.vue similarity index 100% rename from resources/js/Components/ui/job/JobCard.vue rename to resources/js/Components/Layout/Job/JobCard.vue diff --git a/resources/js/Components/Layout/Job/JobForm.vue b/resources/js/Components/Layout/Job/JobForm.vue new file mode 100644 index 0000000..d70eb46 --- /dev/null +++ b/resources/js/Components/Layout/Job/JobForm.vue @@ -0,0 +1,139 @@ + + + + diff --git a/resources/js/Components/Layout/Job/JobFormField.vue b/resources/js/Components/Layout/Job/JobFormField.vue new file mode 100644 index 0000000..3db71d9 --- /dev/null +++ b/resources/js/Components/Layout/Job/JobFormField.vue @@ -0,0 +1,26 @@ + + + diff --git a/resources/js/Components/Layout/MainNav/MainNavJobLink.vue b/resources/js/Components/Layout/MainNav/MainNavJobLink.vue index 772e31f..6aed7c0 100644 --- a/resources/js/Components/Layout/MainNav/MainNavJobLink.vue +++ b/resources/js/Components/Layout/MainNav/MainNavJobLink.vue @@ -8,7 +8,7 @@ defineProps<{ diff --git a/resources/js/Components/ui/button/Button.vue b/resources/js/Components/ui/button/Button.vue index 3af9457..d3e3786 100644 --- a/resources/js/Components/ui/button/Button.vue +++ b/resources/js/Components/ui/button/Button.vue @@ -1,6 +1,6 @@ + + diff --git a/resources/js/Components/ui/checkbox/VModelCheckbox.vue b/resources/js/Components/ui/checkbox/VModelCheckbox.vue new file mode 100644 index 0000000..a10877d --- /dev/null +++ b/resources/js/Components/ui/checkbox/VModelCheckbox.vue @@ -0,0 +1,15 @@ + + + diff --git a/resources/js/Components/ui/checkbox/index.ts b/resources/js/Components/ui/checkbox/index.ts new file mode 100644 index 0000000..8c28c28 --- /dev/null +++ b/resources/js/Components/ui/checkbox/index.ts @@ -0,0 +1 @@ +export { default as Checkbox } from './Checkbox.vue' diff --git a/resources/js/Components/ui/feedback/spinner/LoadingSpinner.vue b/resources/js/Components/ui/feedback/spinner/LoadingSpinner.vue new file mode 100644 index 0000000..0b82e86 --- /dev/null +++ b/resources/js/Components/ui/feedback/spinner/LoadingSpinner.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/resources/js/Components/ui/feedback/spinner/index.ts b/resources/js/Components/ui/feedback/spinner/index.ts new file mode 100644 index 0000000..e93bc3a --- /dev/null +++ b/resources/js/Components/ui/feedback/spinner/index.ts @@ -0,0 +1,19 @@ +import { cva, type VariantProps } from 'class-variance-authority' + +export const spinnerVariants = cva('animate-spin lucide lucide-loader-circle-icon', { + variants: { + size: { + default: 'w-4 h-4 m-2', + xs: 'w-1 h-1 m-1', + sm: 'w-2 h-2 m-1', + lg: 'w-7 h-7 m-3', + xl: 'w-12 h-12 m-4', + icon: 'w-10 h-10 m-4' + } + }, + defaultVariants: { + size: 'default' + } +}) + +export type SpinnerVariants = VariantProps diff --git a/resources/js/Components/ui/input/Input.vue b/resources/js/Components/ui/input/Input.vue new file mode 100644 index 0000000..81140b4 --- /dev/null +++ b/resources/js/Components/ui/input/Input.vue @@ -0,0 +1,24 @@ + + + diff --git a/resources/js/Components/ui/input/index.ts b/resources/js/Components/ui/input/index.ts new file mode 100644 index 0000000..a691dd6 --- /dev/null +++ b/resources/js/Components/ui/input/index.ts @@ -0,0 +1 @@ +export { default as Input } from './Input.vue' diff --git a/resources/js/Components/ui/label/Label.vue b/resources/js/Components/ui/label/Label.vue new file mode 100644 index 0000000..5ad1568 --- /dev/null +++ b/resources/js/Components/ui/label/Label.vue @@ -0,0 +1,27 @@ + + + diff --git a/resources/js/Components/ui/label/index.ts b/resources/js/Components/ui/label/index.ts new file mode 100644 index 0000000..572c2f0 --- /dev/null +++ b/resources/js/Components/ui/label/index.ts @@ -0,0 +1 @@ +export { default as Label } from './Label.vue' diff --git a/resources/js/Components/ui/text/Description.vue b/resources/js/Components/ui/text/Description.vue new file mode 100644 index 0000000..fd150ef --- /dev/null +++ b/resources/js/Components/ui/text/Description.vue @@ -0,0 +1,3 @@ + diff --git a/resources/js/Layouts/JobLayout.vue b/resources/js/Layouts/JobLayout.vue index 56b4e2c..5d36a9a 100644 --- a/resources/js/Layouts/JobLayout.vue +++ b/resources/js/Layouts/JobLayout.vue @@ -4,12 +4,17 @@ import BaseLayout from "./BaseLayout.vue"; import MainNavJobLink from "@/Components/Layout/MainNav/MainNavJobLink.vue"; import { Job } from "@/types/Jobs/job"; import MainNavItem from "@/Components/Layout/MainNav/MainNavItem.vue"; +import { httpApi } from "@/lib/utils"; +import { onMounted, ref } from "vue"; -let jobs = [ - { id: 1, name: "Hellcase" }, - { id: 2, name: "Jeu gratuit Epic Games" }, - { id: 3, name: "Envoyer un post instagram" }, -]; +const jobs = ref([]); + +async function fetchJobs() { + let jobsRaw = await httpApi("/jobs"); + jobs.value = jobsRaw.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); +} + +onMounted(fetchJobs); diff --git a/resources/js/lib/utils.ts b/resources/js/lib/utils.ts index bd0c391..a89aa17 100644 --- a/resources/js/lib/utils.ts +++ b/resources/js/lib/utils.ts @@ -4,3 +4,14 @@ import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } + + +export async function httpApi(route: string): Promise { + let response = await fetch(import.meta.env.VITE_APP_URL + "/api" + route); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return response.json(); +} diff --git a/resources/js/types/Jobs/job.d.ts b/resources/js/types/Jobs/job.d.ts index 0807006..0a6a4f2 100644 --- a/resources/js/types/Jobs/job.d.ts +++ b/resources/js/types/Jobs/job.d.ts @@ -3,5 +3,39 @@ export type Job = { name: string; description: string; is_active: boolean; + + job_infos: JobInfo[]; + created_at: Date; } + +export type JobInfo = { + id: number; + + name: string; + description: string; + placeholder: string; + + value: string | boolean; + is_required: boolean; + job_info_type: JobInfoType; + + job_id: number; +} + +export type JobInfoType = { + id: number; + name: string; + created_at: Date; +} + +export type JobRunArtifact = { + jobId: number; + artifacts: JobArtifact[]; + success: boolean; +} + +export type JobArtifact = { + name: string; + content: string; +} diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php index 5ed39e2..ca3d55e 100644 --- a/resources/views/app.blade.php +++ b/resources/views/app.blade.php @@ -12,7 +12,11 @@ @routes - @vite(['resources/js/app.ts', "resources/js/Pages/{$page['component']}.vue"]) + @if(config('app.env') === 'production') + @vite(['resources/js/app.ts']) + @else + @vite(['resources/js/app.ts', "resources/js/Pages/{$page['component']}.vue"]) + @endif @inertiaHead diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..df6b313 --- /dev/null +++ b/routes/api.php @@ -0,0 +1,19 @@ +json(Job::all())->header('Cache-Control', 'public, max-age=30'); +}); + + +Route::get('/test/{id}', function (Request $request, $id, BrowserJobsInstances $BrowserJobsInstances) { + $log = $BrowserJobsInstances->getJobInstance($id)->execute(); + return response()->json(['message' => 'Job ' . $id . ' ran : ' . $log]); +}); + +Route::get('jobs/{job}/test', [JobController::class, 'test'])->name('jobs.test'); diff --git a/routes/web.php b/routes/web.php index ab1c977..f2ec3e7 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,15 +1,12 @@ name('home'); -Route::get("/job/{jobId}", function ($jobId) { - return Inertia::render('Job', [ - 'job' => Job::find($jobId) - ]); -}); +Route::resource('jobs', JobController::class)->only(['show', 'update']); diff --git a/tests/Browser/ExampleTest.php b/tests/Browser/ExampleTest.php new file mode 100644 index 0000000..d5ad6b6 --- /dev/null +++ b/tests/Browser/ExampleTest.php @@ -0,0 +1,21 @@ +browse(function (Browser $browser) { + $browser->visit('/') + ->assertSee('Laravel'); + }); + } +} diff --git a/tests/Browser/Pages/HomePage.php b/tests/Browser/Pages/HomePage.php new file mode 100644 index 0000000..45d9283 --- /dev/null +++ b/tests/Browser/Pages/HomePage.php @@ -0,0 +1,36 @@ + + */ + public function elements(): array + { + return [ + '@element' => '#selector', + ]; + } +} diff --git a/tests/Browser/Pages/Page.php b/tests/Browser/Pages/Page.php new file mode 100644 index 0000000..eb9a2de --- /dev/null +++ b/tests/Browser/Pages/Page.php @@ -0,0 +1,20 @@ + + */ + public static function siteElements(): array + { + return [ + '@element' => '#selector', + ]; + } +} diff --git a/tests/Browser/console/.gitignore b/tests/Browser/console/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/tests/Browser/console/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/Browser/screenshots/.gitignore b/tests/Browser/screenshots/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/tests/Browser/screenshots/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/Browser/source/.gitignore b/tests/Browser/source/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/tests/Browser/source/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/DuskTestCase.php b/tests/DuskTestCase.php new file mode 100644 index 0000000..3a3cd54 --- /dev/null +++ b/tests/DuskTestCase.php @@ -0,0 +1,47 @@ +addArguments(collect([ + $this->shouldStartMaximized() ? '--start-maximized' : '--window-size=1920,1080', + '--disable-search-engine-choice-screen', + ])->unless($this->hasHeadlessDisabled(), function (Collection $items) { + return $items->merge([ + '--disable-gpu', + '--headless=new', + ]); + })->all()); + + return RemoteWebDriver::create( + $_ENV['DUSK_DRIVER_URL'] ?? env('DUSK_DRIVER_URL') ?? 'http://localhost:9515', + DesiredCapabilities::chrome()->setCapability( + ChromeOptions::CAPABILITY, $options + ) + ); + } +} diff --git a/undetectedChromedriver/chromedriver-linux b/undetectedChromedriver/chromedriver-linux new file mode 100755 index 0000000..50b7b91 Binary files /dev/null and b/undetectedChromedriver/chromedriver-linux differ diff --git a/undetectedChromedriver/getChromeDriver.sh b/undetectedChromedriver/getChromeDriver.sh new file mode 100644 index 0000000..5331f8a --- /dev/null +++ b/undetectedChromedriver/getChromeDriver.sh @@ -0,0 +1 @@ +sudo docker run --rm -it -p 3389:3389 -v ./undetectedChromedriver:/root/.local/share/undetected_chromedriver/ ultrafunk/undetected-chromedriver:latest diff --git a/undetectedChromedriver/seleniumChromedriverDockerfile b/undetectedChromedriver/seleniumChromedriverDockerfile new file mode 100644 index 0000000..34110df --- /dev/null +++ b/undetectedChromedriver/seleniumChromedriverDockerfile @@ -0,0 +1,10 @@ +FROM selenium/standalone-chrome:108.0 AS final + +COPY undetectedChromedriver/chromedriver-linux /bin/chromedriver +RUN mkdir -p /home/seluser/profile/ + +ENV TZ=Europe/Brussels + +HEALTHCHECK --interval=30s --timeout=10s --retries=3 CMD curl -s http://localhost:4444/wd/hub/status | jq -e '.value.ready == true' || exit 1 + + diff --git a/undetectedChromedriver/undetectedChromeDriver.yaml b/undetectedChromedriver/undetectedChromeDriver.yaml new file mode 100644 index 0000000..29ece17 --- /dev/null +++ b/undetectedChromedriver/undetectedChromeDriver.yaml @@ -0,0 +1,16 @@ +services: + undetected-chromedriver: + build: + context: ../ + dockerfile: undetectedChromedriver/seleniumChromedriverDockerfile + volumes: + - /tmp:/tmp + - chromeProfile:/home/seluser/profile/ + shm_size: 2gb + tty: true + ports: + - "4444:4444" + - "7900:7900" + +volumes: + chromeProfile: