Compare commits

...

10 Commits

40 changed files with 483 additions and 137 deletions

5
.env
View File

@@ -43,8 +43,11 @@ REDIS_HOST=redis://redis
MAILER_DSN=null://null MAILER_DSN=null://null
###< symfony/mailer ### ###< symfony/mailer ###
AUTH_METHOD=form_login
###> drenso/symfony-oidc-bundle ### ###> drenso/symfony-oidc-bundle ###
OIDC_WELL_KNOWN_URL="Enter the .well-known url for the OIDC provider" OIDC_WELL_KNOWN_URL="https://oidc/.well-known"
OIDC_CLIENT_ID="Enter your OIDC client id" OIDC_CLIENT_ID="Enter your OIDC client id"
OIDC_CLIENT_SECRET="Enter your OIDC client secret" OIDC_CLIENT_SECRET="Enter your OIDC client secret"
OIDC_BYPASS_FORM_LOGIN=false
###< drenso/symfony-oidc-bundle ### ###< drenso/symfony-oidc-bundle ###

View File

@@ -1,5 +1,23 @@
{ {
"controllers": { "controllers": {
"@spomky-labs/pwa-bundle": {
"connection-status": {
"enabled": true,
"fetch": "eager"
},
"backgroundsync-form": {
"enabled": true,
"fetch": "eager"
},
"sync-broadcast": {
"enabled": true,
"fetch": "eager"
},
"prefetch-on-demand": {
"enabled": true,
"fetch": "eager"
}
},
"@symfony/ux-autocomplete": { "@symfony/ux-autocomplete": {
"autocomplete": { "autocomplete": {
"enabled": true, "enabled": true,

View File

@@ -27,6 +27,7 @@
"php-tmdb/api": "^4.1", "php-tmdb/api": "^4.1",
"predis/predis": "^2.4", "predis/predis": "^2.4",
"runtime/frankenphp-symfony": "^0.2.0", "runtime/frankenphp-symfony": "^0.2.0",
"spomky-labs/pwa-bundle": "^1.2",
"stof/doctrine-extensions-bundle": "^1.14", "stof/doctrine-extensions-bundle": "^1.14",
"symfony/asset": "7.3.*", "symfony/asset": "7.3.*",
"symfony/console": "7.3.*", "symfony/console": "7.3.*",

123
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "bfbdc7ee820da20b824f4b1933fe967b", "content-hash": "0f98dada0a01d471cebf4eb1b51b9006",
"packages": [ "packages": [
{ {
"name": "1tomany/rich-bundle", "name": "1tomany/rich-bundle",
@@ -4866,6 +4866,127 @@
], ],
"time": "2025-06-13T08:35:04+00:00" "time": "2025-06-13T08:35:04+00:00"
}, },
{
"name": "spomky-labs/pwa-bundle",
"version": "1.2.5",
"source": {
"type": "git",
"url": "https://github.com/Spomky-Labs/pwa-bundle.git",
"reference": "c24711ea8428d14a01132d9e42cb17d84218a1ee"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Spomky-Labs/pwa-bundle/zipball/c24711ea8428d14a01132d9e42cb17d84218a1ee",
"reference": "c24711ea8428d14a01132d9e42cb17d84218a1ee",
"shasum": ""
},
"require": {
"php": ">=8.2",
"phpdocumentor/reflection-docblock": "^5.3",
"psr/log": "^1.0|^2.0|^3.0",
"symfony/asset": "^6.4|^7.0",
"symfony/asset-mapper": "^6.4|^7.0",
"symfony/config": "^6.4|^7.0",
"symfony/dependency-injection": "^6.4|^7.0",
"symfony/deprecation-contracts": "^3.5",
"symfony/http-kernel": "^6.4|^7.0",
"symfony/property-access": "^6.4|^7.0",
"symfony/property-info": "^6.4|^7.0",
"symfony/routing": "^6.4|^7.0",
"symfony/serializer": "^6.4|^7.0",
"twig/twig": "^3.8"
},
"require-dev": {
"dbrekelmans/bdi": "^1.1",
"ekino/phpstan-banned-code": "^1.0|^2.0|^3.0",
"ergebnis/phpunit-slow-test-detector": "^2.14",
"ext-sockets": "*",
"infection/infection": "^0.28|^0.29",
"matthiasnoback/symfony-config-test": "^5.1",
"php-parallel-lint/php-parallel-lint": "^1.4",
"phpstan/extension-installer": "^1.1",
"phpstan/phpdoc-parser": "^1.28|^2.0",
"phpstan/phpstan": "^1.0|^2.0",
"phpstan/phpstan-beberlei-assert": "^1.0|^2.0",
"phpstan/phpstan-deprecation-rules": "^1.0|^2.0",
"phpstan/phpstan-phpunit": "^1.4|^2.0",
"phpstan/phpstan-strict-rules": "^1.0|^2.0",
"phpstan/phpstan-symfony": "^1.4|^2.0",
"phpunit/phpunit": "^10.1|^11.0",
"rector/rector": "^1.0|^2.0",
"staabm/phpstan-todo-by": "^0.1.27|^0.2",
"struggle-for-php/sfp-phpstan-psr-log": "^0.21.0|^0.22|^0.23",
"symfony/filesystem": "^6.4|^7.0",
"symfony/framework-bundle": "^6.4|^7.0",
"symfony/mime": "^6.4|^7.0",
"symfony/monolog-bundle": "^3.10",
"symfony/panther": "^2.1",
"symfony/phpunit-bridge": "^6.4|^7.0",
"symfony/translation": "^7.0",
"symfony/yaml": "^6.4|^7.0",
"symplify/easy-coding-standard": "^12.0"
},
"suggest": {
"ext-gd": "Required to generate icons (or Imagick).",
"ext-imagick": "Required to generate icons (or GD).",
"symfony/filesystem": "For generating and manipulating icons or screenshots",
"symfony/mime": "For generating and manipulating icons or screenshots",
"symfony/panther": "For generating screenshots directly from your application"
},
"type": "symfony-bundle",
"extra": {
"thanks": {
"url": "https://github.com/spomky-labs/pwa-bundle",
"name": "spomky-labs/pwa-bundle"
}
},
"autoload": {
"psr-4": {
"SpomkyLabs\\PwaBundle\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Florent Morselli",
"homepage": "https://github.com/Spomky"
},
{
"name": "All contributors",
"homepage": "https://github.com/spomky-labs/pwa-bundle/contributors"
}
],
"description": "Progressive Web App Manifest Generator Bundle for Symfony.",
"homepage": "https://github.com/spomky-labs",
"keywords": [
"bundle",
"pwa",
"symfony",
"symfony-ux"
],
"support": {
"issues": "https://github.com/Spomky-Labs/pwa-bundle/issues",
"source": "https://github.com/Spomky-Labs/pwa-bundle/tree/1.2.5"
},
"funding": [
{
"url": "https://www.buymeacoffee.com/FlorentMorselli",
"type": "custom"
},
{
"url": "https://github.com/Spomky",
"type": "github"
},
{
"url": "https://www.patreon.com/FlorentMorselli",
"type": "patreon"
}
],
"time": "2024-12-16T08:02:21+00:00"
},
{ {
"name": "stof/doctrine-extensions-bundle", "name": "stof/doctrine-extensions-bundle",
"version": "v1.14.0", "version": "v1.14.0",

View File

@@ -22,4 +22,5 @@ return [
Symfony\UX\Autocomplete\AutocompleteBundle::class => ['all' => true], Symfony\UX\Autocomplete\AutocompleteBundle::class => ['all' => true],
SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle::class => ['all' => true], SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle::class => ['all' => true],
Drenso\OidcBundle\DrensoOidcBundle::class => ['all' => true], Drenso\OidcBundle\DrensoOidcBundle::class => ['all' => true],
SpomkyLabs\PwaBundle\SpomkyLabsPwaBundle::class => ['all' => true],
]; ];

21
config/packages/pwa.yaml Normal file
View File

@@ -0,0 +1,21 @@
pwa:
manifest:
enabled: true
name: "Torsearch"
short_name: "torsearch"
start_url: "/"
display: "standalone"
id: "/"
background_color: "#f98e44"
theme_color: "#083344"
description: Torsearch provides a simple and intuitive way to manage your personal media library.
icons:
- src: "icon.png"
sizes: [ 192 ]
- src: "icon.png"
sizes: [ 192 ]
purpose: maskable
categories:
- entertainment
- multimedia
- utilities

View File

@@ -24,9 +24,15 @@ security:
logout: logout:
path: /logout path: /logout
provider: app_oidc provider: app_oidc
form_login:
login_path: app_login
check_path: app_login
enable_csrf: true
oidc: oidc:
login_path: '/login/oidc' login_path: '/login/oidc'
check_path: '/login/oidc/auth' check_path: '/login/oidc/auth'
enable_end_session_listener: true
entry_point: form_login
# activate different ways to authenticate # activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall # https://symfony.com/doc/current/security.html#the-firewall

View File

@@ -1,61 +0,0 @@
security:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
users_in_memory: { memory: null }
app_local:
entity:
class: App\User\Framework\Entity\User
property: email
app_ldap:
id: App\User\Framework\Security\LdapUserProvider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_ldap
entry_point: form_login_ldap
form_login_ldap:
login_path: app_login
check_path: app_login
enable_csrf: true
service: Symfony\Component\Ldap\Ldap
dn_string: '%env(LDAP_DN_STRING)%'
form_login:
login_path: app_login
check_path: app_login
enable_csrf: true
logout:
path: app_logout
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/reset-password, roles: PUBLIC_ACCESS }
- { path: ^/login, roles: PUBLIC_ACCESS }
- { path: ^/, roles: ROLE_USER } # Or ROLE_ADMIN, ROLE_SUPER_ADMIN,
when@test:
security:
password_hashers:
# By default, password hashers are resource intensive and take time. This is
# important to generate secure password hashes. In tests however, secure hashes
# are not important, waste resources and increase test times. The following
# reduces the work factor to the lowest possible values.
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
algorithm: auto
cost: 4 # Lowest possible value for bcrypt
time_cost: 3 # Lowest possible value for argon
memory_cost: 10 # Lowest possible value for argon

View File

@@ -6,6 +6,7 @@
parameters: parameters:
# App # App
app.url: '%env(APP_URL)%' app.url: '%env(APP_URL)%'
app.version: '%env(default:app.default.version:APP_VERSION)%'
# Debrid Services # Debrid Services
app.debrid.real_debrid.key: '%env(REAL_DEBRID_KEY)%' app.debrid.real_debrid.key: '%env(REAL_DEBRID_KEY)%'
@@ -34,7 +35,14 @@ parameters:
app.default.version: '0.dev' app.default.version: '0.dev'
app.default.timezone: 'America/Chicago' app.default.timezone: 'America/Chicago'
app.version: '%env(default:app.default.version:APP_VERSION)%' # Auth
auth.default.method: 'form_login'
auth.method: '%env(default:auth.default.method:AUTH_METHOD)%'
auth.oidc.well_known_url: '%env(OIDC_WELL_KNOWN_URL)%'
auth.oidc.client_id: '%env(OIDC_CLIENT_ID)%'
auth.oidc.client_secret: '%env(OIDC_CLIENT_SECRET)%'
auth.oidc.bypass_form_login: '%env(bool:OIDC_BYPASS_FORM_LOGIN)%'
services: services:
# default configuration for services in *this* file # default configuration for services in *this* file

View File

@@ -7,6 +7,11 @@ APP_URL="https://dev.caldwell.digital"
APP_SECRET="70169beadfbc8101c393cbfbba27a313" APP_SECRET="70169beadfbc8101c393cbfbba27a313"
APP_ENV=prod APP_ENV=prod
# Mercure is a Caddy module built into the webserver
# that facilitates the usage of websockets to transmit
# real time data (download progress, etc.)
MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!"
# Use the DATABASE_URL below to use the MariaDB container # Use the DATABASE_URL below to use the MariaDB container
# provided in the example.compose.yml file, or remove this # provided in the example.compose.yml file, or remove this
# line and fill in the details of your own MySQL/MariaDB server # line and fill in the details of your own MySQL/MariaDB server
@@ -19,39 +24,48 @@ DATABASE_URL="mysql://root:password@database:3306/app?serverVersion=10.6.19.2-Ma
# This key is never saved anywhere # This key is never saved anywhere
# else and is passed to Torrentio # else and is passed to Torrentio
# to retrieve download options # to retrieve download options
#REAL_DEBRID_KEY="" REAL_DEBRID_KEY=""
# Enter you TMDB API key # Enter your TMDB API key
# This is used to provide rich search results # This is used to provide rich search results
# when searching for media and rendering the # when searching for media and rendering the
# Popular Movies and TV Shows section. # Popular Movies and TV Shows section.
#TMDB_API= TMDB_API=""
REAL_DEBRID_KEY=""
TMDB_API=eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI0ZTJjYjJhOGUzOGJhNjdiNjVhOGU1NGM0ZWI1MzhmOCIsIm5iZiI6MTczNzkyNjA0NC41NjQsInN1YiI6IjY3OTZhNTljYzdiMDFiNzJjNzIzZWM5YiIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.e8DbNe9qrSBC1y-ANRv-VWBAtls-ZS2r7aNCiI68mpw
MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!"
# Use your own Redis instance or use the # Use your own Redis instance or use the
# below value to use the container included # below value to use the container included
# in the example compose.yml file. # in the example compose.yml file.
REDIS_HOST="redis://redis" REDIS_HOST="redis://redis"
### Auth ###
# Change to "oidc" to and provide the required
# environment variables below to use OIDC auth.
AUTH_METHOD=form_login
# OIDC
OIDC_WELL_KNOWN_URL=
OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=
# Allows you to skip the login page and directly
# rely on your IdP for auth.
OIDC_BYPASS_FORM_LOGIN=
# LDAP Config: To use LDAP, enter the below fields # LDAP Config: To use LDAP, enter the below fields
# and run 'php bin/console config:set auth.method ldap' # and run 'php bin/console config:set auth.method ldap'
LDAP_HOST= # (LDAP is still in progress and not ready for use)
LDAP_PORT= #LDAP_HOST=
LDAP_ENCRYPTION= #LDAP_PORT=
LDAP_BASE_DN= #LDAP_ENCRYPTION=
LDAP_BIND_USER= #LDAP_BASE_DN=
LDAP_BIND_PASS= #LDAP_BIND_USER=
LDAP_DN_STRING= #LDAP_BIND_PASS=
LDAP_UID_KEY="uid" #LDAP_DN_STRING=
#LDAP_UID_KEY="uid"
# LDAP group that identifies an Admin # LDAP group that identifies an Admin
# Users with this LDAP group will automatically # Users with this LDAP group will automatically
# get the admin role in this system. # get the admin role in this system.
LDAP_ADMIN_ROLE_DN="" #LDAP_ADMIN_ROLE_DN=""
LDAP_EMAIL_ATTRIBUTE=mail #LDAP_EMAIL_ATTRIBUTE=mail
LDAP_USERNAME_ATTRIBUTE=uid #LDAP_USERNAME_ATTRIBUTE=uid
LDAP_NAME_ATTRIBUTE=displayname #LDAP_NAME_ATTRIBUTE=displayname

BIN
public/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -23,6 +23,21 @@ final class ConfigResolver
#[Autowire(param: 'media.tvshows.path')] #[Autowire(param: 'media.tvshows.path')]
private readonly ?string $tvshowsPath = null, private readonly ?string $tvshowsPath = null,
#[Autowire(param: 'auth.method')]
private readonly ?string $authMethod = null,
#[Autowire(param: 'auth.oidc.well_known_url')]
private readonly ?string $authOidcWellKnownUrl = null,
#[Autowire(param: 'auth.oidc.client_id')]
private readonly ?string $authOidcClientId = null,
#[Autowire(param: 'auth.oidc.client_secret')]
private readonly ?string $authOidcClientSecret = null,
#[Autowire(param: 'auth.oidc.bypass_form_login')]
private ?bool $authOidcBypassFormLogin = null,
) {} ) {}
public function validate(): bool public function validate(): bool
@@ -46,4 +61,35 @@ final class ConfigResolver
{ {
return $this->messages; return $this->messages;
} }
public function authIs(string $method): bool
{
if (strtolower($method) === strtolower($this->getAuthMethod())) {
return true;
}
return false;
}
public function getAuthMethod(): string
{
return strtolower($this->authMethod);
}
public function bypassFormLogin(): bool
{
return $this->authOidcBypassFormLogin;
}
public function getAuthConfig(): array
{
return [
'method' => $this->authMethod,
'oidc' => [
'well_known_url' => $this->authOidcWellKnownUrl,
'client_id' => $this->authOidcClientId,
'client_secret' => $this->authOidcClientSecret,
'bypass_form_login' => $this->authOidcBypassFormLogin,
]
];
}
} }

View File

@@ -2,7 +2,7 @@
namespace App\Base\Framework\Controller; namespace App\Base\Framework\Controller;
use App\Base\Util\Broadcaster; use App\Base\Service\Broadcaster;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Base\Util; namespace App\Base\Service;
use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\RequestStack;

View File

@@ -2,9 +2,8 @@
namespace App\Download\Framework\Controller; namespace App\Download\Framework\Controller;
use App\Base\Util\Broadcaster; use App\Base\Service\Broadcaster;
use App\Download\Action\Handler\DeleteDownloadHandler; use App\Download\Action\Handler\DeleteDownloadHandler;
use App\Download\Action\Handler\DownloadSeasonHandler;
use App\Download\Action\Handler\PauseDownloadHandler; use App\Download\Action\Handler\PauseDownloadHandler;
use App\Download\Action\Handler\ResumeDownloadHandler; use App\Download\Action\Handler\ResumeDownloadHandler;
use App\Download\Action\Input\DeleteDownloadInput; use App\Download\Action\Input\DeleteDownloadInput;
@@ -13,8 +12,6 @@ use App\Download\Action\Input\DownloadSeasonInput;
use App\Download\Action\Input\PauseDownloadInput; use App\Download\Action\Input\PauseDownloadInput;
use App\Download\Action\Input\ResumeDownloadInput; use App\Download\Action\Input\ResumeDownloadInput;
use App\Download\Framework\Repository\DownloadRepository; use App\Download\Framework\Repository\DownloadRepository;
use App\User\Dto\UserPreferencesFactory;
use Nihilarr\PTN;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\MessageBusInterface;

View File

@@ -2,7 +2,7 @@
namespace App\Monitor\Framework\Controller; namespace App\Monitor\Framework\Controller;
use App\Base\Util\Broadcaster; use App\Base\Service\Broadcaster;
use App\Monitor\Action\Handler\AddMonitorHandler; use App\Monitor\Action\Handler\AddMonitorHandler;
use App\Monitor\Action\Handler\DeleteMonitorHandler; use App\Monitor\Action\Handler\DeleteMonitorHandler;
use App\Monitor\Action\Input\AddMonitorInput; use App\Monitor\Action\Input\AddMonitorInput;

View File

@@ -2,7 +2,7 @@
namespace App\Torrentio\Framework\Controller; namespace App\Torrentio\Framework\Controller;
use App\Base\Util\Broadcaster; use App\Base\Service\Broadcaster;
use App\Torrentio\Action\Handler\GetMovieOptionsHandler; use App\Torrentio\Action\Handler\GetMovieOptionsHandler;
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler; use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
use App\Torrentio\Action\Input\GetMovieOptionsInput; use App\Torrentio\Action\Input\GetMovieOptionsInput;

View File

@@ -2,7 +2,7 @@
namespace App\Torrentio\Result; namespace App\Torrentio\Result;
use App\Base\Util\CountryLanguages; use App\User\Database\CountryLanguages;
use Nihilarr\PTN; use Nihilarr\PTN;
class ResultFactory class ResultFactory

View File

@@ -3,7 +3,7 @@
namespace App\Twig\Components; namespace App\Twig\Components;
use Aimeos\Map; use Aimeos\Map;
use App\Base\Util\QualityList; use App\User\Database\QualityList;
use App\User\Framework\Repository\PreferencesRepository; use App\User\Framework\Repository\PreferencesRepository;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Base\Util; namespace App\User\Database;
class CountryCodes class CountryCodes
{ {

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Base\Util; namespace App\User\Database;
class CountryLanguages class CountryLanguages
{ {
@@ -137,4 +137,13 @@ class CountryLanguages
return $countryLanguages[$countryName] ?? null; return $countryLanguages[$countryName] ?? null;
} }
public static function asSelectOptions(): array
{
$result = [];
foreach (static::$languages as $language) {
$result[$language] = $language;
}
return $result;
}
} }

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Base\Util; namespace App\User\Database;
class ProviderList class ProviderList
{ {
@@ -23,4 +23,13 @@ class ProviderList
{ {
return self::$providers; return self::$providers;
} }
public static function asSelectOptions(): array
{
$result = [];
foreach (static::$providers as $provider) {
$result[$provider] = $provider;
}
return $result;
}
} }

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Base\Util; namespace App\User\Database;
class QualityList class QualityList
{ {
@@ -100,6 +100,15 @@ class QualityList
return array_search($key, self::$qualities) ?? null; return array_search($key, self::$qualities) ?? null;
} }
public static function asSelectOptions(): array
{
$result = [];
foreach (array_keys(static::$qualities) as $quality) {
$result[$quality] = $quality;
}
return $result;
}
public static function getAsReverseMap(): array public static function getAsReverseMap(): array
{ {
$results = []; $results = [];

View File

@@ -2,29 +2,30 @@
namespace App\User\Framework\Controller\Web; namespace App\User\Framework\Controller\Web;
use App\Base\ConfigResolver;
use App\User\Framework\Repository\UserRepository; use App\User\Framework\Repository\UserRepository;
use App\User\Framework\Security\OidcUserProvider;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Drenso\OidcBundle\Exception\OidcException;
use Drenso\OidcBundle\OidcClientInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class LoginController extends AbstractController class LoginController extends AbstractController
{ {
#[Route(path: '/login', name: 'app_login')] #[Route(path: '/login', name: 'app_login')]
public function login(AuthenticationUtils $authenticationUtils, UserRepository $userRepository): Response public function login(ConfigResolver $config, AuthenticationUtils $authenticationUtils, UserRepository $userRepository): Response
{ {
if ((new ArrayCollection($userRepository->findAll()))->count() === 0) { if ((new ArrayCollection($userRepository->findAll()))->count() === 0) {
return $this->redirectToRoute('app_getting_started'); return $this->redirectToRoute('app_getting_started');
} }
if ($config->authIs('oidc') && $config->bypassFormLogin()) {
return $this->redirectToRoute('app_login_oidc');
}
// get the login error if there is one // get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError(); $error = $authenticationUtils->getLastAuthenticationError();
@@ -32,6 +33,7 @@ class LoginController extends AbstractController
$lastUsername = $authenticationUtils->getLastUsername(); $lastUsername = $authenticationUtils->getLastUsername();
return $this->render('user/login.html.twig', [ return $this->render('user/login.html.twig', [
'show_oidc_button' => $config->authIs('oidc'),
'last_username' => $lastUsername, 'last_username' => $lastUsername,
'error' => $error, 'error' => $error,
]); ]);

View File

@@ -2,6 +2,7 @@
namespace App\User\Framework\Controller\Web; namespace App\User\Framework\Controller\Web;
use App\Base\ConfigResolver;
use Drenso\OidcBundle\OidcClientInterface; use Drenso\OidcBundle\OidcClientInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
@@ -11,16 +12,29 @@ use Symfony\Component\Routing\Attribute\Route;
class LoginOidcController extends AbstractController class LoginOidcController extends AbstractController
{ {
public function __construct(
private ConfigResolver $configResolver,
) {}
#[Route('/login/oidc', name: 'app_login_oidc')] #[Route('/login/oidc', name: 'app_login_oidc')]
public function oidcStart(OidcClientInterface $oidcClient): RedirectResponse public function oidcStart(OidcClientInterface $oidcClient): RedirectResponse
{ {
if (false === $this->configResolver->authIs('oidc')) {
throw new \Exception('You must configure the OIDC environment variables before logging in at this route.');
}
// Redirect to authorization @ OIDC provider // Redirect to authorization @ OIDC provider
return $oidcClient->generateAuthorizationRedirect(); return $oidcClient->generateAuthorizationRedirect(scopes: ['openid', 'profile']);
} }
#[Route('/login/oidc/auth', name: 'app_login_oidc_auth')] #[Route('/login/oidc/auth', name: 'app_login_oidc_auth')]
public function oidcAuthenticate(): RedirectResponse public function oidcAuthenticate(): RedirectResponse
{ {
if (false === $this->configResolver->authIs('oidc')) {
throw new \Exception('You must configure the OIDC environment variables before logging in at this route.');
}
throw new \LogicException('This method can be blank - it will be intercepted by the "oidc" key on your firewall.'); throw new \LogicException('This method can be blank - it will be intercepted by the "oidc" key on your firewall.');
} }

View File

@@ -4,14 +4,16 @@ declare(strict_types=1);
namespace App\User\Framework\Controller\Web; namespace App\User\Framework\Controller\Web;
use App\Base\Util\Broadcaster; use App\Base\Service\Broadcaster;
use App\Base\Util\CountryLanguages;
use App\Base\Util\ProviderList;
use App\Base\Util\QualityList;
use App\User\Action\Handler\SaveUserDownloadPreferencesHandler; use App\User\Action\Handler\SaveUserDownloadPreferencesHandler;
use App\User\Action\Handler\SaveUserMediaPreferencesHandler; use App\User\Action\Handler\SaveUserMediaPreferencesHandler;
use App\User\Action\Input\SaveUserDownloadPreferencesInput; use App\User\Action\Input\SaveUserDownloadPreferencesInput;
use App\User\Action\Input\SaveUserMediaPreferencesInput; use App\User\Action\Input\SaveUserMediaPreferencesInput;
use App\User\Database\CountryLanguages;
use App\User\Database\ProviderList;
use App\User\Database\QualityList;
use App\User\Dto\UserPreferencesFactory;
use App\User\Framework\Form\GettingStartedFilterForm;
use App\User\Framework\Repository\PreferencesRepository; use App\User\Framework\Repository\PreferencesRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
@@ -41,6 +43,7 @@ class PreferencesController extends AbstractController
'qualities' => QualityList::getBaseQualities(), 'qualities' => QualityList::getBaseQualities(),
'mediaPreferences' => $mediaPreferences, 'mediaPreferences' => $mediaPreferences,
'downloadPreferences' => $downloadPreferences, 'downloadPreferences' => $downloadPreferences,
'filterForm' => $this->createForm(GettingStartedFilterForm::class, (array) UserPreferencesFactory::createFromUser($this->getUser())),
] ]
); );
} }
@@ -72,6 +75,7 @@ class PreferencesController extends AbstractController
'qualities' => QualityList::getBaseQualities(), 'qualities' => QualityList::getBaseQualities(),
'mediaPreferences' => $mediaPreferences, 'mediaPreferences' => $mediaPreferences,
'downloadPreferences' => $downloadPreferences, 'downloadPreferences' => $downloadPreferences,
'filterForm' => $this->createForm(GettingStartedFilterForm::class ?? null),
] ]
); );
} }

