Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
70.27% covered (warning)
70.27%
52 / 74
55.56% covered (warning)
55.56%
5 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Login
70.27% covered (warning)
70.27%
52 / 74
55.56% covered (warning)
55.56%
5 / 9
34.72
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 login
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 authenticate
60.61% covered (warning)
60.61%
20 / 33
0.00% covered (danger)
0.00%
0 / 1
4.98
 logout
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 formData
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 hasPanelPermission
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 sanitizedNext
71.43% covered (warning)
71.43%
10 / 14
0.00% covered (danger)
0.00%
0 / 1
8.14
 message
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 redirect
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
2.50
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Controller\Panel;
6
7use Celema\Container\Container;
8use Celema\Core\Factory\Factory;
9use Celema\Core\Request;
10use Celema\Core\Response;
11use Cosray\Auth as CmsAuth;
12use Cosray\Config;
13use Cosray\Validation;
14
15final class Login extends Panel
16{
17    public function __construct(
18        Config $config,
19        Container $container,
20        Request $request,
21        private readonly CmsAuth $auth,
22    ) {
23        parent::__construct($config, $container, $request);
24    }
25
26    public function login(Factory $factory): array|Response
27    {
28        if ($this->hasPanelPermission()) {
29            return $this->redirect($factory, $this->panelPath());
30        }
31
32        return $this->context([
33            'next' => $this->sanitizedNext(),
34            'login' => '',
35            'rememberme' => false,
36            'message' => null,
37        ]);
38    }
39
40    public function authenticate(Factory $factory): array|Response
41    {
42        $data = $this->formData();
43        $shape = new Validation\Login();
44        $result = $shape->validate($data);
45
46        if (!$result->valid()) {
47            return $this->context([
48                'next' => $this->sanitizedNext($data['next'] ?? ''),
49                'login' => (string) ($data['login'] ?? ''),
50                'rememberme' => (bool) ($data['rememberme'] ?? false),
51                'message' => $this->message(__('auth:missing-credentials')),
52            ]);
53        }
54
55        $values = $result->values();
56        $user = $this->auth->authenticate(
57            $values['login'],
58            $values['password'],
59            $values['rememberme'],
60            true,
61        );
62
63        if ($user === false) {
64            return $this->context([
65                'next' => $this->sanitizedNext($data['next'] ?? ''),
66                'login' => (string) ($data['login'] ?? ''),
67                'rememberme' => (bool) ($data['rememberme'] ?? false),
68                'message' => $this->message(__('auth:invalid-credentials')),
69            ]);
70        }
71
72        if (!$user->hasPermission('panel')) {
73            $this->auth->logout();
74
75            return $this->context([
76                'next' => $this->sanitizedNext($data['next'] ?? ''),
77                'login' => (string) ($data['login'] ?? ''),
78                'rememberme' => false,
79                'message' => $this->message(__('auth:no-panel-access')),
80            ]);
81        }
82
83        return $this->redirect($factory, $this->sanitizedNext($data['next'] ?? ''));
84    }
85
86    public function logout(Factory $factory): Response
87    {
88        $this->auth->logout();
89
90        return $this->redirect($factory, $this->panelPath() . '/login');
91    }
92
93    protected function formData(): array
94    {
95        $data = parent::formData();
96        $rememberme = $data['rememberme'] ?? false;
97        $data['rememberme'] = in_array($rememberme, [true, 1, '1', 'true', 'on'], true);
98
99        return $data;
100    }
101
102    private function hasPanelPermission(): bool
103    {
104        $user = $this->auth->user();
105
106        if ($user === null) {
107            return false;
108        }
109
110        return $user->hasPermission('panel');
111    }
112
113    private function sanitizedNext(string $next = ''): string
114    {
115        if ($next === '') {
116            $next = $this->request->param('next', '');
117        }
118
119        if (!is_string($next)) {
120            return $this->panelPath();
121        }
122
123        $next = trim($next);
124
125        if ($next === '') {
126            return $this->panelPath();
127        }
128
129        if (!str_starts_with($next, '/')) {
130            return $this->panelPath();
131        }
132
133        if (preg_match('#^https?://#i', $next)) {
134            return $this->panelPath();
135        }
136
137        if (!str_starts_with($next, $this->panelPath())) {
138            return $this->panelPath();
139        }
140
141        return $next;
142    }
143
144    private function message(string $message): ?string
145    {
146        $message = trim($message);
147
148        return $message === '' ? null : $message;
149    }
150
151    private function redirect(Factory $factory, string $target): Response
152    {
153        $response = Response::create($factory);
154
155        if ($this->request->hasHeader('HX-Request')) {
156            return $response
157                ->status(200)
158                ->header('HX-Redirect', $target);
159        }
160
161        return $response->redirect($target, 303);
162    }
163}