Skip to content

Commit b8c7435

Browse files
committed
Administrators can view/edit accounts
1 parent 6c21a0c commit b8c7435

File tree

9 files changed

+459
-3
lines changed

9 files changed

+459
-3
lines changed

public/static/faqoff.css

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,4 +286,17 @@ footer {
286286
#admin-entry-options {
287287
font-family: monospace;
288288
min-height: 10rem;
289-
}
289+
}
290+
291+
i.color-success {
292+
color: #090;
293+
}
294+
.dark-body i.color-success {
295+
color: #9c9;
296+
}
297+
i.color-error {
298+
color: #900;
299+
}
300+
.dark-body i.color-error {
301+
color: #c66;
302+
}

src/FaqOff/Endpoints/Admin/Accounts.php

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,15 @@
33
namespace Soatok\FaqOff\Endpoints\Admin;
44

55
use Interop\Container\Exception\ContainerException;
6+
use ParagonIE\Ionizer\InvalidDataException;
67
use Psr\Http\Message\RequestInterface;
78
use Psr\Http\Message\ResponseInterface;
9+
use Slim\Container;
810
use Soatok\AnthroKit\Endpoint;
11+
use Soatok\FaqOff\Filter\AdminEditAccountFilter;
12+
use Soatok\FaqOff\MessageOnceTrait;
13+
use Soatok\FaqOff\Splices\Accounts as AccountSplice;
14+
use Soatok\FaqOff\Splices\Authors;
915
use Twig\Error\LoaderError;
1016
use Twig\Error\RuntimeError;
1117
use Twig\Error\SyntaxError;
@@ -16,12 +22,92 @@
1622
*/
1723
class Accounts extends Endpoint
1824
{
25+
use MessageOnceTrait;
26+
27+
/** @var AccountSplice $accounts */
28+
protected $accounts;
29+
30+
/** @var Authors $authors */
31+
protected $authors;
32+
33+
public function __construct(Container $container)
34+
{
35+
parent::__construct($container);
36+
$this->accounts = $this->splice('Accounts');
37+
$this->authors = $this->splice('Authors');
38+
}
39+
40+
/**
41+
* @param RequestInterface $request
42+
* @param int $accountId
43+
* @return ResponseInterface
44+
* @throws ContainerException
45+
* @throws InvalidDataException
46+
* @throws LoaderError
47+
* @throws RuntimeError
48+
* @throws SyntaxError
49+
*/
50+
protected function editAccount(RequestInterface $request, int $accountId): ResponseInterface
51+
{
52+
$filter = new AdminEditAccountFilter();
53+
$post = $this->post($request, self::TYPE_FORM, $filter);
54+
if ($post) {
55+
if ($this->accounts->updateAccountByAdmin($accountId, $post)) {
56+
$this->messageOnce('Account updated successfully.', 'success');
57+
return $this->redirect('/admin/account/edit/' . $accountId);
58+
}
59+
}
60+
return $this->view(
61+
'admin/accounts-edit.twig',
62+
[
63+
'account' => $this->accounts->getInfoByAccountId($accountId)
64+
]
65+
);
66+
}
67+
68+
/**
69+
* @return ResponseInterface
70+
* @throws ContainerException
71+
* @throws LoaderError
72+
* @throws RuntimeError
73+
* @throws SyntaxError
74+
*/
75+
protected function listAccounts(): ResponseInterface
76+
{
77+
return $this->view(
78+
'admin/accounts-list.twig',
79+
[
80+
'accounts' => $this->accounts->listAllWithPublicId()
81+
]
82+
);
83+
}
84+
/**
85+
* @param RequestInterface $request
86+
* @param int $accountId
87+
* @return ResponseInterface
88+
* @throws ContainerException
89+
* @throws LoaderError
90+
* @throws RuntimeError
91+
* @throws SyntaxError
92+
*/
93+
protected function viewAccount(RequestInterface $request, int $accountId): ResponseInterface
94+
{
95+
return $this->view(
96+
'admin/accounts-view.twig',
97+
[
98+
'account' => $this->accounts->getInfoByAccountId($accountId),
99+
'authors' => $this->authors->getByAccount($accountId),
100+
]
101+
);
102+
}
103+
19104
/**
20105
* @param RequestInterface $request
21106
* @param ResponseInterface|null $response
22107
* @param array $routerParams
23108
* @return ResponseInterface
24109
* @throws ContainerException
110+
* @throws InvalidDataException
25111
* @throws LoaderError
26112
* @throws RuntimeError
27113
* @throws SyntaxError
@@ -31,6 +117,16 @@ public function __invoke(
31117
?ResponseInterface $response = null,
32118
array $routerParams = []
33119
): ResponseInterface {
34-
return $this->json($routerParams);
120+
$action = $routerParams['action'] ?? null;
121+
switch ($action) {
122+
case 'edit':
123+
return $this->editAccount($request, (int) $routerParams['id']);
124+
case 'view':
125+
return $this->viewAccount($request, (int) $routerParams['id']);
126+
case '':
127+
return $this->listAccounts();
128+
default:
129+
return $this->redirect('/admin/accounts');
130+
}
35131
}
36132
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
declare(strict_types=1);
3+
namespace Soatok\FaqOff\Filter;
4+
5+
use ParagonIE\Ionizer\Filter\BoolFilter;
6+
use ParagonIE\Ionizer\Filter\StringFilter;
7+
use ParagonIE\Ionizer\InputFilterContainer;
8+
9+
/**
10+
* Class AdminEditAccountFilter
11+
* @package Soatok\FaqOff\Filter
12+
*/
13+
class AdminEditAccountFilter extends InputFilterContainer
14+
{
15+
public function __construct()
16+
{
17+
$this
18+
->addFilter('active', new BoolFilter())
19+
->addFilter('disable-external', new BoolFilter())
20+
->addFilter('disable-two-factor', new BoolFilter())
21+
->addFilter('email', new StringFilter())
22+
->addFilter('login', new StringFilter())
23+
->addFilter('password', new StringFilter())
24+
->addFilter('password2', new StringFilter())
25+
->addFilter('public_id', new StringFilter())
26+
;
27+
}
28+
}

src/FaqOff/Splices/Accounts.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
namespace Soatok\FaqOff\Splices;
44

55
use ParagonIE\ConstantTime\Base32;
6+
use ParagonIE\HiddenString\HiddenString;
67
use Soatok\AnthroKit\Auth\Splices\Accounts as BaseClass;
8+
use Soatok\DholeCrypto\Password;
79

810
/**
911
* Class Accounts
@@ -139,5 +141,81 @@ public function getUnusedInviteCodes(int $accountId): array
139141
}
140142
return $codes;
141143
}
144+
145+
/**
146+
* @return array
147+
*/
148+
public function listAllWithPublicId(): array
149+
{
150+
$accounts = $this->db->run(
151+
"SELECT * FROM faqoff_accounts ORDER BY created ASC"
152+
);
153+
if (empty($accounts)) {
154+
return [];
155+
}
156+
foreach ($accounts as $i => $acc) {
157+
if (!empty($acc['external_auth'])) {
158+
$accounts[$i]['external_auth'] = json_decode($acc['external_auth'], true);
159+
}
160+
}
161+
return $accounts;
162+
}
163+
164+
/**
165+
* @param int $accountId
166+
* @return array
167+
*/
168+
public function getInfoByAccountId(int $accountId): array
169+
{
170+
$acc = $this->db->row(
171+
"SELECT * FROM faqoff_accounts WHERE accountid = ?",
172+
$accountId
173+
);
174+
if (!empty($acc['external_auth'])) {
175+
$acc['external_auth'] = json_decode($acc['external_auth'], true);
176+
}
177+
return $acc;
178+
}
179+
180+
/**
181+
* @param int $accountId
182+
* @param array $post
183+
* @return bool
184+
* @throws \Exception
185+
*/
186+
public function updateAccountByAdmin(int $accountId, array $post): bool
187+
{
188+
$updates = [
189+
'login' => $post['login'],
190+
'active' => $post['active'] ?? false
191+
];
192+
if (empty($updates['public_id'])) {
193+
$this->generatePublicId($accountId);
194+
} else {
195+
$updates['public_id'] = $post['public_id'];
196+
}
197+
if (!empty($post['disable-two-factor'])) {
198+
$updates['twofactor'] = null;
199+
}
200+
if (!empty($post['disable-external-auth'])) {
201+
$updates['external_auth'] = null;
202+
}
203+
if (!empty($post['password']) && !empty($post['password2'])) {
204+
if (hash_equals($post['password'], $post['password2'])) {
205+
$updates['pwhash'] = (new Password($this->passwordKey))->hash(
206+
new HiddenString($post['password']),
207+
(string) $accountId
208+
);
209+
}
210+
}
211+
212+
$this->db->beginTransaction();
213+
$this->db->update(
214+
'faqoff_accounts',
215+
$updates,
216+
['accountid' => $accountId]
217+
);
218+
return $this->db->commit();
219+
}
142220
}
143221

src/FaqOff/Utility.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,8 @@ function () {
233233
)
234234
);
235235

236+
$env->addFilter(new TwigFilter('ucfirst', 'ucfirst'));
237+
236238
$settings = $container->get('settings')['twig-custom'] ?? [];
237239
$env->addGlobal('faqoff_custom', $settings);
238240
$env->addGlobal('theme_id', null);

0 commit comments

Comments
 (0)