View File

@@ -5,30 +5,22 @@ namespace App\User\Framework\Controller\Web;
use App\User\Action\Command\RegisterUserCommand; use App\User\Action\Command\RegisterUserCommand;
use App\User\Action\Handler\RegisterUserHandler; use App\User\Action\Handler\RegisterUserHandler;
use App\User\Framework\Entity\User; use App\User\Framework\Entity\User;
use App\User\Framework\Form\GettingStartedFilterForm;
use App\User\Framework\Form\RegistrationFormType; use App\User\Framework\Form\RegistrationFormType;
use App\User\Framework\Pipeline\GettingStarted\AddPreferencesToDatabase;
use App\User\Framework\Pipeline\GettingStarted\GettingStartedInput;
use App\User\Framework\Pipeline\GettingStarted\MigrateDatabase;
use App\User\Framework\Repository\PreferencesRepository;
use App\User\Framework\Repository\UserRepository; use App\User\Framework\Repository\UserRepository;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use League\Pipeline\Pipeline;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
class RegistrationController extends AbstractController class RegistrationController extends AbstractController
{ {
public function __construct(private readonly RegisterUserHandler $registerUserHandler, public function __construct(private readonly RegisterUserHandler $registerUserHandler,
private readonly RequestStack $requestStack private readonly RequestStack $requestStack,
) ) {}
{
}
#[Route('/register', name: 'app_register')] #[Route('/register', name: 'app_register')]
public function register( public function register(
@@ -57,7 +49,7 @@ class RegistrationController extends AbstractController
} }
#[Route(path: '/getting-started', name: 'app_getting_started')] #[Route(path: '/getting-started', name: 'app_getting_started')]
public function gettingStarted(Request $request, Security $security, UserRepository $userRepository, PreferencesRepository $preferencesRepository, KernelInterface $kernel, LoggerInterface $logger): Response public function gettingStarted(Request $request, Security $security, UserRepository $userRepository): Response
{ {
if ((new ArrayCollection($userRepository->findAll()))->count() !== 0) { if ((new ArrayCollection($userRepository->findAll()))->count() !== 0) {
return $this->redirectToRoute('app_index'); return $this->redirectToRoute('app_index');
@@ -73,14 +65,42 @@ class RegistrationController extends AbstractController
password: $form->get('plainPassword')->getData(), password: $form->get('plainPassword')->getData(),
)); ));
$security->login($user->user); $security->login($user->user, 'form_login');
$this->requestStack->getCurrentRequest()->getSession()->set('mercure_alert_topic', 'alerts_' . uniqid()); $this->requestStack->getCurrentRequest()->getSession()->set('mercure_alert_topic', 'alerts_' . uniqid());
return $this->redirectToRoute('app_index'); return $this->redirectToRoute('app_getting_started_filter');
} }
return $this->render('user/getting-started.html.twig', [ return $this->render('user/getting_started/register-user.html.twig', [
'registrationForm' => $form, 'registrationForm' => $form,
]); ]);
} }
#[Route(path: '/getting-started/filter', name: 'app_getting_started_filter')]
public function gettingStartedPreferences(Request $request, UserRepository $userRepository): Response
{
if ((new ArrayCollection($userRepository->findAll()))->count() !== 0) {
return $this->redirectToRoute('app_index');
}
$form = $this->createForm(GettingStartedFilterForm::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
foreach ($form->getData() as $preference => $value) {
if (null !== $value) {
$this->getUser()->updateUserPreference($preference, $value);
}
}
$userRepository->getEntityManager()->flush();
return $this->redirectToRoute('app_index');
}
return $this->render(
'user/getting_started/filter.html.twig',
[
'form' => $form,
]
);
}
} }

