mirror of
https://github.com/grocy/grocy.git
synced 2025-04-29 01:32:38 +00:00
Replace the single user (defined in /data/config.php) with a multi user management thing
This commit is contained in:
parent
b52ab91606
commit
7f8540ff4e
@ -14,9 +14,9 @@ A household needs to be managed. I did this so far (almost 10 years) with my fir
|
||||
>
|
||||
> See https://github.com/berrnd/grocy-desktop or directly download the [latest release](https://releases.grocy.info/latest-desktop) - the installation is nothing more than just clicking 2 times "next"...
|
||||
|
||||
Just unpack the [latest release](https://releases.grocy.info/latest) on your PHP (SQLite extension required, currently only tested with PHP 7.2) enabled webserver (webservers root should point to the `/public` directory), copy `config-dist.php` to `data/config.php`, edit it to your needs, ensure that the `data` directory is writable and you're ready to go.
|
||||
Just unpack the [latest release](https://releases.grocy.info/latest) on your PHP (SQLite extension required, currently only tested with PHP 7.2) enabled webserver (webservers root should point to the `/public` directory), copy `config-dist.php` to `data/config.php`, edit it to your needs, ensure that the `data` directory is writable and you're ready to go. Default login is user `admin` with password `admin`, please change the password immediately (see user menu).
|
||||
|
||||
Default login is user `admin` with password `admin` - see the `data/config.php` file. Alternatively clone this repository and install Composer and Yarn dependencies manually.
|
||||
Alternatively clone this repository and install Composer and Yarn dependencies manually.
|
||||
|
||||
If you use nginx as your webserver, please include `try_files $uri /index.php;` in your location block.
|
||||
|
||||
|
@ -1,9 +1,5 @@
|
||||
<?php
|
||||
|
||||
# Login credentials
|
||||
Setting('HTTP_USER', 'admin');
|
||||
Setting('HTTP_PASSWORD', 'admin');
|
||||
|
||||
# Either "production" or "dev"
|
||||
Setting('MODE', 'production');
|
||||
|
||||
|
@ -24,10 +24,22 @@ class LoginController extends BaseController
|
||||
$postParams = $request->getParsedBody();
|
||||
if (isset($postParams['username']) && isset($postParams['password']))
|
||||
{
|
||||
if ($postParams['username'] === HTTP_USER && $postParams['password'] === HTTP_PASSWORD)
|
||||
$user = $this->Database->users()->where('username', $postParams['username'])->fetch();
|
||||
$inputPassword = $postParams['password'];
|
||||
|
||||
if ($user !== null && password_verify($inputPassword, $user->password))
|
||||
{
|
||||
$sessionKey = $this->SessionService->CreateSession();
|
||||
$sessionKey = $this->SessionService->CreateSession($user->id);
|
||||
setcookie($this->SessionCookieName, $sessionKey, time() + 31536000); // Cookie expires in 1 year, but session validity is up to SessionService
|
||||
define('GROCY_USER_USERNAME', $user->username);
|
||||
define('GROCY_USER_ID', $user->id);
|
||||
|
||||
if (password_needs_rehash($user->password, PASSWORD_DEFAULT))
|
||||
{
|
||||
$user->update(array(
|
||||
'password' => password_hash($inputPassword, PASSWORD_DEFAULT)
|
||||
));
|
||||
}
|
||||
|
||||
return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/'));
|
||||
}
|
||||
@ -69,6 +81,30 @@ class LoginController extends BaseController
|
||||
return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/stockoverview'));
|
||||
}
|
||||
|
||||
public function UsersList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'users', [
|
||||
'users' => $this->Database->users()->orderBy('username')
|
||||
]);
|
||||
}
|
||||
|
||||
public function UserEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
if ($args['userId'] == 'new')
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'userform', [
|
||||
'mode' => 'create'
|
||||
]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'userform', [
|
||||
'user' => $this->Database->users($args['userId']),
|
||||
'mode' => 'edit'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function GetSessionCookieName()
|
||||
{
|
||||
return $this->SessionCookieName;
|
||||
|
59
controllers/UsersApiController.php
Normal file
59
controllers/UsersApiController.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Controllers;
|
||||
|
||||
use \Grocy\Services\UsersService;
|
||||
|
||||
class UsersApiController extends BaseApiController
|
||||
{
|
||||
public function __construct(\Slim\Container $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->UsersService = new UsersService();
|
||||
}
|
||||
|
||||
protected $UsersService;
|
||||
|
||||
public function CreateUser(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
$requestBody = $request->getParsedBody();
|
||||
|
||||
try
|
||||
{
|
||||
$this->UsersService->CreateUser($requestBody['username'], $requestBody['first_name'], $requestBody['last_name'], $requestBody['password']);
|
||||
return $this->ApiResponse(array('success' => $success));
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function DeleteUser(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
try
|
||||
{
|
||||
$this->UsersService->DeleteUser($args['userId']);
|
||||
return $this->ApiResponse(array('success' => $success));
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function EditUser(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
$requestBody = $request->getParsedBody();
|
||||
|
||||
try
|
||||
{
|
||||
$this->UsersService->EditUser($args['userId'], $requestBody['username'], $requestBody['first_name'], $requestBody['last_name'], $requestBody['password']);
|
||||
return $this->ApiResponse(array('success' => $success));
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
@ -370,6 +370,140 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/create": {
|
||||
"post": {
|
||||
"description": "Creates a new user",
|
||||
"tags": [
|
||||
"User management"
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "A valid user object",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/User"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A VoidApiActionResponse object",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/VoidApiActionResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "A VoidApiActionResponse object",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/edit/{userId}": {
|
||||
"post": {
|
||||
"description": "Edits the given user",
|
||||
"tags": [
|
||||
"User management"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "userId",
|
||||
"required": true,
|
||||
"description": "A valid user id",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "A valid user object",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/User"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A VoidApiActionResponse object",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/VoidApiActionResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "A VoidApiActionResponse object",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/delete/{userId}": {
|
||||
"get": {
|
||||
"description": "Deletes the given user",
|
||||
"tags": [
|
||||
"User management"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "userId",
|
||||
"required": true,
|
||||
"description": "A valid user id",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A VoidApiActionResponse object",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/VoidApiActionResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "A VoidApiActionResponse object",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/stock/add-product/{productId}/{amount}": {
|
||||
"get": {
|
||||
"description": "Adds the the given amount of the given product to stock",
|
||||
@ -1182,6 +1316,30 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"User": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"last_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"row_created_timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ApiKey": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -169,6 +169,17 @@ return array(
|
||||
'Put missing amount on shopping list' => 'Fehlende Menge auf den Einkaufszettel setzen',
|
||||
'Are you sure to put all missing ingredients for recipe "#1" on the shopping list?' => 'Sicher alle fehlenden Zutaten für Rezept "#1" auf die Einkaufsliste zu setzen?',
|
||||
'Added for recipe #1' => 'Hinzugefügt für Rezept #1',
|
||||
'Manage users' => 'Benutzer verwalten',
|
||||
'Users' => 'Benutzer',
|
||||
'Are you sure to delete user "#1"?' => 'Benutzer "#1" wirklich löschen?',
|
||||
'Create user' => 'Benutzer erstellen',
|
||||
'Edit user' => 'Benutzer bearbeiten',
|
||||
'First name' => 'Vorname',
|
||||
'Last name' => 'Nachname',
|
||||
'A username is required' => 'Ein Benutzername ist erforderlich',
|
||||
'Confirm password' => 'Passwort bestätigen',
|
||||
'Passwords do not match' => 'Passwörter stimmen nicht überein',
|
||||
'Change password' => 'Passwort ändern',
|
||||
|
||||
//Constants
|
||||
'manually' => 'Manuell',
|
||||
@ -226,5 +237,6 @@ return array(
|
||||
'German' => 'Deutsch',
|
||||
'Italian' => 'Italienisch',
|
||||
'Demo in different language' => 'Demo in anderer Sprache',
|
||||
'This is the note content of the recipe ingredient' => 'Dies ist der Inhalt der Notiz der Zutat'
|
||||
'This is the note content of the recipe ingredient' => 'Dies ist der Inhalt der Notiz der Zutat',
|
||||
'Demo User' => 'Demo Benutzer'
|
||||
);
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace Grocy\Middleware;
|
||||
|
||||
use \Grocy\Services\SessionService;
|
||||
use \Grocy\Services\LocalizationService;
|
||||
|
||||
class SessionAuthMiddleware extends BaseMiddleware
|
||||
{
|
||||
@ -21,7 +22,15 @@ class SessionAuthMiddleware extends BaseMiddleware
|
||||
|
||||
if ($routeName === 'root' || $this->ApplicationService->IsDemoInstallation() || $this->ApplicationService->IsEmbeddedInstallation())
|
||||
{
|
||||
define('AUTHENTICATED', $this->ApplicationService->IsDemoInstallation() || $this->ApplicationService->IsEmbeddedInstallation());
|
||||
if ($this->ApplicationService->IsDemoInstallation() || $this->ApplicationService->IsEmbeddedInstallation())
|
||||
{
|
||||
define('AUTHENTICATED', true);
|
||||
|
||||
$localizationService = new LocalizationService(CULTURE);
|
||||
define('GROCY_USER_USERNAME', $localizationService->Localize('Demo User'));
|
||||
define('GROCY_USER_ID', -1);
|
||||
}
|
||||
|
||||
$response = $next($request, $response);
|
||||
}
|
||||
else
|
||||
@ -34,7 +43,18 @@ class SessionAuthMiddleware extends BaseMiddleware
|
||||
}
|
||||
else
|
||||
{
|
||||
define('AUTHENTICATED', $routeName !== 'login');
|
||||
if ($routeName !== 'login')
|
||||
{
|
||||
$user = $sessionService->GetUserBySessionKey($_COOKIE[$this->SessionCookieName]);
|
||||
define('AUTHENTICATED', true);
|
||||
define('GROCY_USER_USERNAME', $user->username);
|
||||
define('GROCY_USER_ID', $user->id);
|
||||
}
|
||||
else
|
||||
{
|
||||
define('AUTHENTICATED', false);
|
||||
}
|
||||
|
||||
$response = $next($request, $response);
|
||||
}
|
||||
}
|
||||
|
20
migrations/0026.sql
Normal file
20
migrations/0026.sql
Normal file
@ -0,0 +1,20 @@
|
||||
CREATE TABLE users (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
first_name TEXT,
|
||||
last_name TEXT,
|
||||
password TEXT NOT NULL,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
|
||||
DROP TABLE sessions;
|
||||
|
||||
CREATE TABLE sessions (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
session_key TEXT NOT NULL UNIQUE,
|
||||
user_id INTEGER NOT NULL,
|
||||
expires DATETIME,
|
||||
last_used DATETIME,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
)
|
||||
|
24
migrations/0027.php
Normal file
24
migrations/0027.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
// This is executed inside DatabaseMigrationService class/context
|
||||
|
||||
$db = $this->DatabaseService->GetDbConnection();
|
||||
|
||||
if (defined('HTTP_USER'))
|
||||
{
|
||||
// Migrate old user defined in config file to database
|
||||
$newUserRow = $db->users()->createRow(array(
|
||||
'username' => HTTP_USER,
|
||||
'password' => password_hash(HTTP_PASSWORD, PASSWORD_DEFAULT)
|
||||
));
|
||||
$newUserRow->save();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create default user "admin" with password "admin"
|
||||
$newUserRow = $db->users()->createRow(array(
|
||||
'username' => 'admin',
|
||||
'password' => password_hash('admin', PASSWORD_DEFAULT)
|
||||
));
|
||||
$newUserRow->save();
|
||||
}
|
73
public/viewjs/userform.js
Normal file
73
public/viewjs/userform.js
Normal file
@ -0,0 +1,73 @@
|
||||
$('#save-user-button').on('click', function(e)
|
||||
{
|
||||
e.preventDefault();
|
||||
|
||||
if (Grocy.EditMode === 'create')
|
||||
{
|
||||
Grocy.Api.Post('users/create', $('#user-form').serializeJSON(),
|
||||
function(result)
|
||||
{
|
||||
window.location.href = U('/users');
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
Grocy.Api.Post('users/edit/' + Grocy.EditObjectId, $('#user-form').serializeJSON(),
|
||||
function(result)
|
||||
{
|
||||
window.location.href = U('/users');
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
$('#user-form input').keyup(function (event)
|
||||
{
|
||||
var element = document.getElementById("password_confirm");
|
||||
if ($("#password").val() !== $("#password_confirm").val())
|
||||
{
|
||||
element.setCustomValidity("error");
|
||||
}
|
||||
else
|
||||
{
|
||||
element.setCustomValidity("");
|
||||
}
|
||||
|
||||
Grocy.FrontendHelpers.ValidateForm('user-form');
|
||||
});
|
||||
|
||||
$('#user-form input').keydown(function (event)
|
||||
{
|
||||
if (event.keyCode === 13) //Enter
|
||||
{
|
||||
if (document.getElementById('user-form').checkValidity() === false) //There is at least one validation error
|
||||
{
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
$('#save-user-button').click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (GetUriParam("changepw") === "true")
|
||||
{
|
||||
$('#password').focus();
|
||||
}
|
||||
else
|
||||
{
|
||||
$('#username').focus();
|
||||
}
|
||||
|
||||
Grocy.FrontendHelpers.ValidateForm('user-form');
|
58
public/viewjs/users.js
Normal file
58
public/viewjs/users.js
Normal file
@ -0,0 +1,58 @@
|
||||
var usersTable = $('#users-table').DataTable({
|
||||
'paginate': false,
|
||||
'order': [[1, 'asc']],
|
||||
'columnDefs': [
|
||||
{ 'orderable': false, 'targets': 0 }
|
||||
],
|
||||
'language': JSON.parse(L('datatables_localization')),
|
||||
'scrollY': false,
|
||||
'colReorder': true,
|
||||
'stateSave': true
|
||||
});
|
||||
|
||||
$("#search").on("keyup", function()
|
||||
{
|
||||
var value = $(this).val();
|
||||
if (value === "all")
|
||||
{
|
||||
value = "";
|
||||
}
|
||||
|
||||
usersTable.search(value).draw();
|
||||
});
|
||||
|
||||
$(document).on('click', '.user-delete-button', function (e)
|
||||
{
|
||||
var objectName = $(e.currentTarget).attr('data-user-username');
|
||||
var objectId = $(e.currentTarget).attr('data-user-id');
|
||||
|
||||
bootbox.confirm({
|
||||
message: L('Are you sure to delete user "#1"?', objectName),
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: L('Yes'),
|
||||
className: 'btn-success'
|
||||
},
|
||||
cancel: {
|
||||
label: L('No'),
|
||||
className: 'btn-danger'
|
||||
}
|
||||
},
|
||||
callback: function(result)
|
||||
{
|
||||
if (result === true)
|
||||
{
|
||||
Grocy.Api.Get('users/delete' + objectId,
|
||||
function(result)
|
||||
{
|
||||
window.location.href = U('/users');
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
@ -11,10 +11,12 @@ $app->group('', function()
|
||||
// Base route
|
||||
$this->get('/', 'LoginControllerInstance:Root')->setName('root');
|
||||
|
||||
// Login routes
|
||||
// Login/user routes
|
||||
$this->get('/login', 'LoginControllerInstance:LoginPage')->setName('login');
|
||||
$this->post('/login', 'LoginControllerInstance:ProcessLogin')->setName('login');
|
||||
$this->get('/logout', 'LoginControllerInstance:Logout');
|
||||
$this->get('/users', 'LoginControllerInstance:UsersList');
|
||||
$this->get('/user/{userId}', 'LoginControllerInstance:UserEditForm');
|
||||
|
||||
// Stock routes
|
||||
$this->get('/stockoverview', 'Grocy\Controllers\StockController:Overview');
|
||||
@ -69,6 +71,10 @@ $app->group('/api', function()
|
||||
$this->post('/edit-object/{entity}/{objectId}', 'Grocy\Controllers\GenericEntityApiController:EditObject');
|
||||
$this->get('/delete-object/{entity}/{objectId}', 'Grocy\Controllers\GenericEntityApiController:DeleteObject');
|
||||
|
||||
$this->post('/users/create', 'Grocy\Controllers\UsersApiController:CreateUser');
|
||||
$this->post('/users/edit/{userId}', 'Grocy\Controllers\UsersApiController:EditUser');
|
||||
$this->get('/users/delete/{userId}', 'Grocy\Controllers\UsersApiController:DeleteUser');
|
||||
|
||||
$this->get('/stock/add-product/{productId}/{amount}', 'Grocy\Controllers\StockApiController:AddProduct');
|
||||
$this->get('/stock/consume-product/{productId}/{amount}', 'Grocy\Controllers\StockApiController:ConsumeProduct');
|
||||
$this->get('/stock/inventory-product/{productId}/{newAmount}', 'Grocy\Controllers\StockApiController:InventoryProduct');
|
||||
|
@ -8,21 +8,38 @@ class DatabaseMigrationService extends BaseService
|
||||
{
|
||||
$this->DatabaseService->ExecuteDbStatement("CREATE TABLE IF NOT EXISTS migrations (migration INTEGER NOT NULL PRIMARY KEY UNIQUE, execution_time_timestamp DATETIME DEFAULT (datetime('now', 'localtime')))");
|
||||
|
||||
$migrationFiles = array();
|
||||
$sqlMigrationFiles = array();
|
||||
foreach (new \FilesystemIterator(__DIR__ . '/../migrations') as $file)
|
||||
{
|
||||
$migrationFiles[$file->getBasename('.sql')] = $file->getPathname();
|
||||
if ($file->getExtension() === 'sql')
|
||||
{
|
||||
$sqlMigrationFiles[$file->getBasename('.sql')] = $file->getPathname();
|
||||
}
|
||||
}
|
||||
ksort($migrationFiles);
|
||||
|
||||
foreach($migrationFiles as $migrationNumber => $migrationFile)
|
||||
ksort($sqlMigrationFiles);
|
||||
foreach($sqlMigrationFiles as $migrationNumber => $migrationFile)
|
||||
{
|
||||
$migrationNumber = ltrim($migrationNumber, '0');
|
||||
$this->ExecuteMigrationWhenNeeded($migrationNumber, file_get_contents($migrationFile));
|
||||
$this->ExecuteSqlMigrationWhenNeeded($migrationNumber, file_get_contents($migrationFile));
|
||||
}
|
||||
|
||||
$phpMigrationFiles = array();
|
||||
foreach (new \FilesystemIterator(__DIR__ . '/../migrations') as $file)
|
||||
{
|
||||
if ($file->getExtension() === 'php')
|
||||
{
|
||||
$phpMigrationFiles[$file->getBasename('.php')] = $file->getPathname();
|
||||
}
|
||||
}
|
||||
ksort($phpMigrationFiles);
|
||||
foreach($phpMigrationFiles as $migrationNumber => $migrationFile)
|
||||
{
|
||||
$migrationNumber = ltrim($migrationNumber, '0');
|
||||
$this->ExecutePhpMigrationWhenNeeded($migrationNumber, $migrationFile);
|
||||
}
|
||||
}
|
||||
|
||||
private function ExecuteMigrationWhenNeeded(int $migrationId, string $sql)
|
||||
private function ExecuteSqlMigrationWhenNeeded(int $migrationId, string $sql)
|
||||
{
|
||||
$rowCount = $this->DatabaseService->ExecuteDbQuery('SELECT COUNT(*) FROM migrations WHERE migration = ' . $migrationId)->fetchColumn();
|
||||
if (intval($rowCount) === 0)
|
||||
@ -31,4 +48,14 @@ class DatabaseMigrationService extends BaseService
|
||||
$this->DatabaseService->ExecuteDbStatement('INSERT INTO migrations (migration) VALUES (' . $migrationId . ')');
|
||||
}
|
||||
}
|
||||
|
||||
private function ExecutePhpMigrationWhenNeeded(int $migrationId, string $phpFile)
|
||||
{
|
||||
$rowCount = $this->DatabaseService->ExecuteDbQuery('SELECT COUNT(*) FROM migrations WHERE migration = ' . $migrationId)->fetchColumn();
|
||||
if (intval($rowCount) === 0)
|
||||
{
|
||||
include $phpFile;
|
||||
$this->DatabaseService->ExecuteDbStatement('INSERT INTO migrations (migration) VALUES (' . $migrationId . ')');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,8 @@ class DemoDataGeneratorService extends BaseService
|
||||
$loremIpsum = 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.';
|
||||
|
||||
$sql = "
|
||||
INSERT INTO users (id, username, password) VALUES (-1, '{$localizationService->Localize('Demo User')}', 'x');
|
||||
|
||||
INSERT INTO locations (name) VALUES ('{$localizationService->Localize('Pantry')}'); --2
|
||||
INSERT INTO locations (name) VALUES ('{$localizationService->Localize('Candy cupboard')}'); --3
|
||||
INSERT INTO locations (name) VALUES ('{$localizationService->Localize('Tinned food cupboard')}'); --4
|
||||
|
@ -33,11 +33,12 @@ class SessionService extends BaseService
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function CreateSession()
|
||||
public function CreateSession($userId)
|
||||
{
|
||||
$newSessionKey = $this->GenerateSessionKey();
|
||||
|
||||
$sessionRow = $this->Database->sessions()->createRow(array(
|
||||
'user_id' => $userId,
|
||||
'session_key' => $newSessionKey,
|
||||
'expires' => date('Y-m-d H:i:s', time() + 2592000) // Default is that sessions expire in 30 days
|
||||
));
|
||||
@ -51,6 +52,16 @@ class SessionService extends BaseService
|
||||
$this->Database->sessions()->where('session_key', $sessionKey)->delete();
|
||||
}
|
||||
|
||||
public function GetUserBySessionKey($sessionKey)
|
||||
{
|
||||
$sessionRow = $this->Database->sessions()->where('session_key', $sessionKey)->fetch();
|
||||
if ($sessionRow !== null)
|
||||
{
|
||||
return $this->Database->users($sessionRow->user_id);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function GenerateSessionKey()
|
||||
{
|
||||
return RandomString(50);
|
||||
|
47
services/UsersService.php
Normal file
47
services/UsersService.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Services;
|
||||
|
||||
class UsersService extends BaseService
|
||||
{
|
||||
public function CreateUser(string $username, string $firstName, string $lastName, string $password)
|
||||
{
|
||||
$newUserRow = $this->Database->users()->createRow(array(
|
||||
'username' => $username,
|
||||
'first_name' => $firstName,
|
||||
'last_name' => $lastName,
|
||||
'password' => password_hash($password, PASSWORD_DEFAULT)
|
||||
));
|
||||
$newUserRow->save();
|
||||
}
|
||||
|
||||
public function EditUser(int $userId, string $username, string $firstName, string $lastName, string $password)
|
||||
{
|
||||
if (!$this->UserExists($userId))
|
||||
{
|
||||
throw new \Exception('User does not exist');
|
||||
}
|
||||
|
||||
$user = $this->Database->users($userId);
|
||||
$user->update(array(
|
||||
'username' => $username,
|
||||
'first_name' => $firstName,
|
||||
'last_name' => $lastName,
|
||||
'password' => password_hash($password, PASSWORD_DEFAULT)
|
||||
));
|
||||
}
|
||||
|
||||
public function DeleteUser($userId)
|
||||
{
|
||||
$row = $this->Database->users($args['userId']);
|
||||
$row->delete();
|
||||
$success = $row->isClean();
|
||||
return $this->ApiResponse(array('success' => $success));
|
||||
}
|
||||
|
||||
private function UserExists($userId)
|
||||
{
|
||||
$userRow = $this->Database->users()->where('id = :1', $userId)->fetch();
|
||||
return $userRow !== null;
|
||||
}
|
||||
}
|
@ -166,10 +166,12 @@
|
||||
<ul class="navbar-nav ml-auto">
|
||||
@if(AUTHENTICATED === true && $isEmbeddedInstallation === false)
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle discrete-link" href="#" data-toggle="dropdown"><i class="fas fa-user"></i> {{ HTTP_USER }}</a>
|
||||
<a class="nav-link dropdown-toggle discrete-link" href="#" data-toggle="dropdown"><i class="fas fa-user"></i> {{ GROCY_USER_USERNAME }}</a>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item logout-button discrete-link" href="{{ $U('/logout') }}"><i class="fas fa-sign-out-alt"></i> {{ $L('Logout') }}</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item logout-button discrete-link" href="{{ $U('/user/' . GROCY_USER_ID . '?changepw=true') }}"><i class="fas fa-key"></i> {{ $L('Change password') }}</a>
|
||||
</div>
|
||||
</li>
|
||||
@endif
|
||||
@ -178,11 +180,13 @@
|
||||
<a class="nav-link dropdown-toggle discrete-link" href="#" data-toggle="dropdown"><i class="fas fa-wrench"></i> <span class="d-inline d-lg-none">{{ $L('Settings') }}</span></a>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
@if($isEmbeddedInstallation === false)
|
||||
<a class="dropdown-item discrete-link" href="{{ $U('/users') }}"><i class="fas fa-users"></i> {{ $L('Manage users') }}</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
@endif
|
||||
<a class="dropdown-item discrete-link" href="{{ $U('/manageapikeys') }}"><i class="fas fa-handshake"></i> {{ $L('Manage API keys') }}</a>
|
||||
<a class="dropdown-item discrete-link" target="_blank" href="{{ $U('/api') }}"><i class="fas fa-book"></i> {{ $L('REST API & data model documentation') }}</a>
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
|
||||
<a class="dropdown-item discrete-link" href="#" data-toggle="modal" data-target="#about-modal"><i class="fas fa-info fa-fw"></i> {{ $L('About grocy') }} (Version {{ $version }})</a>
|
||||
</div>
|
||||
</li>
|
||||
|
56
views/userform.blade.php
Normal file
56
views/userform.blade.php
Normal file
@ -0,0 +1,56 @@
|
||||
@extends('layout.default')
|
||||
|
||||
@if($mode == 'edit')
|
||||
@section('title', $L('Edit user'))
|
||||
@else
|
||||
@section('title', $L('Create user'))
|
||||
@endif
|
||||
|
||||
@section('viewJsName', 'userform')
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-lg-6 col-xs-12">
|
||||
<h1>@yield('title')</h1>
|
||||
|
||||
<script>Grocy.EditMode = '{{ $mode }}';</script>
|
||||
|
||||
@if($mode == 'edit')
|
||||
<script>Grocy.EditObjectId = {{ $user->id }};</script>
|
||||
@endif
|
||||
|
||||
<form id="user-form" novalidate>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="username">{{ $L('Username') }}</label>
|
||||
<input type="text" class="form-control" required id="username" name="username" value="@if($mode == 'edit'){{ $user->username }}@endif">
|
||||
<div class="invalid-feedback">{{ $L('A username is required') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="first_name">{{ $L('First name') }}</label>
|
||||
<input type="text" class="form-control" id="first_name" name="first_name" value="@if($mode == 'edit'){{ $user->first_name }}@endif">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="last_name">{{ $L('Last name') }}</label>
|
||||
<input type="text" class="form-control" id="last_name" name="last_name" value="@if($mode == 'edit'){{ $user->last_name }}@endif">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">{{ $L('Password') }}</label>
|
||||
<input type="password" class="form-control" required id="password" name="password">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password_confirm">{{ $L('Confirm password') }}</label>
|
||||
<input type="password" class="form-control" required id="password_confirm" name="password_confirm">
|
||||
<div class="invalid-feedback">{{ $L('Passwords do not match') }}</div>
|
||||
</div>
|
||||
|
||||
<button id="save-user-button" type="submit" class="btn btn-success">{{ $L('Save') }}</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@stop
|
63
views/users.blade.php
Normal file
63
views/users.blade.php
Normal file
@ -0,0 +1,63 @@
|
||||
@extends('layout.default')
|
||||
|
||||
@section('title', $L('Users'))
|
||||
@section('activeNav', '')
|
||||
@section('viewJsName', 'users')
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>
|
||||
@yield('title')
|
||||
<a class="btn btn-outline-dark" href="{{ $U('/user/new') }}">
|
||||
<i class="fas fa-plus"></i> {{ $L('Add') }}
|
||||
</a>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-xs-12 col-md-6 col-xl-3">
|
||||
<label for="search">{{ $L('Search') }}</label> <i class="fas fa-search"></i>
|
||||
<input type="text" class="form-control" id="search">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table id="users-table" class="table table-sm table-striped dt-responsive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>{{ $L('Username') }}</th>
|
||||
<th>{{ $L('First name') }}</th>
|
||||
<th>{{ $L('Last name') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($users as $user)
|
||||
<tr>
|
||||
<td class="fit-content">
|
||||
<a class="btn btn-info btn-sm" href="{{ $U('/user/') }}{{ $user->id }}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<a class="btn btn-danger btn-sm user-delete-button @if($user->id === GROCY_USER_ID){{ disabled }}@endif" href="#" data-user-id="{{ $user->id }}" data-user-username="{{ $user->username }}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ $user->username }}
|
||||
</td>
|
||||
<td>
|
||||
{{ $user->first_name }}
|
||||
</td>
|
||||
<td>
|
||||
{{ $user->last_name }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@stop
|
Loading…
x
Reference in New Issue
Block a user