Compare commits
7 Commits
dev-monito
...
dev-ldap
| Author | SHA1 | Date | |
|---|---|---|---|
| 8aba35fee1 | |||
| 6817bd8c80 | |||
| 854177a121 | |||
| ddb71b3bb0 | |||
| 35a3e48ac9 | |||
| 6e55195e6f | |||
| e325687af5 |
19
.env.dist
19
.env.dist
@@ -10,3 +10,22 @@ MERCURE_JWT_SECRET="%%mercure_jwt_secret%%"
|
|||||||
JELLYFIN_URL=%%jellyfin_url%%
|
JELLYFIN_URL=%%jellyfin_url%%
|
||||||
JELLYFIN_TOKEN=%%jellyfin_token%%
|
JELLYFIN_TOKEN=%%jellyfin_token%%
|
||||||
REDIS_HOST="%%redis_host%%"
|
REDIS_HOST="%%redis_host%%"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
LDAP_HOST=
|
||||||
|
LDAP_PORT=
|
||||||
|
LDAP_ENCRYPTION=
|
||||||
|
LDAP_BASE_DN=
|
||||||
|
LDAP_BIND_USER=
|
||||||
|
LDAP_BIND_PASS=
|
||||||
|
LDAP_DN_STRING=
|
||||||
|
LDAP_UID_KEY="uid"
|
||||||
|
|
||||||
|
# LDAP group that identifies an Admin
|
||||||
|
# Users with this LDAP group will automatically
|
||||||
|
# get the admin role in this system.
|
||||||
|
LDAP_ADMIN_ROLE_DN="cn=admins,cn=groups,cn=accounts,dc=caldwell,dc=local"
|
||||||
|
LDAP_EMAIL_ATTRIBUTE=mail
|
||||||
|
LDAP_USERNAME_ATTRIBUTE=uid
|
||||||
|
LDAP_NAME_ATTRIBUTE=displayname
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
FROM registry.caldwell.digital/library/php:8.4-apache
|
FROM registry.caldwell.digital/library/php:8.4-apache
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install libldap2-dev -y && \
|
||||||
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
|
docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu/ && \
|
||||||
|
docker-php-ext-install ldap
|
||||||
|
|
||||||
COPY ./bash/vhost.conf /etc/apache2/sites-enabled/vhost.conf
|
COPY ./bash/vhost.conf /etc/apache2/sites-enabled/vhost.conf
|
||||||
RUN rm /etc/apache2/sites-enabled/000-default.conf
|
RUN rm /etc/apache2/sites-enabled/000-default.conf
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
FROM registry.caldwell.digital/library/php:8.4-apache
|
FROM registry.caldwell.digital/library/php:8.4-apache
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install libldap2-dev -y && \
|
||||||
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
|
docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu/ && \
|
||||||
|
docker-php-ext-install ldap
|
||||||
|
|
||||||
COPY --chown=www-data:www-data . /var/www
|
COPY --chown=www-data:www-data . /var/www
|
||||||
COPY ./bash/vhost.conf /etc/apache2/sites-enabled/vhost.conf
|
COPY ./bash/vhost.conf /etc/apache2/sites-enabled/vhost.conf
|
||||||
RUN rm /etc/apache2/sites-enabled/000-default.conf
|
RUN rm /etc/apache2/sites-enabled/000-default.conf
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default class extends Controller {
|
|||||||
title: this.element.dataset['title'],
|
title: this.element.dataset['title'],
|
||||||
filename: this.filenameValue,
|
filename: this.filenameValue,
|
||||||
mediaType: this.mediaTypeValue,
|
mediaType: this.mediaTypeValue,
|
||||||
imdbId: this.imdbIdValue
|
imdbId: this.imdbIdValue,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
"symfony/flex": "^2",
|
"symfony/flex": "^2",
|
||||||
"symfony/form": "7.2.*",
|
"symfony/form": "7.2.*",
|
||||||
"symfony/framework-bundle": "7.2.*",
|
"symfony/framework-bundle": "7.2.*",
|
||||||
|
"symfony/ldap": "7.2.*",
|
||||||
"symfony/mercure-bundle": "^0.3.9",
|
"symfony/mercure-bundle": "^0.3.9",
|
||||||
"symfony/messenger": "7.2.*",
|
"symfony/messenger": "7.2.*",
|
||||||
"symfony/runtime": "7.2.*",
|
"symfony/runtime": "7.2.*",
|
||||||
|
|||||||
77
composer.lock
generated
77
composer.lock
generated
@@ -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": "7e29123297e1ac72cd417967d2a761b4",
|
"content-hash": "c179718ee29dbe018b93ea7d46764931",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "1tomany/rich-bundle",
|
"name": "1tomany/rich-bundle",
|
||||||
@@ -5082,6 +5082,81 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-05-02T09:04:03+00:00"
|
"time": "2025-05-02T09:04:03+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/ldap",
|
||||||
|
"version": "v7.2.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/ldap.git",
|
||||||
|
"reference": "48013cfa9d394343162dae7da914112a6206b575"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/ldap/zipball/48013cfa9d394343162dae7da914112a6206b575",
|
||||||
|
"reference": "48013cfa9d394343162dae7da914112a6206b575",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-ldap": "*",
|
||||||
|
"php": ">=8.2",
|
||||||
|
"symfony/options-resolver": "^6.4|^7.0"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"symfony/options-resolver": "<6.4",
|
||||||
|
"symfony/security-core": "<6.4"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/security-core": "^6.4|^7.0",
|
||||||
|
"symfony/security-http": "^6.4|^7.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\Ldap\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Charles Sarrazin",
|
||||||
|
"email": "charles@sarraz.in"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Provides a LDAP client for PHP on top of PHP's ldap extension",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"active-directory",
|
||||||
|
"ldap"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/ldap/tree/v7.2.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2024-11-25T15:21:05+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/mercure",
|
"name": "symfony/mercure",
|
||||||
"version": "v0.6.5",
|
"version": "v0.6.5",
|
||||||
|
|||||||
56
config/dist/ldap.security.yaml
vendored
Normal file
56
config/dist/ldap.security.yaml
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
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
|
||||||
|
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)%'
|
||||||
|
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: ^/register, 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
|
||||||
54
config/dist/local.security.yaml
vendored
Normal file
54
config/dist/local.security.yaml
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
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_local
|
||||||
|
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: ^/register, 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
|
||||||
@@ -5,26 +5,29 @@ security:
|
|||||||
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||||
providers:
|
providers:
|
||||||
users_in_memory: { memory: null }
|
users_in_memory: { memory: null }
|
||||||
app_user_provider:
|
app_local:
|
||||||
entity:
|
entity:
|
||||||
class: App\User\Framework\Entity\User
|
class: App\User\Framework\Entity\User
|
||||||
property: email
|
property: email
|
||||||
|
|
||||||
|
app_ldap:
|
||||||
|
id: App\User\Framework\Security\LdapUserProvider
|
||||||
|
|
||||||
firewalls:
|
firewalls:
|
||||||
dev:
|
dev:
|
||||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||||
security: false
|
security: false
|
||||||
main:
|
main:
|
||||||
lazy: true
|
lazy: true
|
||||||
provider: app_user_provider
|
provider: app_ldap
|
||||||
form_login:
|
form_login_ldap:
|
||||||
login_path: app_login
|
login_path: app_login
|
||||||
check_path: app_login
|
check_path: app_login
|
||||||
enable_csrf: true
|
enable_csrf: true
|
||||||
|
service: Symfony\Component\Ldap\Ldap
|
||||||
|
dn_string: '%env(LDAP_DN_STRING)%'
|
||||||
logout:
|
logout:
|
||||||
path: app_logout
|
path: app_logout
|
||||||
# where to redirect after logout
|
|
||||||
# target: app_any_route
|
|
||||||
|
|
||||||
# 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
|
||||||
|
|||||||
61
config/security.yaml
Normal file
61
config/security.yaml
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
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: ^/register, 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
|
||||||
@@ -28,6 +28,37 @@ services:
|
|||||||
# please note that last definitions always *replace* previous ones
|
# please note that last definitions always *replace* previous ones
|
||||||
App\Download\Downloader\DownloaderInterface: "@App\\Download\\Downloader\\ProcessDownloader"
|
App\Download\Downloader\DownloaderInterface: "@App\\Download\\Downloader\\ProcessDownloader"
|
||||||
|
|
||||||
|
# Session
|
||||||
Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler:
|
Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler:
|
||||||
arguments:
|
arguments:
|
||||||
- '%env(DATABASE_URL)%'
|
- '%env(DATABASE_URL)%'
|
||||||
|
|
||||||
|
# LDAP
|
||||||
|
App\User\Framework\Security\LdapUserProvider:
|
||||||
|
arguments:
|
||||||
|
$userRepository: '@App\User\Framework\Repository\UserRepository'
|
||||||
|
$ldap: '@Symfony\Component\Ldap\LdapInterface'
|
||||||
|
$baseDn: '%env(LDAP_BASE_DN)%'
|
||||||
|
$searchDn: '%env(LDAP_BIND_USER)%'
|
||||||
|
$searchPassword: '%env(LDAP_BIND_PASS)%'
|
||||||
|
$defaultRoles: ['ROLE_USER']
|
||||||
|
$uidKey: '%env(LDAP_UID_KEY)%'
|
||||||
|
# $passwordAttribute: '%env(LDAP_PASSWORD_ATTRIBUTE)%'
|
||||||
|
|
||||||
|
|
||||||
|
Symfony\Component\Ldap\LdapInterface: '@Symfony\Component\Ldap\Ldap'
|
||||||
|
|
||||||
|
Symfony\Component\Ldap\Ldap:
|
||||||
|
arguments: [ '@Symfony\Component\Ldap\Adapter\ExtLdap\Adapter' ]
|
||||||
|
tags:
|
||||||
|
- ldap
|
||||||
|
|
||||||
|
Symfony\Component\Ldap\Adapter\ExtLdap\Adapter:
|
||||||
|
arguments:
|
||||||
|
- host: '%env(LDAP_HOST)%'
|
||||||
|
port: '%env(LDAP_PORT)%'
|
||||||
|
encryption: '%env(LDAP_ENCRYPTION)%'
|
||||||
|
options:
|
||||||
|
protocol_version: 3
|
||||||
|
referrals: false
|
||||||
|
|
||||||
|
|||||||
35
migrations/Version20250510185814.php
Normal file
35
migrations/Version20250510185814.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250510185814 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE user ADD username VARCHAR(255) DEFAULT NULL
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE user DROP username
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
}
|
||||||
47
migrations/Version20250511050008.php
Normal file
47
migrations/Version20250511050008.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250511050008 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE download ADD user_id INT DEFAULT NULL
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE download ADD CONSTRAINT FK_781A8270A76ED395 FOREIGN KEY (user_id) REFERENCES user (id)
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
CREATE INDEX IDX_781A8270A76ED395 ON download (user_id)
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE download DROP FOREIGN KEY FK_781A8270A76ED395
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DROP INDEX IDX_781A8270A76ED395 ON download
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE download DROP user_id
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/Command/ConfigSetCommand.php
Normal file
58
src/Command/ConfigSetCommand.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Command;
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'config:set',
|
||||||
|
description: 'Add a short description for your command',
|
||||||
|
)]
|
||||||
|
class ConfigSetCommand extends Command
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->addArgument('key', InputArgument::REQUIRED, 'Config key')
|
||||||
|
->addArgument('value', InputArgument::REQUIRED, 'Config value')
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
$key = $input->getArgument('key');
|
||||||
|
$handlers = [
|
||||||
|
'auth.method' => 'setAuthMethod',
|
||||||
|
];
|
||||||
|
|
||||||
|
$handler = $handlers[$key];
|
||||||
|
$this->$handler($input, $io);
|
||||||
|
|
||||||
|
$io->success('Success: "' . $input->getArgument('key') . '" set to "' . $input->getArgument('value') . '"');
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setAuthMethod(InputInterface $input, SymfonyStyle $io)
|
||||||
|
{
|
||||||
|
$config = [
|
||||||
|
'local' => 'config/dist/local.security.yaml',
|
||||||
|
'ldap' => 'config/dist/ldap.security.yaml',
|
||||||
|
];
|
||||||
|
$authMethod = $input->getArgument('value');
|
||||||
|
$io->text('> Setting auth method to: ' . $authMethod);
|
||||||
|
copy($config[$authMethod], 'config/packages/security.yaml');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ use App\Download\Action\Input\DownloadMediaInput;
|
|||||||
use App\Download\Framework\Repository\DownloadRepository;
|
use App\Download\Framework\Repository\DownloadRepository;
|
||||||
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\Mercure\HubInterface;
|
||||||
use Symfony\Component\Messenger\MessageBusInterface;
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ class DownloadController extends AbstractController
|
|||||||
DownloadMediaInput $input,
|
DownloadMediaInput $input,
|
||||||
): Response {
|
): Response {
|
||||||
$download = $this->downloadRepository->insert(
|
$download = $this->downloadRepository->insert(
|
||||||
|
$this->getUser(),
|
||||||
$input->url,
|
$input->url,
|
||||||
$input->title,
|
$input->title,
|
||||||
$input->filename,
|
$input->filename,
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ final class IndexController extends AbstractController
|
|||||||
#[Route('/', name: 'app_index')]
|
#[Route('/', name: 'app_index')]
|
||||||
public function index(): Response
|
public function index(): Response
|
||||||
{
|
{
|
||||||
|
// dd($this->getUser()->getActiveDownloads());
|
||||||
return $this->render('index/index.html.twig', [
|
return $this->render('index/index.html.twig', [
|
||||||
'active_downloads' => $this->downloadRepository->getActivePaginated(),
|
'active_downloads' => $this->getUser()->getActiveDownloads(),
|
||||||
'recent_downloads' => $this->downloadRepository->latest(5),
|
'recent_downloads' => $this->getUser()->getDownloads(),
|
||||||
'popular_movies' => $this->tmdb->popularMovies(1, 6),
|
'popular_movies' => $this->tmdb->popularMovies(1, 6),
|
||||||
'popular_tvshows' => $this->tmdb->popularTvShows(1, 6),
|
'popular_tvshows' => $this->tmdb->popularTvShows(1, 6),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -34,24 +34,18 @@ final class SearchController extends AbstractController
|
|||||||
#[Route('/result/{mediaType}/{tmdbId}', name: 'app_search_result')]
|
#[Route('/result/{mediaType}/{tmdbId}', name: 'app_search_result')]
|
||||||
public function result(
|
public function result(
|
||||||
GetMediaInfoInput $input,
|
GetMediaInfoInput $input,
|
||||||
CacheInterface $cache
|
|
||||||
): Response {
|
): Response {
|
||||||
$cacheId = sprintf("page.%s.%s", $input->mediaType, $input->tmdbId);
|
$result = $this->getMediaInfoHandler->handle($input->toCommand());
|
||||||
|
return $this->render('search/result.html.twig', [
|
||||||
// return $cache->get($cacheId, function (ItemInterface $item) use ($input) {
|
'results' => $result,
|
||||||
// $item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
|
'filter' => [
|
||||||
$result = $this->getMediaInfoHandler->handle($input->toCommand());
|
'resolution' => '',
|
||||||
return $this->render('search/result.html.twig', [
|
'codec' => '',
|
||||||
'results' => $result,
|
'provider' => '',
|
||||||
'filter' => [
|
'language' => '',
|
||||||
'resolution' => '',
|
'season' => 1,
|
||||||
'codec' => '',
|
'episode' => ''
|
||||||
'provider' => '',
|
]
|
||||||
'language' => '',
|
]);
|
||||||
'season' => 1,
|
|
||||||
'episode' => ''
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class DownloadMediaCommand implements CommandInterface
|
|||||||
public string $filename,
|
public string $filename,
|
||||||
public string $mediaType,
|
public string $mediaType,
|
||||||
public string $imdbId,
|
public string $imdbId,
|
||||||
|
public int $userId,
|
||||||
public ?int $downloadId = null,
|
public ?int $downloadId = null,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ use App\Download\Action\Command\DownloadMediaCommand;
|
|||||||
use App\Download\Action\Result\DownloadMediaResult;
|
use App\Download\Action\Result\DownloadMediaResult;
|
||||||
use App\Download\Framework\Repository\DownloadRepository;
|
use App\Download\Framework\Repository\DownloadRepository;
|
||||||
use App\Download\Downloader\DownloaderInterface;
|
use App\Download\Downloader\DownloaderInterface;
|
||||||
|
use App\User\Framework\Repository\UserRepository;
|
||||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||||
@@ -17,12 +18,14 @@ readonly class DownloadMediaHandler implements HandlerInterface
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private DownloaderInterface $downloader,
|
private DownloaderInterface $downloader,
|
||||||
private DownloadRepository $downloadRepository,
|
private DownloadRepository $downloadRepository,
|
||||||
|
private UserRepository $userRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function handle(CommandInterface $command): ResultInterface
|
public function handle(CommandInterface $command): ResultInterface
|
||||||
{
|
{
|
||||||
if (null === $command->downloadId) {
|
if (null === $command->downloadId) {
|
||||||
$download = $this->downloadRepository->insert(
|
$download = $this->downloadRepository->insert(
|
||||||
|
$this->userRepository->find($command->userId),
|
||||||
$command->url,
|
$command->url,
|
||||||
$command->title,
|
$command->title,
|
||||||
$command->filename,
|
$command->filename,
|
||||||
@@ -34,7 +37,6 @@ readonly class DownloadMediaHandler implements HandlerInterface
|
|||||||
$download = $this->downloadRepository->find($command->downloadId);
|
$download = $this->downloadRepository->find($command->downloadId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->downloadRepository->updateStatus($download->getId(), 'In Progress');
|
$this->downloadRepository->updateStatus($download->getId(), 'In Progress');
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ class DownloadMediaInput implements InputInterface
|
|||||||
#[SourceRequest('imdbId')]
|
#[SourceRequest('imdbId')]
|
||||||
public string $imdbId,
|
public string $imdbId,
|
||||||
|
|
||||||
|
public ?int $userId = null,
|
||||||
|
|
||||||
public ?int $downloadId = null,
|
public ?int $downloadId = null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -38,6 +40,7 @@ class DownloadMediaInput implements InputInterface
|
|||||||
$this->mediaType,
|
$this->mediaType,
|
||||||
$this->imdbId,
|
$this->imdbId,
|
||||||
$this->downloadId,
|
$this->downloadId,
|
||||||
|
$this->userId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Download\Framework\Entity;
|
namespace App\Download\Framework\Entity;
|
||||||
|
|
||||||
use App\Download\Framework\Repository\DownloadRepository;
|
use App\Download\Framework\Repository\DownloadRepository;
|
||||||
|
use App\User\Framework\Entity\User;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use Symfony\UX\Turbo\Attribute\Broadcast;
|
use Symfony\UX\Turbo\Attribute\Broadcast;
|
||||||
|
|
||||||
@@ -39,6 +40,9 @@ class Download
|
|||||||
#[ORM\Column(length: 255, nullable: true)]
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
private ?string $batchId = null;
|
private ?string $batchId = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(inversedBy: 'downloads')]
|
||||||
|
private ?User $user = null;
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
{
|
{
|
||||||
return $this->id;
|
return $this->id;
|
||||||
@@ -146,4 +150,16 @@ class Download
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getUser(): ?User
|
||||||
|
{
|
||||||
|
return $this->user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUser(?User $user): static
|
||||||
|
{
|
||||||
|
$this->user = $user;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,12 @@
|
|||||||
namespace App\Download\Framework\Repository;
|
namespace App\Download\Framework\Repository;
|
||||||
|
|
||||||
use App\Download\Framework\Entity\Download;
|
use App\Download\Framework\Entity\Download;
|
||||||
use App\ValueObject\DownloadRequest;
|
use App\User\Framework\Entity\User;
|
||||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
use Doctrine\Persistence\ManagerRegistry;
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
use Knp\Component\Pager\Paginator;
|
use Knp\Component\Pager\Paginator;
|
||||||
use Knp\Component\Pager\PaginatorInterface;
|
use Knp\Component\Pager\PaginatorInterface;
|
||||||
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends ServiceEntityRepository<Download>
|
* @extends ServiceEntityRepository<Download>
|
||||||
@@ -51,6 +52,7 @@ class DownloadRepository extends ServiceEntityRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function insert(
|
public function insert(
|
||||||
|
UserInterface $user,
|
||||||
string $url,
|
string $url,
|
||||||
string $title,
|
string $title,
|
||||||
string $filename,
|
string $filename,
|
||||||
@@ -59,7 +61,9 @@ class DownloadRepository extends ServiceEntityRepository
|
|||||||
string $batchId,
|
string $batchId,
|
||||||
string $status = 'New'
|
string $status = 'New'
|
||||||
): Download {
|
): Download {
|
||||||
|
/** @var User $user */
|
||||||
$download = (new Download())
|
$download = (new Download())
|
||||||
|
->setUser($user)
|
||||||
->setUrl($url)
|
->setUrl($url)
|
||||||
->setTitle($title)
|
->setTitle($title)
|
||||||
->setFilename($filename)
|
->setFilename($filename)
|
||||||
@@ -75,22 +79,6 @@ class DownloadRepository extends ServiceEntityRepository
|
|||||||
return $download;
|
return $download;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function insertFromDownloadRequest(DownloadRequest $request): Download
|
|
||||||
{
|
|
||||||
$download = (new Download())
|
|
||||||
->setUrl($request->downloadUrl)
|
|
||||||
->setTitle($request->seriesName)
|
|
||||||
->setFilename($request->filename)
|
|
||||||
->setImdbId($request->imdbCode)
|
|
||||||
->setMediaType($request->mediaType)
|
|
||||||
->setStatus('New');
|
|
||||||
|
|
||||||
$this->getEntityManager()->persist($download);
|
|
||||||
$this->getEntityManager()->flush();
|
|
||||||
|
|
||||||
return $download;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function updateStatus(int $id, string $status): Download
|
public function updateStatus(int $id, string $status): Download
|
||||||
{
|
{
|
||||||
$download = $this->find($id);
|
$download = $this->find($id);
|
||||||
@@ -106,18 +94,6 @@ class DownloadRepository extends ServiceEntityRepository
|
|||||||
$this->getEntityManager()->flush();
|
$this->getEntityManager()->flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPendingByBatchId(string $batchId): ?array
|
|
||||||
{
|
|
||||||
$query = $this->createQueryBuilder('d')
|
|
||||||
->andWhere('d.status IN (:statuses)')
|
|
||||||
->andWhere('d.batchId = :batchId')
|
|
||||||
->setParameter('statuses', ['New', 'In Progress'])
|
|
||||||
->setParameter('batchId', $batchId)
|
|
||||||
->getQuery();
|
|
||||||
|
|
||||||
return $query->getResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function latest(int $limit = 1)
|
public function latest(int $limit = 1)
|
||||||
{
|
{
|
||||||
return $this->createQueryBuilder('d')
|
return $this->createQueryBuilder('d')
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use OneToMany\RichBundle\Contract\CommandInterface;
|
|||||||
class AddMonitorCommand implements CommandInterface
|
class AddMonitorCommand implements CommandInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public string $userEmail,
|
public string $userId,
|
||||||
public string $title,
|
public string $title,
|
||||||
public string $imdbId,
|
public string $imdbId,
|
||||||
public string $tmdbId,
|
public string $tmdbId,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ readonly class AddMonitorHandler implements HandlerInterface
|
|||||||
|
|
||||||
public function handle(CommandInterface $command): ResultInterface
|
public function handle(CommandInterface $command): ResultInterface
|
||||||
{
|
{
|
||||||
$user = $this->userRepository->findOneBy(['email' => $command->userEmail]);
|
$user = $this->userRepository->find($command->userId);
|
||||||
$monitor = (new Monitor())
|
$monitor = (new Monitor())
|
||||||
->setUser($user)
|
->setUser($user)
|
||||||
->setTmdbId($command->tmdbId)
|
->setTmdbId($command->tmdbId)
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Monitor\Action\Handler;
|
namespace App\Monitor\Action\Handler;
|
||||||
|
|
||||||
use App\Monitor\Action\Command\DownloadMediaCommand;
|
use App\Download\Action\Command\DownloadMediaCommand;
|
||||||
use App\Monitor\Action\Command\MonitorMovieCommand;
|
use App\Monitor\Action\Command\MonitorMovieCommand;
|
||||||
use App\Monitor\Action\Result\MonitorMovieResult;
|
use App\Monitor\Action\Result\MonitorMovieResult;
|
||||||
|
use App\Monitor\Framework\Entity\Monitor;
|
||||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||||
use App\Monitor\Service\MonitorOptionEvaluator;
|
use App\Monitor\Service\MonitorOptionEvaluator;
|
||||||
use App\Torrentio\Action\Command\GetMovieOptionsCommand;
|
use App\Torrentio\Action\Command\GetMovieOptionsCommand;
|
||||||
@@ -15,6 +16,7 @@ use OneToMany\RichBundle\Contract\CommandInterface;
|
|||||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
use Symfony\Component\Messenger\MessageBusInterface;
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
|
|
||||||
/** @implements HandlerInterface<MonitorMovieCommand> */
|
/** @implements HandlerInterface<MonitorMovieCommand> */
|
||||||
@@ -27,11 +29,13 @@ readonly class MonitorMovieHandler implements HandlerInterface
|
|||||||
private EntityManagerInterface $entityManager,
|
private EntityManagerInterface $entityManager,
|
||||||
private MessageBusInterface $bus,
|
private MessageBusInterface $bus,
|
||||||
private LoggerInterface $logger,
|
private LoggerInterface $logger,
|
||||||
|
private Security $security,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function handle(CommandInterface $command): ResultInterface
|
public function handle(CommandInterface $command): ResultInterface
|
||||||
{
|
{
|
||||||
$this->logger->info('> [MonitorMovieHandler] Executing MonitorMovieHandler');
|
$this->logger->info('> [MonitorMovieHandler] Executing MonitorMovieHandler');
|
||||||
|
/** @var Monitor $monitor */
|
||||||
$monitor = $this->movieMonitorRepository->find($command->movieMonitorId);
|
$monitor = $this->movieMonitorRepository->find($command->movieMonitorId);
|
||||||
$monitor->setStatus('In Progress');
|
$monitor->setStatus('In Progress');
|
||||||
|
|
||||||
@@ -51,6 +55,7 @@ readonly class MonitorMovieHandler implements HandlerInterface
|
|||||||
$result->filename,
|
$result->filename,
|
||||||
'movies',
|
'movies',
|
||||||
$monitor->getImdbId(),
|
$monitor->getImdbId(),
|
||||||
|
$monitor->getUser()->getId(),
|
||||||
));
|
));
|
||||||
$monitor->setStatus('Complete');
|
$monitor->setStatus('Complete');
|
||||||
$monitor->setDownloadedAt(new DateTimeIMmutable());
|
$monitor->setDownloadedAt(new DateTimeIMmutable());
|
||||||
@@ -59,7 +64,7 @@ readonly class MonitorMovieHandler implements HandlerInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
$monitor->setLastSearch(new DateTimeImmutable());
|
$monitor->setLastSearch(new DateTimeImmutable());
|
||||||
$monitor->setSearchCount($monitor->getSearchCount() + 1);
|
$monitor->incrementSearchCount();
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
return new MonitorMovieResult(
|
return new MonitorMovieResult(
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ readonly class MonitorTvEpisodeHandler implements HandlerInterface
|
|||||||
$result->filename,
|
$result->filename,
|
||||||
'tvshows',
|
'tvshows',
|
||||||
$monitor->getImdbId(),
|
$monitor->getImdbId(),
|
||||||
|
$monitor->getUser()->getId(),
|
||||||
));
|
));
|
||||||
$monitor->setStatus('Complete');
|
$monitor->setStatus('Complete');
|
||||||
$monitor->setDownloadedAt(new DateTimeImmutable());
|
$monitor->setDownloadedAt(new DateTimeImmutable());
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class AddMonitorInput implements InputInterface
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
#[SourceSecurity]
|
#[SourceSecurity]
|
||||||
public string $userEmail,
|
public int|string $userId,
|
||||||
|
|
||||||
#[SourceRequest('tmdbId')]
|
#[SourceRequest('tmdbId')]
|
||||||
public string $tmdbId,
|
public string $tmdbId,
|
||||||
@@ -36,7 +36,7 @@ class AddMonitorInput implements InputInterface
|
|||||||
public function toCommand(): CommandInterface
|
public function toCommand(): CommandInterface
|
||||||
{
|
{
|
||||||
return new AddMonitorCommand(
|
return new AddMonitorCommand(
|
||||||
$this->userEmail,
|
$this->userId,
|
||||||
$this->title,
|
$this->title,
|
||||||
$this->imdbId,
|
$this->imdbId,
|
||||||
$this->tmdbId,
|
$this->tmdbId,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace App\Monitor\Framework\Controller;
|
|||||||
use App\Monitor\Action\Handler\AddMonitorHandler;
|
use App\Monitor\Action\Handler\AddMonitorHandler;
|
||||||
use App\Monitor\Action\Input\AddMonitorInput;
|
use App\Monitor\Action\Input\AddMonitorInput;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
use Symfony\Component\Mercure\HubInterface;
|
use Symfony\Component\Mercure\HubInterface;
|
||||||
use Symfony\Component\Mercure\Update;
|
use Symfony\Component\Mercure\Update;
|
||||||
@@ -17,6 +18,7 @@ class ApiController extends AbstractController
|
|||||||
#[Autowire(service: 'twig')]
|
#[Autowire(service: 'twig')]
|
||||||
private readonly Environment $renderer,
|
private readonly Environment $renderer,
|
||||||
private readonly HubInterface $hub,
|
private readonly HubInterface $hub,
|
||||||
|
private readonly Security $security,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
#[Route('/api/monitor', name: 'api_monitor', methods: ['POST'])]
|
#[Route('/api/monitor', name: 'api_monitor', methods: ['POST'])]
|
||||||
@@ -25,7 +27,9 @@ class ApiController extends AbstractController
|
|||||||
AddMonitorHandler $handler,
|
AddMonitorHandler $handler,
|
||||||
HubInterface $hub,
|
HubInterface $hub,
|
||||||
) {
|
) {
|
||||||
$response = $handler->handle($input->toCommand());
|
$command = $input->toCommand();
|
||||||
|
$command->userId = $this->security->getUser()->getId();
|
||||||
|
$response = $handler->handle($command);
|
||||||
|
|
||||||
$hub->publish(new Update(
|
$hub->publish(new Update(
|
||||||
'alerts',
|
'alerts',
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use Psr\Log\LoggerInterface;
|
|||||||
use Symfony\Component\Messenger\MessageBusInterface;
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
use Symfony\Component\Scheduler\Attribute\AsCronTask;
|
use Symfony\Component\Scheduler\Attribute\AsCronTask;
|
||||||
|
|
||||||
#[AsCronTask('*/10 * * * *', schedule: 'monitor')]
|
#[AsCronTask('* * * * *', schedule: 'monitor')]
|
||||||
class MonitorDispatcher
|
class MonitorDispatcher
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -27,7 +27,7 @@ class MonitorDispatcher
|
|||||||
'movie' => MonitorMovieCommand::class,
|
'movie' => MonitorMovieCommand::class,
|
||||||
'tvepisode' => MonitorTvEpisodeCommand::class,
|
'tvepisode' => MonitorTvEpisodeCommand::class,
|
||||||
'tvseason' => MonitorTvSeasonCommand::class,
|
'tvseason' => MonitorTvSeasonCommand::class,
|
||||||
'tvshow' => MonitorTvShowCommand::class,
|
'tvshows' => MonitorTvShowCommand::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
$monitors = $this->monitorRepository->findBy(['status' => ['New', 'Active']]);
|
$monitors = $this->monitorRepository->findBy(['status' => ['New', 'Active']]);
|
||||||
|
|||||||
@@ -3,12 +3,13 @@
|
|||||||
namespace App\Twig\Components;
|
namespace App\Twig\Components;
|
||||||
|
|
||||||
use App\Download\Framework\Repository\DownloadRepository;
|
use App\Download\Framework\Repository\DownloadRepository;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
|
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
|
||||||
use Symfony\UX\LiveComponent\Attribute\LiveAction;
|
use Symfony\UX\LiveComponent\Attribute\LiveAction;
|
||||||
use Symfony\UX\LiveComponent\DefaultActionTrait;
|
use Symfony\UX\LiveComponent\DefaultActionTrait;
|
||||||
|
|
||||||
#[AsLiveComponent]
|
#[AsLiveComponent]
|
||||||
final class ActiveDownloadList
|
final class ActiveDownloadList extends AbstractController
|
||||||
{
|
{
|
||||||
use DefaultActionTrait;
|
use DefaultActionTrait;
|
||||||
|
|
||||||
@@ -19,6 +20,6 @@ final class ActiveDownloadList
|
|||||||
#[LiveAction]
|
#[LiveAction]
|
||||||
public function getDownloads()
|
public function getDownloads()
|
||||||
{
|
{
|
||||||
return $this->downloadRepository->getActivePaginated();
|
return $this->getUser()->getActiveDownloads();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\User\Framework\Entity;
|
namespace App\User\Framework\Entity;
|
||||||
|
|
||||||
use Aimeos\Map;
|
use Aimeos\Map;
|
||||||
|
use App\Download\Framework\Entity\Download;
|
||||||
use App\Monitor\Framework\Entity\Monitor;
|
use App\Monitor\Framework\Entity\Monitor;
|
||||||
use App\User\Framework\Repository\UserRepository;
|
use App\User\Framework\Repository\UserRepository;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
@@ -22,6 +23,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
#[ORM\Column(type: 'integer')]
|
#[ORM\Column(type: 'integer')]
|
||||||
private int $id;
|
private int $id;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 255, nullable: true)]
|
||||||
|
private ?string $username;
|
||||||
|
|
||||||
#[ORM\Column(type: 'string', length: 180, unique: true)]
|
#[ORM\Column(type: 'string', length: 180, unique: true)]
|
||||||
private ?string $email;
|
private ?string $email;
|
||||||
|
|
||||||
@@ -46,10 +50,17 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
#[ORM\OneToMany(targetEntity: Monitor::class, mappedBy: 'user', orphanRemoval: true)]
|
#[ORM\OneToMany(targetEntity: Monitor::class, mappedBy: 'user', orphanRemoval: true)]
|
||||||
private Collection $yes;
|
private Collection $yes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, Download>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(targetEntity: Download::class, mappedBy: 'user')]
|
||||||
|
private Collection $downloads;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->userPreferences = new ArrayCollection();
|
$this->userPreferences = new ArrayCollection();
|
||||||
$this->yes = new ArrayCollection();
|
$this->yes = new ArrayCollection();
|
||||||
|
$this->downloads = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
@@ -88,7 +99,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
*/
|
*/
|
||||||
public function getUserIdentifier(): string
|
public function getUserIdentifier(): string
|
||||||
{
|
{
|
||||||
return (string) $this->email;
|
return (string) $this->username ?? $this->email;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -241,4 +252,54 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getUsername(): ?string
|
||||||
|
{
|
||||||
|
return $this->username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUsername(?string $username): static
|
||||||
|
{
|
||||||
|
$this->username = $username;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, Download>
|
||||||
|
*/
|
||||||
|
public function getDownloads(): Collection
|
||||||
|
{
|
||||||
|
return $this->downloads;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, Download>
|
||||||
|
*/
|
||||||
|
public function getActiveDownloads(): Collection
|
||||||
|
{
|
||||||
|
return $this->downloads->filter(fn(Download $download) => in_array($download->getStatus(), ['New', 'In Progress']));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addDownload(Download $download): static
|
||||||
|
{
|
||||||
|
if (!$this->downloads->contains($download)) {
|
||||||
|
$this->downloads->add($download);
|
||||||
|
$download->setUser($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeDownload(Download $download): static
|
||||||
|
{
|
||||||
|
if ($this->downloads->removeElement($download)) {
|
||||||
|
// set the owning side to null (unless already changed)
|
||||||
|
if ($download->getUser() === $this) {
|
||||||
|
$download->setUser(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
73
src/User/Framework/Security/LdapAuthenticator.php
Normal file
73
src/User/Framework/Security/LdapAuthenticator.php
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\User\Framework\Security;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://symfony.com/doc/current/security/custom_authenticator.html
|
||||||
|
*/
|
||||||
|
class LdapAuthenticator extends AbstractAuthenticator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Called on every request to decide if this authenticator should be
|
||||||
|
* used for the request. Returning `false` will cause this authenticator
|
||||||
|
* to be skipped.
|
||||||
|
*/
|
||||||
|
public function supports(Request $request): ?bool
|
||||||
|
{
|
||||||
|
// return $request->headers->has('X-AUTH-TOKEN');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function authenticate(Request $request): Passport
|
||||||
|
{
|
||||||
|
// $apiToken = $request->headers->get('X-AUTH-TOKEN');
|
||||||
|
// if (null === $apiToken) {
|
||||||
|
// The token header was empty, authentication fails with HTTP Status
|
||||||
|
// Code 401 "Unauthorized"
|
||||||
|
// throw new CustomUserMessageAuthenticationException('No API token provided');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// implement your own logic to get the user identifier from `$apiToken`
|
||||||
|
// e.g. by looking up a user in the database using its API key
|
||||||
|
// $userIdentifier = /** ... */;
|
||||||
|
|
||||||
|
// return new SelfValidatingPassport(new UserBadge($userIdentifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
||||||
|
{
|
||||||
|
// on success, let the request continue
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
// you may want to customize or obfuscate the message first
|
||||||
|
'message' => strtr($exception->getMessageKey(), $exception->getMessageData()),
|
||||||
|
|
||||||
|
// or to translate this message
|
||||||
|
// $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
|
||||||
|
];
|
||||||
|
|
||||||
|
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// public function start(Request $request, ?AuthenticationException $authException = null): Response
|
||||||
|
// {
|
||||||
|
// /*
|
||||||
|
// * If you would like this class to control what happens when an anonymous user accesses a
|
||||||
|
// * protected page (e.g. redirect to /login), uncomment this method and make this class
|
||||||
|
// * implement Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface.
|
||||||
|
// *
|
||||||
|
// * For more details, see https://symfony.com/doc/current/security/experimental_authenticators.html#configuring-the-authentication-entry-point
|
||||||
|
// */
|
||||||
|
// }
|
||||||
|
}
|
||||||
203
src/User/Framework/Security/LdapUserProvider.php
Normal file
203
src/User/Framework/Security/LdapUserProvider.php
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\User\Framework\Security;
|
||||||
|
|
||||||
|
use App\User\Framework\Entity\User;
|
||||||
|
use App\User\Framework\Repository\UserRepository;
|
||||||
|
use Symfony\Component\Ldap\Entry;
|
||||||
|
use Symfony\Component\Ldap\Exception\ExceptionInterface;
|
||||||
|
use Symfony\Component\Ldap\Exception\InvalidCredentialsException;
|
||||||
|
use Symfony\Component\Ldap\Exception\InvalidSearchCredentialsException;
|
||||||
|
use Symfony\Component\Ldap\LdapInterface;
|
||||||
|
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
|
||||||
|
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
||||||
|
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
|
||||||
|
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||||
|
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||||
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||||
|
use Symfony\Component\Ldap\Security\LdapUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LdapUserProvider is a simple user provider on top of LDAP.
|
||||||
|
*
|
||||||
|
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||||
|
* @author Charles Sarrazin <charles@sarraz.in>
|
||||||
|
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||||
|
*
|
||||||
|
* @template-implements UserProviderInterface<LdapUser>
|
||||||
|
*/
|
||||||
|
class LdapUserProvider implements UserProviderInterface, PasswordUpgraderInterface
|
||||||
|
{
|
||||||
|
private string $uidKey;
|
||||||
|
private string $defaultSearch;
|
||||||
|
private string $usernameAttribute;
|
||||||
|
private string $emailAttribute;
|
||||||
|
private string $displayNameAttribute;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private UserRepository $userRepository,
|
||||||
|
private LdapInterface $ldap,
|
||||||
|
private string $baseDn,
|
||||||
|
private ?string $searchDn = null,
|
||||||
|
#[\SensitiveParameter] private ?string $searchPassword = null,
|
||||||
|
private array $defaultRoles = [],
|
||||||
|
?string $uidKey = null,
|
||||||
|
?string $filter = null,
|
||||||
|
private ?string $passwordAttribute = null,
|
||||||
|
private array $extraFields = [],
|
||||||
|
string $usernameAttribute = 'uid',
|
||||||
|
string $emailAttribute = 'mail',
|
||||||
|
string $displayNameAttribute = 'displayName',
|
||||||
|
) {
|
||||||
|
$uidKey ??= 'sAMAccountName';
|
||||||
|
$filter ??= '({uid_key}={user_identifier})';
|
||||||
|
|
||||||
|
$this->uidKey = $uidKey;
|
||||||
|
$this->defaultSearch = str_replace('{uid_key}', $uidKey, $filter);
|
||||||
|
$this->usernameAttribute = $usernameAttribute;
|
||||||
|
$this->emailAttribute = $emailAttribute;
|
||||||
|
$this->displayNameAttribute = $displayNameAttribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadUserByIdentifier(string $identifier): UserInterface
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->ldap->bind($this->searchDn, $this->searchPassword);
|
||||||
|
} catch (InvalidCredentialsException) {
|
||||||
|
throw new InvalidSearchCredentialsException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$identifier = $this->ldap->escape($identifier, '', LdapInterface::ESCAPE_FILTER);
|
||||||
|
$query = str_replace('{user_identifier}', $identifier, $this->defaultSearch);
|
||||||
|
$search = $this->ldap->query($this->baseDn, $query, ['filter' => 0 == \count($this->extraFields) ? '*' : $this->extraFields]);
|
||||||
|
|
||||||
|
$entries = $search->execute();
|
||||||
|
$count = \count($entries);
|
||||||
|
|
||||||
|
if (!$count) {
|
||||||
|
$e = new UserNotFoundException(\sprintf('User "%s" not found.', $identifier));
|
||||||
|
$e->setUserIdentifier($identifier);
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($count > 1) {
|
||||||
|
$e = new UserNotFoundException('More than one user found.');
|
||||||
|
$e->setUserIdentifier($identifier);
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
$entry = $entries[0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$identifier = $this->getAttributeValue($entry, $this->uidKey);
|
||||||
|
} catch (InvalidArgumentException) {
|
||||||
|
// This is empty
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->loadUser($identifier, $entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function refreshUser(UserInterface $user): UserInterface
|
||||||
|
{
|
||||||
|
if (!$user instanceof LdapUser) {
|
||||||
|
throw new UnsupportedUserException(\sprintf('Instances of "%s" are not supported.', get_debug_type($user)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new LdapUser($user->getEntry(), $user->getUserIdentifier(), $user->getPassword(), $user->getRoles(), $user->getExtraFields());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @final
|
||||||
|
*/
|
||||||
|
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
|
||||||
|
{
|
||||||
|
if (!$user instanceof LdapUser) {
|
||||||
|
throw new UnsupportedUserException(\sprintf('Instances of "%s" are not supported.', get_debug_type($user)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $this->passwordAttribute) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$user->getEntry()->setAttribute($this->passwordAttribute, [$newHashedPassword]);
|
||||||
|
$this->ldap->getEntryManager()->update($user->getEntry());
|
||||||
|
$user->setPassword($newHashedPassword);
|
||||||
|
} catch (ExceptionInterface) {
|
||||||
|
// ignore failed password upgrades
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supportsClass(string $class): bool
|
||||||
|
{
|
||||||
|
return User::class === $class;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a user from an LDAP entry.
|
||||||
|
*/
|
||||||
|
protected function loadUser(string $identifier, Entry $entry): UserInterface
|
||||||
|
{
|
||||||
|
$extraFields = [];
|
||||||
|
|
||||||
|
foreach ($this->extraFields as $field) {
|
||||||
|
$extraFields[$field] = $this->getAttributeValue($entry, $field);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dbUser = $this->getDbUser($identifier);
|
||||||
|
|
||||||
|
if (null === $dbUser) {
|
||||||
|
$dbUser = new User();
|
||||||
|
$dbUser->setPassword("test");
|
||||||
|
}
|
||||||
|
|
||||||
|
$dbUser
|
||||||
|
->setName($entry->getAttribute($this->displayNameAttribute)[0] ?? null)
|
||||||
|
->setEmail($entry->getAttribute($this->emailAttribute)[0] ?? null)
|
||||||
|
->setUsername($entry->getAttribute($this->usernameAttribute)[0] ?? null);
|
||||||
|
|
||||||
|
$this->userRepository->getEntityManager()->persist($dbUser);
|
||||||
|
$this->userRepository->getEntityManager()->flush();
|
||||||
|
|
||||||
|
return $dbUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDbUser(string $identifier): ?UserInterface
|
||||||
|
{
|
||||||
|
if (in_array($this->uidKey, ['mail', 'email'])) {
|
||||||
|
return $this->userRepository->findOneBy(['email' => $identifier]);
|
||||||
|
} else {
|
||||||
|
return $this->userRepository->findOneBy(['username' => $identifier]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getAttributeValue(Entry $entry, string $attribute): mixed
|
||||||
|
{
|
||||||
|
if (!$entry->hasAttribute($attribute)) {
|
||||||
|
throw new InvalidArgumentException(\sprintf('Missing attribute "%s" for user "%s".', $attribute, $entry->getDn()));
|
||||||
|
}
|
||||||
|
|
||||||
|
$values = $entry->getAttribute($attribute);
|
||||||
|
if (!\in_array($attribute, [$this->uidKey, $this->passwordAttribute])) {
|
||||||
|
return $values;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (1 !== \count($values)) {
|
||||||
|
throw new InvalidArgumentException(\sprintf('Attribute "%s" has multiple values.', $attribute));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $values[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,15 +12,14 @@
|
|||||||
{% block importmap %}{{ importmap('app') }}{% endblock %}
|
{% block importmap %}{{ importmap('app') }}{% endblock %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-neutral-700 flex flex-col">
|
<body class="bg-neutral-700 flex flex-col backdrop-filter backdrop-blur-sm bg-opacity-100">
|
||||||
<twig:Header />
|
|
||||||
|
|
||||||
<div class="grid grid-cols-6">
|
<div class="grid grid-cols-6">
|
||||||
<div class="col-span-1">
|
<div class="col-span-1 h-screen">
|
||||||
<twig:NavBar />
|
<twig:NavBar />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-5">
|
<div class="col-span-5 h-screen overflow-y-scroll">
|
||||||
<h2 class="p-4 mb-2 text-3xl font-bold text-gray-50">{% block h2 %}{% endblock %}</h2>
|
<twig:Header />
|
||||||
|
<h2 class="px-2 mb-2 text-3xl font-bold text-gray-50">{% block h2 %}{% endblock %}</h2>
|
||||||
{% block body %}{% endblock %}
|
{% block body %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
<li {{ attributes }} id="alert_{{ alert_id }}" class="alert p-4 text-green-800 border border-green-300 rounded-lg bg-green-50 dark:bg-gray-800 dark:text-green-400 dark:border-green-800" role="alert">
|
<li {{ attributes }} id="alert_{{ alert_id }}" class="
|
||||||
|
text-white bg-green-950 text-sm min-w-[250px]
|
||||||
|
hover:bg-green-900 border border-green-500 px-4 py-3
|
||||||
|
rounded-md z-40"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<svg class="shrink-0 w-4 h-4 me-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
<svg class="shrink-0 w-4 h-4 me-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
|
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<div{{ attributes }}>
|
<div{{ attributes }}>
|
||||||
<div class="flex flex-col bg-white border border-gray-200 border-t-4 border-t-orange-500 shadow-2xs rounded-xl dark:bg-slate-600 dark:border-neutral-700 dark:border-t-orange-500 dark:shadow-neutral-700/70">
|
<div class="flex flex-col bg-white border border-gray-200 border-t-4 border-t-orange-500 shadow-2xs rounded-xl dark:bg-sky-950 dark:border-neutral-700 dark:border-t-orange-500 dark:shadow-neutral-700/70
|
||||||
|
dark:backdrop-filter dark:backdrop-blur-md dark:bg-opacity-40
|
||||||
|
">
|
||||||
<div class="p-4 md:p-5">
|
<div class="p-4 md:p-5">
|
||||||
<h3 class="mb-4 text-lg font-bold text-gray-800 dark:text-white">
|
<h3 class="mb-4 text-lg font-bold text-gray-800 dark:text-white">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<header {{ attributes }} class="bg-cyan-950">
|
<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">
|
||||||
<h1 class="text-3xl font-extrabold text-orange-500">Torsearch</h1>
|
|
||||||
<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">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<nav {{ attributes }} {{ stimulus_controller('navbar') }} {{ stimulus_action('navbar', 'setActive')}} class="flex h-screen flex-col justify-between bg-cyan-950">
|
<nav {{ attributes }} {{ stimulus_controller('navbar') }} {{ stimulus_action('navbar', 'setActive')}} class="flex h-screen flex-col justify-between bg-cyan-950">
|
||||||
<div class="px-4 py-6">
|
<div class="px-4 py-4 flex flex-col gap-12">
|
||||||
<ul class="mt-6 space-y-1">
|
<h1 class="text-3xl font-extrabold text-orange-500 mb-3">Torsearch</h1>
|
||||||
|
<ul class="space-y-1">
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ path('app_index') }}"
|
<a href="{{ path('app_index') }}"
|
||||||
class="block rounded-lg
|
class="block rounded-lg
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
{% block title %}Dashboard — Torsearch{% endblock %}
|
{% block title %}Dashboard — Torsearch{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="p-4 flex flex-col grow gap-4">
|
<div class="p-4 flex flex-col grow gap-4 z-30">
|
||||||
<h2 class="mb-2 text-3xl font-bold text-gray-50">Dashboard</h2>
|
<h2 class="mb-2 text-3xl font-bold text-gray-50">Dashboard</h2>
|
||||||
<div class="flex flex-row gap-4">
|
<div class="flex flex-row gap-4">
|
||||||
<twig:Card title="Active Downloads" class="w-full">
|
<twig:Card title="Active Downloads" class="w-full">
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<label for="username" class="mb-2 flex flex-col">
|
<label for="username" class="mb-2 flex flex-col">
|
||||||
Email
|
User
|
||||||
<input type="email"
|
<input type=""
|
||||||
value="{{ last_username }}"
|
value="{{ last_username }}"
|
||||||
name="_username"
|
name="_username"
|
||||||
id="username"
|
id="username"
|
||||||
|
|||||||
Reference in New Issue
Block a user