View File

@@ -2,6 +2,7 @@
namespace App\User\Framework\Controller\Web; namespace App\User\Framework\Controller\Web;
use App\Base\ConfigResolver;
use App\User\Framework\Entity\User; use App\User\Framework\Entity\User;
use App\User\Framework\Form\ChangePasswordForm; use App\User\Framework\Form\ChangePasswordForm;
use App\User\Framework\Form\ResetPasswordRequestForm; use App\User\Framework\Form\ResetPasswordRequestForm;
@@ -29,6 +30,7 @@ class ResetPasswordController extends AbstractController
public function __construct( public function __construct(
private ResetPasswordHelperInterface $resetPasswordHelper, private ResetPasswordHelperInterface $resetPasswordHelper,
private EntityManagerInterface $entityManager, private EntityManagerInterface $entityManager,
private readonly ConfigResolver $configResolver,
private readonly Security $security private readonly Security $security
) { ) {
} }
@@ -45,6 +47,13 @@ class ResetPasswordController extends AbstractController
$form = $this->createForm(ResetPasswordRequestForm::class); $form = $this->createForm(ResetPasswordRequestForm::class);
$form->handleRequest($request); $form->handleRequest($request);
if ($this->configResolver->authIs('oidc')) {
$this->addFlash('reset_password_error', 'Your auth method is set to "oidc", so you will need to reset your password with your identity provider.');
return $this->render('user/reset_password/request.html.twig', [
'requestForm' => $form,
])->setStatusCode(Response::HTTP_ACCEPTED);
}
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
/** @var string $email */ /** @var string $email */
$email = $form->get('email')->getData(); $email = $form->get('email')->getData();

