Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
69.88% covered (warning)
69.88%
58 / 83
44.44% covered (danger)
44.44%
4 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Login
69.88% covered (warning)
69.88%
58 / 83
44.44% covered (danger)
44.44%
4 / 9
49.42
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
76.92% covered (warning)
76.92%
10 / 13
0.00% covered (danger)
0.00%
0 / 1
7.60
 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 Celemas\Container\Container;
8use Celemas\Core\Factory\Factory;
9use Celemas\Core\Request;
10use Celemas\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(_('Please provide username and password')),
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(_('Invalid username or password')),
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(_('You are not allowed to access the panel')),
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    private function formData(): array
94    {
95        $data = $this->request->form() ?? [];
96        $contentType = strtolower(trim(explode(';', $this->request->header('Content-Type'))[0]));
97
98        if ($data === [] && $contentType === 'application/json') {
99            $decoded = $this->request->json();
100
101            if (is_array($decoded)) {
102                $data = $decoded;
103            }
104        }
105
106        if ($data === [] && $contentType === 'application/x-www-form-urlencoded') {
107            parse_str((string) $this->request->body(), $parsed);
108
109            if (is_array($parsed)) {
110                $data = $parsed;
111            }
112        }
113
114        $rememberme = $data['rememberme'] ?? false;
115        $data['rememberme'] = in_array($rememberme, [true, 1, '1', 'true', 'on'], true);
116
117        return $data;
118    }
119
120    private function hasPanelPermission(): bool
121    {
122        $user = $this->auth->user();
123
124        if ($user === null) {
125            return false;
126        }
127
128        return $user->hasPermission('panel');
129    }
130
131    private function sanitizedNext(string $next = ''): string
132    {
133        if ($next === '') {
134            $next = $this->request->param('next', '');
135        }
136
137        if (!is_string($next)) {
138            return $this->panelPath();
139        }
140
141        $next = trim($next);
142
143        if ($next === '') {
144            return $this->panelPath();
145        }
146
147        if (!str_starts_with($next, '/')) {
148            return $this->panelPath();
149        }
150
151        if (preg_match('#^https?://#i', $next)) {
152            return $this->panelPath();
153        }
154
155        if (!str_starts_with($next, $this->panelPath())) {
156            return $this->panelPath();
157        }
158
159        return $next;
160    }
161
162    private function message(string $message): ?string
163    {
164        $message = trim($message);
165
166        return $message === '' ? null : $message;
167    }
168
169    private function redirect(Factory $factory, string $target): Response
170    {
171        $response = Response::create($factory);
172
173        if ($this->request->hasHeader('HX-Request')) {
174            return $response
175                ->status(200)
176                ->header('HX-Redirect', $target);
177        }
178
179        return $response->redirect($target, 303);
180    }
181}