diff options
author | Evgeny Zinoviev <me@ch1p.io> | 2022-05-26 21:18:29 +0300 |
---|---|---|
committer | Evgeny Zinoviev <me@ch1p.io> | 2022-05-27 01:04:47 +0300 |
commit | cf0b9f036b3e3eb218610e7eeececda1320d9f50 (patch) | |
tree | 39e6d1853aecb3fb77036a941a4c6df12a0ce793 /localwebsite | |
parent | c3ed2483ea508141431be74f29f7c209271897cd (diff) |
auth
Diffstat (limited to 'localwebsite')
23 files changed, 649 insertions, 54 deletions
diff --git a/localwebsite/classes/User.php b/localwebsite/classes/User.php new file mode 100644 index 0000000..9019082 --- /dev/null +++ b/localwebsite/classes/User.php @@ -0,0 +1,11 @@ +<?php + +class User extends model { + + const DB_TABLE = 'users'; + + public int $id; + public string $username; + public string $password; + +} diff --git a/localwebsite/classes/auth.php b/localwebsite/classes/auth.php new file mode 100644 index 0000000..2cdee72 --- /dev/null +++ b/localwebsite/classes/auth.php @@ -0,0 +1,66 @@ +<?php + +class auth { + + public static ?User $authorizedUser = null; + + const SESSION_TIMEOUT = 86400 * 365; + const COOKIE_NAME = 'auth'; + + public static function getToken(): ?string { + return $_COOKIE[self::COOKIE_NAME] ?? null; + } + + public static function setToken(string $token) { + setcookie(self::COOKIE_NAME, + $token, + time() + self::SESSION_TIMEOUT, + '/', + config::get('auth_cookie_host'), + true); + } + + public static function resetToken() { + if (!headers_sent()) + setcookie(self::COOKIE_NAME, null, -1, '/', config::get('auth_cookie_host')); + } + + public static function id(bool $do_check = true): int { + if ($do_check) + self::check(); + + if (!self::$authorizedUser) + return 0; + + return self::$authorizedUser->id; + } + + public static function check(?string $pwhash = null): bool { + if (self::$authorizedUser !== null) + return true; + + // get auth token + if (!$pwhash) + $pwhash = self::getToken(); + + if (!is_string($pwhash)) + return false; + + // find session by given token + $user = users::getUserByPwhash($pwhash); + if (is_null($user)) { + self::resetToken(); + return false; + } + + self::$authorizedUser = $user; + + return true; + } + + public static function logout() { + self::resetToken(); + self::$authorizedUser = null; + } + +}
\ No newline at end of file diff --git a/localwebsite/classes/config.php b/localwebsite/classes/config.php new file mode 100644 index 0000000..87ecf1c --- /dev/null +++ b/localwebsite/classes/config.php @@ -0,0 +1,10 @@ +<?php + +class config { + + public static function get(string $key) { + global $config; + return is_callable($config[$key]) ? $config[$key]() : $config[$key]; + } + +}
\ No newline at end of file diff --git a/localwebsite/classes/users.php b/localwebsite/classes/users.php new file mode 100644 index 0000000..1160dba --- /dev/null +++ b/localwebsite/classes/users.php @@ -0,0 +1,39 @@ +<?php + +class users { + + public static function add(string $username, string $password): int { + $db = getDB(); + $db->insert('users', [ + 'username' => $username, + 'password' => pwhash($password) + ]); + return $db->insertId(); + } + + public static function exists(string $username): bool { + $db = getDB(); + $count = (int)$db->querySingle("SELECT COUNT(*) FROM users WHERE username=?", $username); + return $count > 0; + } + + public static function validatePassword(string $username, string $password): bool { + $db = getDB(); + $row = $db->querySingleRow("SELECT * FROM users WHERE username=?", $username); + if (!$row) + return false; + + return $row['password'] == pwhash($password); + } + + public static function getUserByPwhash(string $pwhash): ?User { + $db = getDB(); + $data = $db->querySingleRow("SELECT * FROM users WHERE password=?", $pwhash); + return $data ? new User($data) : null; + } + + public static function setPassword(int $id, string $new_password) { + getDB()->exec("UPDATE users SET password=? WHERE id=?", pwhash($new_password), $id); + } + +}
\ No newline at end of file diff --git a/localwebsite/config.php b/localwebsite/config.php index 7ce281d..f5f8c80 100644 --- a/localwebsite/config.php +++ b/localwebsite/config.php @@ -57,8 +57,8 @@ return [ ], 'cam_hls_access_key' => '', - 'cam_hls_proto' => 'http', - 'cam_hls_host' => '192.168.1.1', + 'cam_hls_proto' => 'http', // bool|callable + 'cam_hls_host' => '192.168.1.1', // bool|callable 'cam_list' => [ // fill me with names ], @@ -70,4 +70,8 @@ return [ ], 'database_path' => getenv('HOME').'/.config/homekit.localwebsite.sqlite3', + + 'auth_cookie_host' => '', + 'auth_need' => false, // bool|callable + 'auth_pw_salt' => '', ]; diff --git a/localwebsite/engine/database.php b/localwebsite/engine/database.php index 186d2ef..33f36cf 100644 --- a/localwebsite/engine/database.php +++ b/localwebsite/engine/database.php @@ -7,13 +7,19 @@ class database { protected SQLite3 $link; public function __construct(string $db_path) { + $will_create = !file_exists($db_path); $this->link = new SQLite3($db_path); + if ($will_create) + setperm($db_path); $this->link->enableExceptions(true); $this->upgradeSchema(); } protected function upgradeSchema() { $cur = $this->getSchemaVersion(); + if ($cur == self::SCHEMA_VERSION) + return; + if ($cur < 1) { $this->link->exec("CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -77,4 +83,49 @@ class database { return $this->link->querySingle($this->prepareQuery($sql, ...$params), true); } + protected function performInsert(string $command, string $table, array $fields): SQLite3Result { + $names = []; + $values = []; + $count = 0; + foreach ($fields as $k => $v) { + $names[] = $k; + $values[] = $v; + $count++; + } + + $sql = "{$command} INTO `{$table}` (`" . implode('`, `', $names) . "`) VALUES (" . implode(', ', array_fill(0, $count, '?')) . ")"; + array_unshift($values, $sql); + + return call_user_func_array([$this, 'query'], $values); + } + + public function insert(string $table, array $fields): SQLite3Result { + return $this->performInsert('INSERT', $table, $fields); + } + + public function replace(string $table, array $fields): SQLite3Result { + return $this->performInsert('REPLACE', $table, $fields); + } + + public function insertId(): int { + return $this->link->lastInsertRowID(); + } + + public function update($table, $rows, ...$cond): SQLite3Result { + $fields = []; + $args = []; + foreach ($rows as $row_name => $row_value) { + $fields[] = "`{$row_name}`=?"; + $args[] = $row_value; + } + $sql = "UPDATE `$table` SET " . implode(', ', $fields); + if (!empty($cond)) { + $sql .= " WHERE " . $cond[0]; + if (count($cond) > 1) + $args = array_merge($args, array_slice($cond, 1)); + } + return $this->query($sql, ...$args); + } + + }
\ No newline at end of file diff --git a/localwebsite/engine/model.php b/localwebsite/engine/model.php new file mode 100644 index 0000000..4dd981c --- /dev/null +++ b/localwebsite/engine/model.php @@ -0,0 +1,243 @@ +<?php + +abstract class model { + + const DB_TABLE = null; + const DB_KEY = 'id'; + + const STRING = 0; + const INTEGER = 1; + const FLOAT = 2; + const ARRAY = 3; + const BOOLEAN = 4; + const JSON = 5; + const SERIALIZED = 6; + + protected static array $SpecCache = []; + + public static function create_instance(...$args) { + $cl = get_called_class(); + return new $cl(...$args); + } + + public function __construct(array $raw) { + if (!isset(self::$SpecCache[static::class])) { + list($fields, $model_name_map, $db_name_map) = static::get_spec(); + self::$SpecCache[static::class] = [ + 'fields' => $fields, + 'model_name_map' => $model_name_map, + 'db_name_map' => $db_name_map + ]; + } + + foreach (self::$SpecCache[static::class]['fields'] as $field) + $this->{$field['model_name']} = self::cast_to_type($field['type'], $raw[$field['db_name']]); + + if (is_null(static::DB_TABLE)) + trigger_error('class '.get_class($this).' doesn\'t have DB_TABLE defined'); + } + + /** + * @param $fields + * + * TODO: support adding or subtracting (SET value=value+1) + */ + public function edit($fields) { + $db = getDB(); + + $model_upd = []; + $db_upd = []; + + foreach ($fields as $name => $value) { + $index = self::$SpecCache[static::class]['db_name_map'][$name] ?? null; + if (is_null($index)) { + debugError(__METHOD__.': field `'.$name.'` not found in '.static::class); + continue; + } + + $field = self::$SpecCache[static::class]['fields'][$index]; + switch ($field['type']) { + case self::ARRAY: + if (is_array($value)) { + $db_upd[$name] = implode(',', $value); + $model_upd[$field['model_name']] = $value; + } else { + debugError(__METHOD__.': field `'.$name.'` is expected to be array. skipping.'); + } + break; + + case self::INTEGER: + $value = (int)$value; + $db_upd[$name] = $value; + $model_upd[$field['model_name']] = $value; + break; + + case self::FLOAT: + $value = (float)$value; + $db_upd[$name] = $value; + $model_upd[$field['model_name']] = $value; + break; + + case self::BOOLEAN: + $db_upd[$name] = $value ? 1 : 0; + $model_upd[$field['model_name']] = $value; + break; + + case self::JSON: + $db_upd[$name] = jsonEncode($value); + $model_upd[$field['model_name']] = $value; + break; + + case self::SERIALIZED: + $db_upd[$name] = serialize($value); + $model_upd[$field['model_name']] = $value; + break; + + default: + $value = (string)$value; + $db_upd[$name] = $value; + $model_upd[$field['model_name']] = $value; + break; + } + } + + if (!empty($db_upd) && !$db->update(static::DB_TABLE, $db_upd, static::DB_KEY."=?", $this->get_id())) { + debugError(__METHOD__.': failed to update database'); + return; + } + + if (!empty($model_upd)) { + foreach ($model_upd as $name => $value) + $this->{$name} = $value; + } + } + + public function get_id() { + return $this->{to_camel_case(static::DB_KEY)}; + } + + public function as_array(array $fields = [], array $custom_getters = []): array { + if (empty($fields)) + $fields = array_keys(static::$SpecCache[static::class]['db_name_map']); + + $array = []; + foreach ($fields as $field) { + if (isset($custom_getters[$field]) && is_callable($custom_getters[$field])) { + $array[$field] = $custom_getters[$field](); + } else { + $array[$field] = $this->{to_camel_case($field)}; + } + } + + return $array; + } + + protected static function cast_to_type(int $type, $value) { + switch ($type) { + case self::BOOLEAN: + return (bool)$value; + + case self::INTEGER: + return (int)$value; + + case self::FLOAT: + return (float)$value; + + case self::ARRAY: + return array_filter(explode(',', $value)); + + case self::JSON: + $val = jsonDecode($value); + if (!$val) + $val = null; + return $val; + + case self::SERIALIZED: + $val = unserialize($value); + if ($val === false) + $val = null; + return $val; + + default: + return (string)$value; + } + } + + protected static function get_spec(): array { + $rc = new ReflectionClass(static::class); + $props = $rc->getProperties(ReflectionProperty::IS_PUBLIC); + + $list = []; + $index = 0; + + $model_name_map = []; + $db_name_map = []; + + foreach ($props as $prop) { + if ($prop->isStatic()) + continue; + + $name = $prop->getName(); + if (startsWith($name, '_')) + continue; + + $type = $prop->getType(); + $phpdoc = $prop->getDocComment(); + + $mytype = null; + if (!$prop->hasType() && !$phpdoc) + $mytype = self::STRING; + else { + $typename = $type->getName(); + switch ($typename) { + case 'string': + $mytype = self::STRING; + break; + case 'int': + $mytype = self::INTEGER; + break; + case 'float': + $mytype = self::FLOAT; + break; + case 'array': + $mytype = self::ARRAY; + break; + case 'bool': + $mytype = self::BOOLEAN; + break; + } + + if ($phpdoc != '') { + $pos = strpos($phpdoc, '@'); + if ($pos === false) + continue; + + if (substr($phpdoc, $pos+1, 4) == 'json') + $mytype = self::JSON; + else if (substr($phpdoc, $pos+1, 5) == 'array') + $mytype = self::ARRAY; + else if (substr($phpdoc, $pos+1, 10) == 'serialized') + $mytype = self::SERIALIZED; + } + } + + if (is_null($mytype)) + debugError(__METHOD__.": ".$name." is still null in ".static::class); + + $dbname = from_camel_case($name); + $list[] = [ + 'type' => $mytype, + 'model_name' => $name, + 'db_name' => $dbname + ]; + + $model_name_map[$name] = $index; + $db_name_map[$dbname] = $index; + + $index++; + } + + return [$list, $model_name_map, $db_name_map]; + } + +} diff --git a/localwebsite/functions.php b/localwebsite/functions.php index f46a534..4757765 100644 --- a/localwebsite/functions.php +++ b/localwebsite/functions.php @@ -242,6 +242,10 @@ function bytesToUnitsLabel(GMP $b): string { return gmp_strval($b); } +function pwhash(string $s): string { + return hash('sha256', config::get('auth_pw_salt').'|'.$s); +} + $ShutdownFunctions = []; function append_shutdown_function(callable $f) { @@ -255,11 +259,27 @@ function prepend_shutdown_function(callable $f) { } function getDB(): database { - global $config; static $link = null; if (is_null($link)) - $link = new database($config['database_path']); + $link = new database(config::get('database_path')); return $link; +} + +function to_camel_case(string $input, string $separator = '_'): string { + return lcfirst(str_replace($separator, '', ucwords($input, $separator))); +} + +function from_camel_case(string $s): string { + $buf = ''; + $len = strlen($s); + for ($i = 0; $i < $len; $i++) { + if (!ctype_upper($s[$i])) { + $buf .= $s[$i]; + } else { + $buf .= '_'.strtolower($s[$i]); + } + } + return $buf; }
\ No newline at end of file diff --git a/localwebsite/handlers/AuthHandler.php b/localwebsite/handlers/AuthHandler.php new file mode 100644 index 0000000..971f850 --- /dev/null +++ b/localwebsite/handlers/AuthHandler.php @@ -0,0 +1,36 @@ +<?php + +class AuthHandler extends RequestHandler { + + protected function before_dispatch(string $method, string $act) { + return null; + } + + public function GET_auth() { + list($error) = $this->input('error'); + $this->tpl->set(['error' => $error]); + $this->tpl->set_title('Авторизация'); + $this->tpl->render_page('auth.twig'); + } + + public function POST_auth() { + list($username, $password) = $this->input('username, password'); + + $result = users::validatePassword($username, $password); + if (!$result) { + debugError('invalid login attempt: '.$_SERVER['REMOTE_ADDR'].', '.$_SERVER['HTTP_USER_AGENT'].", username=$username, password=$password"); + redirect('/auth/?error='.urlencode('неверный логин или пароль')); + } + + auth::setToken(pwhash($password)); + redirect('/'); + } + + public function GET_deauth() { + if (auth::id()) + auth::logout(); + + redirect('/'); + } + +} diff --git a/localwebsite/handlers/MiscHandler.php b/localwebsite/handlers/MiscHandler.php index ef4d8ef..b7c312a 100644 --- a/localwebsite/handlers/MiscHandler.php +++ b/localwebsite/handlers/MiscHandler.php @@ -8,11 +8,6 @@ class MiscHandler extends RequestHandler $this->tpl->render_page('index.twig'); } - public function GET_phpinfo() { - phpinfo(); - exit; - } - public function GET_sensors_page() { global $config; @@ -68,9 +63,9 @@ class MiscHandler extends RequestHandler $hls_opts['debug'] = true; $this->tpl->add_external_static('js', 'https://cdn.jsdelivr.net/npm/hls.js@latest'); - - $hls_host = is_callable($config['cam_hls_host']) ? $config['cam_hls_host']() : $config['cam_hls_host']; - $hls_proto = is_callable($config['cam_hls_proto']) ? $config['cam_hls_proto']() : $config['cam_hls_proto']; + + $hls_host = config::get('cam_hls_host'); + $hls_proto = config::get('cam_hls_proto'); $this->tpl->set([ 'hls_host' => $hls_host, @@ -89,4 +84,8 @@ class MiscHandler extends RequestHandler print_r($_SERVER); } + public function GET_phpinfo() { + phpinfo(); + } + }
\ No newline at end of file diff --git a/localwebsite/handlers/RequestHandler.php b/localwebsite/handlers/RequestHandler.php index 2fffdc0..136a23e 100644 --- a/localwebsite/handlers/RequestHandler.php +++ b/localwebsite/handlers/RequestHandler.php @@ -15,6 +15,12 @@ class RequestHandler extends request_handler { $this->tpl->add_static('polyfills.js'); $this->tpl->add_static('app.js'); $this->tpl->add_static('app.css'); + + if (auth::id()) { + $this->tpl->set_global([ + 'auth_user' => auth::$authorizedUser + ]); + } } public function dispatch(string $act) { @@ -38,4 +44,9 @@ class RequestHandler extends request_handler { ajax_error('unknown act "'.$act.'"', 404); } + + protected function before_dispatch(string $method, string $act) { + if (config::get('auth_need') && !auth::id()) + redirect('/auth/'); + } }
\ No newline at end of file diff --git a/localwebsite/htdocs/index.php b/localwebsite/htdocs/index.php index 9ce8324..8c9d092 100644 --- a/localwebsite/htdocs/index.php +++ b/localwebsite/htdocs/index.php @@ -28,6 +28,10 @@ $router->add('phpinfo/', 'Misc phpinfo'); $router->add('cams/', 'Misc cams'); $router->add('debug/', 'Misc debug'); +// auth +$router->add('auth/', 'Auth auth'); +$router->add('deauth/', 'Auth deauth'); + $route = routerFind($router); if ($route === false) diff --git a/localwebsite/templates-web/auth.twig b/localwebsite/templates-web/auth.twig new file mode 100644 index 0000000..a0107b3 --- /dev/null +++ b/localwebsite/templates-web/auth.twig @@ -0,0 +1,24 @@ +{% include 'bc.twig' with { + history: [ + {text: "Авторизация" } + ] +} %} + +{% if error %} + <div class="mt-4 alert alert-danger"><b>Ошибка:</b> {{ error }}</div> +{% endif %} + + +<form method="post" action="/auth/"> + <div class="mt-2"> + <input type="text" name="username" placeholder="Логин" class="form-control"> + </div> + + <div class="mt-2"> + <input type="password" name="password" placeholder="Пароль" class="form-control"> + </div> + + <div class="mt-2"> + <button type="submit" class="btn btn-outline-primary">Войти</button> + </div> +</form>
\ No newline at end of file diff --git a/localwebsite/templates-web/bc.twig b/localwebsite/templates-web/bc.twig new file mode 100644 index 0000000..b74ad40 --- /dev/null +++ b/localwebsite/templates-web/bc.twig @@ -0,0 +1,12 @@ +<nav aria-label="breadcrumb"> + <ol class="breadcrumb"> + <li class="breadcrumb-item"><a href="/">Главная</a></li> + {% for item in history %} + <li class="breadcrumb-item"{% if loop.last %} aria-current="page"{% endif %}> + {% if item.link %}<a href="{{ item.link }}">{% endif %} + {{ item.html ? item.html|raw : item.text }} + {% if item.link %}</a>{% endif %} + </li> + {% endfor %} + </ol> +</nav>
\ No newline at end of file diff --git a/localwebsite/templates-web/cams.twig b/localwebsite/templates-web/cams.twig index eb9e6d0..4fc815d 100644 --- a/localwebsite/templates-web/cams.twig +++ b/localwebsite/templates-web/cams.twig @@ -1,9 +1,8 @@ -<nav aria-label="breadcrumb"> - <ol class="breadcrumb"> - <li class="breadcrumb-item"><a href="/">Главная</a></li> - <li class="breadcrumb-item active" aria-current="page">Камеры</li> - </ol> -</nav> +{% include 'bc.twig' with { + history: [ + {text: "Камеры" } + ] +} %} <div id="videos" class="camfeeds"></div> diff --git a/localwebsite/templates-web/index.twig b/localwebsite/templates-web/index.twig index 1635459..8a0bdaf 100644 --- a/localwebsite/templates-web/index.twig +++ b/localwebsite/templates-web/index.twig @@ -5,6 +5,12 @@ </ol> </nav> + {% if auth_user %} + <div class="mb-4 alert alert-secondary"> + Вы авторизованы как <b>{{ auth_user.username }}</b>. <a href="/deauth/">Выйти</a> + </div> + {% endif %} + <h6>Интернет</h6> <ul class="list-group list-group-flush"> <li class="list-group-item"><a href="/modem/status/">Состояние</a></li> diff --git a/localwebsite/templates-web/inverter_page.twig b/localwebsite/templates-web/inverter_page.twig index 2b0af90..2c3f8dd 100644 --- a/localwebsite/templates-web/inverter_page.twig +++ b/localwebsite/templates-web/inverter_page.twig @@ -1,9 +1,8 @@ -<nav aria-label="breadcrumb"> - <ol class="breadcrumb"> - <li class="breadcrumb-item"><a href="/">Главная</a></li> - <li class="breadcrumb-item active" aria-current="page">Инвертор</li> - </ol> -</nav> +{% include 'bc.twig' with { + history: [ + {text: "Инвертор" } + ] +} %} <h6 class="text-primary">Статус</h6> <div id="inverter_status"> diff --git a/localwebsite/templates-web/modem_status_page.twig b/localwebsite/templates-web/modem_status_page.twig index 1aa5cf8..f2b999b 100644 --- a/localwebsite/templates-web/modem_status_page.twig +++ b/localwebsite/templates-web/modem_status_page.twig @@ -1,9 +1,8 @@ -<nav aria-label="breadcrumb"> - <ol class="breadcrumb"> - <li class="breadcrumb-item"><a href="/">Главная</a></li> - <li class="breadcrumb-item active" aria-current="page">Модемы</li> - </ol> -</nav> +{% include 'bc.twig' with { + history: [ + {text: "Модемы" } + ] +} %} {% for modem_key, modem in modems %} <h6 class="text-primary{% if not loop.first %} mt-4{% endif %}">{{ modem.label }}</h6> diff --git a/localwebsite/templates-web/pump.twig b/localwebsite/templates-web/pump.twig index 4a8cad5..3bce0e2 100644 --- a/localwebsite/templates-web/pump.twig +++ b/localwebsite/templates-web/pump.twig @@ -1,9 +1,8 @@ -<nav aria-label="breadcrumb"> - <ol class="breadcrumb"> - <li class="breadcrumb-item"><a href="/">Главная</a></li> - <li class="breadcrumb-item active" aria-current="page">Насос</li> - </ol> -</nav> +{% include 'bc.twig' with { + history: [ + {text: "Насос" } + ] +} %} <form action="/pump/" method="get"> <input type="hidden" name="set" value="{{ status == 'on' ? 'off' : 'on' }}" /> diff --git a/localwebsite/templates-web/routing_header.twig b/localwebsite/templates-web/routing_header.twig index f7322f9..8cb5f47 100644 --- a/localwebsite/templates-web/routing_header.twig +++ b/localwebsite/templates-web/routing_header.twig @@ -1,9 +1,8 @@ -<nav aria-label="breadcrumb"> - <ol class="breadcrumb"> - <li class="breadcrumb-item"><a href="/">Главная</a></li> - <li class="breadcrumb-item active" aria-current="page">Маршрутизация</li> - </ol> -</nav> +{% include 'bc.twig' with { + history: [ + {text: "Маршрутизация" } + ] +} %} {% set routing_tabs = [ {tab: 'smallhome', url: '/routing/', label: 'Маленький дом'}, diff --git a/localwebsite/templates-web/sensors.twig b/localwebsite/templates-web/sensors.twig index 354e4e7..14f8454 100644 --- a/localwebsite/templates-web/sensors.twig +++ b/localwebsite/templates-web/sensors.twig @@ -1,9 +1,8 @@ -<nav aria-label="breadcrumb"> - <ol class="breadcrumb"> - <li class="breadcrumb-item"><a href="/">Главная</a></li> - <li class="breadcrumb-item active" aria-current="page">Датчики</li> - </ol> -</nav> +{% include 'bc.twig' with { + history: [ + {text: "Датчики" } + ] +} %} {% for key, sensor in sensors %} <h6 class="text-primary{% if not loop.first %} mt-4{% endif %}">{{ sensor.name }}</h6> diff --git a/localwebsite/templates-web/sms_page.twig b/localwebsite/templates-web/sms_page.twig index b6551a3..f60d223 100644 --- a/localwebsite/templates-web/sms_page.twig +++ b/localwebsite/templates-web/sms_page.twig @@ -1,9 +1,8 @@ -<nav aria-label="breadcrumb"> - <ol class="breadcrumb"> - <li class="breadcrumb-item"><a href="/">Главная</a></li> - <li class="breadcrumb-item active" aria-current="page">SMS-сообщения</li> - </ol> -</nav> +{% include 'bc.twig' with { + history: [ + {text: "SMS-сообщения" } + ] +} %} <nav> <div class="nav nav-tabs" id="nav-tab"> diff --git a/localwebsite/utils.php b/localwebsite/utils.php new file mode 100755 index 0000000..333ebfc --- /dev/null +++ b/localwebsite/utils.php @@ -0,0 +1,66 @@ +#!/usr/bin/env php +<?php + +require_once __DIR__.'/init.php'; + +function read_stdin(?string $prompt = null, bool $multiline = true) { + if (!is_null($prompt)) + echo $prompt; + + if (!$multiline) + return trim(fgets(STDIN)); + + $fp = fopen('php://stdin', 'r'); + $data = stream_get_contents($fp); + fclose($fp); + + return $data; +} + +function usage() { + global $argv; + echo <<<EOF +usage: {$argv[0]} COMMAND + +Supported commands: + add-user + change-password + +EOF; + exit(1); +} + +if (empty($argv[1])) + usage(); + +switch ($argv[1]) { + case 'add-user': + $username = read_stdin('enter username: ', false); + $password = read_stdin('enter password: ', false); + + if (users::exists($username)) { + fwrite(STDERR, "user already exists\n"); + exit(1); + } + + $id = users::add($username, $password); + echo "added user, id = $id\n"; + + break; + + case 'change-password': + $id = (int)read_stdin('enter ID: ', false); + if (!$id) + die("invalid id\n"); + + $password = read_stdin('enter new password: ', false); + if (!$password) + die("invalid password\n"); + + users::setPassword($id, $password); + break; + + default: + fwrite(STDERR, "invalid command\n"); + exit(1); +}
\ No newline at end of file |