View File

@@ -3,7 +3,7 @@
namespace App\User\Framework\EventListener; namespace App\User\Framework\EventListener;
use App\Base\ConfigResolver; use App\Base\ConfigResolver;
use App\Base\Util\Broadcaster; use App\Base\Service\Broadcaster;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener; use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;

View File

@@ -0,0 +1,59 @@
<?php
namespace App\User\Framework\Form;
use Aimeos\Map;
use App\User\Database\CountryLanguages;
use App\User\Database\ProviderList;
use App\User\Database\QualityList;
use App\User\Framework\Repository\PreferenceOptionRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class GettingStartedFilterForm extends AbstractType
{
public function __construct(
private readonly PreferenceOptionRepository $preferenceOptionRepository,
) {}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$this->addChoiceField($builder, 'language', CountryLanguages::asSelectOptions());
$this->addChoiceField($builder, 'quality', QualityList::asSelectOptions());
$this->addChoiceField($builder, 'provider', ProviderList::asSelectOptions());
$this->addChoiceField($builder, 'resolution', $this->getPreferenceChoices('resolution'));
$this->addChoiceField($builder, 'codec', $this->getPreferenceChoices('codec'));
}
private function addChoiceField(FormBuilderInterface $builder, string $fieldName, array $choices): void
{
$question = [
'attr' => ['class' => 'w-full text-input mb-4'],
'label_attr' => ['class' => 'w-full block font-semibold'],
'choices' => $this->addDefaultChoice($choices),
];
$builder->add($fieldName, ChoiceType::class, $question);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([]);
}
private function getPreferenceChoices(string $preference): array
{
$options = $this->preferenceOptionRepository->findBy(['preference' => $preference]);
$result = [];
foreach ($options as $item) {
$result[$item->getName()] = $item->getId();
}
return $result;
}
private function addDefaultChoice(array $choices): iterable
{
return ['n/a' => null] + $choices;
}
}

View File

@@ -27,7 +27,7 @@ class RegistrationFormType extends AbstractType
'message' => 'Please enter a password', 'message' => 'Please enter a password',
]), ]),
new Length([ new Length([
'min' => 6, 'min' => 8,
'minMessage' => 'Your password should be at least {{ limit }} characters', 'minMessage' => 'Your password should be at least {{ limit }} characters',
// max length allowed by Symfony for security reasons // max length allowed by Symfony for security reasons
'max' => 4096, 'max' => 4096,

View File

@@ -8,6 +8,7 @@ use Drenso\OidcBundle\Exception\OidcException;
use Drenso\OidcBundle\Model\OidcTokens; use Drenso\OidcBundle\Model\OidcTokens;
use Drenso\OidcBundle\Model\OidcUserData; use Drenso\OidcBundle\Model\OidcUserData;
use Drenso\OidcBundle\Security\UserProvider\OidcUserProviderInterface; use Drenso\OidcBundle\Security\UserProvider\OidcUserProviderInterface;
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException; use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\OidcUser; use Symfony\Component\Security\Core\User\OidcUser;
@@ -25,8 +26,9 @@ class OidcUserProvider implements OidcUserProviderInterface
if (null === $user) { if (null === $user) {
$user = new User() $user = new User()
->setEmail($userData->getEmail()) ->setEmail(!empty($userData->getEmail()) ? $userData->getEmail() : $userData->getSub())
->setName($userData->getFullName()) ->setName(!empty($userData->getFullName()) ? $userData->getFullName() : $userData->getGivenName())
->setPassword('n/a')
; ;
$this->userRepository->getEntityManager()->persist($user); $this->userRepository->getEntityManager()->persist($user);
$this->userRepository->getEntityManager()->flush(); $this->userRepository->getEntityManager()->flush();

View File

@@ -86,6 +86,9 @@
"phpstan.dist.neon" "phpstan.dist.neon"
] ]
}, },
"spomky-labs/pwa-bundle": {
"version": "1.2.5"
},
"stof/doctrine-extensions-bundle": { "stof/doctrine-extensions-bundle": {
"version": "1.14", "version": "1.14",
"recipe": { "recipe": {

View File

@@ -3,6 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
{{ pwa() }}
<title>{% block title %}Welcome!{% endblock %}</title> <title>{% block title %}Welcome!{% endblock %}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>">
{% block stylesheets %} {% block stylesheets %}

View File

@@ -3,6 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
{{ pwa() }}
<title>{% block title %}Welcome!{% endblock %}</title> <title>{% block title %}Welcome!{% endblock %}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>">
{% block stylesheets %} {% block stylesheets %}

View File

@@ -1,7 +1,7 @@
<header {{ attributes }} class="bg-cyan-950 z-40"> <header {{ attributes }} class="bg-cyan-950 z-40">
<div class="px-4 sm:px-6 lg:px-8"> <div class="px-4 sm:px-6 lg:px-8">
<div class="h-16 flex flex-row items-center justify-between"> <div class="h-16 flex flex-row items-center justify-between">
<a href="{{ path('app_index') }}" class="text-2xl text-orange-500 mr-4 md:hidden">T</a> <a href="{{ path('app_index') }}" class="text-2xl font-extrabold text-orange-500 mr-4 md:hidden">T</a>
<twig:SearchBar /> <twig:SearchBar />
<div class="md:flex md:items-center md:gap-12"> <div class="md:flex md:items-center md:gap-12">
<nav aria-label="Global" class="md:block"> <nav aria-label="Global" class="md:block">
@@ -32,6 +32,9 @@
{% for message in app.flashes('warning') %} {% for message in app.flashes('warning') %}
<twig:Alert :title="'Warning'" :message="message" :alert_id="''" type="warning" data-controller="alert" /> <twig:Alert :title="'Warning'" :message="message" :alert_id="''" type="warning" data-controller="alert" />
{% endfor %} {% endfor %}
{% for message in app.flashes('success') %}
<twig:Alert :title="'Success'" :message="message" :alert_id="''" type="warning" data-controller="alert" />
{% endfor %}
</ul> </ul>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,20 @@
{% extends 'bare.html.twig' %}
{% block title %}Getting Started &mdash; Torsearch{% endblock %}
{% block body %}
<div class="flex flex-col bg-orange-500/50 p-4 rounded-lg gap-4 w-full md:w-[420px] border-orange-500 border-2 text-gray-50 animate-fade">
<h2 class="text-2xl text-bold text-center text-gray-50">Getting Started</h2>
<p class="mb-1">Now let's create your first Filter.</p>
{# <p class="mb-2 text-sm">Your filter will be pre-applied to your results, so you're only shown what you want to see. Don't worry, though, you can toggle each filter option afterwards, so you can see the rest of the results.</p>#}
{{ form_start(form) }}
{{ form_row(form.language) }}
{{ form_row(form.quality) }}
{{ form_row(form.provider) }}
{{ form_row(form.resolution) }}
{{ form_row(form.codec) }}
<button class="submit-button">Save</button>
{{ form_end(form) }}
</div>
{% endblock %}

View File

@@ -3,7 +3,7 @@
{% block title %}Getting Started &mdash; Torsearch{% endblock %} {% block title %}Getting Started &mdash; Torsearch{% endblock %}
{% block body %} {% block body %}
<div class="flex flex-col bg-orange-500/50 p-4 rounded-lg gap-4 w-full md:w-[420px] border-orange-500 border-2 text-gray-50"> <div class="flex flex-col bg-orange-500/50 p-4 rounded-lg gap-4 w-full md:w-[420px] border-orange-500 border-2 text-gray-50 animate-fade">
<h2 class="text-2xl text-bold text-center text-gray-50">Getting Started</h2> <h2 class="text-2xl text-bold text-center text-gray-50">Getting Started</h2>
<p class="mb-2">Let's get started by creating your first User.</p> <p class="mb-2">Let's get started by creating your first User.</p>

View File

@@ -3,7 +3,7 @@
{% block title %}Log in &mdash; Torsearch{% endblock %} {% block title %}Log in &mdash; Torsearch{% endblock %}
{% block body %} {% block body %}
<div class="flex flex-col bg-orange-500/50 p-4 rounded-lg gap-4 w-full md:w-[420px] border-orange-500 border-2 text-gray-50"> <div class="flex flex-col bg-orange-500/50 p-4 rounded-lg gap-4 w-full md:w-[420px] border-orange-500 border-2 text-gray-50 animate-fade">
<h2 class="text-xl font-bold">Login</h2> <h2 class="text-xl font-bold">Login</h2>
<form method="post" class="flex flex-col gap-2"> <form method="post" class="flex flex-col gap-2">
{% if error %} {% if error %}
@@ -52,10 +52,16 @@
<button type="submit" class="bg-green-600/40 px-1.5 py-1 w-full rounded-md text-gray-50 backdrop-filter backdrop-blur-sm border-2 border-green-500 hover:bg-green-700/40"> <button type="submit" class="bg-green-600/40 px-1.5 py-1 w-full rounded-md text-gray-50 backdrop-filter backdrop-blur-sm border-2 border-green-500 hover:bg-green-700/40">
Sign in Sign in
</button> </button>
<div class="flex">
<a href="{{ path('app_forgot_password_request') }}">Forgot password?</a>
</div>
</form> </form>
{% if show_oidc_button == "oidc" %}
<a href="{{ path('app_login_oidc') }}" class="bg-sky-950/60 px-1.5 py-1 w-full rounded-md text-gray-50 text-center backdrop-filter backdrop-blur-sm border-2 border-gray-950 hover:bg-orange-700/40">
Sign in with OIDC
</a>
{% endif %}
<div class="flex">
<a href="{{ path('app_forgot_password_request') }}">Forgot password?</a>
</div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -12,7 +12,7 @@
<form name="reset_password_request_form" method="post" class="flex flex-col gap-2"> <form name="reset_password_request_form" method="post" class="flex flex-col gap-2">
{% for flash_error in app.flashes('reset_password_error') %} {% for flash_error in app.flashes('reset_password_error') %}
<div class="mb-3 p-2 bg-rose-500 text-black text-semibold rounded-md" role="alert">{{ flash_error }}</div> <div class="mb-3 p-2 bg-rose-500 text-black font-semibold rounded-md" role="alert">{{ flash_error }}</div>
{% endfor %} {% endfor %}
<label for="reset_password_request_form_email" class="required flex flex-col mb-2"> <label for="reset_password_request_form_email" class="required flex flex-col mb-2">